Skip to content

Commit 5d802a8

Browse files
authored
[fix] Add git worktree support for ratchet functionality (#2779 fixes #2728)
2 parents 4a59207 + c55693e commit 5d802a8

3 files changed

Lines changed: 109 additions & 5 deletions

File tree

CHANGES.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,19 @@ This document is intended for Spotless developers.
1010
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
1111

1212
## [Unreleased]
13+
1314
### Added
1415
- Add a `expandWildcardImports` API for java ([#2679](https://github.com/diffplug/spotless/issues/2594))
1516
- Add the ability to specify a wildcard version (`*`) for external formatter executables. ([#2757](https://github.com/diffplug/spotless/issues/2757))
1617
### Fixed
1718
- Prevent race conditions when multiple npm-based formatters launch the server process simultaneously while sharing the same `node_modules` directory. ([#2786](https://github.com/diffplug/spotless/pull/2786))
19+
- Git ratchet no longer throws an error with Git worktrees. ([#2779](https://github.com/diffplug/spotless/issues/2779))
1820
### Changes
1921
- Bump default `ktfmt` version to latest `0.59` -> `0.61`. ([2804](https://github.com/diffplug/spotless/pull/2804))
2022
- Bump default `ktlint` version to latest `1.7.1` -> `1.8.0`. ([2763](https://github.com/diffplug/spotless/pull/2763))
2123
- Bump default `gherkin-utils` version to latest `9.2.0` -> `10.0.0`. ([#2619](https://github.com/diffplug/spotless/pull/2619))
2224

25+
2326
## [4.1.0] - 2025-11-18
2427
### Changes
2528
- Bump default `ktfmt` version to latest `0.58` -> `0.59`. ([#2681](https://github.com/diffplug/spotless/pull/2681)
@@ -138,7 +141,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
138141
### Changed
139142
* **BREAKING** Moved `PaddedCell.DirtyState` to its own top-level class with new methods. ([#2148](https://github.com/diffplug/spotless/pull/2148))
140143
* **BREAKING** Removed `isClean`, `applyTo`, and `applyToAndReturnResultIfDirty` from `Formatter` because users should instead use `DirtyState`.
141-
* `FenceStep` now uses `ConfigurationCacheHack`. ([#2378](https://github.com/diffplug/spotless/pull/2378) fixes [#2317](https://github.com/diffplug/spotless/issues/2317))
144+
* `FenceStep` now uses `ConfigurationCacheHack`. ([#2378](https://github.com/diffplug/spotless/pull/2378) fixes [#2317](https://github.com/diffplug/spotless/issues/2317))
142145
### Fixed
143146
* `ktlint` steps now read from the `string` instead of the `file` so they don't clobber earlier steps. (fixes [#1599](https://github.com/diffplug/spotless/issues/1599))
144147

@@ -149,7 +152,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
149152
* `ConfigurationCacheHack` so we can support Gradle's configuration cache and remote build cache at the same time. ([#2298](https://github.com/diffplug/spotless/pull/2298) fixes [#2168](https://github.com/diffplug/spotless/issues/2168))
150153
### Changed
151154
* Support configuring the Equo P2 cache. ([#2238](https://github.com/diffplug/spotless/pull/2238))
152-
* Add explicit support for JSONC / CSS via biome, via the file extensions `.css` and `.jsonc`.
155+
* Add explicit support for JSONC / CSS via biome, via the file extensions `.css` and `.jsonc`.
153156
([#2259](https://github.com/diffplug/spotless/pull/2259))
154157
* Bump default `buf` version to latest `1.24.0` -> `1.44.0`. ([#2291](https://github.com/diffplug/spotless/pull/2291))
155158
* Bump default `google-java-format` version to latest `1.23.0` -> `1.24.0`. ([#2294](https://github.com/diffplug/spotless/pull/2294))

lib-extra/src/main/java/com/diffplug/spotless/extra/GitRatchet.java

Lines changed: 24 additions & 2 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.
@@ -147,7 +147,7 @@ private static boolean worktreeIsCleanCheckout(TreeWalk treeWalk) {
147147
*/
148148
protected Repository repositoryFor(Project project) throws IOException {
149149
File projectGitDir = GitWorkarounds.getDotGitDir(getDir(project));
150-
if (projectGitDir == null || !RepositoryCache.FileKey.isGitRepository(projectGitDir, FS.DETECTED)) {
150+
if (projectGitDir == null || !isGitRepository(projectGitDir)) {
151151
throw new IllegalArgumentException("Cannot find git repository in any parent directory");
152152
}
153153
Repository repo = gitRoots.get(projectGitDir);
@@ -158,6 +158,28 @@ protected Repository repositoryFor(Project project) throws IOException {
158158
return repo;
159159
}
160160

161+
/**
162+
* Checks if the given directory is a valid git repository, including worktree repositories.
163+
* This is more lenient than {@link RepositoryCache.FileKey#isGitRepository} which doesn't
164+
* properly handle worktrees where some files are in the common directory.
165+
*/
166+
private static boolean isGitRepository(File gitDir) {
167+
if (!gitDir.isDirectory()) {
168+
return false;
169+
}
170+
// For worktrees, HEAD and commondir must exist (objects, refs, etc. are in commondir)
171+
// For regular repos, HEAD and objects must exist
172+
File headFile = new File(gitDir, Constants.HEAD);
173+
File commonDirFile = new File(gitDir, "commondir");
174+
if (commonDirFile.exists()) {
175+
// This is a worktree - just check for HEAD and commondir
176+
return headFile.exists();
177+
} else {
178+
// This is a regular repository - use standard check
179+
return RepositoryCache.FileKey.isGitRepository(gitDir, FS.DETECTED);
180+
}
181+
}
182+
161183
protected abstract File getDir(Project project);
162184

163185
protected abstract @Nullable Project getParent(Project project);

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

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 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.
@@ -15,7 +15,9 @@
1515
*/
1616
package com.diffplug.spotless.maven;
1717

18+
import java.io.File;
1819
import java.io.IOException;
20+
import java.nio.file.Files;
1921

2022
import org.eclipse.jgit.api.Git;
2123
import org.eclipse.jgit.api.errors.GitAPIException;
@@ -150,4 +152,81 @@ private void assertClean() throws Exception {
150152
private void assertDirty() throws Exception {
151153
mavenRunner().withArguments("spotless:check").runHasError();
152154
}
155+
156+
@Test
157+
void worktreeSupport() throws Exception {
158+
// Set up main repository with explicit 'main' branch
159+
// (Git.init() uses system default which may be 'master' on some systems)
160+
Git mainGit = Git.init().setDirectory(rootFolder()).call();
161+
RefDatabase refDB = mainGit.getRepository().getRefDatabase();
162+
refDB.newUpdate(Constants.R_HEADS + "main", false).setNewObjectId(ObjectId.zeroId());
163+
refDB.newUpdate(Constants.HEAD, false).link(Constants.R_HEADS + "main");
164+
refDB.newUpdate(Constants.R_HEADS + Constants.MASTER, false).delete();
165+
166+
setFile(TEST_PATH).toContent("HELLO");
167+
mainGit.add().addFilepattern(TEST_PATH).call();
168+
mainGit.commit().setMessage("Initial commit").call();
169+
mainGit.tag().setName("baseline").call();
170+
171+
// Set up a worktree manually (JGit doesn't support worktrees)
172+
File worktreeDir = newFolder("worktree");
173+
File mainGitDir = new File(rootFolder(), ".git");
174+
File worktreeGitDir = new File(rootFolder(), ".git/worktrees/worktree");
175+
176+
// Create worktree structure
177+
setFile(".git/worktrees/worktree/gitdir").toContent(worktreeDir.getAbsolutePath() + "/.git");
178+
setFile(".git/worktrees/worktree/commondir").toContent("../..");
179+
setFile(".git/worktrees/worktree/HEAD").toContent("ref: refs/heads/main");
180+
setFile("worktree/.git").toContent("gitdir: " + worktreeGitDir.getAbsolutePath());
181+
182+
// Copy the test file to worktree (same as baseline so ratchet considers it clean)
183+
setFile("worktree/" + TEST_PATH).toContent("HELLO");
184+
185+
// Copy Maven wrapper files to worktree
186+
setFile("worktree/.gitattributes").toContent("* text eol=lf");
187+
Files.copy(new File(rootFolder(), "mvnw").toPath(), new File(worktreeDir, "mvnw").toPath());
188+
new File(worktreeDir, "mvnw").setExecutable(true);
189+
Files.copy(new File(rootFolder(), "mvnw.cmd").toPath(), new File(worktreeDir, "mvnw.cmd").toPath());
190+
new File(worktreeDir, ".mvn/wrapper").mkdirs();
191+
Files.copy(new File(rootFolder(), ".mvn/wrapper/maven-wrapper.jar").toPath(),
192+
new File(worktreeDir, ".mvn/wrapper/maven-wrapper.jar").toPath());
193+
Files.copy(new File(rootFolder(), ".mvn/wrapper/maven-wrapper.properties").toPath(),
194+
new File(worktreeDir, ".mvn/wrapper/maven-wrapper.properties").toPath());
195+
196+
// Create a pom.xml in the worktree
197+
setFile("worktree/pom.xml").toLines(
198+
"<project xmlns='http://maven.apache.org/POM/4.0.0' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd'>",
199+
" <modelVersion>4.0.0</modelVersion>",
200+
" <groupId>test</groupId>",
201+
" <artifactId>test</artifactId>",
202+
" <version>1.0.0</version>",
203+
" <build>",
204+
" <plugins>",
205+
" <plugin>",
206+
" <groupId>com.diffplug.spotless</groupId>",
207+
" <artifactId>spotless-maven-plugin</artifactId>",
208+
" <version>" + System.getProperty("spotlessMavenPluginVersion") + "</version>",
209+
" <configuration>",
210+
" <formats>",
211+
" <format>",
212+
" <ratchetFrom>baseline</ratchetFrom>",
213+
" <includes>",
214+
" <include>" + TEST_PATH + "</include>",
215+
" </includes>",
216+
" <replace>",
217+
" <name>Lowercase hello</name>",
218+
" <search>HELLO</search>",
219+
" <replacement>hello</replacement>",
220+
" </replace>",
221+
" </format>",
222+
" </formats>",
223+
" </configuration>",
224+
" </plugin>",
225+
" </plugins>",
226+
" </build>",
227+
"</project>");
228+
229+
// Verify that spotless works correctly in a git worktree
230+
mavenRunner().withProjectDir(worktreeDir).withArguments("spotless:check").runNoError();
231+
}
153232
}

0 commit comments

Comments
 (0)