diff --git a/CHANGES.md b/CHANGES.md index a431ee8c18..64f1e42e37 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Changed * Bump default ktfmt `0.30` -> `0.31` ([#1118](https://github.com/diffplug/spotless/pull/1118)). +### Fixed +* Add full support for git worktrees ([#1119](https://github.com/diffplug/spotless/pull/1119)). ## [2.22.1] - 2022-02-01 ### Changed diff --git a/gradle.properties b/gradle.properties index fcad5c5ed4..032beae95c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # To fix metaspace errors -org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8 name=spotless description=Spotless - keep your code spotless with Gradle org=diffplug diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/GitAttributesLineEndings.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/GitAttributesLineEndings.java index 11ed730e6d..4aab23e142 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/GitAttributesLineEndings.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/GitAttributesLineEndings.java @@ -38,10 +38,10 @@ import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.CoreConfig; import org.eclipse.jgit.lib.CoreConfig.AutoCRLF; import org.eclipse.jgit.lib.CoreConfig.EOL; import org.eclipse.jgit.storage.file.FileBasedConfig; -import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.SystemReader; @@ -52,6 +52,7 @@ import com.diffplug.spotless.FileSignature; import com.diffplug.spotless.LazyForwardingEquality; import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.extra.GitWorkarounds.RepositorySpecificResolver; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -132,8 +133,11 @@ public String endingFor(File file) { } static class RuntimeInit { - /** /etc/gitconfig (system-global), ~/.gitconfig, project/.git/config (each might-not exist). */ - final FileBasedConfig systemConfig, userConfig, repoConfig; + /** /etc/gitconfig (system-global), ~/.gitconfig (each might-not exist). */ + final FileBasedConfig systemConfig, userConfig; + + /** Repository specific config, can be $GIT_COMMON_DIR/config, project/.git/config or .git/worktrees//config.worktree if enabled by extension */ + final Config repoConfig; /** Global .gitattributes file pointed at by systemConfig or userConfig, and the file in the repo. */ final @Nullable File globalAttributesFile, repoAttributesFile; @@ -142,7 +146,7 @@ static class RuntimeInit { final @Nullable File workTree; @SuppressFBWarnings("SIC_INNER_SHOULD_BE_STATIC_ANON") - RuntimeInit(File projectDir, Iterable toFormat) throws IOException { + RuntimeInit(File projectDir, Iterable toFormat) { requireElementsNonNull(toFormat); ///////////////////////////////// // USER AND SYSTEM-WIDE VALUES // @@ -152,9 +156,8 @@ static class RuntimeInit { userConfig = SystemReader.getInstance().openUserConfig(systemConfig, FS.DETECTED); Errors.log().run(userConfig::load); - // copy-pasted from org.eclipse.jgit.lib.CoreConfig - String globalAttributesPath = userConfig.getString(ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_ATTRIBUTESFILE); // copy-pasted from org.eclipse.jgit.internal.storage.file.GlobalAttributesNode + String globalAttributesPath = userConfig.get(CoreConfig.KEY).getAttributesFile(); if (globalAttributesPath != null) { FS fs = FS.detect(); if (globalAttributesPath.startsWith("~/")) { //$NON-NLS-1$ @@ -169,29 +172,16 @@ static class RuntimeInit { ////////////////////////// // REPO-SPECIFIC VALUES // ////////////////////////// - FileRepositoryBuilder builder = GitWorkarounds.fileRepositoryBuilderForProject(projectDir); - if (builder.getGitDir() != null) { - workTree = builder.getWorkTree(); - repoConfig = new FileBasedConfig(userConfig, new File(builder.getGitDir(), Constants.CONFIG), FS.DETECTED); - repoAttributesFile = new File(builder.getGitDir(), Constants.INFO_ATTRIBUTES); + RepositorySpecificResolver repositoryResolver = GitWorkarounds.fileRepositoryResolverForProject(projectDir); + if (repositoryResolver.getGitDir() != null) { + workTree = repositoryResolver.getWorkTree(); + repoConfig = repositoryResolver.getRepositoryConfig(); + repoAttributesFile = repositoryResolver.resolveWithCommonDir(Constants.INFO_ATTRIBUTES); } else { workTree = null; - // null would make repoConfig.getFile() bomb below - repoConfig = new FileBasedConfig(userConfig, null, FS.DETECTED) { - @Override - public void load() { - // empty, do not load - } - - @Override - public boolean isOutdated() { - // regular class would bomb here - return false; - } - }; + repoConfig = new Config(); repoAttributesFile = null; } - Errors.log().run(repoConfig::load); } private Runtime atRuntime() { diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/GitWorkarounds.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/GitWorkarounds.java index ee13fefc81..a66650b38f 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/GitWorkarounds.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/GitWorkarounds.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 DiffPlug + * Copyright 2020-2022 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,17 +17,27 @@ import java.io.File; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import javax.annotation.Nullable; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ConfigConstants; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.util.IO; +import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.SystemReader; + +import com.diffplug.common.base.Errors; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Utility methods for Git workarounds. */ -public class GitWorkarounds { +public final class GitWorkarounds { private GitWorkarounds() {} /** @@ -40,46 +50,155 @@ private GitWorkarounds() {} * @return the path to the .git directory. */ static @Nullable File getDotGitDir(File projectDir) { - return fileRepositoryBuilderForProject(projectDir).getGitDir(); + return fileRepositoryResolverForProject(projectDir).getGitDir(); } /** - * Creates a {@link FileRepositoryBuilder} for the given project directory. + * Creates a {@link RepositorySpecificResolver} for the given project directory. * * This applies a workaround for JGit not supporting worktrees properly. * * @param projectDir the project directory. * @return the builder. */ - static FileRepositoryBuilder fileRepositoryBuilderForProject(File projectDir) { - FileRepositoryBuilder builder = new FileRepositoryBuilder(); - builder.findGitDir(projectDir); - File gitDir = builder.getGitDir(); - if (gitDir != null) { - builder.setGitDir(resolveRealGitDirIfWorktreeDir(gitDir)); + static RepositorySpecificResolver fileRepositoryResolverForProject(File projectDir) { + RepositorySpecificResolver repositoryResolver = new RepositorySpecificResolver(); + repositoryResolver.findGitDir(projectDir); + repositoryResolver.readEnvironment(); + if (repositoryResolver.getGitDir() != null || repositoryResolver.getWorkTree() != null) { + Errors.rethrow().get(repositoryResolver::setup); } - return builder; + return repositoryResolver; } /** - * If the dir is a worktree directory (typically .git/worktrees/something) then - * returns the actual .git directory. + * Piggyback on the {@link FileRepositoryBuilder} mechanics for finding the git directory. * - * @param dir the directory which may be a worktree directory or may be a .git directory. - * @return the .git directory. + * Here we take into account that git repositories can share a common directory. This directory + * will contain ./config ./objects/, ./info/, and ./refs/. */ - private static File resolveRealGitDirIfWorktreeDir(File dir) { - File pointerFile = new File(dir, "gitdir"); - if (pointerFile.isFile()) { - try { - String content = new String(Files.readAllBytes(pointerFile.toPath()), StandardCharsets.UTF_8).trim(); - return new File(content); - } catch (IOException e) { - System.err.println("failed to parse git meta: " + e.getMessage()); - return dir; + static class RepositorySpecificResolver extends FileRepositoryBuilder { + /** + * The common directory file is used to define $GIT_COMMON_DIR if environment variable is not set. + * https://github.com/git/git/blob/b23dac905bde28da47543484320db16312c87551/Documentation/gitrepository-layout.txt#L259 + */ + private static final String COMMON_DIR = "commondir"; + private static final String GIT_COMMON_DIR_ENV_KEY = "GIT_COMMON_DIR"; + + /** + * Using an extension it is possible to have per-worktree config. + * https://github.com/git/git/blob/b23dac905bde28da47543484320db16312c87551/Documentation/git-worktree.txt#L366 + */ + private static final String EXTENSIONS_WORKTREE_CONFIG = "worktreeConfig"; + private static final String EXTENSIONS_WORKTREE_CONFIG_FILENAME = "config.worktree"; + + private File commonDirectory; + + /** @return the repository specific configuration. */ + Config getRepositoryConfig() { + return Errors.rethrow().get(this::getConfig); + } + + /** + * @return the repository's configuration. + * @throws IOException on errors accessing the configuration file. + * @throws IllegalArgumentException on malformed configuration. + */ + @Override + protected Config loadConfig() throws IOException { + if (getGitDir() != null) { + File path = resolveWithCommonDir(Constants.CONFIG); + FileBasedConfig cfg = new FileBasedConfig(path, safeFS()); + try { + cfg.load(); + + // Check for per-worktree config, it should be parsed after the common config + if (cfg.getBoolean(ConfigConstants.CONFIG_EXTENSIONS_SECTION, EXTENSIONS_WORKTREE_CONFIG, false)) { + File worktreeSpecificConfig = safeFS().resolve(getGitDir(), EXTENSIONS_WORKTREE_CONFIG_FILENAME); + if (safeFS().exists(worktreeSpecificConfig) && safeFS().isFile(worktreeSpecificConfig)) { + // It is important to base this on the common config, as both the common config and the per-worktree config should be used + cfg = new FileBasedConfig(cfg, worktreeSpecificConfig, safeFS()); + try { + cfg.load(); + } catch (ConfigInvalidException err) { + throw new IllegalArgumentException("Failed to parse config " + worktreeSpecificConfig.getAbsolutePath(), err); + } + } + } + } catch (ConfigInvalidException err) { + throw new IllegalArgumentException("Failed to parse config " + path.getAbsolutePath(), err); + } + return cfg; + } + return super.loadConfig(); + } + + @Override + protected void setupGitDir() throws IOException { + super.setupGitDir(); + + // Setup common directory + if (commonDirectory == null) { + File commonDirFile = safeFS().resolve(getGitDir(), COMMON_DIR); + if (safeFS().exists(commonDirFile) && safeFS().isFile(commonDirFile)) { + byte[] content = IO.readFully(commonDirFile); + if (content.length < 1) { + throw emptyFile(commonDirFile); + } + + int lineEnd = RawParseUtils.nextLF(content, 0); + while (content[lineEnd - 1] == '\n' || (content[lineEnd - 1] == '\r' && SystemReader.getInstance().isWindows())) { + lineEnd--; + } + if (lineEnd <= 1) { + throw emptyFile(commonDirFile); + } + + String commonPath = RawParseUtils.decode(content, 0, lineEnd); + File common = new File(commonPath); + if (common.isAbsolute()) { + commonDirectory = common; + } else { + commonDirectory = safeFS().resolve(getGitDir(), commonPath).getCanonicalFile(); + } + } + } + + // Setup object directory + if (getObjectDirectory() == null) { + setObjectDirectory(resolveWithCommonDir(Constants.OBJECTS)); + } + } + + private static IOException emptyFile(File commonDir) { + return new IOException("Empty 'commondir' file: " + commonDir.getAbsolutePath()); + } + + @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE") + @Override + public FileRepositoryBuilder readEnvironment(SystemReader sr) { + super.readEnvironment(sr); + + // Always overwrite, will trump over the common dir file + String val = sr.getenv(GIT_COMMON_DIR_ENV_KEY); + if (val != null) { + commonDirectory = new File(val); + } + + return self(); + } + + /** + * For repository with multiple linked worktrees some data might be shared in a "common" directory. + * + * @param target the file we want to resolve. + * @return a file resolved from the {@link #getGitDir()}, or possibly in the path specified by $GIT_COMMON_DIR or {@code commondir} file. + */ + File resolveWithCommonDir(String target) { + if (commonDirectory != null) { + return safeFS().resolve(commonDirectory, target); } - } else { - return dir; + return safeFS().resolve(getGitDir(), target); } } } diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/GitAttributesTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/GitAttributesTest.java index 330a432d40..ed657c365a 100644 --- a/lib-extra/src/test/java/com/diffplug/spotless/extra/GitAttributesTest.java +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/GitAttributesTest.java @@ -32,12 +32,13 @@ import com.diffplug.spotless.ResourceHarness; class GitAttributesTest extends ResourceHarness { - private List testFiles() { + private List testFiles(String prefix) { try { List result = new ArrayList<>(); for (String path : TEST_PATHS) { - setFile(path).toContent(""); - result.add(newFile(path)); + String prefixedPath = prefix + path; + setFile(prefixedPath).toContent(""); + result.add(newFile(prefixedPath)); } return result; } catch (IOException e) { @@ -45,7 +46,11 @@ private List testFiles() { } } - private static List TEST_PATHS = Arrays.asList("someFile", "subfolder/someFile", "MANIFEST.MF", "subfolder/MANIFEST.MF"); + private List testFiles() { + return testFiles(""); + } + + private static final List TEST_PATHS = Arrays.asList("someFile", "subfolder/someFile", "MANIFEST.MF", "subfolder/MANIFEST.MF"); @Test void cacheTest() throws IOException { @@ -101,4 +106,42 @@ void policyDefaultLineEndingTest() throws GitAPIException, IOException { LineEnding.Policy policy = LineEnding.GIT_ATTRIBUTES.createPolicy(rootFolder(), () -> testFiles()); Assertions.assertThat(policy.getEndingFor(newFile("someFile"))).isEqualTo("\r\n"); } + + @Test + void policyTestWithExternalGitDir() throws IOException, GitAPIException { + File projectFolder = newFolder("project"); + File gitDir = newFolder("project.git"); + Git.init().setDirectory(projectFolder).setGitDir(gitDir).call(); + + setFile("project.git/info/attributes").toContent(StringPrinter.buildStringFromLines( + "* eol=lf", + "*.MF eol=crlf")); + LineEnding.Policy policy = LineEnding.GIT_ATTRIBUTES.createPolicy(projectFolder, () -> testFiles("project/")); + Assertions.assertThat(policy.getEndingFor(newFile("project/someFile"))).isEqualTo("\n"); + Assertions.assertThat(policy.getEndingFor(newFile("project/subfolder/someFile"))).isEqualTo("\n"); + Assertions.assertThat(policy.getEndingFor(newFile("project/MANIFEST.MF"))).isEqualTo("\r\n"); + Assertions.assertThat(policy.getEndingFor(newFile("project/subfolder/MANIFEST.MF"))).isEqualTo("\r\n"); + } + + @Test + void policyTestWithCommonDir() throws IOException, GitAPIException { + File projectFolder = newFolder("project"); + File commonGitDir = newFolder("project.git"); + Git.init().setDirectory(projectFolder).setGitDir(commonGitDir).call(); + newFolder("project.git/worktrees/"); + + File projectGitDir = newFolder("project.git/worktrees/project/"); + setFile("project.git/worktrees/project/gitdir").toContent(projectFolder.getAbsolutePath() + "/.git"); + setFile("project.git/worktrees/project/commondir").toContent("../.."); + setFile("project/.git").toContent("gitdir: " + projectGitDir.getAbsolutePath()); + + setFile("project.git/info/attributes").toContent(StringPrinter.buildStringFromLines( + "* eol=lf", + "*.MF eol=crlf")); + LineEnding.Policy policy = LineEnding.GIT_ATTRIBUTES.createPolicy(projectFolder, () -> testFiles("project/")); + Assertions.assertThat(policy.getEndingFor(newFile("project/someFile"))).isEqualTo("\n"); + Assertions.assertThat(policy.getEndingFor(newFile("project/subfolder/someFile"))).isEqualTo("\n"); + Assertions.assertThat(policy.getEndingFor(newFile("project/MANIFEST.MF"))).isEqualTo("\r\n"); + Assertions.assertThat(policy.getEndingFor(newFile("project/subfolder/MANIFEST.MF"))).isEqualTo("\r\n"); + } } diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/GitWorkaroundsTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/GitWorkaroundsTest.java new file mode 100644 index 0000000000..a90716f5de --- /dev/null +++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/GitWorkaroundsTest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2022 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.extra; + +import java.io.File; +import java.io.IOException; + +import org.assertj.core.api.Assertions; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Constants; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.extra.GitWorkarounds.RepositorySpecificResolver; + +class GitWorkaroundsTest extends ResourceHarness { + @Test + void inline() throws IOException, GitAPIException { + File projectFolder = newFolder("project"); + Git.init().setDirectory(projectFolder).call(); + + RepositorySpecificResolver repositorySpecificResolver = GitWorkarounds.fileRepositoryResolverForProject(projectFolder); + Assertions.assertThat(repositorySpecificResolver.getGitDir()).isEqualTo(new File(projectFolder, ".git")); + } + + @Test + void external() throws IOException, GitAPIException { + File projectFolder = newFolder("project"); + File gitDir = newFolder("project.git"); + Git.init().setDirectory(projectFolder).setGitDir(gitDir).call(); + + RepositorySpecificResolver repositorySpecificResolver = GitWorkarounds.fileRepositoryResolverForProject(projectFolder); + Assertions.assertThat(repositorySpecificResolver.getGitDir()).isEqualTo(gitDir); + } + + @Nested + @DisplayName("Worktrees") + class Worktrees { + private File project1Tree; + private File project1GitDir; + private File project2Tree; + private File project2GitDir; + private File commonGitDir; + + @BeforeEach + void setUp() throws IOException, GitAPIException { + project1Tree = newFolder("project-w1"); + project2Tree = newFolder("project-w2"); + commonGitDir = newFolder("project.git"); + Git.init().setDirectory(project1Tree).setGitDir(commonGitDir).call(); + + // Setup worktrees manually since JGit does not support it + newFolder("project.git/worktrees/"); + + project1GitDir = newFolder("project.git/worktrees/project-w1/"); + setFile("project.git/worktrees/project-w1/gitdir").toContent(project1Tree.getAbsolutePath() + "/.git"); + setFile("project.git/worktrees/project-w1/commondir").toContent("../.."); // Relative path + setFile("project-w1/.git").toContent("gitdir: " + project1GitDir.getAbsolutePath()); + + project2GitDir = newFolder("project.git/worktrees/project-w2/"); + setFile("project.git/worktrees/project-w2/gitdir").toContent(project2Tree.getAbsolutePath() + "/.git"); + setFile("project.git/worktrees/project-w2/commondir").toContent(commonGitDir.getAbsolutePath()); // Absolute path + setFile("project-w2/.git").toContent("gitdir: " + project2GitDir.getAbsolutePath()); + } + + @Test + void resolveGitDir() { + // Test worktree 1 + { + RepositorySpecificResolver repositorySpecificResolver = GitWorkarounds.fileRepositoryResolverForProject(project1Tree); + Assertions.assertThat(repositorySpecificResolver.getGitDir()).isEqualTo(project1GitDir); + Assertions.assertThat(repositorySpecificResolver.resolveWithCommonDir(Constants.CONFIG)).isEqualTo(new File(commonGitDir, Constants.CONFIG)); + } + + // Test worktree 2 + { + RepositorySpecificResolver repositorySpecificResolver = GitWorkarounds.fileRepositoryResolverForProject(project2Tree); + Assertions.assertThat(repositorySpecificResolver.getGitDir()).isEqualTo(project2GitDir); + Assertions.assertThat(repositorySpecificResolver.resolveWithCommonDir(Constants.CONFIG)).isEqualTo(new File(commonGitDir, Constants.CONFIG)); + } + } + + @Test + void perWorktreeConfig() throws IOException { + setFile("project.git/config").toLines("[core]", "mySetting = true"); + + Assertions.assertThat(getMySetting(project1Tree)).isTrue(); + Assertions.assertThat(getMySetting(project2Tree)).isTrue(); + + // Override setting for project 1, but don't enable extension yet + setFile("project.git/worktrees/project-w1/config.worktree").toLines("[core]", "mySetting = false"); + + Assertions.assertThat(getMySetting(project1Tree)).isTrue(); + Assertions.assertThat(getMySetting(project2Tree)).isTrue(); + + // Enable extension + setFile("project.git/config").toLines("[core]", "mySetting = true", "[extensions]", "worktreeConfig = true"); + + Assertions.assertThat(getMySetting(project1Tree)).isFalse(); // Should now be overridden by config.worktree + Assertions.assertThat(getMySetting(project2Tree)).isTrue(); + } + + private boolean getMySetting(File projectDir) { + return GitWorkarounds.fileRepositoryResolverForProject(projectDir).getRepositoryConfig().getBoolean("core", "mySetting", false); + } + } +} diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 265b77084f..ee6a753893 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -5,6 +5,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Changed * Bump default ktfmt `0.30` -> `0.31` ([#1118](https://github.com/diffplug/spotless/pull/1118)). +### Fixed +* Add full support for git worktrees ([#1119](https://github.com/diffplug/spotless/pull/1119)). ## [6.2.1] - 2022-02-01 ### Changed diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index a506bf17d1..07021c68ef 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -5,6 +5,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Changed * Bump default ktfmt `0.30` -> `0.31` ([#1118](https://github.com/diffplug/spotless/pull/1118)). +### Fixed +* Add full support for git worktrees ([#1119](https://github.com/diffplug/spotless/pull/1119)). ## [2.20.1] - 2022-02-01 * Bump default versions of formatters ([#1095](https://github.com/diffplug/spotless/pull/1095)).