From 7cf749cb25ac1eb661e4658e5c8e1273cb5ff12d Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Wed, 3 Dec 2025 15:23:56 -0800 Subject: [PATCH 1/2] [fix] Add git worktree support for ratchet functionality Git ratchet functionality failed in worktree directories with "Cannot find git repository in any parent directory" because JGit's RepositoryCache.FileKey.isGitRepository() doesn't properly recognize worktree git directories. Worktrees have a different structure where some files (objects, refs, etc.) are shared via a commondir file pointing to the main repository. This commit adds a custom isGitRepository() method that detects worktrees by checking for the commondir file and validates them appropriately. Also adds comprehensive test coverage for worktree support. Fixes #2728 Co-Authored-By: Claude --- .../diffplug/spotless/extra/GitRatchet.java | 24 +++++- .../spotless/maven/GitRatchetMavenTest.java | 73 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/GitRatchet.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/GitRatchet.java index 76d8652b1f..6e022f22e4 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/GitRatchet.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/GitRatchet.java @@ -147,7 +147,7 @@ private static boolean worktreeIsCleanCheckout(TreeWalk treeWalk) { */ protected Repository repositoryFor(Project project) throws IOException { File projectGitDir = GitWorkarounds.getDotGitDir(getDir(project)); - if (projectGitDir == null || !RepositoryCache.FileKey.isGitRepository(projectGitDir, FS.DETECTED)) { + if (projectGitDir == null || !isGitRepository(projectGitDir)) { throw new IllegalArgumentException("Cannot find git repository in any parent directory"); } Repository repo = gitRoots.get(projectGitDir); @@ -158,6 +158,28 @@ protected Repository repositoryFor(Project project) throws IOException { return repo; } + /** + * Checks if the given directory is a valid git repository, including worktree repositories. + * This is more lenient than {@link RepositoryCache.FileKey#isGitRepository} which doesn't + * properly handle worktrees where some files are in the common directory. + */ + private static boolean isGitRepository(File gitDir) { + if (!gitDir.isDirectory()) { + return false; + } + // For worktrees, HEAD and commondir must exist (objects, refs, etc. are in commondir) + // For regular repos, HEAD and objects must exist + File headFile = new File(gitDir, Constants.HEAD); + File commonDirFile = new File(gitDir, "commondir"); + if (commonDirFile.exists()) { + // This is a worktree - just check for HEAD and commondir + return headFile.exists(); + } else { + // This is a regular repository - use standard check + return RepositoryCache.FileKey.isGitRepository(gitDir, FS.DETECTED); + } + } + protected abstract File getDir(Project project); protected abstract @Nullable Project getParent(Project project); diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/GitRatchetMavenTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/GitRatchetMavenTest.java index c1cb0f78d7..d786b641c9 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/GitRatchetMavenTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/GitRatchetMavenTest.java @@ -15,7 +15,9 @@ */ package com.diffplug.spotless.maven; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; @@ -150,4 +152,75 @@ private void assertClean() throws Exception { private void assertDirty() throws Exception { mavenRunner().withArguments("spotless:check").runHasError(); } + + @Test + void worktreeSupport() throws Exception { + // Set up main repository + Git mainGit = Git.init().setDirectory(rootFolder()).call(); + setFile(TEST_PATH).toContent("HELLO"); + mainGit.add().addFilepattern(TEST_PATH).call(); + mainGit.commit().setMessage("Initial commit").call(); + mainGit.tag().setName("baseline").call(); + + // Set up a worktree manually (JGit doesn't support worktrees) + File worktreeDir = newFolder("worktree"); + File mainGitDir = new File(rootFolder(), ".git"); + File worktreeGitDir = new File(rootFolder(), ".git/worktrees/worktree"); + + // Create worktree structure + setFile(".git/worktrees/worktree/gitdir").toContent(worktreeDir.getAbsolutePath() + "/.git"); + setFile(".git/worktrees/worktree/commondir").toContent("../.."); + setFile(".git/worktrees/worktree/HEAD").toContent("ref: refs/heads/main"); + setFile("worktree/.git").toContent("gitdir: " + worktreeGitDir.getAbsolutePath()); + + // Copy the test file to worktree (same as baseline so ratchet considers it clean) + setFile("worktree/" + TEST_PATH).toContent("HELLO"); + + // Copy Maven wrapper files to worktree + setFile("worktree/.gitattributes").toContent("* text eol=lf"); + Files.copy(new File(rootFolder(), "mvnw").toPath(), new File(worktreeDir, "mvnw").toPath()); + new File(worktreeDir, "mvnw").setExecutable(true); + Files.copy(new File(rootFolder(), "mvnw.cmd").toPath(), new File(worktreeDir, "mvnw.cmd").toPath()); + new File(worktreeDir, ".mvn/wrapper").mkdirs(); + Files.copy(new File(rootFolder(), ".mvn/wrapper/maven-wrapper.jar").toPath(), + new File(worktreeDir, ".mvn/wrapper/maven-wrapper.jar").toPath()); + Files.copy(new File(rootFolder(), ".mvn/wrapper/maven-wrapper.properties").toPath(), + new File(worktreeDir, ".mvn/wrapper/maven-wrapper.properties").toPath()); + + // Create a pom.xml in the worktree + setFile("worktree/pom.xml").toLines( + "", + " 4.0.0", + " test", + " test", + " 1.0.0", + " ", + " ", + " ", + " com.diffplug.spotless", + " spotless-maven-plugin", + " " + System.getProperty("spotlessMavenPluginVersion") + "", + " ", + " ", + " ", + " baseline", + " ", + " " + TEST_PATH + "", + " ", + " ", + " Lowercase hello", + " HELLO", + " hello", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + ""); + + // Verify that spotless works correctly in a git worktree + mavenRunner().withProjectDir(worktreeDir).withArguments("spotless:check").runNoError(); + } } From 985f1a78f544ae392b3efa3769dfe841ab076168 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Wed, 3 Dec 2025 15:33:16 -0800 Subject: [PATCH 2/2] changes.md: add changes --- CHANGES.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d3bb7eacf1..2dc51dfaa2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Changes * Bump default `ktlint` version to latest `1.7.1` -> `1.8.0`. ([2763](https://github.com/diffplug/spotless/pull/2763)) * Bump default `gherkin-utils` version to latest `9.2.0` -> `10.0.0`. ([#2619](https://github.com/diffplug/spotless/pull/2619)) +### Fixed +- Git ratchet no longer throws an error with Git worktrees. ([#2779](https://github.com/diffplug/spotless/issues/2779)) ## [4.1.0] - 2025-11-18 ### Changes @@ -134,7 +136,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Changed * **BREAKING** Moved `PaddedCell.DirtyState` to its own top-level class with new methods. ([#2148](https://github.com/diffplug/spotless/pull/2148)) * **BREAKING** Removed `isClean`, `applyTo`, and `applyToAndReturnResultIfDirty` from `Formatter` because users should instead use `DirtyState`. -* `FenceStep` now uses `ConfigurationCacheHack`. ([#2378](https://github.com/diffplug/spotless/pull/2378) fixes [#2317](https://github.com/diffplug/spotless/issues/2317)) +* `FenceStep` now uses `ConfigurationCacheHack`. ([#2378](https://github.com/diffplug/spotless/pull/2378) fixes [#2317](https://github.com/diffplug/spotless/issues/2317)) ### Fixed * `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)) @@ -145,7 +147,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( * `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)) ### Changed * Support configuring the Equo P2 cache. ([#2238](https://github.com/diffplug/spotless/pull/2238)) -* Add explicit support for JSONC / CSS via biome, via the file extensions `.css` and `.jsonc`. +* Add explicit support for JSONC / CSS via biome, via the file extensions `.css` and `.jsonc`. ([#2259](https://github.com/diffplug/spotless/pull/2259)) * Bump default `buf` version to latest `1.24.0` -> `1.44.0`. ([#2291](https://github.com/diffplug/spotless/pull/2291)) * Bump default `google-java-format` version to latest `1.23.0` -> `1.24.0`. ([#2294](https://github.com/diffplug/spotless/pull/2294))