Skip to content

Commit e6e9a07

Browse files
committed
add git pre push hook
1 parent b7c829e commit e6e9a07

File tree

17 files changed

+780
-1
lines changed

17 files changed

+780
-1
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
1212
## [Unreleased]
1313
### Added
1414
* Allow specifying path to Biome JSON config file directly in `biome` step. Requires biome 2.x. ([#2548](https://github.com/diffplug/spotless/pull/2548))
15+
- `GitPrePushHookInstaller`, a reusable library component for installing a Git `pre-push` hook that runs formatter checks.
1516

1617
## Changed
1718
* Bump default `gson` version to latest `2.11.0` -> `2.13.1`. ([#2414](https://github.com/diffplug/spotless/pull/2414))
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package com.diffplug.spotless;
2+
3+
import static java.nio.charset.StandardCharsets.UTF_8;
4+
5+
import java.io.File;
6+
import java.io.FileWriter;
7+
import java.io.IOException;
8+
import java.nio.file.Files;
9+
10+
/**
11+
* Abstract class responsible for installing a Git pre-push hook in a repository.
12+
* This class ensures that specific checks and logic are run before a push operation in Git.
13+
*
14+
* Subclasses should define specific behavior for hook installation by implementing the required abstract methods.
15+
*/
16+
public abstract class GitPrePushHookInstaller {
17+
18+
/**
19+
* Logger for recording informational and error messages during the installation process.
20+
*/
21+
protected final GitPreHookLogger logger;
22+
23+
/**
24+
* The root directory of the Git repository where the hook will be installed.
25+
*/
26+
protected final File root;
27+
28+
/**
29+
* Constructor to initialize the GitPrePushHookInstaller with a logger and repository root path.
30+
*
31+
* @param logger The logger for recording messages.
32+
* @param root The root directory of the Git repository.
33+
*/
34+
public GitPrePushHookInstaller(GitPreHookLogger logger, File root) {
35+
this.logger = logger;
36+
this.root = root;
37+
}
38+
39+
/**
40+
* Installs the Git pre-push hook into the repository.
41+
*
42+
* <p>This method checks for the following:
43+
* <ul>
44+
* <li>Ensures Git is installed and the `.git/config` file exists.</li>
45+
* <li>Checks if an executor required by the hook is available.</li>
46+
* <li>Creates and writes the pre-push hook file if it does not exist.</li>
47+
* <li>Skips installation if the hook is already installed.</li>
48+
* </ul>
49+
* If an issue occurs during installation, error messages are logged.
50+
*
51+
* @throws Exception if any error occurs during installation.
52+
*/
53+
public void install() throws Exception {
54+
logger.info("Installing git pre-push hook");
55+
56+
if (!isGitInstalled()) {
57+
logger.error("Git not found in root directory");
58+
return;
59+
}
60+
61+
if (!isExecutorInstalled()) {
62+
return;
63+
}
64+
65+
var hookContent = "";
66+
final var gitHookFile = root.toPath().resolve(".git/hooks/pre-push").toFile();
67+
if (!gitHookFile.exists()) {
68+
logger.info("Git pre-push hook not found, creating it");
69+
gitHookFile.getParentFile().mkdirs();
70+
if (!gitHookFile.createNewFile()) {
71+
logger.error("Failed to create pre-push hook file");
72+
return;
73+
}
74+
75+
if (!gitHookFile.setExecutable(true, false)) {
76+
logger.error("Can not make file executable");
77+
return;
78+
}
79+
80+
hookContent += "#!/bin/sh\n";
81+
}
82+
83+
if (isGitHookInstalled(gitHookFile)) {
84+
logger.info("Skipping, git pre-push hook already installed %s", gitHookFile.getAbsolutePath());
85+
return;
86+
}
87+
88+
hookContent += preHookContent();
89+
writeFile(gitHookFile, hookContent);
90+
91+
logger.info("Git pre-push hook installed successfully to the file %s", gitHookFile.getAbsolutePath());
92+
}
93+
94+
/**
95+
* Checks if the required executor for performing the desired pre-push actions is installed.
96+
*
97+
* @return {@code true} if the executor is installed, {@code false} otherwise.
98+
*/
99+
protected abstract boolean isExecutorInstalled();
100+
101+
/**
102+
* Provides the content of the hook that should be inserted into the pre-push script.
103+
*
104+
* @return A string representing the content to include in the pre-push script.
105+
*/
106+
protected abstract String preHookContent();
107+
108+
/**
109+
* Checks if Git is installed by validating the existence of `.git/config` in the repository root.
110+
*
111+
* @return {@code true} if Git is installed, {@code false} otherwise.
112+
*/
113+
private boolean isGitInstalled() {
114+
return root.toPath().resolve(".git/config").toFile().exists();
115+
}
116+
117+
/**
118+
* Verifies if the pre-push hook file already contains the custom Spotless hook content.
119+
*
120+
* @param gitHookFile The file representing the Git hook.
121+
* @return {@code true} if the hook is already installed, {@code false} otherwise.
122+
* @throws Exception if an error occurs when reading the file.
123+
*/
124+
private boolean isGitHookInstalled(File gitHookFile) throws Exception {
125+
final var hook = Files.readString(gitHookFile.toPath(), UTF_8);
126+
return hook.contains("##### SPOTLESS HOOK START #####");
127+
}
128+
129+
/**
130+
* Writes the specified content into a file.
131+
*
132+
* @param file The file to which the content should be written.
133+
* @param content The content to write into the file.
134+
* @throws IOException if an error occurs while writing to the file.
135+
*/
136+
private void writeFile(File file, String content) throws IOException {
137+
try (final var writer = new FileWriter(file, UTF_8, true)) {
138+
writer.write(content);
139+
}
140+
}
141+
142+
/**
143+
* Generates a pre-push template script that defines the commands to check and apply changes
144+
* using an executor and Spotless.
145+
*
146+
* @param executor The tool to execute the check and apply commands.
147+
* @param commandCheck The command to check for issues.
148+
* @param commandApply The command to apply corrections.
149+
* @return A string template representing the Spotless Git pre-push hook content.
150+
*/
151+
protected String preHookTemplate(String executor, String commandCheck, String commandApply) {
152+
var spotlessHook = "\n";
153+
spotlessHook += "\n##### SPOTLESS HOOK START #####";
154+
spotlessHook += "\nSPOTLESS_EXECUTOR=" + executor;
155+
spotlessHook += "\nif ! $SPOTLESS_EXECUTOR " + commandCheck + " ; then";
156+
spotlessHook += "\n echo 1>&2 \"spotless found problems, running " + commandApply + "; commit the result and re-push\"";
157+
spotlessHook += "\n $SPOTLESS_EXECUTOR " + commandApply;
158+
spotlessHook += "\n exit 1";
159+
spotlessHook += "\nfi";
160+
spotlessHook += "\n##### SPOTLESS HOOK END #####";
161+
spotlessHook += "\n\n";
162+
return spotlessHook;
163+
}
164+
165+
public interface GitPreHookLogger {
166+
void info(String format, Object... arguments);
167+
void error(String format, Object... arguments);
168+
}
169+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.diffplug.spotless;
2+
3+
import java.io.File;
4+
5+
/**
6+
* Implementation of {@link GitPrePushHookInstaller} specifically for Gradle-based projects.
7+
* This class installs a Git pre-push hook that uses Gradle's `gradlew` executable to check and apply Spotless formatting.
8+
*/
9+
public class GitPrePushHookInstallerGradle extends GitPrePushHookInstaller {
10+
11+
/**
12+
* The Gradle wrapper file (`gradlew`) located in the root directory of the project.
13+
*/
14+
private final File gradlew;
15+
16+
public GitPrePushHookInstallerGradle(GitPreHookLogger logger, File root) {
17+
super(logger, root);
18+
this.gradlew = root.toPath().resolve("gradlew").toFile();
19+
}
20+
21+
/**
22+
* Checks if the Gradle wrapper (`gradlew`) is present in the root directory.
23+
* This ensures that the executor used for formatting (`spotlessCheck` and `spotlessApply`) is available.
24+
*
25+
* @return {@code true} if the Gradle wrapper is found, {@code false} otherwise.
26+
* An error is logged if the wrapper is not found.
27+
*/
28+
@Override
29+
protected boolean isExecutorInstalled() {
30+
if (gradlew.exists()) {
31+
return true;
32+
}
33+
34+
logger.error("Failed to find gradlew in root directory");
35+
return false;
36+
}
37+
38+
@Override
39+
protected String preHookContent() {
40+
return preHookTemplate(gradlew.getAbsolutePath(), "spotlessCheck", "spotlessApply");
41+
}
42+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.diffplug.spotless;
2+
3+
import java.io.File;
4+
5+
/**
6+
* Implementation of {@link GitPrePushHookInstaller} specifically for Maven-based projects.
7+
* This class installs a Git pre-push hook that uses Maven to check and apply Spotless formatting.
8+
*/
9+
public class GitPrePushHookInstallerMaven extends GitPrePushHookInstaller {
10+
11+
public GitPrePushHookInstallerMaven(GitPreHookLogger logger, File root) {
12+
super(logger, root);
13+
}
14+
15+
/**
16+
* Confirms that Maven is installed and available for use.
17+
*
18+
* <p>This method assumes that if this code is running, then Maven is already properly installed and configured,
19+
* so it always returns {@code true}.
20+
*
21+
* @return {@code true}, indicating that Maven is available.
22+
*/
23+
@Override
24+
protected boolean isExecutorInstalled() {
25+
return true;
26+
}
27+
28+
@Override
29+
protected String preHookContent() {
30+
return preHookTemplate("mvn", "spotless:check", "spotless:apply");
31+
}
32+
}

plugin-gradle/CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
55
## [Unreleased]
66
### Added
77
* Allow specifying path to Biome JSON config file directly in `biome` step. Requires biome 2.x. ([#2548](https://github.com/diffplug/spotless/pull/2548))
8+
- `spotlessInstallGitPrePushHook` task, which installs a Git `pre-push` hook to run `spotlessCheck` and `spotlessApply`.
9+
Uses shared implementation from `GitPrePushHookInstaller`.
10+
[#2553](https://github.com/diffplug/spotless/pull/2553)
811

912
## Changed
1013
* Bump default `gson` version to latest `2.11.0` -> `2.13.1`. ([#2414](https://github.com/diffplug/spotless/pull/2414))

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,17 @@ public abstract class SpotlessExtension {
3838
private final RegisterDependenciesTask registerDependenciesTask;
3939

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

4446
static final String EXTENSION = "spotless";
4547
static final String EXTENSION_PREDECLARE = "spotlessPredeclare";
4648
static final String CHECK = "Check";
4749
static final String APPLY = "Apply";
4850
static final String DIAGNOSE = "Diagnose";
51+
static final String INSTALL_GIT_PRE_PUSH_HOOK = "InstallGitPrePushHook";
4952

5053
protected SpotlessExtension(Project project) {
5154
this.project = requireNonNull(project);

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import org.gradle.api.tasks.TaskProvider;
2424

2525
public class SpotlessExtensionImpl extends SpotlessExtension {
26-
final TaskProvider<?> rootCheckTask, rootApplyTask, rootDiagnoseTask;
26+
final TaskProvider<?> rootCheckTask, rootApplyTask, rootDiagnoseTask, rootInstallPreHook;
2727

2828
public SpotlessExtensionImpl(Project project) {
2929
super(project);
@@ -38,6 +38,10 @@ public SpotlessExtensionImpl(Project project) {
3838
rootDiagnoseTask = project.getTasks().register(EXTENSION + DIAGNOSE, task -> {
3939
task.setGroup(TASK_GROUP); // no description on purpose
4040
});
41+
rootInstallPreHook = project.getTasks().register(EXTENSION + INSTALL_GIT_PRE_PUSH_HOOK, SpotlessInstallPrePushHookTask.class, task -> {
42+
task.setGroup(BUILD_SETUP_TASK_GROUP);
43+
task.setDescription(INSTALL_GIT_PRE_PUSH_HOOK_DESCRIPTION);
44+
});
4145

4246
project.afterEvaluate(unused -> {
4347
if (enforceCheck) {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.diffplug.gradle.spotless;
2+
3+
import org.gradle.api.DefaultTask;
4+
import org.gradle.api.tasks.TaskAction;
5+
import org.gradle.work.DisableCachingByDefault;
6+
7+
import com.diffplug.spotless.GitPrePushHookInstaller.GitPreHookLogger;
8+
import com.diffplug.spotless.GitPrePushHookInstallerGradle;
9+
10+
/**
11+
* A Gradle task responsible for installing a Git pre-push hook for the Spotless plugin.
12+
* This hook ensures that Spotless formatting rules are automatically checked and applied
13+
* before performing a Git push operation.
14+
*
15+
* <p>The task leverages {@link GitPrePushHookInstallerGradle} to implement the installation process.
16+
*/
17+
@DisableCachingByDefault(because = "not worth caching")
18+
public class SpotlessInstallPrePushHookTask extends DefaultTask {
19+
20+
/**
21+
* Executes the task to install the Git pre-push hook.
22+
*
23+
* <p>This method creates an instance of {@link GitPrePushHookInstallerGradle},
24+
* providing a logger to record informational and error messages during the installation process.
25+
* The installer then installs the hook in the root directory of the Gradle project.
26+
*
27+
* @throws Exception if an error occurs during the hook installation process.
28+
*/
29+
@TaskAction
30+
public void performAction() throws Exception {
31+
final var logger = new GitPreHookLogger() {
32+
@Override
33+
public void info(String format, Object... arguments) {
34+
getLogger().lifecycle(String.format(format, arguments));
35+
}
36+
37+
@Override
38+
public void error(String format, Object... arguments) {
39+
getLogger().error(String.format(format, arguments));
40+
}
41+
};
42+
43+
final var installer = new GitPrePushHookInstallerGradle(logger, getProject().getRootDir());
44+
installer.install();
45+
}
46+
}

0 commit comments

Comments
 (0)