Skip to content

Add opt-in respectGitignore to skip files ignored by git (#2443) - #3176

Open
Pankraz76 wants to merge 2 commits into
diffplug:mainfrom
Pankraz76:feat/respect-gitignore-2443
Open

Pankraz76 wants to merge 2 commits into
diffplug:mainfrom
Pankraz76:feat/respect-gitignore-2443

Conversation

@Pankraz76

Copy link
Copy Markdown

Closes #2443

Why

Spotless picks files with globs (target '**/*.md', <include>src/**/*.java</include>). Broad globs easily pick up files that are not part of the project's source: generated code, node_modules, local scratch files, IDE output. These are already listed once, in .gitignore. Today users have to copy those rules into targetExclude / <excludes> and keep both lists in sync. Even then they can't express negations (!keep.log) or nested .gitignore files. The workaround in the issue thread shells out to an npm tool to turn .gitignore into globs, which shows how much this is wanted (6 👍).

What

One opt-in flag in both build plugins, off by default:

Gradle

spotless {
  respectGitignore()            // all formats
  format 'misc', {
    target '**/*.md'
    respectGitignore(false)     // per-format override
  }
}

Maven

<configuration>
  <respectGitignore>true</respectGitignore>     <!-- all formats -->
  <formats>
    <format>
      <respectGitignore>false</respectGitignore> <!-- per-format override -->

Same rules as git status

A file is skipped only if both are true:

  • it matches a pattern from any .gitignore (root or nested), .git/info/exclude, or core.excludesFile
  • it is not tracked in the index

Tracked files are always formatted, even if they match an ignore pattern. That is how git behaves, and it avoids silently dropping committed sources from formatting.

Behaviour details

  • Off by default, so no existing build changes.
  • The format-level setting overrides the global one (same model as ratchetFrom).
  • Works together with ratchetFrom (a Maven test covers this).
  • Outside a git repository the build fails fast with: Respecting .gitignore requires a git repository, but none was found in ….
  • Gradle: the filter is applied to the task's input FileCollection, so up-to-date checks see the filtered set. It is configuration-cache safe: the computed git state is transient, so it is recomputed every build and .gitignore edits take effect on the next run. Tested with the configuration cache on and off.

How

  • lib-extra: new GitIgnoredFiles.
    • Uses a JGit IndexDiff (index vs. working tree, no HEAD needed), limited to the project subtree, and reads getIgnoredNotInIndex().
    • JGit reports a fully ignored directory as a single entry and does not descend into it, so large ignored folders (node_modules, build) cost almost nothing.
    • A lookup checks the file and its parent directories against that set.
    • The state is computed lazily, once per instance, and is thread-safe. The class is Serializable with transient state.
    • It reuses GitWorkarounds to find the repository, so git worktrees are handled the same way as for ratchetFrom.
  • plugin-maven: a respectGitignore parameter on the mojo and on FormatterFactory. Filtering happens in collectFiles, after include/exclude (and ratchet) resolution.
  • plugin-gradle: respectGitignore() / respectGitignore(boolean) / setRespectGitignore / isRespectGitignore on SpotlessExtension and FormatExtension. setupTask wraps the target in FileCollection.filter(...) with a small serializable Spec.

Tests

Written test-first: each suite was run red against an API-only stub or missing DSL, then made green.

Area Test class Scenarios Result
Core GitIgnoredFilesTest root pattern; non-matching file; file deep in ignored dir; negation !keep.log; tracked-but-matching is not ignored; nested .gitignore scoping; .git/info/exclude; core.excludesFile; subproject root inherits repo rules; file outside root; not a git repo → clear error; serialization round-trip 12/12 ✅
Maven RespectGitignoreTest default unchanged; global skip on check + apply; non-ignored files still fixed; format-level flag; format false overrides global true; combined with ratchetFrom 6/6 ✅
Gradle RespectGitignoreTest same matrix, plus .gitignore edited between builds and the not-a-repo error; parameterized with configuration cache on and off 11/11 ✅

Also run:

  • spotlessCheck and SpotBugs for lib-extra, plugin-maven, plugin-gradle ✅
  • Existing suites around file collection, git and caching:
    • lib-extra: GitRachetMergeBaseTest, GitAttributesTest, GitWorkaroundsTest, all ✅ (34/34 including the new tests)
    • plugin-gradle: GitRatchetGradleTest, ConfigurationCacheTest, TargetExclude*, MultipleTargetsTest, UpToDateTest, MultiProjectTest, GradleIncrementalResolutionTest, all ✅ (47/47; ConfigurationCacheTest passed on rerun after Maven Central rate-limited a download)
    • plugin-maven: GitRatchetMavenTest, IncludesExcludesTest, MultipleFormatsTest, SpotlessCheckMojoTest ✅ (23/27)
  • The 4 remaining Maven failures (SpecificFilesTest ×3, MultiModuleProjectTest.testConfigurationDependency) are pre-existing. They fail the same way on unmodified main (7cc923d), with google-java-format(java.lang.reflect.InvocationTargetException) in my environment (JDK 21), and are unrelated to this PR.

Out of scope / possible follow-ups

  • In Gradle, each format with the flag computes its own IndexDiff for the project subtree. This is cheap because ignored directories are skipped, but it could be shared per project later if anyone measures a hot spot.
  • Files inside a nested repository (submodule) under the project are checked against the outer repository's rules.

Checklist (from the PR template)

  • Allow edits from maintainers
  • [Unreleased] entries in CHANGES.md, plugin-gradle/CHANGES.md, plugin-maven/CHANGES.md (linking Support .gitignore #2443)
  • README sections for both plugins ("How can I skip files ignored by git?") + TOC entries

🤖 Generated with Claude Code

https://claude.ai/code/session_01MTr23vs1UZzH1yBNJUacfL

Adds `GitIgnoredFiles` to lib-extra, which answers whether git ignores a
file using the same rules as `git status`: the file matches a pattern from
any .gitignore, .git/info/exclude or core.excludesFile AND is not tracked.
Tracked files are never skipped. Ignored paths are computed once via a
JGit IndexDiff restricted to the project subtree; fully ignored
directories (node_modules, build, ...) are not descended into.

- Maven: `<respectGitignore>true</respectGitignore>` globally and per
  format (format-level wins); composes with ratchetFrom.
- Gradle: `respectGitignore()` / `respectGitignore(boolean)` globally and
  per format. Filters the task's input FileCollection so up-to-date
  checks stay correct, and is configuration-cache safe: the git state is
  transient, so .gitignore edits are honored by the next build.
- Fails fast with a clear message outside a git repository.

Default is off, so existing builds are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTr23vs1UZzH1yBNJUacfL
@Pankraz76 Pankraz76 mentioned this pull request Nov 8, 2026
The check violation message lists paths with native separators, so
asserting on "src/generated/Ignored.md" failed on Windows where the
output contains "src\generated\Ignored.md".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MTr23yqwK9BPLYGVJwwsVB

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support .gitignore

2 participants