From e6e9a075b8cf97f871254da5f12adc922e331b2b Mon Sep 17 00:00:00 2001 From: Alex Danylenko Date: Fri, 11 Jul 2025 23:17:35 -0700 Subject: [PATCH 1/7] add git pre push hook --- CHANGES.md | 1 + .../spotless/GitPrePushHookInstaller.java | 169 ++++++++++++++++++ .../GitPrePushHookInstallerGradle.java | 42 +++++ .../GitPrePushHookInstallerMaven.java | 32 ++++ plugin-gradle/CHANGES.md | 3 + .../gradle/spotless/SpotlessExtension.java | 3 + .../spotless/SpotlessExtensionImpl.java | 6 +- .../SpotlessInstallPrePushHookTask.java | 46 +++++ .../SpotlessInstallPrePushHookTaskTest.java | 70 ++++++++ plugin-maven/CHANGES.md | 3 + .../spotless/maven/AbstractSpotlessMojo.java | 1 + .../maven/SpotlessInstallPrePushHookMojo.java | 63 +++++++ .../SpotlessInstallPrePushHookMojoTest.java | 69 +++++++ .../resources/git_pre_hook/pre-push.created | 12 ++ .../resources/git_pre_hook/pre-push.existing | 51 ++++++ .../git_pre_hook/pre-push.existing-added | 62 +++++++ .../spotless/GitPrePushHookInstallerTest.java | 148 +++++++++++++++ 17 files changed, 780 insertions(+), 1 deletion(-) create mode 100644 lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java create mode 100644 lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java create mode 100644 lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerMaven.java create mode 100644 plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTask.java create mode 100644 plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java create mode 100644 plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojo.java create mode 100644 plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojoTest.java create mode 100644 testlib/src/main/resources/git_pre_hook/pre-push.created create mode 100644 testlib/src/main/resources/git_pre_hook/pre-push.existing create mode 100644 testlib/src/main/resources/git_pre_hook/pre-push.existing-added create mode 100644 testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java diff --git a/CHANGES.md b/CHANGES.md index e98e32f712..a57d1c1e2c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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)) diff --git a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java new file mode 100644 index 0000000000..a317689b35 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java @@ -0,0 +1,169 @@ +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. + * + *

This method checks for the following: + *

+ * 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"); + gitHookFile.getParentFile().mkdirs(); + 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); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java new file mode 100644 index 0000000000..8c4817c54c --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java @@ -0,0 +1,42 @@ +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"); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerMaven.java b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerMaven.java new file mode 100644 index 0000000000..c54b2e4d02 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerMaven.java @@ -0,0 +1,32 @@ +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. + * + *

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"); + } +} diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index dd1e9c425f..b318ae421f 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -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)) diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java index e883953eaa..885d697688 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java @@ -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); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java index 75168f690a..45d7ffade7 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java @@ -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); @@ -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) { diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTask.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTask.java new file mode 100644 index 0000000000..df6b9bfc8e --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTask.java @@ -0,0 +1,46 @@ +package com.diffplug.gradle.spotless; + +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.TaskAction; +import org.gradle.work.DisableCachingByDefault; + +import com.diffplug.spotless.GitPrePushHookInstaller.GitPreHookLogger; +import com.diffplug.spotless.GitPrePushHookInstallerGradle; + +/** + * A Gradle task responsible for installing a Git pre-push hook for the Spotless plugin. + * This hook ensures that Spotless formatting rules are automatically checked and applied + * before performing a Git push operation. + * + *

