Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
### Added
* Allow specifying path to Biome JSON config file directly in `biome` step. Requires biome 2.x. ([#2548](https://github.com/diffplug/spotless/pull/2548))
- `GitPrePushHookInstaller`, a reusable library component for installing a Git `pre-push` hook that runs formatter checks.

## Changed
* Bump default `gson` version to latest `2.11.0` -> `2.13.1`. ([#2414](https://github.com/diffplug/spotless/pull/2414))
Expand Down
189 changes: 189 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* Copyright 2025 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.spotless;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;

/**
* Abstract class responsible for installing a Git pre-push hook in a repository.
* This class ensures that specific checks and logic are run before a push operation in Git.
*
* Subclasses should define specific behavior for hook installation by implementing the required abstract methods.
*/
public abstract class GitPrePushHookInstaller {

/**
* Logger for recording informational and error messages during the installation process.
*/
protected final GitPreHookLogger logger;

/**
* The root directory of the Git repository where the hook will be installed.
*/
protected final File root;

/**
* Constructor to initialize the GitPrePushHookInstaller with a logger and repository root path.
*
* @param logger The logger for recording messages.
* @param root The root directory of the Git repository.
*/
public GitPrePushHookInstaller(GitPreHookLogger logger, File root) {
this.logger = logger;
this.root = root;
}

/**
* Installs the Git pre-push hook into the repository.
*
* <p>This method checks for the following:
* <ul>
* <li>Ensures Git is installed and the `.git/config` file exists.</li>
* <li>Checks if an executor required by the hook is available.</li>
* <li>Creates and writes the pre-push hook file if it does not exist.</li>
* <li>Skips installation if the hook is already installed.</li>
* </ul>
* If an issue occurs during installation, error messages are logged.
*
* @throws Exception if any error occurs during installation.
*/
public void install() throws Exception {
logger.info("Installing git pre-push hook");

if (!isGitInstalled()) {
logger.error("Git not found in root directory");
return;
}

if (!isExecutorInstalled()) {
return;
}

var hookContent = "";
final var gitHookFile = root.toPath().resolve(".git/hooks/pre-push").toFile();
if (!gitHookFile.exists()) {
logger.info("Git pre-push hook not found, creating it");
if (!gitHookFile.getParentFile().exists() && !gitHookFile.getParentFile().mkdirs()) {
logger.error("Failed to create pre-push hook directory");
return;
}

if (!gitHookFile.createNewFile()) {
logger.error("Failed to create pre-push hook file");
return;
}

if (!gitHookFile.setExecutable(true, false)) {
logger.error("Can not make file executable");
return;
}

hookContent += "#!/bin/sh\n";
}

if (isGitHookInstalled(gitHookFile)) {
logger.info("Skipping, git pre-push hook already installed %s", gitHookFile.getAbsolutePath());
return;
}

hookContent += preHookContent();
writeFile(gitHookFile, hookContent);

logger.info("Git pre-push hook installed successfully to the file %s", gitHookFile.getAbsolutePath());
}

/**
* Checks if the required executor for performing the desired pre-push actions is installed.
*
* @return {@code true} if the executor is installed, {@code false} otherwise.
*/
protected abstract boolean isExecutorInstalled();

/**
* Provides the content of the hook that should be inserted into the pre-push script.
*
* @return A string representing the content to include in the pre-push script.
*/
protected abstract String preHookContent();

/**
* Checks if Git is installed by validating the existence of `.git/config` in the repository root.
*
* @return {@code true} if Git is installed, {@code false} otherwise.
*/
private boolean isGitInstalled() {
return root.toPath().resolve(".git/config").toFile().exists();
}

/**
* Verifies if the pre-push hook file already contains the custom Spotless hook content.
*
* @param gitHookFile The file representing the Git hook.
* @return {@code true} if the hook is already installed, {@code false} otherwise.
* @throws Exception if an error occurs when reading the file.
*/
private boolean isGitHookInstalled(File gitHookFile) throws Exception {
final var hook = Files.readString(gitHookFile.toPath(), UTF_8);
return hook.contains("##### SPOTLESS HOOK START #####");
}

/**
* Writes the specified content into a file.
*
* @param file The file to which the content should be written.
* @param content The content to write into the file.
* @throws IOException if an error occurs while writing to the file.
*/
private void writeFile(File file, String content) throws IOException {
try (final var writer = new FileWriter(file, UTF_8, true)) {
writer.write(content);
}
}

/**
* Generates a pre-push template script that defines the commands to check and apply changes
* using an executor and Spotless.
*
* @param executor The tool to execute the check and apply commands.
* @param commandCheck The command to check for issues.
* @param commandApply The command to apply corrections.
* @return A string template representing the Spotless Git pre-push hook content.
*/
protected String preHookTemplate(String executor, String commandCheck, String commandApply) {
var spotlessHook = "\n";
spotlessHook += "\n##### SPOTLESS HOOK START #####";
spotlessHook += "\nSPOTLESS_EXECUTOR=" + executor;
spotlessHook += "\nif ! $SPOTLESS_EXECUTOR " + commandCheck + " ; then";
spotlessHook += "\n echo 1>&2 \"spotless found problems, running " + commandApply + "; commit the result and re-push\"";
spotlessHook += "\n $SPOTLESS_EXECUTOR " + commandApply;
spotlessHook += "\n exit 1";
spotlessHook += "\nfi";
spotlessHook += "\n##### SPOTLESS HOOK END #####";
spotlessHook += "\n\n";
return spotlessHook;
}

public interface GitPreHookLogger {
void info(String format, Object... arguments);

void error(String format, Object... arguments);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2025 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.spotless;

import java.io.File;

/**
* Implementation of {@link GitPrePushHookInstaller} specifically for Gradle-based projects.
* This class installs a Git pre-push hook that uses Gradle's `gradlew` executable to check and apply Spotless formatting.
*/
public class GitPrePushHookInstallerGradle extends GitPrePushHookInstaller {

/**
* The Gradle wrapper file (`gradlew`) located in the root directory of the project.
*/
private final File gradlew;

public GitPrePushHookInstallerGradle(GitPreHookLogger logger, File root) {
super(logger, root);
this.gradlew = root.toPath().resolve("gradlew").toFile();
}

/**
* Checks if the Gradle wrapper (`gradlew`) is present in the root directory.
* This ensures that the executor used for formatting (`spotlessCheck` and `spotlessApply`) is available.
*
* @return {@code true} if the Gradle wrapper is found, {@code false} otherwise.
* An error is logged if the wrapper is not found.
*/
@Override
protected boolean isExecutorInstalled() {
if (gradlew.exists()) {
return true;
}

logger.error("Failed to find gradlew in root directory");
return false;
}

@Override
protected String preHookContent() {
return preHookTemplate(gradlew.getAbsolutePath(), "spotlessCheck", "spotlessApply");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2025 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.spotless;

import java.io.File;

/**
* Implementation of {@link GitPrePushHookInstaller} specifically for Maven-based projects.
* This class installs a Git pre-push hook that uses Maven to check and apply Spotless formatting.
*/
public class GitPrePushHookInstallerMaven extends GitPrePushHookInstaller {

public GitPrePushHookInstallerMaven(GitPreHookLogger logger, File root) {
super(logger, root);
}

/**
* Confirms that Maven is installed and available for use.
*
* <p>This method assumes that if this code is running, then Maven is already properly installed and configured,
* so it always returns {@code true}.
*
* @return {@code true}, indicating that Maven is available.
*/
@Override
protected boolean isExecutorInstalled() {
return true;
}

@Override
protected String preHookContent() {
return preHookTemplate("mvn", "spotless:check", "spotless:apply");
}
}
3 changes: 3 additions & 0 deletions plugin-gradle/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
### Added
* Allow specifying path to Biome JSON config file directly in `biome` step. Requires biome 2.x. ([#2548](https://github.com/diffplug/spotless/pull/2548))
- `spotlessInstallGitPrePushHook` task, which installs a Git `pre-push` hook to run `spotlessCheck` and `spotlessApply`.
Uses shared implementation from `GitPrePushHookInstaller`.
[#2553](https://github.com/diffplug/spotless/pull/2553)

## Changed
* Bump default `gson` version to latest `2.11.0` -> `2.13.1`. ([#2414](https://github.com/diffplug/spotless/pull/2414))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2024 DiffPlug
* Copyright 2016-2025 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -38,14 +38,17 @@ public abstract class SpotlessExtension {
private final RegisterDependenciesTask registerDependenciesTask;

protected static final String TASK_GROUP = LifecycleBasePlugin.VERIFICATION_GROUP;
protected static final String BUILD_SETUP_TASK_GROUP = "build setup";
protected static final String CHECK_DESCRIPTION = "Checks that sourcecode satisfies formatting steps.";
protected static final String APPLY_DESCRIPTION = "Applies code formatting steps to sourcecode in-place.";
protected static final String INSTALL_GIT_PRE_PUSH_HOOK_DESCRIPTION = "Installs Spotless Git pre-push hook.";

static final String EXTENSION = "spotless";
static final String EXTENSION_PREDECLARE = "spotlessPredeclare";
static final String CHECK = "Check";
static final String APPLY = "Apply";
static final String DIAGNOSE = "Diagnose";
static final String INSTALL_GIT_PRE_PUSH_HOOK = "InstallGitPrePushHook";

protected SpotlessExtension(Project project) {
this.project = requireNonNull(project);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2024 DiffPlug
* Copyright 2016-2025 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -23,7 +23,7 @@
import org.gradle.api.tasks.TaskProvider;

public class SpotlessExtensionImpl extends SpotlessExtension {
final TaskProvider<?> rootCheckTask, rootApplyTask, rootDiagnoseTask;
final TaskProvider<?> rootCheckTask, rootApplyTask, rootDiagnoseTask, rootInstallPreHook;

public SpotlessExtensionImpl(Project project) {
super(project);
Expand All @@ -38,6 +38,10 @@ public SpotlessExtensionImpl(Project project) {
rootDiagnoseTask = project.getTasks().register(EXTENSION + DIAGNOSE, task -> {
task.setGroup(TASK_GROUP); // no description on purpose
});
rootInstallPreHook = project.getTasks().register(EXTENSION + INSTALL_GIT_PRE_PUSH_HOOK, SpotlessInstallPrePushHookTask.class, task -> {
task.setGroup(BUILD_SETUP_TASK_GROUP);
task.setDescription(INSTALL_GIT_PRE_PUSH_HOOK_DESCRIPTION);
});

project.afterEvaluate(unused -> {
if (enforceCheck) {
Expand Down
Loading