Skip to content

Commit 32a8292

Browse files
committed
git pre push hook gradle config cache fix + parallel execution handling
1 parent 5e6ab47 commit 32a8292

File tree

5 files changed

+138
-39
lines changed

5 files changed

+138
-39
lines changed

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import java.nio.file.Files;
2525
import java.util.Locale;
2626

27+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
28+
2729
/**
2830
* Abstract class responsible for installing a Git pre-push hook in a repository.
2931
* This class ensures that specific checks and logic are run before a push operation in Git.
@@ -35,6 +37,10 @@ public abstract class GitPrePushHookInstaller {
3537
private static final String HOOK_HEADER = "##### SPOTLESS HOOK START #####";
3638
private static final String HOOK_FOOTER = "##### SPOTLESS HOOK END #####";
3739

40+
private static final Object LOCK = new Object();
41+
42+
private static volatile boolean installing = false;
43+
3844
/**
3945
* Logger for recording informational and error messages during the installation process.
4046
*/
@@ -56,6 +62,41 @@ public GitPrePushHookInstaller(GitPreHookLogger logger, File root) {
5662
this.root = requireNonNull(root, "root file can not be null");
5763
}
5864

65+
/**
66+
* Installs the Git pre-push hook while ensuring thread safety and preventing parallel installations.
67+
*
68+
* The method:
69+
* 1. Uses a thread-safe mechanism to prevent concurrent installations
70+
* 2. If a parallel installation is detected, logs a warning and skips the installation
71+
* 3. Uses a synchronized block with a static lock object to ensure thread safety
72+
*
73+
* The installation process sets a flag during installation and resets it afterwards,
74+
* using the {@link #doInstall()} method to perform the actual installation.
75+
*
76+
* @throws Exception if any error occurs during installation
77+
*/
78+
@SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", justification = "This is a safe usage of a static lock object")
79+
public void install() throws Exception {
80+
if (installing) {
81+
logger.warn("Parallel Spotless Git pre-push hook installation detected, skipping installation");
82+
return;
83+
}
84+
85+
synchronized (LOCK) {
86+
if (installing) {
87+
logger.warn("Parallel Spotless Git pre-push hook installation detected, skipping installation");
88+
return;
89+
}
90+
91+
try {
92+
installing = true;
93+
doInstall();
94+
} finally {
95+
installing = false;
96+
}
97+
}
98+
}
99+
59100
/**
60101
* Installs the Git pre-push hook into the repository.
61102
*
@@ -70,7 +111,7 @@ public GitPrePushHookInstaller(GitPreHookLogger logger, File root) {
70111
*
71112
* @throws Exception if any error occurs during installation.
72113
*/
73-
public void install() throws Exception {
114+
private void doInstall() throws Exception {
74115
logger.info("Installing git pre-push hook");
75116

76117
if (!isGitInstalled()) {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public SpotlessExtensionImpl(Project project) {
4141
rootInstallPreHook = project.getTasks().register(EXTENSION + INSTALL_GIT_PRE_PUSH_HOOK, SpotlessInstallPrePushHookTask.class, task -> {
4242
task.setGroup(BUILD_SETUP_TASK_GROUP);
4343
task.setDescription(INSTALL_GIT_PRE_PUSH_HOOK_DESCRIPTION);
44+
task.getRootDir().set(project.getRootDir());
4445
});
4546

4647
project.afterEvaluate(unused -> {

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
*/
1616
package com.diffplug.gradle.spotless;
1717

18+
import java.io.File;
19+
1820
import org.gradle.api.DefaultTask;
21+
import org.gradle.api.provider.Property;
22+
import org.gradle.api.tasks.Internal;
1923
import org.gradle.api.tasks.TaskAction;
2024
import org.gradle.work.DisableCachingByDefault;
2125

@@ -30,7 +34,10 @@
3034
* <p>The task leverages {@link GitPrePushHookInstallerGradle} to implement the installation process.
3135
*/
3236
@DisableCachingByDefault(because = "not worth caching")
33-
public class SpotlessInstallPrePushHookTask extends DefaultTask {
37+
public abstract class SpotlessInstallPrePushHookTask extends DefaultTask {
38+
39+
@Internal
40+
abstract Property<File> getRootDir();
3441

3542
/**
3643
* Executes the task to install the Git pre-push hook.
@@ -60,7 +67,7 @@ public void error(String format, Object... arguments) {
6067
}
6168
};
6269

63-
final var installer = new GitPrePushHookInstallerGradle(logger, getProject().getRootDir());
70+
final var installer = new GitPrePushHookInstallerGradle(logger, getRootDir().get());
6471
installer.install();
6572
}
6673
}

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
@@ -35,7 +35,7 @@ public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws
3535

3636
// when
3737
var output = gradleRunner()
38-
.withArguments("spotlessInstallGitPrePushHook")
38+
.withArguments("spotlessInstallGitPrePushHook", "--system-prop=org.gradle.configuration-cache=true")
3939
.build()
4040
.getOutput();
4141

@@ -65,7 +65,7 @@ public void should_append_to_existing_pre_hook_file_when_hook_file_exists() thro
6565

6666
// when
6767
final var output = gradleRunner()
68-
.withArguments("spotlessInstallGitPrePushHook")
68+
.withArguments("spotlessInstallGitPrePushHook", "--system-prop=org.gradle.configuration-cache=true")
6969
.build()
7070
.getOutput();
7171

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

Lines changed: 84 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717

1818
import static com.diffplug.spotless.GitPrePushHookInstaller.Executor.GRADLE;
1919
import static com.diffplug.spotless.GitPrePushHookInstaller.Executor.MAVEN;
20+
import static java.util.stream.Collectors.toList;
2021
import static org.assertj.core.api.Assertions.assertThat;
2122

22-
import java.util.ArrayList;
2323
import java.util.List;
24+
import java.util.concurrent.CopyOnWriteArrayList;
25+
import java.util.stream.IntStream;
2426

2527
import org.junit.jupiter.api.AfterEach;
2628
import org.junit.jupiter.api.BeforeEach;
@@ -31,7 +33,7 @@
3133
class GitPrePushHookInstallerTest extends ResourceHarness {
3234
private final static String OS = System.getProperty("os.name");
3335

34-
private final List<String> logs = new ArrayList<>();
36+
private final List<String> logs = new CopyOnWriteArrayList<>();
3537
private final GitPreHookLogger logger = new GitPreHookLogger() {
3638
@Override
3739
public void info(String format, Object... arguments) {
@@ -68,9 +70,9 @@ public void should_not_create_pre_hook_file_when_git_is_not_installed() throws E
6870
gradle.install();
6971

7072
// then
71-
assertThat(logs).hasSize(2);
72-
assertThat(logs).element(0).isEqualTo("Installing git pre-push hook");
73-
assertThat(logs).element(1).isEqualTo("Git not found in root directory");
73+
assertThat(logs).containsExactly(
74+
"Installing git pre-push hook",
75+
"Git not found in root directory");
7476
assertThat(newFile(".git/hooks/pre-push")).doesNotExist();
7577
}
7678

@@ -84,11 +86,11 @@ public void should_use_global_gradle_when_gradlew_is_not_installed() throws Exce
8486
gradle.install();
8587

8688
// then
87-
assertThat(logs).hasSize(4);
88-
assertThat(logs).element(0).isEqualTo("Installing git pre-push hook");
89-
assertThat(logs).element(1).isEqualTo("Git pre-push hook not found, creating it");
90-
assertThat(logs).element(2).isEqualTo("Local gradle wrapper (gradlew) not found, falling back to global command 'gradle'");
91-
assertThat(logs).element(3).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath());
89+
assertThat(logs).containsExactly(
90+
"Installing git pre-push hook",
91+
"Git pre-push hook not found, creating it",
92+
"Local gradle wrapper (gradlew) not found, falling back to global command 'gradle'",
93+
"Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath());
9294

9395
final var content = gradleHookContent("git_pre_hook/pre-push.created-tpl", ExecutorType.GLOBAL);
9496
assertFile(".git/hooks/pre-push").hasContent(content);
@@ -108,10 +110,10 @@ public void should_reinstall_pre_hook_file_when_hook_already_installed() throws
108110
gradle.install();
109111

110112
// then
111-
assertThat(logs).hasSize(3);
112-
assertThat(logs).element(0).isEqualTo("Installing git pre-push hook");
113-
assertThat(logs).element(1).isEqualTo("Git pre-push hook already installed, reinstalling it");
114-
assertThat(logs).element(2).isEqualTo("Git pre-push hook installed successfully to the file " + hookFile.getAbsolutePath());
113+
assertThat(logs).containsExactly(
114+
"Installing git pre-push hook",
115+
"Git pre-push hook already installed, reinstalling it",
116+
"Git pre-push hook installed successfully to the file " + hookFile.getAbsolutePath());
115117

116118
final var content = gradleHookContent("git_pre_hook/pre-push.existing-installed-end-tpl", ExecutorType.WRAPPER);
117119
assertFile(".git/hooks/pre-push").hasContent(content);
@@ -131,10 +133,10 @@ public void should_reinstall_pre_hook_file_when_hook_already_installed_in_the_mi
131133
gradle.install();
132134

133135
// then
134-
assertThat(logs).hasSize(3);
135-
assertThat(logs).element(0).isEqualTo("Installing git pre-push hook");
136-
assertThat(logs).element(1).isEqualTo("Git pre-push hook already installed, reinstalling it");
137-
assertThat(logs).element(2).isEqualTo("Git pre-push hook installed successfully to the file " + hookFile.getAbsolutePath());
136+
assertThat(logs).containsExactly(
137+
"Installing git pre-push hook",
138+
"Git pre-push hook already installed, reinstalling it",
139+
"Git pre-push hook installed successfully to the file " + hookFile.getAbsolutePath());
138140

139141
final var content = gradleHookContent("git_pre_hook/pre-push.existing-reinstalled-middle-tpl", ExecutorType.WRAPPER);
140142
assertFile(".git/hooks/pre-push").hasContent(content);
@@ -171,10 +173,10 @@ public void should_create_pre_hook_file_when_hook_file_does_not_exists() throws
171173
gradle.install();
172174

173175
// then
174-
assertThat(logs).hasSize(3);
175-
assertThat(logs).element(0).isEqualTo("Installing git pre-push hook");
176-
assertThat(logs).element(1).isEqualTo("Git pre-push hook not found, creating it");
177-
assertThat(logs).element(2).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath());
176+
assertThat(logs).containsExactly(
177+
"Installing git pre-push hook",
178+
"Git pre-push hook not found, creating it",
179+
"Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath());
178180

179181
final var content = gradleHookContent("git_pre_hook/pre-push.created-tpl", ExecutorType.WRAPPER);
180182
assertFile(".git/hooks/pre-push").hasContent(content);
@@ -192,9 +194,9 @@ public void should_append_to_existing_pre_hook_file_when_hook_file_exists() thro
192194
gradle.install();
193195

194196
// then
195-
assertThat(logs).hasSize(2);
196-
assertThat(logs).element(0).isEqualTo("Installing git pre-push hook");
197-
assertThat(logs).element(1).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath());
197+
assertThat(logs).containsExactly(
198+
"Installing git pre-push hook",
199+
"Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath());
198200

199201
final var content = gradleHookContent("git_pre_hook/pre-push.existing-installed-end-tpl", ExecutorType.WRAPPER);
200202
assertFile(".git/hooks/pre-push").hasContent(content);
@@ -211,10 +213,10 @@ public void should_create_pre_hook_file_for_maven_when_hook_file_does_not_exists
211213
gradle.install();
212214

213215
// then
214-
assertThat(logs).hasSize(3);
215-
assertThat(logs).element(0).isEqualTo("Installing git pre-push hook");
216-
assertThat(logs).element(1).isEqualTo("Git pre-push hook not found, creating it");
217-
assertThat(logs).element(2).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath());
216+
assertThat(logs).containsExactly(
217+
"Installing git pre-push hook",
218+
"Git pre-push hook not found, creating it",
219+
"Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath());
218220

219221
final var content = mavenHookContent("git_pre_hook/pre-push.created-tpl", ExecutorType.WRAPPER);
220222
assertFile(".git/hooks/pre-push").hasContent(content);
@@ -230,11 +232,11 @@ public void should_use_global_maven_when_maven_wrapper_is_not_installed() throws
230232
gradle.install();
231233

232234
// then
233-
assertThat(logs).hasSize(4);
234-
assertThat(logs).element(0).isEqualTo("Installing git pre-push hook");
235-
assertThat(logs).element(1).isEqualTo("Git pre-push hook not found, creating it");
236-
assertThat(logs).element(2).isEqualTo("Local maven wrapper (mvnw) not found, falling back to global command 'mvn'");
237-
assertThat(logs).element(3).isEqualTo("Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath());
235+
assertThat(logs).containsExactly(
236+
"Installing git pre-push hook",
237+
"Git pre-push hook not found, creating it",
238+
"Local maven wrapper (mvnw) not found, falling back to global command 'mvn'",
239+
"Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath());
238240

239241
final var content = mavenHookContent("git_pre_hook/pre-push.created-tpl", ExecutorType.GLOBAL);
240242
assertFile(".git/hooks/pre-push").hasContent(content);
@@ -334,6 +336,30 @@ public void should_use_gradle_global_when_bat_and_cmd_files_not_exists_for_windo
334336
assertThat(hook).contains("SPOTLESS_EXECUTOR=gradle");
335337
}
336338

339+
@Test
340+
public void should_handle_parallel_installation() {
341+
// given
342+
setFile(".git/config").toContent("");
343+
344+
// when
345+
parallelRun(() -> {
346+
final var gradle = new GitPrePushHookInstallerGradle(logger, rootFolder());
347+
gradle.install();
348+
});
349+
350+
// then
351+
assertThat(logs).containsExactlyInAnyOrder(
352+
"Installing git pre-push hook",
353+
"Git pre-push hook not found, creating it",
354+
"Parallel Spotless Git pre-push hook installation detected, skipping installation",
355+
"Parallel Spotless Git pre-push hook installation detected, skipping installation",
356+
"Local gradle wrapper (gradlew) not found, falling back to global command 'gradle'",
357+
"Git pre-push hook installed successfully to the file " + newFile(".git/hooks/pre-push").getAbsolutePath());
358+
359+
final var content = gradleHookContent("git_pre_hook/pre-push.created-tpl", ExecutorType.GLOBAL);
360+
assertFile(".git/hooks/pre-push").hasContent(content);
361+
}
362+
337363
private String gradleHookContent(String resourcePath, ExecutorType executorType) {
338364
return getTestResource(resourcePath)
339365
.replace("${executor}", executorType == ExecutorType.WRAPPER ? "./" + newFile("gradlew").getName() : "gradle")
@@ -348,7 +374,31 @@ private String mavenHookContent(String resourcePath, ExecutorType executorType)
348374
.replace("${applyCommand}", "spotless:apply");
349375
}
350376

377+
private void parallelRun(ThrowableRun runnable) {
378+
IntStream.range(0, 3)
379+
.mapToObj(i -> new Thread(() -> {
380+
try {
381+
runnable.run();
382+
} catch (Exception e) {
383+
throw new RuntimeException(e);
384+
}
385+
}))
386+
.peek(Thread::start)
387+
.collect(toList())
388+
.forEach(t -> {
389+
try {
390+
t.join();
391+
} catch (InterruptedException e) {
392+
throw new RuntimeException(e);
393+
}
394+
});
395+
}
396+
351397
private enum ExecutorType {
352398
WRAPPER, GLOBAL
353399
}
400+
401+
private interface ThrowableRun {
402+
void run() throws Exception;
403+
}
354404
}

0 commit comments

Comments
 (0)