The task leverages {@link GitPrePushHookInstallerGradle} to implement the installation process. + */ +@DisableCachingByDefault(because = "not worth caching") +public class SpotlessInstallPrePushHookTask extends DefaultTask { + + /** + * Executes the task to install the Git pre-push hook. + * + *

This method creates an instance of {@link GitPrePushHookInstallerGradle}, + * providing a logger to record informational and error messages during the installation process. + * The installer then installs the hook in the root directory of the Gradle project. + * + * @throws Exception if an error occurs during the hook installation process. + */ + @TaskAction + public void performAction() throws Exception { + final var logger = new GitPreHookLogger() { + @Override + public void info(String format, Object... arguments) { + getLogger().lifecycle(String.format(format, arguments)); + } + + @Override + public void error(String format, Object... arguments) { + getLogger().error(String.format(format, arguments)); + } + }; + + final var installer = new GitPrePushHookInstallerGradle(logger, getProject().getRootDir()); + installer.install(); + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java new file mode 100644 index 0000000000..1f74e14fc3 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java @@ -0,0 +1,70 @@ +package com.diffplug.gradle.spotless; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class SpotlessInstallPrePushHookTaskTest extends GradleIntegrationHarness { + + @Test + public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws Exception { + // given + final var gradlew = setFile("gradlew").toContent(""); + setFile(".git/config").toContent(""); + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }" + ); + + // when + var output = gradleRunner() + .withArguments("spotlessInstallGitPrePushHook") + .build() + .getOutput(); + + // then + assertThat(output).contains("Installing git pre-push hook"); + assertThat(output).contains("Git pre-push hook not found, creating it"); + assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push")); + + final var content = getTestResource("git_pre_hook/pre-push.created") + .replace("${executor}", gradlew.getAbsolutePath()) + .replace("${checkCommand}", "spotlessCheck") + .replace("${applyCommand}", "spotlessApply"); + assertFile(".git/hooks/pre-push").hasContent(content); + } + + @Test + public void should_append_to_existing_pre_hook_file_when_hook_file_exists() throws Exception { + // given + final var gradlew = setFile("gradlew").toContent(""); + setFile(".git/config").toContent(""); + setFile("build.gradle").toLines( + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }" + ); + setFile(".git/hooks/pre-push").toResource("git_pre_hook/pre-push.existing"); + + // when + final var output = gradleRunner() + .withArguments("spotlessInstallGitPrePushHook") + .build() + .getOutput(); + + // then + assertThat(output).contains("Installing git pre-push hook"); + assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push")); + + final var content = getTestResource("git_pre_hook/pre-push.existing-added") + .replace("${executor}", gradlew.getAbsolutePath()) + .replace("${checkCommand}", "spotlessCheck") + .replace("${applyCommand}", "spotlessApply"); + assertFile(".git/hooks/pre-push").hasContent(content); + } +} diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index 6829753b2e..f44ad846d4 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -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)) +- `spotless:install-git-pre-push-hook` goal, which installs a Git `pre-push` hook to run `spotless:check` and `spotless:apply`. + 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)) diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java index d21a1b8113..5a21a7258a 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java @@ -91,6 +91,7 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo { static final String GOAL_CHECK = "check"; static final String GOAL_APPLY = "apply"; + static final String GOAL_PRE_PUSH_HOOK = "install-git-pre-push-hook"; @Component private RepositorySystem repositorySystem; diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojo.java new file mode 100644 index 0000000000..356a52e913 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojo.java @@ -0,0 +1,63 @@ +package com.diffplug.spotless.maven; + +import java.io.File; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.GitPrePushHookInstaller.GitPreHookLogger; +import com.diffplug.spotless.GitPrePushHookInstallerMaven; + +/** + * A Maven Mojo responsible for installing a Git pre-push hook for the Spotless plugin. + * This hook ensures that Spotless formatting rules are automatically checked and applied + * before performing a Git push operation. + * + *

The class leverages {@link GitPrePushHookInstallerMaven} to perform the installation process + * and uses a Maven logger to log installation events and errors to the console. + */ +@Mojo(name = AbstractSpotlessMojo.GOAL_PRE_PUSH_HOOK, threadSafe = true) +public class SpotlessInstallPrePushHookMojo extends AbstractMojo { + + /** + * The base directory of the Maven project where the Git pre-push hook will be installed. + * This parameter is automatically set to the root directory of the current project. + */ + @Parameter(defaultValue = "${project.basedir}", readonly = true, required = true) + private File baseDir; + + /** + * Executes the Mojo, installing the Git pre-push hook for the Spotless plugin. + * + *

This method creates an instance of {@link GitPrePushHookInstallerMaven}, + * providing a logger for logging the process of hook installation and any potential errors. + * The installation process runs in the root directory of the current Maven project. + * + * @throws MojoExecutionException if an error occurs during the installation process. + * @throws MojoFailureException if the hook fails to install for any reason. + */ + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + final var logger = new GitPreHookLogger() { + @Override + public void info(String format, Object... arguments) { + getLog().info(String.format(format, arguments)); + } + + @Override + public void error(String format, Object... arguments) { + getLog().error(String.format(format, arguments)); + } + }; + + try { + final var installer = new GitPrePushHookInstallerMaven(logger, baseDir); + installer.install(); + } catch (Exception e) { + throw new MojoExecutionException("Unable to install pre-push hook", e); + } + } +} diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojoTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojoTest.java new file mode 100644 index 0000000000..ad6faf823b --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojoTest.java @@ -0,0 +1,69 @@ +package com.diffplug.spotless.maven; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class SpotlessInstallPrePushHookMojoTest extends MavenIntegrationHarness { + + @Test + public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws Exception { + // given + setFile(".git/config").toContent(""); + setFile("license.txt").toResource("license/TestLicense"); + writePomWithJavaLicenseHeaderStep(); + + // when + final var output = mavenRunner() + .withArguments("spotless:install-git-pre-push-hook") + .runNoError() + .stdOutUtf8(); + + // then + assertThat(output).contains("Installing git pre-push hook"); + assertThat(output).contains("Git pre-push hook not found, creating it"); + assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push")); + + + final var content = getTestResource("git_pre_hook/pre-push.created") + .replace("${executor}", "mvn") + .replace("${checkCommand}", "spotless:check") + .replace("${applyCommand}", "spotless:apply"); + assertFile(".git/hooks/pre-push").hasContent(content); + } + + @Test + public void should_append_to_existing_pre_hook_file_when_hook_file_exists() throws Exception { + // given + setFile(".git/config").toContent(""); + setFile("license.txt").toResource("license/TestLicense"); + setFile(".git/hooks/pre-push").toResource("git_pre_hook/pre-push.existing"); + + writePomWithJavaLicenseHeaderStep(); + + // when + final var output = mavenRunner() + .withArguments("spotless:install-git-pre-push-hook") + .runNoError() + .stdOutUtf8(); + + // then + assertThat(output).contains("Installing git pre-push hook"); + assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push")); + + final var content = getTestResource("git_pre_hook/pre-push.existing-added") + .replace("${executor}", "mvn") + .replace("${checkCommand}", "spotless:check") + .replace("${applyCommand}", "spotless:apply"); + assertFile(".git/hooks/pre-push").hasContent(content); + } + + private void writePomWithJavaLicenseHeaderStep() throws IOException { + writePomWithJavaSteps( + "", + " ${basedir}/license.txt", + ""); + } +} diff --git a/testlib/src/main/resources/git_pre_hook/pre-push.created b/testlib/src/main/resources/git_pre_hook/pre-push.created new file mode 100644 index 0000000000..376598c605 --- /dev/null +++ b/testlib/src/main/resources/git_pre_hook/pre-push.created @@ -0,0 +1,12 @@ +#!/bin/sh + + +##### SPOTLESS HOOK START ##### +SPOTLESS_EXECUTOR=${executor} +if ! $SPOTLESS_EXECUTOR ${checkCommand} ; then + echo 1>&2 "spotless found problems, running ${applyCommand}; commit the result and re-push" + $SPOTLESS_EXECUTOR ${applyCommand} + exit 1 +fi +##### SPOTLESS HOOK END ##### + diff --git a/testlib/src/main/resources/git_pre_hook/pre-push.existing b/testlib/src/main/resources/git_pre_hook/pre-push.existing new file mode 100644 index 0000000000..a46210c6c1 --- /dev/null +++ b/testlib/src/main/resources/git_pre_hook/pre-push.existing @@ -0,0 +1,51 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done diff --git a/testlib/src/main/resources/git_pre_hook/pre-push.existing-added b/testlib/src/main/resources/git_pre_hook/pre-push.existing-added new file mode 100644 index 0000000000..fd107efaec --- /dev/null +++ b/testlib/src/main/resources/git_pre_hook/pre-push.existing-added @@ -0,0 +1,62 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + + +##### SPOTLESS HOOK START ##### +SPOTLESS_EXECUTOR=${executor} +if ! $SPOTLESS_EXECUTOR ${checkCommand} ; then + echo 1>&2 "spotless found problems, running ${applyCommand}; commit the result and re-push" + $SPOTLESS_EXECUTOR ${applyCommand} + exit 1 +fi +##### SPOTLESS HOOK END ##### + diff --git a/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java b/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java new file mode 100644 index 0000000000..78ed927ed0 --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java @@ -0,0 +1,148 @@ +package com.diffplug.spotless; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.GitPrePushHookInstaller.GitPreHookLogger; + +class GitPrePushHookInstallerTest extends ResourceHarness { + private final List logs = new ArrayList<>(); + private final GitPreHookLogger logger = new GitPreHookLogger() { + @Override + public void info(String format, Object... arguments) { + logs.add(String.format(format, arguments)); + } + + @Override + public void error(String format, Object... arguments) { + logs.add(String.format(format, arguments)); + } + }; + + @Test + public void should_not_create_pre_hook_file_when_git_is_not_installed() throws Exception { + // given + final var gradle = new GitPrePushHookInstallerGradle(logger, rootFolder()); + + // when + gradle.install(); + + // then + assertThat(logs).hasSize(2); + assertThat(logs).element(0).isEqualTo("Installing git pre-push hook"); + assertThat(logs).element(1).isEqualTo("Git not found in root directory"); + assertThat(newFile(".git/hooks/pre-push")).doesNotExist(); + } + + @Test + public void should_not_create_pre_hook_file_when_gradlew_is_not_installed() throws Exception { + // given + final var gradle = new GitPrePushHookInstallerGradle(logger, rootFolder()); + setFile(".git/config").toContent(""); + + // when + gradle.install(); + + // then + assertThat(logs).hasSize(2); + assertThat(logs).element(0).isEqualTo("Installing git pre-push hook"); + assertThat(logs).element(1).isEqualTo("Failed to find gradlew in root directory"); + assertThat(newFile(".git/hooks/pre-push")).doesNotExist(); + } + + @Test + public void should_not_create_pre_hook_file_when_hook_already_installed() throws Exception { + // given + final var gradle = new GitPrePushHookInstallerGradle(logger, rootFolder()); + final var hookFile = setFile(".git/hooks/pre-push").toResource("git_pre_hook/pre-push.existing-added"); + + setFile("gradlew").toContent(""); + setFile(".git/config").toContent(""); + + // when + gradle.install(); + + // then + assertThat(logs).hasSize(2); + assertThat(logs).element(0).isEqualTo("Installing git pre-push hook"); + assertThat(logs).element(1).isEqualTo("Skipping, git pre-push hook already installed " + hookFile.getAbsolutePath()); + assertThat(hookFile).content().isEqualTo(getTestResource("git_pre_hook/pre-push.existing-added")); + } + + @Test + public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws Exception { + // given + final var gradle = new GitPrePushHookInstallerGradle(logger, rootFolder()); + final var gradlew = setFile("gradlew").toContent(""); + setFile(".git/config").toContent(""); + + // when + gradle.install(); + + // then + assertThat(logs).hasSize(3); + assertThat(logs).element(0).isEqualTo("Installing git pre-push hook"); + assertThat(logs).element(1).isEqualTo("Git pre-push hook not found, creating it"); + assertThat(logs).element(2).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath()); + + final var content = gradleHookContent("git_pre_hook/pre-push.created"); + assertFile(".git/hooks/pre-push").hasContent(content); + } + + @Test + public void should_append_to_existing_pre_hook_file_when_hook_file_exists() throws Exception { + // given + final var gradle = new GitPrePushHookInstallerGradle(logger, rootFolder()); + final var gradlew = setFile("gradlew").toContent(""); + setFile(".git/config").toContent(""); + setFile(".git/hooks/pre-push").toResource("git_pre_hook/pre-push.existing"); + + // when + gradle.install(); + + // then + assertThat(logs).hasSize(2); + assertThat(logs).element(0).isEqualTo("Installing git pre-push hook"); + assertThat(logs).element(1).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath()); + + final var content = gradleHookContent("git_pre_hook/pre-push.existing-added"); + assertFile(".git/hooks/pre-push").hasContent(content); + } + + @Test + public void should_create_pre_hook_file_for_maven_when_hook_file_does_not_exists() throws Exception { + // given + final var gradle = new GitPrePushHookInstallerMaven(logger, rootFolder()); + setFile(".git/config").toContent(""); + + // when + gradle.install(); + + // then + assertThat(logs).hasSize(3); + assertThat(logs).element(0).isEqualTo("Installing git pre-push hook"); + assertThat(logs).element(1).isEqualTo("Git pre-push hook not found, creating it"); + assertThat(logs).element(2).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath()); + + final var content = mavenHookContent("git_pre_hook/pre-push.created"); + assertFile(".git/hooks/pre-push").hasContent(content); + } + + private String gradleHookContent(String resourcePath) { + return getTestResource(resourcePath) + .replace("${executor}", setFile("gradlew").toContent("").getAbsolutePath()) + .replace("${checkCommand}", "spotlessCheck") + .replace("${applyCommand}", "spotlessApply"); + } + + private String mavenHookContent(String resourcePath) { + return getTestResource(resourcePath) + .replace("${executor}", "mvn") + .replace("${checkCommand}", "spotless:check") + .replace("${applyCommand}", "spotless:apply"); + } +} From 6528b0bbba29d130fcdcd1432eeb6b1f928b6493 Mon Sep 17 00:00:00 2001 From: Alex Danylenko Date: Fri, 11 Jul 2025 23:56:21 -0700 Subject: [PATCH 2/7] spotlessApply --- .../spotless/GitPrePushHookInstaller.java | 22 ++++++- .../GitPrePushHookInstallerGradle.java | 15 +++++ .../GitPrePushHookInstallerMaven.java | 15 +++++ .../gradle/spotless/SpotlessExtension.java | 2 +- .../spotless/SpotlessExtensionImpl.java | 2 +- .../SpotlessInstallPrePushHookTask.java | 15 +++++ .../SpotlessInstallPrePushHookTaskTest.java | 62 ++++++++++++------- .../spotless/maven/AbstractSpotlessMojo.java | 2 +- .../maven/SpotlessInstallPrePushHookMojo.java | 15 +++++ .../SpotlessInstallPrePushHookMojoTest.java | 50 +++++++++------ .../spotless/GitPrePushHookInstallerTest.java | 33 +++++++--- 11 files changed, 178 insertions(+), 55 deletions(-) diff --git a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java index a317689b35..f8f6a19a88 100644 --- a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java +++ b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java @@ -1,3 +1,18 @@ +/* + * 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; @@ -66,7 +81,11 @@ public void install() throws Exception { final var gitHookFile = root.toPath().resolve(".git/hooks/pre-push").toFile(); if (!gitHookFile.exists()) { logger.info("Git pre-push hook not found, creating it"); - gitHookFile.getParentFile().mkdirs(); + 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; @@ -164,6 +183,7 @@ protected String preHookTemplate(String executor, String commandCheck, String co public interface GitPreHookLogger { void info(String format, Object... arguments); + void error(String format, Object... arguments); } } diff --git a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java index 8c4817c54c..26ba4b5442 100644 --- a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java +++ b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerMaven.java b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerMaven.java index c54b2e4d02..1148c07267 100644 --- a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerMaven.java +++ b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerMaven.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java index 885d697688..92079d67c4 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java @@ -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. diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java index 45d7ffade7..53b7b011a1 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java @@ -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. diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTask.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTask.java index df6b9bfc8e..2b8d19264b 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTask.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTask.java @@ -1,3 +1,18 @@ +/* + * 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.gradle.spotless; import org.gradle.api.DefaultTask; diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java index 1f74e14fc3..9146a773a2 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java @@ -1,3 +1,18 @@ +/* + * 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.gradle.spotless; import static org.assertj.core.api.Assertions.assertThat; @@ -11,19 +26,19 @@ public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws // given final var gradlew = setFile("gradlew").toContent(""); setFile(".git/config").toContent(""); + newFile(".git/hooks").mkdirs(); setFile("build.gradle").toLines( - "plugins {", - " id 'java'", - " id 'com.diffplug.spotless'", - "}", - "repositories { mavenCentral() }" - ); + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }"); // when var output = gradleRunner() - .withArguments("spotlessInstallGitPrePushHook") - .build() - .getOutput(); + .withArguments("spotlessInstallGitPrePushHook") + .build() + .getOutput(); // then assertThat(output).contains("Installing git pre-push hook"); @@ -31,9 +46,9 @@ public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push")); final var content = getTestResource("git_pre_hook/pre-push.created") - .replace("${executor}", gradlew.getAbsolutePath()) - .replace("${checkCommand}", "spotlessCheck") - .replace("${applyCommand}", "spotlessApply"); + .replace("${executor}", gradlew.getAbsolutePath()) + .replace("${checkCommand}", "spotlessCheck") + .replace("${applyCommand}", "spotlessApply"); assertFile(".git/hooks/pre-push").hasContent(content); } @@ -43,28 +58,27 @@ public void should_append_to_existing_pre_hook_file_when_hook_file_exists() thro final var gradlew = setFile("gradlew").toContent(""); setFile(".git/config").toContent(""); setFile("build.gradle").toLines( - "plugins {", - " id 'java'", - " id 'com.diffplug.spotless'", - "}", - "repositories { mavenCentral() }" - ); + "plugins {", + " id 'java'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }"); setFile(".git/hooks/pre-push").toResource("git_pre_hook/pre-push.existing"); // when final var output = gradleRunner() - .withArguments("spotlessInstallGitPrePushHook") - .build() - .getOutput(); + .withArguments("spotlessInstallGitPrePushHook") + .build() + .getOutput(); // then assertThat(output).contains("Installing git pre-push hook"); assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push")); final var content = getTestResource("git_pre_hook/pre-push.existing-added") - .replace("${executor}", gradlew.getAbsolutePath()) - .replace("${checkCommand}", "spotlessCheck") - .replace("${applyCommand}", "spotlessApply"); + .replace("${executor}", gradlew.getAbsolutePath()) + .replace("${checkCommand}", "spotlessCheck") + .replace("${applyCommand}", "spotlessApply"); assertFile(".git/hooks/pre-push").hasContent(content); } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java index 5a21a7258a..e5a8004f97 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java @@ -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. diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojo.java index 356a52e913..691ddd4969 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojo.java @@ -1,3 +1,18 @@ +/* + * 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.maven; import java.io.File; diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojoTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojoTest.java index ad6faf823b..b9d5d6a343 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojoTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojoTest.java @@ -1,11 +1,26 @@ +/* + * 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.maven; +import static org.assertj.core.api.Assertions.assertThat; + import java.io.IOException; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; - class SpotlessInstallPrePushHookMojoTest extends MavenIntegrationHarness { @Test @@ -17,20 +32,19 @@ public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws // when final var output = mavenRunner() - .withArguments("spotless:install-git-pre-push-hook") - .runNoError() - .stdOutUtf8(); + .withArguments("spotless:install-git-pre-push-hook") + .runNoError() + .stdOutUtf8(); // then assertThat(output).contains("Installing git pre-push hook"); assertThat(output).contains("Git pre-push hook not found, creating it"); assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push")); - final var content = getTestResource("git_pre_hook/pre-push.created") - .replace("${executor}", "mvn") - .replace("${checkCommand}", "spotless:check") - .replace("${applyCommand}", "spotless:apply"); + .replace("${executor}", "mvn") + .replace("${checkCommand}", "spotless:check") + .replace("${applyCommand}", "spotless:apply"); assertFile(".git/hooks/pre-push").hasContent(content); } @@ -45,25 +59,25 @@ public void should_append_to_existing_pre_hook_file_when_hook_file_exists() thro // when final var output = mavenRunner() - .withArguments("spotless:install-git-pre-push-hook") - .runNoError() - .stdOutUtf8(); + .withArguments("spotless:install-git-pre-push-hook") + .runNoError() + .stdOutUtf8(); // then assertThat(output).contains("Installing git pre-push hook"); assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push")); final var content = getTestResource("git_pre_hook/pre-push.existing-added") - .replace("${executor}", "mvn") - .replace("${checkCommand}", "spotless:check") - .replace("${applyCommand}", "spotless:apply"); + .replace("${executor}", "mvn") + .replace("${checkCommand}", "spotless:check") + .replace("${applyCommand}", "spotless:apply"); assertFile(".git/hooks/pre-push").hasContent(content); } private void writePomWithJavaLicenseHeaderStep() throws IOException { writePomWithJavaSteps( - "", - " ${basedir}/license.txt", - ""); + "", + " ${basedir}/license.txt", + ""); } } diff --git a/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java b/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java index 78ed927ed0..4fd9eed749 100644 --- a/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java @@ -1,3 +1,18 @@ +/* + * 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 org.assertj.core.api.Assertions.assertThat; @@ -25,13 +40,13 @@ public void error(String format, Object... arguments) { @Test public void should_not_create_pre_hook_file_when_git_is_not_installed() throws Exception { - // given + // given final var gradle = new GitPrePushHookInstallerGradle(logger, rootFolder()); - // when + // when gradle.install(); - // then + // then assertThat(logs).hasSize(2); assertThat(logs).element(0).isEqualTo("Installing git pre-push hook"); assertThat(logs).element(1).isEqualTo("Git not found in root directory"); @@ -134,15 +149,15 @@ public void should_create_pre_hook_file_for_maven_when_hook_file_does_not_exists private String gradleHookContent(String resourcePath) { return getTestResource(resourcePath) - .replace("${executor}", setFile("gradlew").toContent("").getAbsolutePath()) - .replace("${checkCommand}", "spotlessCheck") - .replace("${applyCommand}", "spotlessApply"); + .replace("${executor}", setFile("gradlew").toContent("").getAbsolutePath()) + .replace("${checkCommand}", "spotlessCheck") + .replace("${applyCommand}", "spotlessApply"); } private String mavenHookContent(String resourcePath) { return getTestResource(resourcePath) - .replace("${executor}", "mvn") - .replace("${checkCommand}", "spotless:check") - .replace("${applyCommand}", "spotless:apply"); + .replace("${executor}", "mvn") + .replace("${checkCommand}", "spotless:check") + .replace("${applyCommand}", "spotless:apply"); } } From b9f488017e4902cf98f37d074f9380fac54df807 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 17 Jul 2025 09:53:18 -0700 Subject: [PATCH 3/7] Placeholders for docs. --- plugin-gradle/README.md | 5 +++++ plugin-maven/README.md | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 1fa7519a8c..6e6e648ed6 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -53,6 +53,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui - [**Quickstart**](#quickstart) - [Requirements](#requirements) + - [Git hook](#git-hook) - [Linting](#linting) - **Languages** - [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [clang-format](#clang-format), [prettier](#prettier), [palantir-java-format](#palantir-java-format), [formatAnnotations](#formatAnnotations), [cleanthat](#cleanthat), [IntelliJ IDEA](#intellij-idea)) @@ -141,6 +142,10 @@ Spotless requires JRE 11+ and Gradle 6.1.1 or newer. - If you're stuck on JRE 8, use [`id 'com.diffplug.spotless' version '6.13.0'` or older](https://github.com/diffplug/spotless/blob/main/plugin-gradle/CHANGES.md#6130---2023-01-14). - If you're stuck on an older version of Gradle, [`id 'com.diffplug.gradle.spotless' version '4.5.1'` supports all the way back to Gradle 2.x](https://github.com/diffplug/spotless/blob/main/plugin-gradle/CHANGES.md#451---2020-07-04). +### Git hook + +TODO + ### Linting Starting in version `7.0.0`, Spotless now supports linting in addition to formatting. To Spotless, all lints are errors which must be either fixed or suppressed. Lints show up like this: diff --git a/plugin-maven/README.md b/plugin-maven/README.md index f633ca5635..8cd01731ba 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -37,6 +37,7 @@ user@machine repo % mvn spotless:check - [**Quickstart**](#quickstart) - [Requirements](#requirements) + - [Git hook](#git-hook) - [Binding to maven phase](#binding-to-maven-phase) - **Languages** - [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [prettier](#prettier), [palantir-java-format](#palantir-java-format), [formatAnnotations](#formatAnnotations), [cleanthat](#cleanthat), [IntelliJ IDEA](#intellij-idea)) @@ -145,7 +146,10 @@ Spotless consists of a list of formats (in the example above, `misc` and `java`) Spotless requires Maven to be running on JRE 11+. To use JRE 8, go back to [`2.30.0` or older](https://github.com/diffplug/spotless/blob/main/plugin-maven/CHANGES.md#2300---2023-01-13). - + +### Git hook + +TODO ### Binding to maven phase @@ -176,6 +180,8 @@ any other maven phase (i.e. compile) then it can be configured as below; ``` + + ## Java [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Java.java). [available steps](https://github.com/diffplug/spotless/tree/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/java). From a790659b8936a782ca5eae3fe2b7ea721e9f4764 Mon Sep 17 00:00:00 2001 From: Alex Danylenko Date: Thu, 17 Jul 2025 21:45:19 -0700 Subject: [PATCH 4/7] maven wrapper support --- .../spotless/GitPrePushHookInstaller.java | 29 ++++------ .../GitPrePushHookInstallerGradle.java | 24 ++++---- .../GitPrePushHookInstallerMaven.java | 24 ++++---- .../SpotlessInstallPrePushHookTask.java | 5 ++ .../SpotlessInstallPrePushHookTaskTest.java | 6 +- .../maven/SpotlessInstallPrePushHookMojo.java | 5 ++ .../spotless/GitPrePushHookInstallerTest.java | 56 ++++++++++++++----- 7 files changed, 89 insertions(+), 60 deletions(-) diff --git a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java index f8f6a19a88..9c317d94ab 100644 --- a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java +++ b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java @@ -16,6 +16,7 @@ package com.diffplug.spotless; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; import java.io.File; import java.io.FileWriter; @@ -30,6 +31,9 @@ */ public abstract class GitPrePushHookInstaller { + private static final String HOOK_HEADLINE = "##### SPOTLESS HOOK START #####"; + private static final String HOOK_FOOTER = "##### SPOTLESS HOOK END #####"; + /** * Logger for recording informational and error messages during the installation process. */ @@ -47,8 +51,8 @@ public abstract class GitPrePushHookInstaller { * @param root The root directory of the Git repository. */ public GitPrePushHookInstaller(GitPreHookLogger logger, File root) { - this.logger = logger; - this.root = root; + this.logger = requireNonNull(logger, "logger can not be null"); + this.root = requireNonNull(root, "root file can not be null"); } /** @@ -73,10 +77,6 @@ public void install() throws Exception { return; } - if (!isExecutorInstalled()) { - return; - } - var hookContent = ""; final var gitHookFile = root.toPath().resolve(".git/hooks/pre-push").toFile(); if (!gitHookFile.exists()) { @@ -100,7 +100,7 @@ public void install() throws Exception { } if (isGitHookInstalled(gitHookFile)) { - logger.info("Skipping, git pre-push hook already installed %s", gitHookFile.getAbsolutePath()); + logger.warn("Skipping, git pre-push hook already installed %s", gitHookFile.getAbsolutePath()); return; } @@ -110,13 +110,6 @@ public void install() throws Exception { 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. * @@ -142,7 +135,7 @@ private boolean isGitInstalled() { */ private boolean isGitHookInstalled(File gitHookFile) throws Exception { final var hook = Files.readString(gitHookFile.toPath(), UTF_8); - return hook.contains("##### SPOTLESS HOOK START #####"); + return hook.contains(HOOK_HEADLINE); } /** @@ -169,21 +162,21 @@ private void writeFile(File file, String content) throws IOException { */ protected String preHookTemplate(String executor, String commandCheck, String commandApply) { var spotlessHook = "\n"; - spotlessHook += "\n##### SPOTLESS HOOK START #####"; + spotlessHook += "\n" + HOOK_HEADLINE; 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" + HOOK_FOOTER; spotlessHook += "\n\n"; return spotlessHook; } public interface GitPreHookLogger { void info(String format, Object... arguments); - + void warn(String format, Object... arguments); void error(String format, Object... arguments); } } diff --git a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java index 26ba4b5442..8ae6e6cb94 100644 --- a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java +++ b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java @@ -33,25 +33,21 @@ public GitPrePushHookInstallerGradle(GitPreHookLogger logger, File 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. + * {@inheritDoc} */ @Override - protected boolean isExecutorInstalled() { + protected String preHookContent() { + return preHookTemplate(executor(), "spotlessCheck", "spotlessApply"); + } + + private String executor() { if (gradlew.exists()) { - return true; + return gradlew.getAbsolutePath(); } - logger.error("Failed to find gradlew in root directory"); - return false; - } - - @Override - protected String preHookContent() { - return preHookTemplate(gradlew.getAbsolutePath(), "spotlessCheck", "spotlessApply"); + logger.info("Gradle wrapper is not installed, using global gradle"); + return "gradle"; } } diff --git a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerMaven.java b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerMaven.java index 1148c07267..7f84d85429 100644 --- a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerMaven.java +++ b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerMaven.java @@ -23,25 +23,27 @@ */ public class GitPrePushHookInstallerMaven extends GitPrePushHookInstaller { + private final File mvnw; + public GitPrePushHookInstallerMaven(GitPreHookLogger logger, File root) { super(logger, root); + this.mvnw = root.toPath().resolve("mvnw").toFile(); } /** - * Confirms that Maven is installed and available for use. - * - *

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. + * {@inheritDoc} */ @Override - protected boolean isExecutorInstalled() { - return true; + protected String preHookContent() { + return preHookTemplate(executor(), "spotless:check", "spotless:apply"); } - @Override - protected String preHookContent() { - return preHookTemplate("mvn", "spotless:check", "spotless:apply"); + private String executor() { + if (mvnw.exists()) { + return mvnw.getAbsolutePath(); + } + + logger.info("Maven wrapper is not installed, using global maven"); + return "mvn"; } } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTask.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTask.java index 2b8d19264b..3e59e94b0a 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTask.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTask.java @@ -49,6 +49,11 @@ public void info(String format, Object... arguments) { getLogger().lifecycle(String.format(format, arguments)); } + @Override + public void warn(String format, Object... arguments) { + getLogger().warn(String.format(format, arguments)); + } + @Override public void error(String format, Object... arguments) { getLogger().error(String.format(format, arguments)); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java index 9146a773a2..140bb8a755 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java @@ -24,7 +24,6 @@ class SpotlessInstallPrePushHookTaskTest extends GradleIntegrationHarness { @Test public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws Exception { // given - final var gradlew = setFile("gradlew").toContent(""); setFile(".git/config").toContent(""); newFile(".git/hooks").mkdirs(); setFile("build.gradle").toLines( @@ -46,7 +45,7 @@ public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push")); final var content = getTestResource("git_pre_hook/pre-push.created") - .replace("${executor}", gradlew.getAbsolutePath()) + .replace("${executor}", "gradle") .replace("${checkCommand}", "spotlessCheck") .replace("${applyCommand}", "spotlessApply"); assertFile(".git/hooks/pre-push").hasContent(content); @@ -55,7 +54,6 @@ public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws @Test public void should_append_to_existing_pre_hook_file_when_hook_file_exists() throws Exception { // given - final var gradlew = setFile("gradlew").toContent(""); setFile(".git/config").toContent(""); setFile("build.gradle").toLines( "plugins {", @@ -76,7 +74,7 @@ public void should_append_to_existing_pre_hook_file_when_hook_file_exists() thro assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push")); final var content = getTestResource("git_pre_hook/pre-push.existing-added") - .replace("${executor}", gradlew.getAbsolutePath()) + .replace("${executor}", "gradle") .replace("${checkCommand}", "spotlessCheck") .replace("${applyCommand}", "spotlessApply"); assertFile(".git/hooks/pre-push").hasContent(content); diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojo.java index 691ddd4969..984724a688 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojo.java @@ -62,6 +62,11 @@ public void info(String format, Object... arguments) { getLog().info(String.format(format, arguments)); } + @Override + public void warn(String format, Object... arguments) { + getLog().warn(String.format(format, arguments)); + } + @Override public void error(String format, Object... arguments) { getLog().error(String.format(format, arguments)); diff --git a/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java b/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java index 4fd9eed749..b6fbcd26e8 100644 --- a/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java @@ -32,6 +32,11 @@ public void info(String format, Object... arguments) { logs.add(String.format(format, arguments)); } + @Override + public void warn(String format, Object... arguments) { + logs.add(String.format(format, arguments)); + } + @Override public void error(String format, Object... arguments) { logs.add(String.format(format, arguments)); @@ -54,7 +59,7 @@ public void should_not_create_pre_hook_file_when_git_is_not_installed() throws E } @Test - public void should_not_create_pre_hook_file_when_gradlew_is_not_installed() throws Exception { + public void should_use_global_gradle_when_gradlew_is_not_installed() throws Exception { // given final var gradle = new GitPrePushHookInstallerGradle(logger, rootFolder()); setFile(".git/config").toContent(""); @@ -63,10 +68,14 @@ public void should_not_create_pre_hook_file_when_gradlew_is_not_installed() thro gradle.install(); // then - assertThat(logs).hasSize(2); + assertThat(logs).hasSize(4); assertThat(logs).element(0).isEqualTo("Installing git pre-push hook"); - assertThat(logs).element(1).isEqualTo("Failed to find gradlew in root directory"); - assertThat(newFile(".git/hooks/pre-push")).doesNotExist(); + assertThat(logs).element(1).isEqualTo("Git pre-push hook not found, creating it"); + assertThat(logs).element(2).isEqualTo("Gradle wrapper is not installed, using global gradle"); + assertThat(logs).element(3).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath()); + + final var content = gradleHookContent("git_pre_hook/pre-push.created", false); + assertFile(".git/hooks/pre-push").hasContent(content); } @Test @@ -92,7 +101,7 @@ public void should_not_create_pre_hook_file_when_hook_already_installed() throws public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws Exception { // given final var gradle = new GitPrePushHookInstallerGradle(logger, rootFolder()); - final var gradlew = setFile("gradlew").toContent(""); + setFile("gradlew").toContent(""); setFile(".git/config").toContent(""); // when @@ -104,7 +113,7 @@ public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws assertThat(logs).element(1).isEqualTo("Git pre-push hook not found, creating it"); assertThat(logs).element(2).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath()); - final var content = gradleHookContent("git_pre_hook/pre-push.created"); + final var content = gradleHookContent("git_pre_hook/pre-push.created", true); assertFile(".git/hooks/pre-push").hasContent(content); } @@ -112,7 +121,7 @@ public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws public void should_append_to_existing_pre_hook_file_when_hook_file_exists() throws Exception { // given final var gradle = new GitPrePushHookInstallerGradle(logger, rootFolder()); - final var gradlew = setFile("gradlew").toContent(""); + setFile("gradlew").toContent(""); setFile(".git/config").toContent(""); setFile(".git/hooks/pre-push").toResource("git_pre_hook/pre-push.existing"); @@ -124,7 +133,7 @@ public void should_append_to_existing_pre_hook_file_when_hook_file_exists() thro assertThat(logs).element(0).isEqualTo("Installing git pre-push hook"); assertThat(logs).element(1).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath()); - final var content = gradleHookContent("git_pre_hook/pre-push.existing-added"); + final var content = gradleHookContent("git_pre_hook/pre-push.existing-added", true); assertFile(".git/hooks/pre-push").hasContent(content); } @@ -132,6 +141,7 @@ public void should_append_to_existing_pre_hook_file_when_hook_file_exists() thro public void should_create_pre_hook_file_for_maven_when_hook_file_does_not_exists() throws Exception { // given final var gradle = new GitPrePushHookInstallerMaven(logger, rootFolder()); + setFile("mvnw").toContent(""); setFile(".git/config").toContent(""); // when @@ -143,20 +153,40 @@ public void should_create_pre_hook_file_for_maven_when_hook_file_does_not_exists assertThat(logs).element(1).isEqualTo("Git pre-push hook not found, creating it"); assertThat(logs).element(2).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath()); - final var content = mavenHookContent("git_pre_hook/pre-push.created"); + final var content = mavenHookContent("git_pre_hook/pre-push.created", true); + assertFile(".git/hooks/pre-push").hasContent(content); + } + + @Test + public void should_use_global_maven_when_maven_wrapper_is_not_installed() throws Exception { + // given + final var gradle = new GitPrePushHookInstallerMaven(logger, rootFolder()); + setFile(".git/config").toContent(""); + + // when + gradle.install(); + + // then + assertThat(logs).hasSize(4); + assertThat(logs).element(0).isEqualTo("Installing git pre-push hook"); + assertThat(logs).element(1).isEqualTo("Git pre-push hook not found, creating it"); + assertThat(logs).element(2).isEqualTo("Maven wrapper is not installed, using global maven"); + assertThat(logs).element(3).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath()); + + final var content = mavenHookContent("git_pre_hook/pre-push.created", false); assertFile(".git/hooks/pre-push").hasContent(content); } - private String gradleHookContent(String resourcePath) { + private String gradleHookContent(String resourcePath, boolean isWrapper) { return getTestResource(resourcePath) - .replace("${executor}", setFile("gradlew").toContent("").getAbsolutePath()) + .replace("${executor}", isWrapper ? newFile("gradlew").getAbsolutePath() : "gradle") .replace("${checkCommand}", "spotlessCheck") .replace("${applyCommand}", "spotlessApply"); } - private String mavenHookContent(String resourcePath) { + private String mavenHookContent(String resourcePath, boolean isWrapper) { return getTestResource(resourcePath) - .replace("${executor}", "mvn") + .replace("${executor}", isWrapper ? newFile("mvnw").getAbsolutePath() : "mvn") .replace("${checkCommand}", "spotless:check") .replace("${applyCommand}", "spotless:apply"); } From 525e70eb8751e432e527cc23434f0c009fa4ae90 Mon Sep 17 00:00:00 2001 From: Alex Danylenko Date: Thu, 17 Jul 2025 22:44:46 -0700 Subject: [PATCH 5/7] reinstall support --- .../spotless/GitPrePushHookInstaller.java | 86 +++++++++++++------ .../GitPrePushHookInstallerGradle.java | 1 - plugin-gradle/README.md | 31 ++++++- .../SpotlessInstallPrePushHookTaskTest.java | 4 +- plugin-maven/README.md | 31 ++++++- .../SpotlessInstallPrePushHookMojoTest.java | 8 +- ...{pre-push.created => pre-push.created-tpl} | 1 - ...ting-added => pre-push.existing-added-tpl} | 1 - .../git_pre_hook/pre-push.reinstalled-tpl | 64 ++++++++++++++ .../spotless/GitPrePushHookInstallerTest.java | 36 +++++--- 10 files changed, 206 insertions(+), 57 deletions(-) rename testlib/src/main/resources/git_pre_hook/{pre-push.created => pre-push.created-tpl} (99%) rename testlib/src/main/resources/git_pre_hook/{pre-push.existing-added => pre-push.existing-added-tpl} (99%) create mode 100644 testlib/src/main/resources/git_pre_hook/pre-push.reinstalled-tpl diff --git a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java index 9c317d94ab..b5070a003b 100644 --- a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java +++ b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java @@ -100,16 +100,44 @@ public void install() throws Exception { } if (isGitHookInstalled(gitHookFile)) { - logger.warn("Skipping, git pre-push hook already installed %s", gitHookFile.getAbsolutePath()); - return; + logger.info("Git pre-push hook already installed, reinstalling it"); + uninstall(gitHookFile); } hookContent += preHookContent(); - writeFile(gitHookFile, hookContent); + writeFile(gitHookFile, hookContent, true); logger.info("Git pre-push hook installed successfully to the file %s", gitHookFile.getAbsolutePath()); } + /** + * Uninstalls the Spotless Git pre-push hook from the specified hook file by removing + * the custom hook content between the defined hook markers. + * + *

This method: + *

+ * + * @param gitHookFile The Git pre-push hook file from which to remove the Spotless hook + * @throws Exception if any error occurs during the uninstallation process, + * such as file reading or writing errors + */ + private void uninstall(File gitHookFile) throws Exception { + final var hook = Files.readString(gitHookFile.toPath(), UTF_8); + final var hookStart = hook.indexOf(HOOK_HEADLINE); + final var hookEnd = hook.indexOf(HOOK_FOOTER) + HOOK_FOOTER.length(); + + final var hookScript = hook.substring(hookStart, hookEnd); + + final var uninstalledHook = hook.replace(hookScript, ""); + + writeFile(gitHookFile, uninstalledHook, false); + } + /** * Provides the content of the hook that should be inserted into the pre-push script. * @@ -117,6 +145,29 @@ public void install() throws Exception { */ protected abstract String preHookContent(); + /** + * 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" + HOOK_HEADLINE; + 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" + HOOK_FOOTER; + spotlessHook += "\n"; + return spotlessHook; + } + /** * Checks if Git is installed by validating the existence of `.git/config` in the repository root. * @@ -145,38 +196,17 @@ private boolean isGitHookInstalled(File gitHookFile) throws Exception { * @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)) { + private void writeFile(File file, String content, boolean append) throws IOException { + try (final var writer = new FileWriter(file, UTF_8, append)) { 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" + HOOK_HEADLINE; - 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" + HOOK_FOOTER; - spotlessHook += "\n\n"; - return spotlessHook; - } - public interface GitPreHookLogger { void info(String format, Object... arguments); + void warn(String format, Object... arguments); + void error(String format, Object... arguments); } } diff --git a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java index 8ae6e6cb94..cfed7cdd58 100644 --- a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java +++ b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java @@ -33,7 +33,6 @@ public GitPrePushHookInstallerGradle(GitPreHookLogger logger, File root) { this.gradlew = root.toPath().resolve("gradlew").toFile(); } - /** * {@inheritDoc} */ diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 6e6e648ed6..75c7c885c9 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -53,7 +53,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui - [**Quickstart**](#quickstart) - [Requirements](#requirements) - - [Git hook](#git-hook) + - [Git pre-push hook](#git-pre-push-hook) - [Linting](#linting) - **Languages** - [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [clang-format](#clang-format), [prettier](#prettier), [palantir-java-format](#palantir-java-format), [formatAnnotations](#formatAnnotations), [cleanthat](#cleanthat), [IntelliJ IDEA](#intellij-idea)) @@ -142,9 +142,34 @@ Spotless requires JRE 11+ and Gradle 6.1.1 or newer. - If you're stuck on JRE 8, use [`id 'com.diffplug.spotless' version '6.13.0'` or older](https://github.com/diffplug/spotless/blob/main/plugin-gradle/CHANGES.md#6130---2023-01-14). - If you're stuck on an older version of Gradle, [`id 'com.diffplug.gradle.spotless' version '4.5.1'` supports all the way back to Gradle 2.x](https://github.com/diffplug/spotless/blob/main/plugin-gradle/CHANGES.md#451---2020-07-04). -### Git hook +### Git pre-push hook -TODO +You can install a Git pre-push hook that ensures code is properly formatted before being pushed to a remote repository. +This helps catch formatting issues early — before CI fails — and is especially useful for teams not using IDE integrations or pre-commit tools. + +#### What the hook does + +When installed, the Git `pre-push` hook will: + +1. Run `spotlessCheck` +2. If formatting issues are found: + - It automatically runs `spotlessApply` to fix them + - Aborts the push with a message + - You can then commit the changes and push again + +This ensures your code is always clean before it leaves your machine. + +#### Installation + +Run the following task once in your project: +```console +gradle spotlessInstallGitPrePushHook +``` + +This installs a `.git/hooks/pre-push` script that runs `spotlessCheck`, and runs `spotlessApply` if needed. + +> [!WARNING] +> The hook will not install automatically — you must run the install command manually. ### Linting diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java index 140bb8a755..2d6046d6fe 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java @@ -44,7 +44,7 @@ public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws assertThat(output).contains("Git pre-push hook not found, creating it"); assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push")); - final var content = getTestResource("git_pre_hook/pre-push.created") + final var content = getTestResource("git_pre_hook/pre-push.created-tpl") .replace("${executor}", "gradle") .replace("${checkCommand}", "spotlessCheck") .replace("${applyCommand}", "spotlessApply"); @@ -73,7 +73,7 @@ public void should_append_to_existing_pre_hook_file_when_hook_file_exists() thro assertThat(output).contains("Installing git pre-push hook"); assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push")); - final var content = getTestResource("git_pre_hook/pre-push.existing-added") + final var content = getTestResource("git_pre_hook/pre-push.existing-added-tpl") .replace("${executor}", "gradle") .replace("${checkCommand}", "spotlessCheck") .replace("${applyCommand}", "spotlessApply"); diff --git a/plugin-maven/README.md b/plugin-maven/README.md index 8cd01731ba..ade17248be 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -37,7 +37,7 @@ user@machine repo % mvn spotless:check - [**Quickstart**](#quickstart) - [Requirements](#requirements) - - [Git hook](#git-hook) + - [Git pre-push hook](#git-pre-push-hook) - [Binding to maven phase](#binding-to-maven-phase) - **Languages** - [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [prettier](#prettier), [palantir-java-format](#palantir-java-format), [formatAnnotations](#formatAnnotations), [cleanthat](#cleanthat), [IntelliJ IDEA](#intellij-idea)) @@ -147,9 +147,34 @@ Spotless consists of a list of formats (in the example above, `misc` and `java`) Spotless requires Maven to be running on JRE 11+. To use JRE 8, go back to [`2.30.0` or older](https://github.com/diffplug/spotless/blob/main/plugin-maven/CHANGES.md#2300---2023-01-13). -### Git hook +### Git pre-push hook -TODO +You can install a Git pre-push hook that ensures code is properly formatted before being pushed to a remote repository. +This helps catch formatting issues early — before CI fails — and is especially useful for teams not using IDE integrations or pre-commit tools. + +#### What the hook does + +When installed, the Git `pre-push` hook will: + +1. Run `spotless:check` +2. If formatting issues are found: + - It automatically runs `spotless:apply` to fix them + - Aborts the push with a message + - You can then commit the changes and push again + +This ensures your code is always clean before it leaves your machine. + +#### Installation + +Run the following task once in your project: +```console +mvn spotless:install-git-pre-push-hook +``` + +This installs a `.git/hooks/pre-push` script that runs `spotless:check`, and runs `spotless:apply` if needed. + +> [!WARNING] +> The hook will not install automatically — you must run the install command manually. ### Binding to maven phase diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojoTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojoTest.java index b9d5d6a343..dd11e10c36 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojoTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojoTest.java @@ -41,8 +41,8 @@ public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws assertThat(output).contains("Git pre-push hook not found, creating it"); assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push")); - final var content = getTestResource("git_pre_hook/pre-push.created") - .replace("${executor}", "mvn") + final var content = getTestResource("git_pre_hook/pre-push.created-tpl") + .replace("${executor}", newFile("mvnw").getAbsolutePath()) .replace("${checkCommand}", "spotless:check") .replace("${applyCommand}", "spotless:apply"); assertFile(".git/hooks/pre-push").hasContent(content); @@ -67,8 +67,8 @@ public void should_append_to_existing_pre_hook_file_when_hook_file_exists() thro assertThat(output).contains("Installing git pre-push hook"); assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push")); - final var content = getTestResource("git_pre_hook/pre-push.existing-added") - .replace("${executor}", "mvn") + final var content = getTestResource("git_pre_hook/pre-push.existing-added-tpl") + .replace("${executor}", newFile("mvnw").getAbsolutePath()) .replace("${checkCommand}", "spotless:check") .replace("${applyCommand}", "spotless:apply"); assertFile(".git/hooks/pre-push").hasContent(content); diff --git a/testlib/src/main/resources/git_pre_hook/pre-push.created b/testlib/src/main/resources/git_pre_hook/pre-push.created-tpl similarity index 99% rename from testlib/src/main/resources/git_pre_hook/pre-push.created rename to testlib/src/main/resources/git_pre_hook/pre-push.created-tpl index 376598c605..8edbebebf6 100644 --- a/testlib/src/main/resources/git_pre_hook/pre-push.created +++ b/testlib/src/main/resources/git_pre_hook/pre-push.created-tpl @@ -9,4 +9,3 @@ if ! $SPOTLESS_EXECUTOR ${checkCommand} ; then exit 1 fi ##### SPOTLESS HOOK END ##### - diff --git a/testlib/src/main/resources/git_pre_hook/pre-push.existing-added b/testlib/src/main/resources/git_pre_hook/pre-push.existing-added-tpl similarity index 99% rename from testlib/src/main/resources/git_pre_hook/pre-push.existing-added rename to testlib/src/main/resources/git_pre_hook/pre-push.existing-added-tpl index fd107efaec..48330e5842 100644 --- a/testlib/src/main/resources/git_pre_hook/pre-push.existing-added +++ b/testlib/src/main/resources/git_pre_hook/pre-push.existing-added-tpl @@ -59,4 +59,3 @@ if ! $SPOTLESS_EXECUTOR ${checkCommand} ; then exit 1 fi ##### SPOTLESS HOOK END ##### - diff --git a/testlib/src/main/resources/git_pre_hook/pre-push.reinstalled-tpl b/testlib/src/main/resources/git_pre_hook/pre-push.reinstalled-tpl new file mode 100644 index 0000000000..ef970ea413 --- /dev/null +++ b/testlib/src/main/resources/git_pre_hook/pre-push.reinstalled-tpl @@ -0,0 +1,64 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + + + + + +##### SPOTLESS HOOK START ##### +SPOTLESS_EXECUTOR=${executor} +if ! $SPOTLESS_EXECUTOR ${checkCommand} ; then + echo 1>&2 "spotless found problems, running ${applyCommand}; commit the result and re-push" + $SPOTLESS_EXECUTOR ${applyCommand} + exit 1 +fi +##### SPOTLESS HOOK END ##### diff --git a/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java b/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java index b6fbcd26e8..4cd10ce8b8 100644 --- a/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java @@ -74,15 +74,16 @@ public void should_use_global_gradle_when_gradlew_is_not_installed() throws Exce assertThat(logs).element(2).isEqualTo("Gradle wrapper is not installed, using global gradle"); assertThat(logs).element(3).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath()); - final var content = gradleHookContent("git_pre_hook/pre-push.created", false); + final var content = gradleHookContent("git_pre_hook/pre-push.created-tpl", ExecutorType.GLOBAL); assertFile(".git/hooks/pre-push").hasContent(content); } @Test - public void should_not_create_pre_hook_file_when_hook_already_installed() throws Exception { + public void should_reinstall_pre_hook_file_when_hook_already_installed() throws Exception { // given final var gradle = new GitPrePushHookInstallerGradle(logger, rootFolder()); - final var hookFile = setFile(".git/hooks/pre-push").toResource("git_pre_hook/pre-push.existing-added"); + final var installedGlobally = gradleHookContent("git_pre_hook/pre-push.existing-added-tpl", ExecutorType.GLOBAL); + final var hookFile = setFile(".git/hooks/pre-push").toContent(installedGlobally); setFile("gradlew").toContent(""); setFile(".git/config").toContent(""); @@ -91,10 +92,13 @@ public void should_not_create_pre_hook_file_when_hook_already_installed() throws gradle.install(); // then - assertThat(logs).hasSize(2); + assertThat(logs).hasSize(3); assertThat(logs).element(0).isEqualTo("Installing git pre-push hook"); - assertThat(logs).element(1).isEqualTo("Skipping, git pre-push hook already installed " + hookFile.getAbsolutePath()); - assertThat(hookFile).content().isEqualTo(getTestResource("git_pre_hook/pre-push.existing-added")); + assertThat(logs).element(1).isEqualTo("Git pre-push hook already installed, reinstalling it"); + assertThat(logs).element(2).isEqualTo("Git pre-push hook installed successfully to the file " + hookFile.getAbsolutePath()); + + final var content = gradleHookContent("git_pre_hook/pre-push.reinstalled-tpl", ExecutorType.WRAPPER); + assertFile(".git/hooks/pre-push").hasContent(content); } @Test @@ -113,7 +117,7 @@ public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws assertThat(logs).element(1).isEqualTo("Git pre-push hook not found, creating it"); assertThat(logs).element(2).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath()); - final var content = gradleHookContent("git_pre_hook/pre-push.created", true); + final var content = gradleHookContent("git_pre_hook/pre-push.created-tpl", ExecutorType.WRAPPER); assertFile(".git/hooks/pre-push").hasContent(content); } @@ -133,7 +137,7 @@ public void should_append_to_existing_pre_hook_file_when_hook_file_exists() thro assertThat(logs).element(0).isEqualTo("Installing git pre-push hook"); assertThat(logs).element(1).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath()); - final var content = gradleHookContent("git_pre_hook/pre-push.existing-added", true); + final var content = gradleHookContent("git_pre_hook/pre-push.existing-added-tpl", ExecutorType.WRAPPER); assertFile(".git/hooks/pre-push").hasContent(content); } @@ -153,7 +157,7 @@ public void should_create_pre_hook_file_for_maven_when_hook_file_does_not_exists assertThat(logs).element(1).isEqualTo("Git pre-push hook not found, creating it"); assertThat(logs).element(2).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath()); - final var content = mavenHookContent("git_pre_hook/pre-push.created", true); + final var content = mavenHookContent("git_pre_hook/pre-push.created-tpl", ExecutorType.WRAPPER); assertFile(".git/hooks/pre-push").hasContent(content); } @@ -173,21 +177,25 @@ public void should_use_global_maven_when_maven_wrapper_is_not_installed() throws assertThat(logs).element(2).isEqualTo("Maven wrapper is not installed, using global maven"); assertThat(logs).element(3).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath()); - final var content = mavenHookContent("git_pre_hook/pre-push.created", false); + final var content = mavenHookContent("git_pre_hook/pre-push.created-tpl", ExecutorType.GLOBAL); assertFile(".git/hooks/pre-push").hasContent(content); } - private String gradleHookContent(String resourcePath, boolean isWrapper) { + private String gradleHookContent(String resourcePath, ExecutorType executorType) { return getTestResource(resourcePath) - .replace("${executor}", isWrapper ? newFile("gradlew").getAbsolutePath() : "gradle") + .replace("${executor}", executorType == ExecutorType.WRAPPER ? newFile("gradlew").getAbsolutePath() : "gradle") .replace("${checkCommand}", "spotlessCheck") .replace("${applyCommand}", "spotlessApply"); } - private String mavenHookContent(String resourcePath, boolean isWrapper) { + private String mavenHookContent(String resourcePath, ExecutorType executorType) { return getTestResource(resourcePath) - .replace("${executor}", isWrapper ? newFile("mvnw").getAbsolutePath() : "mvn") + .replace("${executor}", executorType == ExecutorType.WRAPPER ? newFile("mvnw").getAbsolutePath() : "mvn") .replace("${checkCommand}", "spotless:check") .replace("${applyCommand}", "spotless:apply"); } + + private enum ExecutorType { + WRAPPER, GLOBAL + } } From 259ec38f714178310e8625945ed74b83cb9a99ea Mon Sep 17 00:00:00 2001 From: ntwigg Date: Fri, 18 Jul 2025 12:53:35 -0700 Subject: [PATCH 6/7] Modify git-hook docs, with an eye towards a future where there is both a pre-push hook or a pre-commit hook, depending on what the user prefers. --- plugin-gradle/README.md | 25 +++++-------------------- plugin-maven/README.md | 31 ++++++++----------------------- 2 files changed, 13 insertions(+), 43 deletions(-) diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 75c7c885c9..05a99350fd 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -53,7 +53,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui - [**Quickstart**](#quickstart) - [Requirements](#requirements) - - [Git pre-push hook](#git-pre-push-hook) + - [Git hook (optional)](#git-hook) - [Linting](#linting) - **Languages** - [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [clang-format](#clang-format), [prettier](#prettier), [palantir-java-format](#palantir-java-format), [formatAnnotations](#formatAnnotations), [cleanthat](#cleanthat), [IntelliJ IDEA](#intellij-idea)) @@ -142,16 +142,11 @@ Spotless requires JRE 11+ and Gradle 6.1.1 or newer. - If you're stuck on JRE 8, use [`id 'com.diffplug.spotless' version '6.13.0'` or older](https://github.com/diffplug/spotless/blob/main/plugin-gradle/CHANGES.md#6130---2023-01-14). - If you're stuck on an older version of Gradle, [`id 'com.diffplug.gradle.spotless' version '4.5.1'` supports all the way back to Gradle 2.x](https://github.com/diffplug/spotless/blob/main/plugin-gradle/CHANGES.md#451---2020-07-04). -### Git pre-push hook +### Git hook -You can install a Git pre-push hook that ensures code is properly formatted before being pushed to a remote repository. -This helps catch formatting issues early — before CI fails — and is especially useful for teams not using IDE integrations or pre-commit tools. +If you want, you can run `./gradlew spotlessInstallGitPrePushHook` and it will install a hook such that -#### What the hook does - -When installed, the Git `pre-push` hook will: - -1. Run `spotlessCheck` +1. When you push, it runs `spotlessCheck` 2. If formatting issues are found: - It automatically runs `spotlessApply` to fix them - Aborts the push with a message @@ -159,17 +154,7 @@ When installed, the Git `pre-push` hook will: This ensures your code is always clean before it leaves your machine. -#### Installation - -Run the following task once in your project: -```console -gradle spotlessInstallGitPrePushHook -``` - -This installs a `.git/hooks/pre-push` script that runs `spotlessCheck`, and runs `spotlessApply` if needed. - -> [!WARNING] -> The hook will not install automatically — you must run the install command manually. +If you prefer instead to have a "pre-commit" hook so that every single commit is clean, see [#623](https://github.com/diffplug/spotless/issues/623) for a workaround or to contribute a permanent fix. ### Linting diff --git a/plugin-maven/README.md b/plugin-maven/README.md index ade17248be..50b90ada1f 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -37,7 +37,7 @@ user@machine repo % mvn spotless:check - [**Quickstart**](#quickstart) - [Requirements](#requirements) - - [Git pre-push hook](#git-pre-push-hook) + - [Git hook (optional)](#git-hook) - [Binding to maven phase](#binding-to-maven-phase) - **Languages** - [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [prettier](#prettier), [palantir-java-format](#palantir-java-format), [formatAnnotations](#formatAnnotations), [cleanthat](#cleanthat), [IntelliJ IDEA](#intellij-idea)) @@ -147,34 +147,19 @@ Spotless consists of a list of formats (in the example above, `misc` and `java`) Spotless requires Maven to be running on JRE 11+. To use JRE 8, go back to [`2.30.0` or older](https://github.com/diffplug/spotless/blob/main/plugin-maven/CHANGES.md#2300---2023-01-13). -### Git pre-push hook +### Git hook -You can install a Git pre-push hook that ensures code is properly formatted before being pushed to a remote repository. -This helps catch formatting issues early — before CI fails — and is especially useful for teams not using IDE integrations or pre-commit tools. +If you want, you can run `mvn spotless:install-git-pre-push-hook` and it will install a hook such that -#### What the hook does - -When installed, the Git `pre-push` hook will: - -1. Run `spotless:check` +1. When you push, it runs `spotless:check` 2. If formatting issues are found: - - It automatically runs `spotless:apply` to fix them - - Aborts the push with a message - - You can then commit the changes and push again + - It automatically runs `spotless:apply` to fix them + - Aborts the push with a message + - You can then commit the changes and push again This ensures your code is always clean before it leaves your machine. -#### Installation - -Run the following task once in your project: -```console -mvn spotless:install-git-pre-push-hook -``` - -This installs a `.git/hooks/pre-push` script that runs `spotless:check`, and runs `spotless:apply` if needed. - -> [!WARNING] -> The hook will not install automatically — you must run the install command manually. +If you prefer instead to have a "pre-commit" hook so that every single commit is clean, see [#623](https://github.com/diffplug/spotless/issues/623) for a workaround or to contribute a permanent fix. ### Binding to maven phase From 2dadeb609c02b26930a30086a277a14d0d0466b7 Mon Sep 17 00:00:00 2001 From: Alex Danylenko Date: Fri, 18 Jul 2025 20:09:10 -0700 Subject: [PATCH 7/7] uninstall fix --- .../spotless/GitPrePushHookInstaller.java | 55 +++++++++-- .../GitPrePushHookInstallerGradle.java | 4 +- .../GitPrePushHookInstallerMaven.java | 4 +- .../SpotlessInstallPrePushHookTaskTest.java | 2 +- .../SpotlessInstallPrePushHookMojoTest.java | 2 +- ...pl => pre-push.existing-installed-end-tpl} | 0 ...=> pre-push.existing-installed-middle-tpl} | 36 ++++++- .../pre-push.existing-reinstalled-middle-tpl | 94 +++++++++++++++++++ .../spotless/GitPrePushHookInstallerTest.java | 49 +++++++++- 9 files changed, 227 insertions(+), 19 deletions(-) rename testlib/src/main/resources/git_pre_hook/{pre-push.existing-added-tpl => pre-push.existing-installed-end-tpl} (100%) rename testlib/src/main/resources/git_pre_hook/{pre-push.reinstalled-tpl => pre-push.existing-installed-middle-tpl} (67%) create mode 100644 testlib/src/main/resources/git_pre_hook/pre-push.existing-reinstalled-middle-tpl diff --git a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java index b5070a003b..167e089b7d 100644 --- a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java +++ b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java @@ -31,7 +31,7 @@ */ public abstract class GitPrePushHookInstaller { - private static final String HOOK_HEADLINE = "##### SPOTLESS HOOK START #####"; + private static final String HOOK_HEADER = "##### SPOTLESS HOOK START #####"; private static final String HOOK_FOOTER = "##### SPOTLESS HOOK END #####"; /** @@ -128,10 +128,48 @@ public void install() throws Exception { */ private void uninstall(File gitHookFile) throws Exception { final var hook = Files.readString(gitHookFile.toPath(), UTF_8); - final var hookStart = hook.indexOf(HOOK_HEADLINE); - final var hookEnd = hook.indexOf(HOOK_FOOTER) + HOOK_FOOTER.length(); + final int hookStart = hook.indexOf(HOOK_HEADER); + final int hookEnd = hook.indexOf(HOOK_FOOTER) + HOOK_FOOTER.length(); // hookEnd exclusive, so must be last symbol \n + + /* Detailed explanation: + * 1. hook.indexOf(HOOK_FOOTER) - finds the starting position of footer "##### SPOTLESS HOOK END #####" + * 2. + HOOK_FOOTER.length() is needed because String.substring(startIndex, endIndex) treats endIndex as exclusive + * + * For example, if file content is: + * #!/bin/sh + * ##### SPOTLESS HOOK START ##### + * ... hook code ... + * ##### SPOTLESS HOOK END ##### + * other content + * + * When we later use this in: hook.substring(hookStart, hookEnd) + * - Since substring's endIndex is exclusive (it stops BEFORE that index) + * - We need hookEnd to point to the position AFTER the last '#' + * - This ensures the entire footer "##### SPOTLESS HOOK END #####" is included in the substring + * + * This exclusive behavior is why in the subsequent code: + * if (hook.charAt(hookEnd) == '\n') { + * hookScript += "\n"; + * } + * + * We can directly use hookEnd to check the next character after the footer + * - Since hookEnd is already pointing to the position AFTER the footer + * - No need for hookEnd + 1 in charAt() + * - This makes the code more consistent with the substring's exclusive nature + */ + + var hookScript = hook.substring(hookStart, hookEnd); + if (hookStart >= 1 && hook.charAt(hookStart - 1) == '\n') { + hookScript = "\n" + hookScript; + } + + if (hookStart >= 2 && hook.charAt(hookStart - 2) == '\n') { + hookScript = "\n" + hookScript; + } - final var hookScript = hook.substring(hookStart, hookEnd); + if (hook.charAt(hookEnd) == '\n') { + hookScript += "\n"; + } final var uninstalledHook = hook.replace(hookScript, ""); @@ -155,8 +193,10 @@ private void uninstall(File gitHookFile) throws Exception { * @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" + HOOK_HEADLINE; + var spotlessHook = ""; + + spotlessHook += "\n"; + spotlessHook += "\n" + HOOK_HEADER; 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\""; @@ -165,6 +205,7 @@ protected String preHookTemplate(String executor, String commandCheck, String co spotlessHook += "\nfi"; spotlessHook += "\n" + HOOK_FOOTER; spotlessHook += "\n"; + return spotlessHook; } @@ -186,7 +227,7 @@ private boolean isGitInstalled() { */ private boolean isGitHookInstalled(File gitHookFile) throws Exception { final var hook = Files.readString(gitHookFile.toPath(), UTF_8); - return hook.contains(HOOK_HEADLINE); + return hook.contains(HOOK_HEADER) && hook.contains(HOOK_FOOTER); } /** diff --git a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java index cfed7cdd58..0b11db5cab 100644 --- a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java +++ b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerGradle.java @@ -38,10 +38,10 @@ public GitPrePushHookInstallerGradle(GitPreHookLogger logger, File root) { */ @Override protected String preHookContent() { - return preHookTemplate(executor(), "spotlessCheck", "spotlessApply"); + return preHookTemplate(executorPath(), "spotlessCheck", "spotlessApply"); } - private String executor() { + private String executorPath() { if (gradlew.exists()) { return gradlew.getAbsolutePath(); } diff --git a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerMaven.java b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerMaven.java index 7f84d85429..138d27f9a3 100644 --- a/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerMaven.java +++ b/lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstallerMaven.java @@ -35,10 +35,10 @@ public GitPrePushHookInstallerMaven(GitPreHookLogger logger, File root) { */ @Override protected String preHookContent() { - return preHookTemplate(executor(), "spotless:check", "spotless:apply"); + return preHookTemplate(executorPath(), "spotless:check", "spotless:apply"); } - private String executor() { + private String executorPath() { if (mvnw.exists()) { return mvnw.getAbsolutePath(); } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java index 2d6046d6fe..d5ff385b82 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java @@ -73,7 +73,7 @@ public void should_append_to_existing_pre_hook_file_when_hook_file_exists() thro assertThat(output).contains("Installing git pre-push hook"); assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push")); - final var content = getTestResource("git_pre_hook/pre-push.existing-added-tpl") + final var content = getTestResource("git_pre_hook/pre-push.existing-installed-end-tpl") .replace("${executor}", "gradle") .replace("${checkCommand}", "spotlessCheck") .replace("${applyCommand}", "spotlessApply"); diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojoTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojoTest.java index dd11e10c36..ebfc13d982 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojoTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojoTest.java @@ -67,7 +67,7 @@ public void should_append_to_existing_pre_hook_file_when_hook_file_exists() thro assertThat(output).contains("Installing git pre-push hook"); assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push")); - final var content = getTestResource("git_pre_hook/pre-push.existing-added-tpl") + final var content = getTestResource("git_pre_hook/pre-push.existing-installed-end-tpl") .replace("${executor}", newFile("mvnw").getAbsolutePath()) .replace("${checkCommand}", "spotless:check") .replace("${applyCommand}", "spotless:apply"); diff --git a/testlib/src/main/resources/git_pre_hook/pre-push.existing-added-tpl b/testlib/src/main/resources/git_pre_hook/pre-push.existing-installed-end-tpl similarity index 100% rename from testlib/src/main/resources/git_pre_hook/pre-push.existing-added-tpl rename to testlib/src/main/resources/git_pre_hook/pre-push.existing-installed-end-tpl diff --git a/testlib/src/main/resources/git_pre_hook/pre-push.reinstalled-tpl b/testlib/src/main/resources/git_pre_hook/pre-push.existing-installed-middle-tpl similarity index 67% rename from testlib/src/main/resources/git_pre_hook/pre-push.reinstalled-tpl rename to testlib/src/main/resources/git_pre_hook/pre-push.existing-installed-middle-tpl index ef970ea413..63f7da0a0b 100644 --- a/testlib/src/main/resources/git_pre_hook/pre-push.reinstalled-tpl +++ b/testlib/src/main/resources/git_pre_hook/pre-push.existing-installed-middle-tpl @@ -51,9 +51,6 @@ do done - - - ##### SPOTLESS HOOK START ##### SPOTLESS_EXECUTOR=${executor} if ! $SPOTLESS_EXECUTOR ${checkCommand} ; then @@ -62,3 +59,36 @@ if ! $SPOTLESS_EXECUTOR ${checkCommand} ; then exit 1 fi ##### SPOTLESS HOOK END ##### + + +# some additional pre-push code +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done diff --git a/testlib/src/main/resources/git_pre_hook/pre-push.existing-reinstalled-middle-tpl b/testlib/src/main/resources/git_pre_hook/pre-push.existing-reinstalled-middle-tpl new file mode 100644 index 0000000000..7265927c13 --- /dev/null +++ b/testlib/src/main/resources/git_pre_hook/pre-push.existing-reinstalled-middle-tpl @@ -0,0 +1,94 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + + +# some additional pre-push code +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + + +##### SPOTLESS HOOK START ##### +SPOTLESS_EXECUTOR=${executor} +if ! $SPOTLESS_EXECUTOR ${checkCommand} ; then + echo 1>&2 "spotless found problems, running ${applyCommand}; commit the result and re-push" + $SPOTLESS_EXECUTOR ${applyCommand} + exit 1 +fi +##### SPOTLESS HOOK END ##### diff --git a/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java b/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java index 4cd10ce8b8..6167e26b8c 100644 --- a/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java @@ -82,7 +82,7 @@ public void should_use_global_gradle_when_gradlew_is_not_installed() throws Exce public void should_reinstall_pre_hook_file_when_hook_already_installed() throws Exception { // given final var gradle = new GitPrePushHookInstallerGradle(logger, rootFolder()); - final var installedGlobally = gradleHookContent("git_pre_hook/pre-push.existing-added-tpl", ExecutorType.GLOBAL); + final var installedGlobally = gradleHookContent("git_pre_hook/pre-push.existing-installed-end-tpl", ExecutorType.GLOBAL); final var hookFile = setFile(".git/hooks/pre-push").toContent(installedGlobally); setFile("gradlew").toContent(""); @@ -97,7 +97,50 @@ public void should_reinstall_pre_hook_file_when_hook_already_installed() throws assertThat(logs).element(1).isEqualTo("Git pre-push hook already installed, reinstalling it"); assertThat(logs).element(2).isEqualTo("Git pre-push hook installed successfully to the file " + hookFile.getAbsolutePath()); - final var content = gradleHookContent("git_pre_hook/pre-push.reinstalled-tpl", ExecutorType.WRAPPER); + final var content = gradleHookContent("git_pre_hook/pre-push.existing-installed-end-tpl", ExecutorType.WRAPPER); + assertFile(".git/hooks/pre-push").hasContent(content); + } + + @Test + public void should_reinstall_pre_hook_file_when_hook_already_installed_in_the_middle_of_file() throws Exception { + // given + final var gradle = new GitPrePushHookInstallerGradle(logger, rootFolder()); + final var installedGlobally = gradleHookContent("git_pre_hook/pre-push.existing-installed-middle-tpl", ExecutorType.GLOBAL); + final var hookFile = setFile(".git/hooks/pre-push").toContent(installedGlobally); + + setFile("gradlew").toContent(""); + setFile(".git/config").toContent(""); + + // when + gradle.install(); + + // then + assertThat(logs).hasSize(3); + assertThat(logs).element(0).isEqualTo("Installing git pre-push hook"); + assertThat(logs).element(1).isEqualTo("Git pre-push hook already installed, reinstalling it"); + assertThat(logs).element(2).isEqualTo("Git pre-push hook installed successfully to the file " + hookFile.getAbsolutePath()); + + final var content = gradleHookContent("git_pre_hook/pre-push.existing-reinstalled-middle-tpl", ExecutorType.WRAPPER); + assertFile(".git/hooks/pre-push").hasContent(content); + } + + @Test + public void should_reinstall_a_few_times_pre_hook_file_when_hook_already_installed_in_the_middle_of_file() throws Exception { + // given + final var gradle = new GitPrePushHookInstallerGradle(logger, rootFolder()); + final var installedGlobally = gradleHookContent("git_pre_hook/pre-push.existing-installed-middle-tpl", ExecutorType.GLOBAL); + setFile(".git/hooks/pre-push").toContent(installedGlobally); + + setFile("gradlew").toContent(""); + setFile(".git/config").toContent(""); + + // when + gradle.install(); + gradle.install(); + gradle.install(); + + // then + final var content = gradleHookContent("git_pre_hook/pre-push.existing-reinstalled-middle-tpl", ExecutorType.WRAPPER); assertFile(".git/hooks/pre-push").hasContent(content); } @@ -137,7 +180,7 @@ public void should_append_to_existing_pre_hook_file_when_hook_file_exists() thro assertThat(logs).element(0).isEqualTo("Installing git pre-push hook"); assertThat(logs).element(1).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath()); - final var content = gradleHookContent("git_pre_hook/pre-push.existing-added-tpl", ExecutorType.WRAPPER); + final var content = gradleHookContent("git_pre_hook/pre-push.existing-installed-end-tpl", ExecutorType.WRAPPER); assertFile(".git/hooks/pre-push").hasContent(content); }