Skip to content

Commit 4ece095

Browse files
committed
reinstall support
1 parent a790659 commit 4ece095

File tree

9 files changed

+185
-52
lines changed

9 files changed

+185
-52
lines changed

lib/src/main/java/com/diffplug/spotless/GitPrePushHookInstaller.java

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,12 @@ public void install() throws Exception {
100100
}
101101

102102
if (isGitHookInstalled(gitHookFile)) {
103-
logger.warn("Skipping, git pre-push hook already installed %s", gitHookFile.getAbsolutePath());
104-
return;
103+
logger.info("Git pre-push hook already installed, reinstalling it");
104+
uninstall(gitHookFile);
105105
}
106106

107107
hookContent += preHookContent();
108-
writeFile(gitHookFile, hookContent);
108+
writeFile(gitHookFile, hookContent, true);
109109

110110
logger.info("Git pre-push hook installed successfully to the file %s", gitHookFile.getAbsolutePath());
111111
}
@@ -117,6 +117,29 @@ public void install() throws Exception {
117117
*/
118118
protected abstract String preHookContent();
119119

120+
/**
121+
* Generates a pre-push template script that defines the commands to check and apply changes
122+
* using an executor and Spotless.
123+
*
124+
* @param executor The tool to execute the check and apply commands.
125+
* @param commandCheck The command to check for issues.
126+
* @param commandApply The command to apply corrections.
127+
* @return A string template representing the Spotless Git pre-push hook content.
128+
*/
129+
protected String preHookTemplate(String executor, String commandCheck, String commandApply) {
130+
var spotlessHook = "\n";
131+
spotlessHook += "\n" + HOOK_HEADLINE;
132+
spotlessHook += "\nSPOTLESS_EXECUTOR=" + executor;
133+
spotlessHook += "\nif ! $SPOTLESS_EXECUTOR " + commandCheck + " ; then";
134+
spotlessHook += "\n echo 1>&2 \"spotless found problems, running " + commandApply + "; commit the result and re-push\"";
135+
spotlessHook += "\n $SPOTLESS_EXECUTOR " + commandApply;
136+
spotlessHook += "\n exit 1";
137+
spotlessHook += "\nfi";
138+
spotlessHook += "\n" + HOOK_FOOTER;
139+
spotlessHook += "\n";
140+
return spotlessHook;
141+
}
142+
120143
/**
121144
* Checks if Git is installed by validating the existence of `.git/config` in the repository root.
122145
*
@@ -145,33 +168,22 @@ private boolean isGitHookInstalled(File gitHookFile) throws Exception {
145168
* @param content The content to write into the file.
146169
* @throws IOException if an error occurs while writing to the file.
147170
*/
148-
private void writeFile(File file, String content) throws IOException {
149-
try (final var writer = new FileWriter(file, UTF_8, true)) {
171+
private void writeFile(File file, String content, boolean append) throws IOException {
172+
try (final var writer = new FileWriter(file, UTF_8, append)) {
150173
writer.write(content);
151174
}
152175
}
153176

154-
/**
155-
* Generates a pre-push template script that defines the commands to check and apply changes
156-
* using an executor and Spotless.
157-
*
158-
* @param executor The tool to execute the check and apply commands.
159-
* @param commandCheck The command to check for issues.
160-
* @param commandApply The command to apply corrections.
161-
* @return A string template representing the Spotless Git pre-push hook content.
162-
*/
163-
protected String preHookTemplate(String executor, String commandCheck, String commandApply) {
164-
var spotlessHook = "\n";
165-
spotlessHook += "\n" + HOOK_HEADLINE;
166-
spotlessHook += "\nSPOTLESS_EXECUTOR=" + executor;
167-
spotlessHook += "\nif ! $SPOTLESS_EXECUTOR " + commandCheck + " ; then";
168-
spotlessHook += "\n echo 1>&2 \"spotless found problems, running " + commandApply + "; commit the result and re-push\"";
169-
spotlessHook += "\n $SPOTLESS_EXECUTOR " + commandApply;
170-
spotlessHook += "\n exit 1";
171-
spotlessHook += "\nfi";
172-
spotlessHook += "\n" + HOOK_FOOTER;
173-
spotlessHook += "\n\n";
174-
return spotlessHook;
177+
private void uninstall(File gitHookFile) throws Exception {
178+
final var hook = Files.readString(gitHookFile.toPath(), UTF_8);
179+
final var hookStart = hook.indexOf(HOOK_HEADLINE);
180+
final var hookEnd = hook.indexOf(HOOK_FOOTER) + HOOK_FOOTER.length();
181+
182+
final var hookScript = hook.substring(hookStart, hookEnd);
183+
184+
final var uninstalledHook = hook.replace(hookScript, "");
185+
186+
writeFile(gitHookFile, uninstalledHook, false);
175187
}
176188

177189
public interface GitPreHookLogger {

plugin-gradle/README.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui
5353

5454
- [**Quickstart**](#quickstart)
5555
- [Requirements](#requirements)
56-
- [Git hook](#git-hook)
56+
- [Git pre-push hook](#git-pre-push-hook)
5757
- [Linting](#linting)
5858
- **Languages**
5959
- [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.
142142
- 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).
143143
- 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).
144144

145-
### Git hook
145+
### Git pre-push hook
146146

147-
TODO
147+
You can install a Git pre-push hook that ensures code is properly formatted before being pushed to a remote repository.
148+
This helps catch formatting issues early — before CI fails — and is especially useful for teams not using IDE integrations or pre-commit tools.
149+
150+
#### What the hook does
151+
152+
When installed, the Git `pre-push` hook will:
153+
154+
1. Run `spotlessCheck`
155+
2. If formatting issues are found:
156+
- It automatically runs `spotlessApply` to fix them
157+
- Aborts the push with a message
158+
- You can then commit the changes and push again
159+
160+
This ensures your code is always clean before it leaves your machine.
161+
162+
#### Installation
163+
164+
Run the following task once in your project:
165+
```console
166+
gradle spotlessInstallGitPrePushHook
167+
```
168+
169+
This installs a `.git/hooks/pre-push` script that runs `spotlessCheck`, and runs `spotlessApply` if needed.
170+
171+
> [!WARNING]
172+
> The hook will not install automatically — you must run the install command manually.
148173
149174
### Linting
150175

plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessInstallPrePushHookTaskTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws
4444
assertThat(output).contains("Git pre-push hook not found, creating it");
4545
assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push"));
4646

47-
final var content = getTestResource("git_pre_hook/pre-push.created")
47+
final var content = getTestResource("git_pre_hook/pre-push.created-tpl")
4848
.replace("${executor}", "gradle")
4949
.replace("${checkCommand}", "spotlessCheck")
5050
.replace("${applyCommand}", "spotlessApply");
@@ -73,7 +73,7 @@ public void should_append_to_existing_pre_hook_file_when_hook_file_exists() thro
7373
assertThat(output).contains("Installing git pre-push hook");
7474
assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push"));
7575

76-
final var content = getTestResource("git_pre_hook/pre-push.existing-added")
76+
final var content = getTestResource("git_pre_hook/pre-push.existing-added-tpl")
7777
.replace("${executor}", "gradle")
7878
.replace("${checkCommand}", "spotlessCheck")
7979
.replace("${applyCommand}", "spotlessApply");

plugin-maven/README.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ user@machine repo % mvn spotless:check
3737

3838
- [**Quickstart**](#quickstart)
3939
- [Requirements](#requirements)
40-
- [Git hook](#git-hook)
40+
- [Git pre-push hook](#git-pre-push-hook)
4141
- [Binding to maven phase](#binding-to-maven-phase)
4242
- **Languages**
4343
- [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`)
147147
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).
148148

149149

150-
### Git hook
150+
### Git pre-push hook
151151

152-
TODO
152+
You can install a Git pre-push hook that ensures code is properly formatted before being pushed to a remote repository.
153+
This helps catch formatting issues early — before CI fails — and is especially useful for teams not using IDE integrations or pre-commit tools.
154+
155+
#### What the hook does
156+
157+
When installed, the Git `pre-push` hook will:
158+
159+
1. Run `spotless:check`
160+
2. If formatting issues are found:
161+
- It automatically runs `spotless:apply` to fix them
162+
- Aborts the push with a message
163+
- You can then commit the changes and push again
164+
165+
This ensures your code is always clean before it leaves your machine.
166+
167+
#### Installation
168+
169+
Run the following task once in your project:
170+
```console
171+
mvn spotless:install-git-pre-push-hook
172+
```
173+
174+
This installs a `.git/hooks/pre-push` script that runs `spotless:check`, and runs `spotless:apply` if needed.
175+
176+
> [!WARNING]
177+
> The hook will not install automatically — you must run the install command manually.
153178
154179
### Binding to maven phase
155180

plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessInstallPrePushHookMojoTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws
4141
assertThat(output).contains("Git pre-push hook not found, creating it");
4242
assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push"));
4343

44-
final var content = getTestResource("git_pre_hook/pre-push.created")
44+
final var content = getTestResource("git_pre_hook/pre-push.created-tpl")
4545
.replace("${executor}", "mvn")
4646
.replace("${checkCommand}", "spotless:check")
4747
.replace("${applyCommand}", "spotless:apply");
@@ -67,7 +67,7 @@ public void should_append_to_existing_pre_hook_file_when_hook_file_exists() thro
6767
assertThat(output).contains("Installing git pre-push hook");
6868
assertThat(output).contains("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push"));
6969

70-
final var content = getTestResource("git_pre_hook/pre-push.existing-added")
70+
final var content = getTestResource("git_pre_hook/pre-push.existing-added-tpl")
7171
.replace("${executor}", "mvn")
7272
.replace("${checkCommand}", "spotless:check")
7373
.replace("${applyCommand}", "spotless:apply");

testlib/src/main/resources/git_pre_hook/pre-push.created renamed to testlib/src/main/resources/git_pre_hook/pre-push.created-tpl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,3 @@ if ! $SPOTLESS_EXECUTOR ${checkCommand} ; then
99
exit 1
1010
fi
1111
##### SPOTLESS HOOK END #####
12-

testlib/src/main/resources/git_pre_hook/pre-push.existing-added renamed to testlib/src/main/resources/git_pre_hook/pre-push.existing-added-tpl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,3 @@ if ! $SPOTLESS_EXECUTOR ${checkCommand} ; then
5959
exit 1
6060
fi
6161
##### SPOTLESS HOOK END #####
62-
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/bin/sh
2+
3+
# An example hook script to verify what is about to be pushed. Called by "git
4+
# push" after it has checked the remote status, but before anything has been
5+
# pushed. If this script exits with a non-zero status nothing will be pushed.
6+
#
7+
# This hook is called with the following parameters:
8+
#
9+
# $1 -- Name of the remote to which the push is being done
10+
# $2 -- URL to which the push is being done
11+
#
12+
# If pushing without using a named remote those arguments will be equal.
13+
#
14+
# Information about the commits which are being pushed is supplied as lines to
15+
# the standard input in the form:
16+
#
17+
# <local ref> <local oid> <remote ref> <remote oid>
18+
#
19+
# This sample shows how to prevent push of commits where the log message starts
20+
# with "WIP" (work in progress).
21+
22+
remote="$1"
23+
url="$2"
24+
25+
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
26+
27+
while read local_ref local_oid remote_ref remote_oid
28+
do
29+
if test "$local_oid" = "$zero"
30+
then
31+
# Handle delete
32+
:
33+
else
34+
if test "$remote_oid" = "$zero"
35+
then
36+
# New branch, examine all commits
37+
range="$local_oid"
38+
else
39+
# Update to existing branch, examine new commits
40+
range="$remote_oid..$local_oid"
41+
fi
42+
43+
# Check for WIP commit
44+
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
45+
if test -n "$commit"
46+
then
47+
echo >&2 "Found WIP commit in $local_ref, not pushing"
48+
exit 1
49+
fi
50+
fi
51+
done
52+
53+
54+
55+
56+
57+
##### SPOTLESS HOOK START #####
58+
SPOTLESS_EXECUTOR=${executor}
59+
if ! $SPOTLESS_EXECUTOR ${checkCommand} ; then
60+
echo 1>&2 "spotless found problems, running ${applyCommand}; commit the result and re-push"
61+
$SPOTLESS_EXECUTOR ${applyCommand}
62+
exit 1
63+
fi
64+
##### SPOTLESS HOOK END #####

testlib/src/test/java/com/diffplug/spotless/GitPrePushHookInstallerTest.java

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,16 @@ public void should_use_global_gradle_when_gradlew_is_not_installed() throws Exce
7474
assertThat(logs).element(2).isEqualTo("Gradle wrapper is not installed, using global gradle");
7575
assertThat(logs).element(3).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath());
7676

77-
final var content = gradleHookContent("git_pre_hook/pre-push.created", false);
77+
final var content = gradleHookContent("git_pre_hook/pre-push.created-tpl", ExecutorType.GLOBAL);
7878
assertFile(".git/hooks/pre-push").hasContent(content);
7979
}
8080

8181
@Test
82-
public void should_not_create_pre_hook_file_when_hook_already_installed() throws Exception {
82+
public void should_reinstall_pre_hook_file_when_hook_already_installed() throws Exception {
8383
// given
8484
final var gradle = new GitPrePushHookInstallerGradle(logger, rootFolder());
85-
final var hookFile = setFile(".git/hooks/pre-push").toResource("git_pre_hook/pre-push.existing-added");
85+
final var installedGlobally = gradleHookContent("git_pre_hook/pre-push.existing-added-tpl", ExecutorType.GLOBAL);
86+
final var hookFile = setFile(".git/hooks/pre-push").toContent(installedGlobally);
8687

8788
setFile("gradlew").toContent("");
8889
setFile(".git/config").toContent("");
@@ -91,10 +92,13 @@ public void should_not_create_pre_hook_file_when_hook_already_installed() throws
9192
gradle.install();
9293

9394
// then
94-
assertThat(logs).hasSize(2);
95+
assertThat(logs).hasSize(3);
9596
assertThat(logs).element(0).isEqualTo("Installing git pre-push hook");
96-
assertThat(logs).element(1).isEqualTo("Skipping, git pre-push hook already installed " + hookFile.getAbsolutePath());
97-
assertThat(hookFile).content().isEqualTo(getTestResource("git_pre_hook/pre-push.existing-added"));
97+
assertThat(logs).element(1).isEqualTo("Git pre-push hook already installed, reinstalling it");
98+
assertThat(logs).element(2).isEqualTo("Git pre-push hook installed successfully to the file " + hookFile.getAbsolutePath());
99+
100+
final var content = gradleHookContent("git_pre_hook/pre-push.reinstalled-tpl", ExecutorType.WRAPPER);
101+
assertFile(".git/hooks/pre-push").hasContent(content);
98102
}
99103

100104
@Test
@@ -113,7 +117,7 @@ public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws
113117
assertThat(logs).element(1).isEqualTo("Git pre-push hook not found, creating it");
114118
assertThat(logs).element(2).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath());
115119

116-
final var content = gradleHookContent("git_pre_hook/pre-push.created", true);
120+
final var content = gradleHookContent("git_pre_hook/pre-push.created-tpl", ExecutorType.WRAPPER);
117121
assertFile(".git/hooks/pre-push").hasContent(content);
118122
}
119123

@@ -133,7 +137,7 @@ public void should_append_to_existing_pre_hook_file_when_hook_file_exists() thro
133137
assertThat(logs).element(0).isEqualTo("Installing git pre-push hook");
134138
assertThat(logs).element(1).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath());
135139

136-
final var content = gradleHookContent("git_pre_hook/pre-push.existing-added", true);
140+
final var content = gradleHookContent("git_pre_hook/pre-push.existing-added-tpl", ExecutorType.WRAPPER);
137141
assertFile(".git/hooks/pre-push").hasContent(content);
138142
}
139143

@@ -153,7 +157,7 @@ public void should_create_pre_hook_file_for_maven_when_hook_file_does_not_exists
153157
assertThat(logs).element(1).isEqualTo("Git pre-push hook not found, creating it");
154158
assertThat(logs).element(2).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath());
155159

156-
final var content = mavenHookContent("git_pre_hook/pre-push.created", true);
160+
final var content = mavenHookContent("git_pre_hook/pre-push.created-tpl", ExecutorType.WRAPPER);
157161
assertFile(".git/hooks/pre-push").hasContent(content);
158162
}
159163

@@ -173,21 +177,26 @@ public void should_use_global_maven_when_maven_wrapper_is_not_installed() throws
173177
assertThat(logs).element(2).isEqualTo("Maven wrapper is not installed, using global maven");
174178
assertThat(logs).element(3).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath());
175179

176-
final var content = mavenHookContent("git_pre_hook/pre-push.created", false);
180+
final var content = mavenHookContent("git_pre_hook/pre-push.created-tpl", ExecutorType.GLOBAL);
177181
assertFile(".git/hooks/pre-push").hasContent(content);
178182
}
179183

180-
private String gradleHookContent(String resourcePath, boolean isWrapper) {
184+
private String gradleHookContent(String resourcePath, ExecutorType executorType) {
181185
return getTestResource(resourcePath)
182-
.replace("${executor}", isWrapper ? newFile("gradlew").getAbsolutePath() : "gradle")
186+
.replace("${executor}", executorType == ExecutorType.WRAPPER ? newFile("gradlew").getAbsolutePath() : "gradle")
183187
.replace("${checkCommand}", "spotlessCheck")
184188
.replace("${applyCommand}", "spotlessApply");
185189
}
186190

187-
private String mavenHookContent(String resourcePath, boolean isWrapper) {
191+
private String mavenHookContent(String resourcePath, ExecutorType executorType) {
188192
return getTestResource(resourcePath)
189-
.replace("${executor}", isWrapper ? newFile("mvnw").getAbsolutePath() : "mvn")
193+
.replace("${executor}", executorType == ExecutorType.WRAPPER ? newFile("mvnw").getAbsolutePath() : "mvn")
190194
.replace("${checkCommand}", "spotless:check")
191195
.replace("${applyCommand}", "spotless:apply");
192196
}
197+
198+
private enum ExecutorType {
199+
WRAPPER,
200+
GLOBAL
201+
}
193202
}

0 commit comments

Comments
 (0)