Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))

Expand All @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
"<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'>",
" <modelVersion>4.0.0</modelVersion>",
" <groupId>test</groupId>",
" <artifactId>test</artifactId>",
" <version>1.0.0</version>",
" <build>",
" <plugins>",
" <plugin>",
" <groupId>com.diffplug.spotless</groupId>",
" <artifactId>spotless-maven-plugin</artifactId>",
" <version>" + System.getProperty("spotlessMavenPluginVersion") + "</version>",
" <configuration>",
" <formats>",
" <format>",
" <ratchetFrom>baseline</ratchetFrom>",
" <includes>",
" <include>" + TEST_PATH + "</include>",
" </includes>",
" <replace>",
" <name>Lowercase hello</name>",
" <search>HELLO</search>",
" <replacement>hello</replacement>",
" </replace>",
" </format>",
" </formats>",
" </configuration>",
" </plugin>",
" </plugins>",
" </build>",
"</project>");

// Verify that spotless works correctly in a git worktree
mavenRunner().withProjectDir(worktreeDir).withArguments("spotless:check").runNoError();
}
}