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)) 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(); + } }