diff --git a/CHANGES.md b/CHANGES.md
index 733abfd3d9..e5faa59090 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -12,6 +12,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
### Added
- Add a `groupArtifact` option to `PalantirJavaFormatStep`. ([#3179](https://github.com/diffplug/spotless/pull/3179))
+- Add `GitIgnoredFiles` to `lib-extra`, which answers whether git ignores a file using the same rules as `git status`. ([#2443](https://github.com/diffplug/spotless/issues/2443))
### Fixed
- GrEclipse no longer leaves a `file%3A...` directory in the working directory. ([#2019](https://github.com/diffplug/spotless/issues/2019))
- DBeaver SQL formatter no longer prints an `IndexOutOfBoundsException` when a file starts with a keyword like `SET`. ([#1552](https://github.com/diffplug/spotless/issues/1552))
diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/GitIgnoredFiles.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/GitIgnoredFiles.java
new file mode 100644
index 0000000000..2bd5fe8d42
--- /dev/null
+++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/GitIgnoredFiles.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2026 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 java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import org.eclipse.jgit.dircache.DirCacheIterator;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.WorkingTreeIterator;
+import org.eclipse.jgit.treewalk.filter.PathFilter;
+
+import com.diffplug.spotless.FileSignature;
+
+/**
+ * Answers whether git considers a file to be ignored, using the same rules as {@code git status}:
+ * a file is ignored if it matches a pattern from any {@code .gitignore}, from {@code .git/info/exclude}
+ * or from the {@code core.excludesFile} configured in git, and it is not tracked in the index.
+ * Tracked files are never ignored, even when they match an ignore pattern.
+ *
+ * The ignored paths below the root directory are computed once, lazily, on the first call to
+ * {@link #isIgnored(File)} and are cached for the lifetime of this instance, so an instance must not
+ * outlive a build. The computation only lists directories and evaluates ignore rules, it never reads
+ * the content of a file. Ignored directories without tracked files are not descended into, so large
+ * ignored folders such as {@code node_modules} or {@code build} are cheap.
+ *
+ * Instances are thread-safe.
+ */
+public final class GitIgnoredFiles {
+ private final File rootDir;
+ private volatile @Nullable State state;
+
+ private GitIgnoredFiles(File rootDir) {
+ this.rootDir = Objects.requireNonNull(rootDir).getAbsoluteFile();
+ }
+
+ /**
+ * Creates an instance which answers questions about files below {@code rootDir}, which must be inside
+ * a git working tree. The repository is not read until {@link #isIgnored(File)} is first called.
+ */
+ public static GitIgnoredFiles under(File rootDir) {
+ return new GitIgnoredFiles(rootDir);
+ }
+
+ /** The directory whose files this instance answers questions about. */
+ public File getRootDir() {
+ return rootDir;
+ }
+
+ /**
+ * Returns true if git ignores the given file. Files outside of the git working tree, or outside of
+ * {@link #getRootDir()}, are never considered ignored.
+ *
+ * @throws IllegalArgumentException if {@link #getRootDir()} is not inside a git working tree
+ */
+ public boolean isIgnored(File file) {
+ return state().isIgnored(file);
+ }
+
+ /** Negation of {@link #isIgnored(File)}, handy as a method reference for filtering. */
+ public boolean isNotIgnored(File file) {
+ return !isIgnored(file);
+ }
+
+ private State state() {
+ State result = state;
+ if (result == null) {
+ synchronized (this) {
+ result = state;
+ if (result == null) {
+ try {
+ result = State.compute(rootDir);
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to determine git-ignored files below " + rootDir, e);
+ }
+ state = result;
+ }
+ }
+ }
+ return result;
+ }
+
+ private static final class State {
+ private final Path workTree;
+ /** Unix-style paths relative to the work tree, which are either ignored files or fully-ignored directories. */
+ private final Set ignoredPaths;
+
+ private State(Path workTree, Set ignoredPaths) {
+ this.workTree = workTree;
+ this.ignoredPaths = ignoredPaths;
+ }
+
+ static State compute(File rootDir) throws IOException {
+ GitWorkarounds.RepositorySpecificResolver resolver = GitWorkarounds.fileRepositoryResolverForProject(rootDir);
+ if (resolver.getGitDir() == null) {
+ throw new IllegalArgumentException("Excluding files using .gitignore requires a git repository, but none was found in "
+ + rootDir + " or any of its parent directories");
+ }
+ try (Repository repo = resolver.build()) {
+ if (repo.isBare()) {
+ throw new IllegalArgumentException("Excluding files using .gitignore requires a git working tree, but " + repo.getDirectory() + " is bare");
+ }
+ Path workTree = repo.getWorkTree().toPath().toAbsolutePath().normalize();
+ String subPath = toUnixRelative(workTree, rootDir.toPath());
+ return new State(workTree, Set.copyOf(ignoredPaths(repo, subPath)));
+ }
+ }
+
+ /**
+ * Walks the index and the working tree side by side, and collects every untracked path which is ignored.
+ * An ignored directory is only entered if the index has entries below it, because git never ignores a tracked file.
+ */
+ private static Set ignoredPaths(Repository repo, @Nullable String subPath) throws IOException {
+ Set ignoredPaths = new HashSet<>();
+ try (TreeWalk walk = new TreeWalk(repo)) {
+ int indexTree = walk.addTree(new DirCacheIterator(repo.readDirCache()));
+ FileTreeIterator workingTreeIterator = new FileTreeIterator(repo);
+ workingTreeIterator.setDirCacheIterator(walk, indexTree);
+ int workingTree = walk.addTree(workingTreeIterator);
+ walk.setRecursive(false);
+ if (subPath != null && !subPath.isEmpty()) {
+ walk.setFilter(PathFilter.create(subPath));
+ }
+ while (walk.next()) {
+ WorkingTreeIterator onDisk = walk.getTree(workingTree, WorkingTreeIterator.class);
+ if (onDisk == null) {
+ // only in the index, e.g. deleted from the working tree
+ continue;
+ }
+ boolean tracked = walk.getTree(indexTree, DirCacheIterator.class) != null;
+ if (!tracked && onDisk.isEntryIgnored()) {
+ ignoredPaths.add(walk.getPathString());
+ } else if (walk.isSubtree()) {
+ walk.enterSubtree();
+ }
+ }
+ }
+ return ignoredPaths;
+ }
+
+ boolean isIgnored(File file) {
+ if (ignoredPaths.isEmpty()) {
+ return false;
+ }
+ String path = toUnixRelative(workTree, file.toPath());
+ if (path == null || path.isEmpty()) {
+ return false;
+ }
+ // the file itself, or any of its parent directories, may be the ignored entry
+ while (true) {
+ if (ignoredPaths.contains(path)) {
+ return true;
+ }
+ int lastSlash = path.lastIndexOf('/');
+ if (lastSlash == -1) {
+ return false;
+ }
+ path = path.substring(0, lastSlash);
+ }
+ }
+
+ /** Returns the unix-style relative path of {@code file} within {@code workTree}, or null if it is outside. */
+ private static @Nullable String toUnixRelative(Path workTree, Path file) {
+ Path absolute = file.toAbsolutePath().normalize();
+ if (!absolute.startsWith(workTree)) {
+ return null;
+ }
+ return FileSignature.pathNativeToUnix(workTree.relativize(absolute).toString());
+ }
+ }
+}
diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/GitIgnoredFilesTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/GitIgnoredFilesTest.java
new file mode 100644
index 0000000000..bc24ca0fec
--- /dev/null
+++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/GitIgnoredFilesTest.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2026 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 static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import com.diffplug.spotless.ClearGitConfig;
+import com.diffplug.spotless.ResourceHarness;
+
+@ClearGitConfig
+class GitIgnoredFilesTest extends ResourceHarness {
+
+ private Git initRepo() throws GitAPIException {
+ return Git.init().setDirectory(rootFolder()).call();
+ }
+
+ private static void addAndCommit(Git git) throws GitAPIException {
+ git.add().addFilepattern(".").call();
+ git.commit().setSign(false).setMessage("commit").call();
+ }
+
+ private GitIgnoredFiles underRoot() {
+ return GitIgnoredFiles.under(rootFolder());
+ }
+
+ @Nested
+ @DisplayName("given a root .gitignore")
+ class RootGitignore {
+ @Test
+ @DisplayName("when an untracked file matches a pattern, then it is ignored")
+ void untrackedMatchingFileIsIgnored() throws Exception {
+ // given
+ try (Git git = initRepo()) {
+ setFile(".gitignore").toContent("*.log\n");
+ addAndCommit(git);
+ File log = setFile("debug.log").toContent("x");
+ // when / then
+ assertThat(underRoot().isIgnored(log)).isTrue();
+ assertThat(underRoot().isNotIgnored(log)).isFalse();
+ }
+ }
+
+ @Test
+ @DisplayName("when an untracked file does not match any pattern, then it is not ignored")
+ void untrackedNonMatchingFileIsNotIgnored() throws Exception {
+ try (Git git = initRepo()) {
+ setFile(".gitignore").toContent("*.log\n");
+ addAndCommit(git);
+ File java = setFile("src/Main.java").toContent("class Main {}");
+ assertThat(underRoot().isIgnored(java)).isFalse();
+ }
+ }
+
+ @Test
+ @DisplayName("when a file is deep inside an ignored directory, then it is ignored")
+ void fileInsideIgnoredDirectoryIsIgnored() throws Exception {
+ try (Git git = initRepo()) {
+ setFile(".gitignore").toContent("node_modules/\n");
+ addAndCommit(git);
+ File dep = setFile("web/node_modules/lib/deep/index.js").toContent("x");
+ assertThat(underRoot().isIgnored(dep)).isTrue();
+ }
+ }
+
+ @Test
+ @DisplayName("when a negation pattern re-includes a file, then it is not ignored")
+ void negatedPatternIsNotIgnored() throws Exception {
+ try (Git git = initRepo()) {
+ setFile(".gitignore").toContent("*.log\n!keep.log\n");
+ addAndCommit(git);
+ File keep = setFile("keep.log").toContent("x");
+ File drop = setFile("drop.log").toContent("x");
+ GitIgnoredFiles ignored = underRoot();
+ assertThat(ignored.isIgnored(keep)).isFalse();
+ assertThat(ignored.isIgnored(drop)).isTrue();
+ }
+ }
+
+ @Test
+ @DisplayName("when a file matching a pattern is tracked, then it is NOT ignored (same as git status)")
+ void trackedMatchingFileIsNotIgnored() throws Exception {
+ try (Git git = initRepo()) {
+ setFile("generated/Tracked.java").toContent("class Tracked {}");
+ addAndCommit(git);
+ setFile(".gitignore").toContent("generated/\n");
+ addAndCommit(git);
+ File untrackedSibling = setFile("generated/Untracked.java").toContent("class Untracked {}");
+
+ GitIgnoredFiles ignored = underRoot();
+ assertThat(ignored.isIgnored(newFile("generated/Tracked.java"))).isFalse();
+ assertThat(ignored.isIgnored(untrackedSibling)).isTrue();
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("given ignore rules from other sources")
+ class OtherSources {
+ @Test
+ @DisplayName("when a nested .gitignore has a pattern, then it applies only below its directory")
+ void nestedGitignoreIsScoped() throws Exception {
+ try (Git git = initRepo()) {
+ setFile("sub/.gitignore").toContent("*.tmp\n");
+ addAndCommit(git);
+ File inSub = setFile("sub/a.tmp").toContent("x");
+ File atRoot = setFile("b.tmp").toContent("x");
+ GitIgnoredFiles ignored = underRoot();
+ assertThat(ignored.isIgnored(inSub)).isTrue();
+ assertThat(ignored.isIgnored(atRoot)).isFalse();
+ }
+ }
+
+ @Test
+ @DisplayName("when .git/info/exclude has a pattern, then matching files are ignored")
+ void infoExcludeIsHonored() throws Exception {
+ try (Git git = initRepo()) {
+ setFile(".git/info/exclude").toContent("secret.txt\n");
+ File secret = setFile("secret.txt").toContent("x");
+ assertThat(underRoot().isIgnored(secret)).isTrue();
+ }
+ }
+
+ @Test
+ @DisplayName("when core.excludesFile points to a file with a pattern, then matching files are ignored")
+ void coreExcludesFileIsHonored() throws Exception {
+ try (Git git = initRepo()) {
+ File excludes = setFile("../global-gitignore").toContent("*.bak\n");
+ StoredConfig config = git.getRepository().getConfig();
+ config.setString("core", null, "excludesFile", excludes.getAbsolutePath());
+ config.save();
+ File bak = setFile("old.bak").toContent("x");
+ assertThat(underRoot().isIgnored(bak)).isTrue();
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("given the root dir is a subproject of the repository")
+ class Subproject {
+ @Test
+ @DisplayName("when the repository root .gitignore matches a subproject file, then it is ignored")
+ void parentRulesApplyToSubproject() throws Exception {
+ try (Git git = initRepo()) {
+ setFile(".gitignore").toContent("build/\n");
+ addAndCommit(git);
+ File generated = setFile("moduleA/build/Gen.java").toContent("class Gen {}");
+ File source = setFile("moduleA/src/Main.java").toContent("class Main {}");
+ GitIgnoredFiles ignored = GitIgnoredFiles.under(newFile("moduleA"));
+ assertThat(ignored.isIgnored(generated)).isTrue();
+ assertThat(ignored.isIgnored(source)).isFalse();
+ }
+ }
+
+ @Test
+ @DisplayName("when a file lies outside the root dir, then it is not ignored")
+ void fileOutsideRootIsNotIgnored() throws Exception {
+ try (Git git = initRepo()) {
+ setFile(".gitignore").toContent("*.log\n");
+ addAndCommit(git);
+ setFile("moduleA/keep.txt").toContent("x");
+ File elsewhere = setFile("moduleB/x.log").toContent("x");
+ assertThat(GitIgnoredFiles.under(newFile("moduleA")).isIgnored(elsewhere)).isFalse();
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("given edge cases")
+ class EdgeCases {
+ @Test
+ @DisplayName("when the root dir itself lies inside an ignored directory, then its files are ignored")
+ void rootDirInsideIgnoredDirectory() throws Exception {
+ try (Git git = initRepo()) {
+ setFile(".gitignore").toContent("scratch/\n");
+ addAndCommit(git);
+ File file = setFile("scratch/moduleA/src/Main.java").toContent("class Main {}");
+ assertThat(GitIgnoredFiles.under(newFile("scratch/moduleA")).isIgnored(file)).isTrue();
+ }
+ }
+
+ @Test
+ @DisplayName("when a tracked file was deleted from the working tree, then its untracked siblings are still answered correctly")
+ void trackedFileDeletedFromWorkingTree() throws Exception {
+ try (Git git = initRepo()) {
+ setFile(".gitignore").toContent("*.log\n");
+ File deleted = setFile("dir/Deleted.java").toContent("class Deleted {}");
+ addAndCommit(git);
+ assertThat(deleted.delete()).isTrue();
+ File log = setFile("dir/debug.log").toContent("x");
+ File java = setFile("dir/Main.java").toContent("class Main {}");
+
+ GitIgnoredFiles ignored = underRoot();
+ assertThat(ignored.isIgnored(log)).isTrue();
+ assertThat(ignored.isIgnored(java)).isFalse();
+ assertThat(ignored.isIgnored(deleted)).isFalse();
+ }
+ }
+
+ @Test
+ @DisplayName("when the root dir is not inside a git repository, then a clear error is thrown")
+ void notAGitRepositoryFails() throws IOException {
+ File file = setFile("a.txt").toContent("x");
+ assertThatThrownBy(() -> underRoot().isIgnored(file))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("requires a git repository");
+ }
+ }
+}
diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md
index 27c78dde65..cf1ae9500f 100644
--- a/plugin-gradle/CHANGES.md
+++ b/plugin-gradle/CHANGES.md
@@ -6,6 +6,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
### Added
- Add `groupArtifact(String)` to `palantirJavaFormat()`. ([#3179](https://github.com/diffplug/spotless/pull/3179))
- `SpotlessApply` writes a report of the files it rewrote to `build/spotless/-changed-files.txt`, configurable via `changedFilesReport`. The report is a task output, so tasks which take it as an input automatically depend on the apply task. ([#2715](https://github.com/diffplug/spotless/issues/2715))
+- Add `targetExcludeUsingGitignore()` to exclude the files of a format which git ignores. ([#2443](https://github.com/diffplug/spotless/issues/2443))
### Changes
- `SpotlessApply` now only reports that it did work (and is otherwise `UP-TO-DATE`) when it actually rewrote a file. ([#2715](https://github.com/diffplug/spotless/issues/2715))
### Fixed
diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md
index e762f1d5c5..4a21c0e561 100644
--- a/plugin-gradle/README.md
+++ b/plugin-gradle/README.md
@@ -108,6 +108,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui
- [Generic steps](#generic-steps)
- [License header](#license-header) ([slurp year from git](#retroactively-slurp-years-from-git-history))
- [How can I enforce formatting gradually? (aka "ratchet")](#ratchet)
+ - [How can I skip files ignored by git?](#exclude-using-gitignore)
- [`spotless:off` and `spotless:on`](#spotlessoff-and-spotlesson)
- [Line endings and encodings (invisible stuff)](#line-endings-and-encodings-invisible-stuff)
- [Custom steps](#custom-steps)
@@ -1969,6 +1970,25 @@ Many popular CI systems (GitHub, GitLab, BitBucket, and Travis) use a "shallow c
- calling `git fetch origin main` before you call Spotless
- disabling the shallow clone [like so](https://github.com/diffplug/spotless/issues/710)
+
+
+## How can I skip files ignored by git?
+
+The fastest way to keep Spotless away from generated sources or `node_modules` is a tight `target`, e.g. `target 'src/**/*.md'` rather than `target '**/*.md'`. If that is not possible, add a `targetExclude`: patterns ending in `/**` (e.g. `targetExclude '**/node_modules/**'`) are not even walked. Only if the ignored files cannot be described with a few globs, you can ask git instead:
+
+```gradle
+spotless {
+ format 'misc', {
+ target '**/*.md'
+ targetExcludeUsingGitignore() // in addition to targetExclude
+ }
+}
+```
+
+A file is excluded exactly when `git status` would consider it ignored: it matches a pattern from any `.gitignore`, from `.git/info/exclude` or from `core.excludesFile`, **and** it is not tracked. Tracked files are always formatted, even when they match an ignore pattern. Changes to `.gitignore` are picked up by the next build, also with the configuration cache. The project must be inside a git repository, otherwise the build fails.
+
+This is the slowest of the three options, which is why it has to be set per format: the ignored files are still found by `target` first, and Spotless then has to list the working tree of the project once per build to evaluate git's ignore rules.
+
## `spotless:off` and `spotless:on`
Sometimes there is a chunk of code which you have carefully handcrafted, and you would like to exclude just this one little part from getting clobbered by the autoformat. Some formatters have a way to do this, many don't, but who cares. If you setup your spotless like this:
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java
index 19ea0ac5a3..cc424390b4 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java
@@ -53,6 +53,7 @@
import org.gradle.api.file.FileSystemLocation;
import org.gradle.api.plugins.BasePlugin;
import org.gradle.api.provider.Provider;
+import org.gradle.api.specs.Spec;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.util.GradleVersion;
import org.slf4j.Logger;
@@ -246,6 +247,9 @@ public void encoding(String charset) {
/** The rest of {@link #targetExclude}, or null if it consists only of prunable patterns. */
@Nullable private FileCollection targetExcludeRest;
+ /** If true, files which git ignores are excluded, see {@link #targetExcludeUsingGitignore()}. */
+ protected boolean targetExcludeUsingGitignore;
+
/** The value from which files will be excluded if their content contain it. */
@Nullable protected String targetExcludeContentPattern;
@@ -317,6 +321,22 @@ private static boolean endsWithDoubleStarSegment(String pattern) {
return pattern.equals("**") || pattern.endsWith("/**") || pattern.endsWith("\\**");
}
+ /**
+ * Excludes all files which git ignores, in addition to {@link #targetExclude(Object...)}.
+ *
+ * A file is excluded exactly when {@code git status} would consider it ignored: it matches a pattern
+ * from any {@code .gitignore}, from {@code .git/info/exclude} or from {@code core.excludesFile},
+ * and it is not tracked. The project must be inside a git repository.
+ *
+ * This is the slowest way to narrow a target, because the ignored files are still found by
+ * {@link #target(Object...)} first, and git's ignore rules have to be evaluated on top. Prefer a
+ * tight {@link #target(Object...)}, then {@link #targetExclude(Object...)}, and use this only when
+ * the ignored files cannot be described with a few globs.
+ */
+ public void targetExcludeUsingGitignore() {
+ this.targetExcludeUsingGitignore = true;
+ }
+
/**
* Excludes all files whose content contains {@code string}.
*
@@ -1149,7 +1169,10 @@ public void toggleOffOnDisable() {
protected void setupTask(SpotlessTask task) {
task.setEncoding(getEncoding().name());
task.setLintSuppressions(lintSuppressions);
- FileCollection totalTarget = targetExclude == null ? target : excludeFrom(target);
+ FileCollection targetMinusExclude = targetExclude == null ? target : excludeFrom(target);
+ FileCollection totalTarget = targetExcludeUsingGitignore
+ ? targetMinusExclude.filter(new NotGitIgnored(spotless.getSpotlessTaskService(), getProject().getProjectDir()))
+ : targetMinusExclude;
task.setTarget(totalTarget);
List steps;
if (toggleFence != null) {
@@ -1194,6 +1217,26 @@ private FileCollection excludeFrom(FileCollection fromTarget) {
}
}
+ /**
+ * Keeps the files which git does not ignore. The git state lives in the {@link SpotlessTaskService},
+ * so it is computed at most once per project and build, no matter how many formats use it, and
+ * changes to {@code .gitignore} are picked up by the next build, also with the configuration cache.
+ */
+ private static final class NotGitIgnored implements Spec {
+ private final Provider taskService;
+ private final File projectDir;
+
+ NotGitIgnored(Provider taskService, File projectDir) {
+ this.taskService = taskService;
+ this.projectDir = projectDir;
+ }
+
+ @Override
+ public boolean isSatisfiedBy(File file) {
+ return taskService.get().gitIgnoredFilesFor(projectDir).isNotIgnored(file);
+ }
+ }
+
/** Returns the project that this extension is attached to. */
protected Project getProject() {
return spotless.project;
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java
index c6fd993d20..2d01873b09 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java
@@ -22,6 +22,7 @@
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
@@ -47,6 +48,7 @@
import com.diffplug.common.base.Unhandled;
import com.diffplug.spotless.Lint;
import com.diffplug.spotless.Provisioner;
+import com.diffplug.spotless.extra.GitIgnoredFiles;
import com.diffplug.spotless.extra.P2Provisioner;
/**
@@ -136,6 +138,15 @@ public void close() throws Exception {
}
//
+ //
+ private final Map gitIgnoredFiles = new ConcurrentHashMap<>();
+
+ /** The git-ignored files below the given project, shared by every format of that project for the duration of the build. */
+ GitIgnoredFiles gitIgnoredFilesFor(File projectDir) {
+ return gitIgnoredFiles.computeIfAbsent(projectDir, GitIgnoredFiles::under);
+ }
+ //
+
static String INDEPENDENT_HELPER = "Helper";
static void usesServiceTolerateTestFailure(DefaultTask task, Provider serviceProvider) {
diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TargetExcludeUsingGitignoreTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TargetExcludeUsingGitignoreTest.java
new file mode 100644
index 0000000000..4675601227
--- /dev/null
+++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TargetExcludeUsingGitignoreTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2026 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.gradle.spotless;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.gradle.testkit.runner.BuildResult;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import com.diffplug.spotless.ClearGitConfig;
+
+/** Integration tests for {@code targetExcludeUsingGitignore()}, see https://github.com/diffplug/spotless/issues/2443 */
+@ClearGitConfig
+class TargetExcludeUsingGitignoreTest extends GradleIntegrationHarness {
+ private static final String IGNORED = "src/generated/Ignored.md";
+ private static final String TRACKED = "src/main/Tracked.md";
+ private static final String OPT_IN = "targetExcludeUsingGitignore()";
+
+ private void givenBuild(boolean configCache, String... miscFormatLines) throws IOException {
+ givenBuildWithFormats(configCache, format("misc", miscFormatLines));
+ }
+
+ private void givenBuildWithFormats(boolean configCache, String... formats) throws IOException {
+ if (configCache) {
+ setFile("gradle.properties").toContent("org.gradle.configuration-cache=true");
+ }
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "spotless {",
+ String.join("\n", formats),
+ "}");
+ }
+
+ private static String format(String name, String... extraLines) {
+ return String.join("\n",
+ " format '" + name + "', {",
+ " target 'src/**/*.md'",
+ " " + String.join("\n ", extraLines),
+ " replace 'lowercase hello', 'HELLO', 'hello'",
+ " }");
+ }
+
+ /** Repo with a committed .gitignore for src/generated/, a clean tracked file, and a dirty ignored file. */
+ private Git givenRepoWithDirtyIgnoredFile() throws GitAPIException, IOException {
+ Git git = Git.init().setDirectory(rootFolder()).call();
+ setFile(".gitignore").toContent("src/generated/\n.gradle/\nbuild/\n");
+ setFile(TRACKED).toContent("hello");
+ git.add().addFilepattern(".gitignore").addFilepattern(TRACKED).call();
+ git.commit().setSign(false).setMessage("init").call();
+ setFile(IGNORED).toContent("HELLO");
+ return git;
+ }
+
+ private static String nativePath(String unixPath) {
+ // the violation message lists paths with native separators
+ return unixPath.replace('/', File.separatorChar);
+ }
+
+ @ParameterizedTest(name = "configCache={0}")
+ @ValueSource(booleans = {false, true})
+ @DisplayName("given targetExcludeUsingGitignore is not configured, when checking, then ignored files are still formatted (unchanged default)")
+ void defaultStillFormatsIgnoredFiles(boolean configCache) throws Exception {
+ try (Git git = givenRepoWithDirtyIgnoredFile()) {
+ givenBuild(configCache);
+ BuildResult result = gradleRunner().withArguments("spotlessCheck").buildAndFail();
+ assertThat(result.getOutput()).contains(nativePath(IGNORED));
+ }
+ }
+
+ @ParameterizedTest(name = "configCache={0}")
+ @ValueSource(booleans = {false, true})
+ @DisplayName("given targetExcludeUsingGitignore(), when checking and applying, then ignored files are skipped but others are fixed")
+ void skipsIgnoredFiles(boolean configCache) throws Exception {
+ try (Git git = givenRepoWithDirtyIgnoredFile()) {
+ givenBuild(configCache, OPT_IN);
+ gradleRunner().withArguments("spotlessCheck").build();
+
+ setFile(TRACKED).toContent("HELLO");
+ gradleRunner().withArguments("spotlessCheck").buildAndFail();
+ gradleRunner().withArguments("spotlessApply").build();
+ assertFile(TRACKED).hasContent("hello");
+ assertFile(IGNORED).hasContent("HELLO");
+ }
+ }
+
+ @ParameterizedTest(name = "configCache={0}")
+ @ValueSource(booleans = {false, true})
+ @DisplayName("given two formats and only one opts in, when checking, then only the other format still sees ignored files")
+ void onlyAffectsTheFormatWhichOptsIn(boolean configCache) throws Exception {
+ try (Git git = givenRepoWithDirtyIgnoredFile()) {
+ givenBuildWithFormats(configCache, format("optedIn", OPT_IN), format("notOptedIn"));
+ gradleRunner().withArguments("spotlessOptedInCheck").build();
+ BuildResult result = gradleRunner().withArguments("spotlessNotOptedInCheck").buildAndFail();
+ assertThat(result.getOutput()).contains(nativePath(IGNORED));
+ }
+ }
+
+ @ParameterizedTest(name = "configCache={0}")
+ @ValueSource(booleans = {false, true})
+ @DisplayName("given targetExcludeUsingGitignore() together with targetExclude, when checking, then both exclusions apply")
+ void combinesWithTargetExclude(boolean configCache) throws Exception {
+ try (Git git = givenRepoWithDirtyIgnoredFile()) {
+ givenBuild(configCache, OPT_IN, "targetExclude 'src/vendor/**'");
+ setFile("src/vendor/Vendored.md").toContent("HELLO");
+ gradleRunner().withArguments("spotlessCheck").build();
+ }
+ }
+
+ @ParameterizedTest(name = "configCache={0}")
+ @ValueSource(booleans = {false, true})
+ @DisplayName("given targetExcludeUsingGitignore(), when .gitignore changes between builds, then the next build honors the new rules")
+ void gitignoreChangeIsPickedUp(boolean configCache) throws Exception {
+ try (Git git = givenRepoWithDirtyIgnoredFile()) {
+ givenBuild(configCache, OPT_IN);
+ setFile("src/other/Dirty.md").toContent("HELLO");
+ gradleRunner().withArguments("spotlessCheck").buildAndFail();
+
+ setFile(".gitignore").toContent("src/generated/\nsrc/other/\n.gradle/\nbuild/\n");
+ gradleRunner().withArguments("spotlessCheck").build();
+
+ setFile(".gitignore").toContent(".gradle/\nbuild/\n");
+ gradleRunner().withArguments("spotlessCheck").buildAndFail();
+ }
+ }
+
+ @Test
+ @DisplayName("given targetExcludeUsingGitignore() outside of a git repository, when checking, then the build fails with a clear message")
+ void notAGitRepositoryFails() throws Exception {
+ givenBuild(false, OPT_IN);
+ setFile(TRACKED).toContent("hello");
+ BuildResult result = gradleRunner().withArguments("spotlessCheck").buildAndFail();
+ assertThat(result.getOutput()).contains("requires a git repository");
+ }
+
+ @Test
+ @DisplayName("given targetExcludeUsingGitignore() at the top level of the spotless block, when configuring, then the build fails because it is format-level only")
+ void isNotAvailableGlobally() throws Exception {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "spotless {",
+ " " + OPT_IN,
+ "}");
+ BuildResult result = gradleRunner().withArguments("spotlessCheck").buildAndFail();
+ assertThat(result.getOutput()).contains("targetExcludeUsingGitignore");
+ }
+}
diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md
index 7120684723..cad621559e 100644
--- a/plugin-maven/CHANGES.md
+++ b/plugin-maven/CHANGES.md
@@ -7,6 +7,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
- Add a `` parameter to ``. ([#3179](https://github.com/diffplug/spotless/pull/3179))
- Add a `spotless:report` goal that puts a Spotless page in the Maven site. ([#3175](https://github.com/diffplug/spotless/pull/3175))
List `spotless-maven-plugin` under `` and run `mvn site`: the page shows which files need formatting, with a colored diff for each, and their lint problems. It works with `maven-site-plugin` 3.12.1 and 3.20.0 or later.
+- Add `true` to exclude the files of a format which git ignores. ([#2443](https://github.com/diffplug/spotless/issues/2443))
### Fixed
- GrEclipse no longer leaves a `file%3A...` directory in the working directory. ([#2019](https://github.com/diffplug/spotless/issues/2019))
- DBeaver SQL formatter no longer prints an `IndexOutOfBoundsException` when a file starts with a keyword like `SET`. ([#1552](https://github.com/diffplug/spotless/issues/1552))
diff --git a/plugin-maven/README.md b/plugin-maven/README.md
index cb94daa3de..fd01a0880d 100644
--- a/plugin-maven/README.md
+++ b/plugin-maven/README.md
@@ -68,6 +68,7 @@ user@machine repo % mvn spotless:check
- [Generic steps](#generic-steps)
- [License header](#license-header) ([slurp year from git](#retroactively-slurp-years-from-git-history))
- [How can I enforce formatting gradually? (aka "ratchet")](#ratchet)
+ - [How can I skip files ignored by git?](#exclude-using-gitignore)
- [`spotless:off` and `spotless:on`](#spotlessoff-and-spotlesson)
- [Line endings and encodings (invisible stuff)](#line-endings-and-encodings-invisible-stuff)
- [Disabling warnings and error messages](#disabling-warnings-and-error-messages)
@@ -2131,6 +2132,26 @@ Many popular CI systems (GitHub, GitLab, BitBucket, and Travis) use a "shallow c
- calling `git fetch origin main` before you call Spotless
- disabling the shallow clone [like so](https://github.com/diffplug/spotless/issues/710)
+
+
+## How can I skip files ignored by git?
+
+The fastest way to keep Spotless away from generated sources or `node_modules` is a tight ``, e.g. `src/**/*.md` rather than `**/*.md`, followed by ``. Only if the ignored files cannot be described with a few globs, you can ask git instead:
+
+```xml
+
+
+
+ **/*.md
+
+ true
+ ...
+```
+
+A file is excluded exactly when `git status` would consider it ignored: it matches a pattern from any `.gitignore`, from `.git/info/exclude` or from `core.excludesFile`, **and** it is not tracked. Tracked files are always formatted, even when they match an ignore pattern. It can be combined with [`ratchetFrom`](#ratchet). The project must be inside a git repository, otherwise the build fails.
+
+This is the slowest of the three options, which is why it has to be set per format: the ignored files are still found by `` first, and Spotless then has to list the working tree of the module once per build to evaluate git's ignore rules.
+
## `spotless:off` and `spotless:on`
Sometimes there is a chunk of code which you have carefully handcrafted, and you would like to exclude just this one little part from getting clobbered by the autoformat. Some formatters have a way to do this, many don't, but who cares. If you setup your spotless like this:
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java
index e7401be073..7ab4a7c650 100644
--- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java
@@ -62,6 +62,7 @@
import com.diffplug.spotless.LintState;
import com.diffplug.spotless.LintSuppression;
import com.diffplug.spotless.Provisioner;
+import com.diffplug.spotless.extra.GitIgnoredFiles;
import com.diffplug.spotless.extra.P2Provisioner;
import com.diffplug.spotless.maven.antlr4.Antlr4;
import com.diffplug.spotless.maven.asciidoc.Asciidoc;
@@ -146,6 +147,9 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo {
@Parameter
private String ratchetFrom;
+ /** Computed at most once per module, shared by every format which sets {@code }. */
+ private GitIgnoredFiles gitIgnoredFiles;
+
@Parameter
private LicenseHeader licenseHeader;
@@ -332,12 +336,15 @@ protected boolean isGoalSpecificSkip() {
private List collectFiles(FormatterFactory formatterFactory, FormatterConfig config) {
Optional ratchetFrom = formatterFactory.ratchetFrom(config);
try {
- final List files;
+ List files;
if (ratchetFrom.isPresent()) {
files = collectFilesFromGit(formatterFactory, ratchetFrom.orElseThrow());
} else {
files = collectFilesFromFormatterFactory(formatterFactory);
}
+ if (formatterFactory.excludeUsingGitignore()) {
+ files = withoutGitIgnored(files);
+ }
if (isNullOrEmpty(filePatterns)) {
return files;
}
@@ -358,6 +365,22 @@ private List collectFiles(FormatterFactory formatterFactory, FormatterConf
}
}
+ private List withoutGitIgnored(List files) {
+ GitIgnoredFiles ignored = gitIgnoredFiles();
+ try {
+ return files.stream().filter(ignored::isNotIgnored).collect(toList());
+ } catch (IllegalArgumentException e) {
+ throw new PluginException("Unable to apply : " + e.getMessage(), e);
+ }
+ }
+
+ private synchronized GitIgnoredFiles gitIgnoredFiles() {
+ if (gitIgnoredFiles == null) {
+ gitIgnoredFiles = GitIgnoredFiles.under(baseDir);
+ }
+ return gitIgnoredFiles;
+ }
+
private List collectFilesFromGit(FormatterFactory formatterFactory, String ratchetFrom) {
MatchPatterns includePatterns = MatchPatterns.from(
withNormalizedFileSeparators(getIncludes(formatterFactory)));
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java
index 83579b9744..56b5f79b47 100644
--- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java
@@ -63,6 +63,13 @@ public abstract class FormatterFactory {
@Parameter
private String ratchetFrom = RATCHETFROM_NOT_SET_AT_FORMAT_LEVEL;
+ /**
+ * If {@code true}, files which git ignores (via {@code .gitignore}, {@code .git/info/exclude} or
+ * {@code core.excludesFile}) and which are not tracked are excluded, in addition to {@code }.
+ */
+ @Parameter
+ private boolean excludeUsingGitignore;
+
@Parameter
private String[] includes;
@@ -181,6 +188,10 @@ Optional ratchetFrom(FormatterConfig config) {
.filter(ratchet -> !RATCHETFROM_NONE.equals(ratchet));
}
+ boolean excludeUsingGitignore() {
+ return excludeUsingGitignore;
+ }
+
private Optional optionalRatchetFrom() {
return Optional.ofNullable(ratchetFrom)
.filter(ratchet -> !RATCHETFROM_NOT_SET_AT_FORMAT_LEVEL.equals(ratchet));
diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/ExcludeUsingGitignoreTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/ExcludeUsingGitignoreTest.java
new file mode 100644
index 0000000000..efa7adf879
--- /dev/null
+++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/ExcludeUsingGitignoreTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2026 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.maven;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import com.diffplug.common.base.StringPrinter;
+import com.diffplug.spotless.ClearGitConfig;
+
+/** Integration tests for {@code }, see https://github.com/diffplug/spotless/issues/2443 */
+@ClearGitConfig
+class ExcludeUsingGitignoreTest extends MavenIntegrationHarness {
+ private static final String IGNORED = "src/generated/Ignored.md";
+ private static final String TRACKED = "src/main/Tracked.md";
+ private static final String OPT_IN = "true";
+
+ private static String withFormats(String... formats) {
+ return StringPrinter.buildStringFromLines(
+ "",
+ String.join("\n", formats),
+ "");
+ }
+
+ /** A format which lowercases HELLO in the given files, with extra configuration lines. */
+ private static String format(String include, String... extraLines) {
+ return StringPrinter.buildStringFromLines(
+ " ",
+ String.join("\n", extraLines),
+ " ",
+ " " + include + "",
+ " ",
+ " ",
+ " Lowercase hello",
+ " HELLO",
+ " hello",
+ " ",
+ " ");
+ }
+
+ /** Repo with a committed .gitignore for src/generated/, a clean tracked file, and a dirty ignored file. */
+ private Git givenRepoWithDirtyIgnoredFile() throws GitAPIException, IOException {
+ Git git = Git.init().setDirectory(rootFolder()).call();
+ setFile(".gitignore").toContent("src/generated/\n");
+ setFile(TRACKED).toContent("hello");
+ git.add().addFilepattern(".gitignore").addFilepattern(TRACKED).call();
+ git.commit().setSign(false).setMessage("init").call();
+ git.tag().setName("baseline").call();
+ setFile(IGNORED).toContent("HELLO");
+ return git;
+ }
+
+ @Test
+ @DisplayName("given excludeUsingGitignore is not configured, when checking, then ignored files are still formatted (unchanged default)")
+ void defaultStillFormatsIgnoredFiles() throws Exception {
+ try (Git git = givenRepoWithDirtyIgnoredFile()) {
+ writePom(withFormats(format("src/**/*.md")));
+ mavenRunner().withArguments("spotless:check").runHasError();
+ }
+ }
+
+ @Test
+ @DisplayName("given excludeUsingGitignore on the format, when checking and applying, then ignored files are skipped")
+ void skipsIgnoredFiles() throws Exception {
+ try (Git git = givenRepoWithDirtyIgnoredFile()) {
+ writePom(withFormats(format("src/**/*.md", OPT_IN)));
+ mavenRunner().withArguments("spotless:check").runNoError();
+ mavenRunner().withArguments("spotless:apply").runNoError();
+ assertFile(IGNORED).hasContent("HELLO");
+ }
+ }
+
+ @Test
+ @DisplayName("given excludeUsingGitignore on the format, when a non-ignored file is dirty, then it is still reported and fixed")
+ void stillFormatsNonIgnoredFiles() throws Exception {
+ try (Git git = givenRepoWithDirtyIgnoredFile()) {
+ writePom(withFormats(format("src/**/*.md", OPT_IN)));
+ setFile(TRACKED).toContent("HELLO");
+ mavenRunner().withArguments("spotless:check").runHasError();
+ mavenRunner().withArguments("spotless:apply").runNoError();
+ assertFile(TRACKED).hasContent("hello");
+ assertFile(IGNORED).hasContent("HELLO");
+ }
+ }
+
+ @Test
+ @DisplayName("given two formats and only one opts in, when applying, then only the other format still formats ignored files")
+ void onlyAffectsTheFormatWhichOptsIn() throws Exception {
+ try (Git git = givenRepoWithDirtyIgnoredFile()) {
+ setFile("src/generated/Ignored.txt").toContent("HELLO");
+ writePom(withFormats(format("src/**/*.md", OPT_IN), format("src/**/*.txt")));
+ mavenRunner().withArguments("spotless:apply").runNoError();
+ assertFile(IGNORED).hasContent("HELLO");
+ assertFile("src/generated/Ignored.txt").hasContent("hello");
+ }
+ }
+
+ @Test
+ @DisplayName("given excludeUsingGitignore together with excludes, when checking, then both exclusions apply")
+ void combinesWithExcludes() throws Exception {
+ try (Git git = givenRepoWithDirtyIgnoredFile()) {
+ setFile("src/vendor/Vendored.md").toContent("HELLO");
+ writePom(withFormats(format("src/**/*.md", OPT_IN, "src/vendor/**")));
+ mavenRunner().withArguments("spotless:check").runNoError();
+ }
+ }
+
+ @Test
+ @DisplayName("given excludeUsingGitignore with ratchetFrom, when an ignored file is new, then it is skipped")
+ void combinesWithRatchetFrom() throws Exception {
+ try (Git git = givenRepoWithDirtyIgnoredFile()) {
+ writePom("baseline\n" + withFormats(format("src/**/*.md", OPT_IN)));
+ mavenRunner().withArguments("spotless:check").runNoError();
+ }
+ }
+
+ @Test
+ @DisplayName("given excludeUsingGitignore outside of a git repository, when checking, then the build fails with a clear message")
+ void notAGitRepositoryFails() throws Exception {
+ setFile(TRACKED).toContent("hello");
+ writePom(withFormats(format("src/**/*.md", OPT_IN)));
+ assertThat(mavenRunner().withArguments("spotless:check").runHasError().stdOutUtf8()).contains("requires a git repository");
+ }
+}