Skip to content

Commit 22ca994

Browse files
committed
Use ValueSource for git-aware line ending resolution to fix configuration cache
The default GIT_ATTRIBUTES_FAST_ALLSAME line ending policy reads ~/.gitconfig via JGit during configuration. Gradle's configuration cache fingerprints this read, so when CI workers inject per-build auth tokens into ~/.gitconfig, the cache is invalidated every build. Wrap the git config resolution in a Gradle ValueSource. File reads inside ValueSource.obtain() are not tracked as configuration cache inputs; only the returned value is fingerprinted. This means ~/.gitconfig changes that do not affect the resolved line ending (e.g. auth tokens) no longer invalidate the cache.
1 parent ca71301 commit 22ca994

4 files changed

Lines changed: 152 additions & 45 deletions

File tree

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,8 +1096,18 @@ protected void setupTask(SpotlessTask task) {
10961096
task.setSteps(steps);
10971097
Directory projectDir = getProject().getLayout().getProjectDirectory();
10981098
LineEnding lineEndings = getLineEndings();
1099-
task.setLineEndingsPolicy(
1100-
getProject().provider(() -> lineEndings.createPolicy(projectDir.getAsFile(), () -> totalTarget)));
1099+
if (lineEndings == LineEnding.GIT_ATTRIBUTES_FAST_ALLSAME || lineEndings == LineEnding.GIT_ATTRIBUTES) {
1100+
// Wrap git-aware line ending resolution in a ValueSource so that file reads
1101+
// (e.g. ~/.gitconfig) are not tracked as configuration cache inputs;
1102+
// only the resolved line ending string is fingerprinted.
1103+
task.setLineEndingsPolicy(
1104+
getProject().getProviders().of(GitConfigLineEndingValueSource.class, spec -> {
1105+
spec.getParameters().getProjectDir().set(projectDir);
1106+
}));
1107+
} else {
1108+
task.setLineEndingsPolicy(
1109+
getProject().provider(() -> lineEndings.createPolicy(projectDir.getAsFile(), () -> totalTarget)));
1110+
}
11011111
spotless.getSpotlessTaskService().get().hookSubprojectTask(getProject(), task);
11021112
task.setupRatchet(getRatchetFrom() != null ? getRatchetFrom() : "");
11031113
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2025-2026 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.gradle.spotless;
17+
18+
import java.io.File;
19+
20+
import javax.annotation.Nullable;
21+
22+
import org.eclipse.jgit.lib.Config;
23+
import org.eclipse.jgit.lib.ConfigConstants;
24+
import org.eclipse.jgit.lib.Constants;
25+
import org.eclipse.jgit.lib.CoreConfig.AutoCRLF;
26+
import org.eclipse.jgit.lib.CoreConfig.EOL;
27+
import org.eclipse.jgit.storage.file.FileBasedConfig;
28+
import org.eclipse.jgit.util.FS;
29+
import org.eclipse.jgit.util.SystemReader;
30+
import org.gradle.api.file.DirectoryProperty;
31+
import org.gradle.api.provider.ValueSource;
32+
import org.gradle.api.provider.ValueSourceParameters;
33+
34+
import com.diffplug.common.base.Errors;
35+
import com.diffplug.spotless.LineEnding;
36+
37+
/**
38+
* A Gradle {@link ValueSource} that resolves the default line ending from git config.
39+
*
40+
* <p>File reads inside {@code obtain()} are not tracked as configuration cache inputs;
41+
* only the returned value is fingerprinted. This prevents {@code ~/.gitconfig} changes
42+
* (e.g. CI-injected auth tokens) from invalidating the configuration cache, while still
43+
* correctly invalidating when the resolved line ending actually changes.
44+
*/
45+
public abstract class GitConfigLineEndingValueSource implements ValueSource<LineEnding.Policy, GitConfigLineEndingValueSource.Params> {
46+
47+
public interface Params extends ValueSourceParameters {
48+
DirectoryProperty getProjectDir();
49+
}
50+
51+
@Override
52+
public @Nullable LineEnding.Policy obtain() {
53+
File projectDir = getParameters().getProjectDir().get().getAsFile();
54+
55+
FS.DETECTED.setGitSystemConfig(new File("no-global-git-config-for-spotless"));
56+
57+
FileBasedConfig systemConfig = SystemReader.getInstance().openSystemConfig(null, FS.DETECTED);
58+
Errors.log().run(systemConfig::load);
59+
FileBasedConfig userConfig = SystemReader.getInstance().openUserConfig(systemConfig, FS.DETECTED);
60+
Errors.log().run(userConfig::load);
61+
62+
// Read repo-specific config if we're in a git repo
63+
Config config = userConfig;
64+
File gitDir = findGitDir(projectDir);
65+
if (gitDir != null) {
66+
FileBasedConfig repoConfig = new FileBasedConfig(userConfig, new File(gitDir, Constants.CONFIG), FS.DETECTED);
67+
Errors.log().run(repoConfig::load);
68+
config = repoConfig;
69+
}
70+
71+
return defaultLineEnding(config).createPolicy();
72+
}
73+
74+
/** Walks up from projectDir looking for a .git directory. */
75+
private static @Nullable File findGitDir(File dir) {
76+
while (dir != null) {
77+
File dotGit = new File(dir, Constants.DOT_GIT);
78+
if (dotGit.isDirectory()) {
79+
return dotGit;
80+
}
81+
dir = dir.getParentFile();
82+
}
83+
return null;
84+
}
85+
86+
private static LineEnding defaultLineEnding(Config config) {
87+
AutoCRLF autoCRLF = config.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_AUTOCRLF, AutoCRLF.FALSE);
88+
if (autoCRLF == AutoCRLF.TRUE) {
89+
return LineEnding.WINDOWS;
90+
} else if (autoCRLF == AutoCRLF.INPUT) {
91+
return LineEnding.UNIX;
92+
} else if (autoCRLF == AutoCRLF.FALSE) {
93+
EOL eol = config.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_EOL, EOL.NATIVE);
94+
switch (eol) {
95+
case CRLF:
96+
return LineEnding.WINDOWS;
97+
case LF:
98+
return LineEnding.UNIX;
99+
case NATIVE:
100+
return LineEnding.PLATFORM_NATIVE;
101+
default:
102+
throw new IllegalArgumentException("Unknown eol " + eol);
103+
}
104+
} else {
105+
throw new IllegalStateException("Unexpected value for autoCRLF " + autoCRLF);
106+
}
107+
}
108+
}

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,9 @@ void registerApplyAlreadyRan(SpotlessApply task) {
9999
}
100100

