Skip to content

SpotlessApply: report changed files and only set didWork when files changed (#2715) - #3177

Merged
nedtwigg merged 4 commits into
diffplug:mainfrom
Pankraz76:feat/2715-apply-changed-files-report
Nov 10, 2026
Merged

nedtwigg merged 4 commits into
diffplug:mainfrom
Pankraz76:feat/2715-apply-changed-files-report

Conversation

@Pankraz76

Copy link
Copy Markdown

Closes #2715

Why

Many builds run spotlessApply locally as part of build, so developers never see what Spotless fixed. The issue asks for a way to tell when spotlessApply actually changed something, and which files. The obvious workaround (doLast { if (didWork) ... }) doesn't work today, because SpotlessApply reports didWork = true even when it wrote nothing:

  • when there is nothing to apply, it copies the format task's didWork, so it shows SUCCESS every time spotlessJava re-runs (e.g. on the build right after a fix)
  • clean files that are byte-identical to the source were still copied and counted

What

  • Accurate didWork: SpotlessApply reports work only when it actually rewrote a source file; otherwise it shows UP-TO-DATE. Clean files identical to the source are no longer copied, so their mtimes aren't touched.
  • Changed-files report: every SpotlessApply writes the files its last run rewrote to build/spotless/<taskName>-changed-files.txt. That's one project-relative /-separated path per line, sorted, UTF-8. The report is empty if nothing changed, and it is still written when the task then fails on lints. The location is configurable via the new changedFilesReport property:
tasks.withType(com.diffplug.gradle.spotless.SpotlessApply).configureEach {
  def report = changedFilesReport
  doLast {
    if (didWork) {
      logger.lifecycle("Spotless fixed:\n" + report.get().asFile.text)
    }
  }
}

The report property is @Internal on purpose. A task that declares outputs but no inputs can be considered up-to-date by Gradle, and apply must always run.

Not changed (the "load-bearing" part from the issue discussion)

registerApplyAlreadyRan still runs first, so spotlessCheck still skips its work after apply ran. The lint failure path is unchanged.

Visible behaviour change

On the build right after a fix, the format task still runs (its input was rewritten), but the apply tasks are now UP-TO-DATE instead of SUCCESS. UpToDateTest asserted the old outcome in exactly that step, so those three steps now assert per-task outcomes.

Tests

New SpotlessApplyChangedFilesTest, written first and confirmed failing before the fix:

  • unformatted files → report lists exactly the rewritten files
  • only clean files → apply UP-TO-DATE, empty report
  • second run with nothing to fix → report reset to empty
  • lint failure → report still lists the fixed file
  • custom report location read from a build script in doLast

Ran locally, all passing (76/76):

  • the new test
  • UpToDateTest, ConfigurationCacheTest, GitRatchetGradleTest, IdeHookTest, MultiProjectTest, ErrorShouldRethrowTest
  • IndependentTaskTest, PaddedCellTaskTest, DiffMessageFormatterTest, CompositeBuildRunToFixMessageTest, SpotlessTaskImplTest
  • spotlessCheck and :plugin-gradle:check, including SpotBugs

plugin-gradle/CHANGES.md and the README are updated. Only plugin-gradle is affected, so the lib and Maven changelogs are untouched.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MTr23z6ccMd2shvyTaTFVY

…hanged (diffplug#2715)

- Each SpotlessApply writes the files its most recent run rewrote to
  build/spotless/<taskName>-changed-files.txt (configurable via
  changedFilesReport). The report is empty if nothing changed and is
  written even when the task then fails on lints.
- didWork is now true only if a source file was actually rewritten.
  Clean files that are byte-identical to the source are no longer copied.
- UpToDateTest: the run after a fix now shows the apply tasks UP-TO-DATE
  while the format task still runs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MTr23z6ccMd2shvyTaTFVY
…diffplug#2715)

The second apply after a fix re-runs the format task, but there is nothing
left to apply, so the apply tasks are now UP-TO-DATE. Move the assertion
for that step into GradleIntegrationHarness#applyOnlyRerunsFormatTask and
use it from both UpToDateTest and BumpThisNumberIfACustomStepChangesTest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MTr23z6ccMd2shvyTaTFVY
Addresses review feedback on diffplug#2715: the changed-files report should be a
proper task output, so tasks that consume it work with Gradle's task graph
and caching.

- changedFilesReport is now @OutputFile instead of @internal, so a task
  that uses the provider as an input gets an implicit dependency on the
  apply task (no manual dependsOn).
- Since apply declares outputs but no inputs, Gradle would otherwise mark
  it UP-TO-DATE whenever the report is unchanged and skip rewriting
  sources; outputs.upToDateWhen { false } keeps it always running (same
  approach as the IDE hook). Apply stays @DisableCachingByDefault.
- Tests: consumer gets implicit dependency, consumer works with the
  configuration cache, and apply is never skipped because its report is
  unchanged (fails without upToDateWhen).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MTr23wUKk2wLNAcmdt8aq3
@nedtwigg
nedtwigg merged commit 1c057b7 into diffplug:main Nov 10, 2026
18 checks passed
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.

feat(spotless/gradle-plugin): Allow SpotlessApply to notify when/what work was done

3 participants