101101
// <GitRatchet>
102-
private GitRatchetGradle ratchet;
102+
private final GitRatchetGradle ratchet = new GitRatchetGradle();
103103

104104
GitRatchetGradle getRatchet() {
105-
if (ratchet == null) {
106-
ratchet = new GitRatchetGradle();
107-
}
108105
return ratchet;
109106
}
110107

@@ -115,9 +112,7 @@ public void onFinish(FinishEvent var1) {
115112

116113
@Override
117114
public void close() throws Exception {
118-
if (ratchet != null) {
119-
ratchet.close();
120-
}
115+
ratchet.close();
121116
}
122117
// </GitRatchet>
123118

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

Lines changed: 30 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2025 DiffPlug
2+
* Copyright 2020-2026 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,7 +18,6 @@
1818
import java.io.File;
1919
import java.io.IOException;
2020
import java.nio.file.Files;
21-
import java.nio.file.Path;
2221

2322
import org.assertj.core.api.Assertions;
2423
import org.gradle.testkit.runner.GradleRunner;
@@ -66,46 +65,41 @@ public void helpConfiguresIfTasksAreCreated() throws IOException {
6665

6766
@Test
6867
public void configurationCacheNotInvalidatedByGitconfig() throws IOException {
69-
// ~/.gitconfig is read by JGit at class-load time via the SystemReader.
70-
// If GitRatchetGradle is loaded eagerly during configuration, Gradle's
71-
// configuration cache fingerprints ~/.gitconfig. When its content changes
72-
// (e.g. CI workers inject per-build auth tokens), the cache is invalidated.
73-
// This test verifies that changing ~/.gitconfig between runs does not
74-
// invalidate the configuration cache.
75-
Path gitconfig = Path.of(System.getProperty("user.home"), ".gitconfig");
76-
boolean existed = Files.exists(gitconfig);
77-
String originalContent = existed ? Files.readString(gitconfig) : null;
78-
try {
79-
Files.writeString(gitconfig, "[user]\n\tname = test\n");
68+
// The default GIT_ATTRIBUTES_FAST_ALLSAME line ending policy reads
69+
// ~/.gitconfig via JGit to resolve core.eol / core.autocrlf. This test
70+
// verifies that changing ~/.gitconfig between runs does not invalidate
71+
// the configuration cache, because the git config reads happen inside a
72+
// ValueSource whose file accesses are not tracked as config cache inputs.
73+
File gitconfig = new File(System.getProperty("user.home"), ".gitconfig");
74+
byte[] originalContent = gitconfig.exists() ? Files.readAllBytes(gitconfig.toPath()) : null;
8075

81-
setFile("build.gradle").toLines(
82-
"plugins {",
83-
" id 'com.diffplug.spotless'",
84-
"}",
85-
"repositories { mavenCentral() }",
86-
"apply plugin: 'java'",
87-
"spotless {",
88-
" java {",
89-
" googleJavaFormat()",
90-
" }",
91-
"}");
92-
setFile("src/main/java/test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test");
76+
setFile("build.gradle").toLines(
77+
"plugins {",
78+
" id 'com.diffplug.spotless'",
79+
"}",
80+
"repositories { mavenCentral() }",
81+
"apply plugin: 'java'",
82+
"spotless {",
83+
" java {",
84+
" googleJavaFormat()",
85+
" }",
86+
"}");
87+
setFile("src/main/java/test.java").toResource("java/googlejavaformat/JavaCodeFormatted.test");
9388

94-
// first run stores the configuration cache
95-
gradleRunner().withArguments("help").build();
89+
try {
90+
Files.writeString(gitconfig.toPath(), "[user]\n\tname = test\n");
91+
gradleRunner().withArguments("spotlessCheck").build();
9692

97-
// change ~/.gitconfig content between runs
98-
Files.writeString(gitconfig, "[user]\n\tname = test\n[http]\n\textraheader = changed\n");
93+
// change .gitconfig content between runs (simulates CI auth token injection)
94+
Files.writeString(gitconfig.toPath(), "[user]\n\tname = test\n[http]\n\textraheader = changed\n");
9995

100-
// second run must reuse the configuration cache despite the change
101-
String output = gradleRunner().withArguments("help").build().getOutput();
102-
Assertions.assertThat(output).contains("Reusing configuration cache.");
96+
String output = gradleRunner().withArguments("spotlessCheck").build().getOutput();
97+
Assertions.assertThat(output).contains("Reusing configuration cache");
10398
} finally {
104-
// restore original ~/.gitconfig
10599
if (originalContent != null) {
106-
Files.writeString(gitconfig, originalContent);
107-
} else if (Files.exists(gitconfig)) {
108-
Files.delete(gitconfig);
100+
Files.write(gitconfig.toPath(), originalContent);
101+
} else {
102+
Files.deleteIfExists(gitconfig.toPath());
109103
}
110104
}
111105
}

0 commit comments

Comments
 (0)