Skip to content

Commit bebaa59

Browse files
authored
Up-to-date index for Maven plugin (#935)
2 parents e342b1c + 93151cd commit bebaa59

30 files changed

+1919
-36
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ output = [
4242
'| Toggle with [`spotless:off` and `spotless:on`](plugin-gradle/#spotlessoff-and-spotlesson) | {{yes}} | {{yes}} | {{no}} | {{no}} |',
4343
'| [Ratchet from](https://github.com/diffplug/spotless/tree/main/plugin-gradle#ratchet) `origin/main` or other git ref | {{yes}} | {{yes}} | {{no}} | {{no}} |',
4444
'| Define [line endings using git](https://github.com/diffplug/spotless/tree/main/plugin-gradle#line-endings-and-encodings-invisible-stuff) | {{yes}} | {{yes}} | {{yes}} | {{no}} |',
45-
'| Fast incremental format and up-to-date check | {{yes}} | {{no}} | {{no}} | {{no}} |',
45+
'| Fast incremental format and up-to-date check | {{yes}} | {{yes}} | {{no}} | {{no}} |',
4646
'| Fast format on fresh checkout using buildcache | {{yes}} | {{no}} | {{no}} | {{no}} |',
4747
lib('generic.EndWithNewlineStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
4848
lib('generic.IndentStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
@@ -81,7 +81,7 @@ extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}}
8181
| Toggle with [`spotless:off` and `spotless:on`](plugin-gradle/#spotlessoff-and-spotlesson) | :+1: | :+1: | :white_large_square: | :white_large_square: |
8282
| [Ratchet from](https://github.com/diffplug/spotless/tree/main/plugin-gradle#ratchet) `origin/main` or other git ref | :+1: | :+1: | :white_large_square: | :white_large_square: |
8383
| Define [line endings using git](https://github.com/diffplug/spotless/tree/main/plugin-gradle#line-endings-and-encodings-invisible-stuff) | :+1: | :+1: | :+1: | :white_large_square: |
84-
| Fast incremental format and up-to-date check | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
84+
| Fast incremental format and up-to-date check | :+1: | :+1: | :white_large_square: | :white_large_square: |
8585
| Fast format on fresh checkout using buildcache | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
8686
| [`generic.EndWithNewlineStep`](lib/src/main/java/com/diffplug/spotless/generic/EndWithNewlineStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
8787
| [`generic.IndentStep`](lib/src/main/java/com/diffplug/spotless/generic/IndentStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |

gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ artifactIdGradle=spotless-plugin-gradle
1616
# Build requirements
1717
VER_JAVA=1.8
1818
VER_SPOTBUGS=4.5.0
19+
VER_JSR_305=3.0.2
1920

2021
# Dependencies provided by Spotless plugin
2122
VER_SLF4J=[1.6,2.0[

gradle/java-setup.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,5 @@ tasks.named('spotbugsMain') {
3535
dependencies {
3636
compileOnly 'net.jcip:jcip-annotations:1.0'
3737
compileOnly "com.github.spotbugs:spotbugs-annotations:${VER_SPOTBUGS}"
38-
compileOnly 'com.google.code.findbugs:jsr305:3.0.2'
38+
compileOnly "com.google.code.findbugs:jsr305:${VER_JSR_305}"
3939
}

plugin-maven/CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
44

55
## [Unreleased]
6+
### Added
7+
* Incremental up-to-date checking ([#935](https://github.com/diffplug/spotless/pull/935)).
68

79
## [2.17.7] - 2021-12-16
810
### Fixed

plugin-maven/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,47 @@ If your project has not been rigorous with copyright headers, and you'd like to
900900

901901
<a name="ratchet"></a>
902902

903+
## Incremental up-to-date checking and formatting
904+
905+
**This feature is turned off by default.**
906+
907+
Execution of `spotless:check` and `spotless:apply` for large projects can take time.
908+
By default, Spotless Maven plugin needs to read and format each source file.
909+
Repeated executions of `spotless:check` or `spotless:apply` are completely independent.
910+
911+
If your project has many source files managed by Spotless and formatting takes a long time, you can
912+
enable incremental up-to-date checking with the following configuration:
913+
914+
```xml
915+
<configuration>
916+
<upToDateChecking>
917+
<enabled>true</enabled>
918+
</upToDateChecking>
919+
<!-- ... define formats ... -->
920+
</configuration>
921+
```
922+
923+
With up-to-date checking enabled, Spotless creates an index file in the `target` directory.
924+
The index file contains source file paths and corresponding last modified timestamps.
925+
It allows Spotless to skip already formatted files that have not changed.
926+
927+
**Note:** the index file is located in the `target` directory. Executing `mvn clean` will delete
928+
the index file, and Spotless will need to check/format all the source files.
929+
930+
Spotless will remove the index file when up-to-date checking is explicitly turned off with the
931+
following configuration:
932+
933+
```xml
934+
<configuration>
935+
<upToDateChecking>
936+
<enabled>false</enabled>
937+
</upToDateChecking>
938+
<!-- ... define formats ... -->
939+
</configuration>
940+
```
941+
942+
Consider using this configuration if you experience issues with up-to-date checking.
943+
903944
## How can I enforce formatting gradually? (aka "ratchet")
904945

905946
If your project is not currently enforcing formatting, then it can be a noisy transition. Having a giant commit where every single file gets changed makes the history harder to read. To address this, you can use the `ratchet` feature:

plugin-maven/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ dependencies {
7979

8080
compileOnly "org.apache.maven:maven-plugin-api:${VER_MAVEN_API}"
8181
compileOnly "org.apache.maven.plugin-tools:maven-plugin-annotations:${VER_MAVEN_API}"
82+
compileOnly "org.apache.maven:maven-core:${VER_MAVEN_API}"
8283
compileOnly "org.eclipse.aether:aether-api:${VER_ECLIPSE_AETHER}"
8384
compileOnly "org.eclipse.aether:aether-util:${VER_ECLIPSE_AETHER}"
8485

@@ -95,6 +96,7 @@ dependencies {
9596
testImplementation "org.apache.maven:maven-plugin-api:${VER_MAVEN_API}"
9697
testImplementation "org.eclipse.aether:aether-api:${VER_ECLIPSE_AETHER}"
9798
testImplementation "org.codehaus.plexus:plexus-resources:${VER_PLEXUS_RESOURCES}"
99+
testImplementation "org.apache.maven:maven-core:${VER_MAVEN_API}"
98100
}
99101

100102
task cleanMavenProjectDir(type: Delete) { delete MAVEN_PROJECT_DIR }
@@ -159,6 +161,7 @@ task createPomXml(dependsOn: installLocalDependencies) {
159161
mavenApiVersion : VER_MAVEN_API,
160162
eclipseAetherVersion : VER_ECLIPSE_AETHER,
161163
spotlessLibVersion : libVersion,
164+
jsr305Version : VER_JSR_305,
162165
additionalDependencies : additionalDependencies
163166
]
164167

plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,16 @@
2222
import java.util.ArrayList;
2323
import java.util.Arrays;
2424
import java.util.Collections;
25+
import java.util.HashMap;
2526
import java.util.HashSet;
2627
import java.util.List;
28+
import java.util.Map;
29+
import java.util.Map.Entry;
2730
import java.util.Objects;
2831
import java.util.Optional;
2932
import java.util.Set;
3033
import java.util.function.Predicate;
34+
import java.util.function.Supplier;
3135
import java.util.regex.Pattern;
3236
import java.util.stream.Collectors;
3337
import java.util.stream.Stream;
@@ -37,6 +41,7 @@
3741
import org.apache.maven.plugin.MojoExecutionException;
3842
import org.apache.maven.plugins.annotations.Component;
3943
import org.apache.maven.plugins.annotations.Parameter;
44+
import org.apache.maven.project.MavenProject;
4045
import org.codehaus.plexus.resource.ResourceManager;
4146
import org.codehaus.plexus.resource.loader.FileResourceLoader;
4247
import org.codehaus.plexus.util.FileUtils;
@@ -54,6 +59,8 @@
5459
import com.diffplug.spotless.maven.generic.Format;
5560
import com.diffplug.spotless.maven.generic.LicenseHeader;
5661
import com.diffplug.spotless.maven.groovy.Groovy;
62+
import com.diffplug.spotless.maven.incremental.UpToDateChecker;
63+
import com.diffplug.spotless.maven.incremental.UpToDateChecking;
5764
import com.diffplug.spotless.maven.java.Java;
5865
import com.diffplug.spotless.maven.kotlin.Kotlin;
5966
import com.diffplug.spotless.maven.pom.Pom;
@@ -84,6 +91,9 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo {
8491
@Parameter(property = "spotless.check.skip", defaultValue = "false")
8592
private boolean checkSkip;
8693

94+
@Parameter(defaultValue = "${project}", required = true, readonly = true)
95+
private MavenProject project;
96+
8797
@Parameter(defaultValue = "${repositorySystemSession}", required = true, readonly = true)
8898
private RepositorySystemSession repositorySystemSession;
8999

@@ -147,13 +157,36 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo {
147157
@Parameter(property = LicenseHeaderStep.spotlessSetLicenseHeaderYearsFromGitHistory)
148158
private String setLicenseHeaderYearsFromGitHistory;
149159

150-
protected abstract void process(Iterable<File> files, Formatter formatter) throws MojoExecutionException;
160+
@Parameter
161+
private UpToDateChecking upToDateChecking;
162+
163+
protected abstract void process(Iterable<File> files, Formatter formatter, UpToDateChecker upToDateChecker) throws MojoExecutionException;
151164

152165
@Override
153166
public final void execute() throws MojoExecutionException {
167+
if (shouldSkip()) {
168+
getLog().info(String.format("Spotless %s skipped", goal));
169+
return;
170+
}
171+
154172
List<FormatterFactory> formatterFactories = getFormatterFactories();
173+
FormatterConfig config = getFormatterConfig();
174+
175+
Map<FormatterFactory, Supplier<Iterable<File>>> formatterFactoryToFiles = new HashMap<>();
155176
for (FormatterFactory formatterFactory : formatterFactories) {
156-
execute(formatterFactory);
177+
Supplier<Iterable<File>> filesToFormat = () -> collectFiles(formatterFactory, config);
178+
formatterFactoryToFiles.put(formatterFactory, filesToFormat);
179+
}
180+
181+
try (FormattersHolder formattersHolder = FormattersHolder.create(formatterFactoryToFiles, config);
182+
UpToDateChecker upToDateChecker = createUpToDateChecker(formattersHolder.getFormatters())) {
183+
for (Entry<Formatter, Supplier<Iterable<File>>> entry : formattersHolder.getFormattersWithFiles().entrySet()) {
184+
Formatter formatter = entry.getKey();
185+
Iterable<File> files = entry.getValue().get();
186+
process(files, formatter, upToDateChecker);
187+
}
188+
} catch (PluginException e) {
189+
throw e.asMojoExecutionException();
157190
}
158191
}
159192

@@ -169,21 +202,7 @@ private boolean shouldSkip() {
169202
return false;
170203
}
171204

172-
private void execute(FormatterFactory formatterFactory) throws MojoExecutionException {
173-
if (shouldSkip()) {
174-
getLog().info(String.format("Spotless %s skipped", goal));
175-
return;
176-
}
177-
178-
FormatterConfig config = getFormatterConfig();
179-
List<File> files = collectFiles(formatterFactory, config);
180-
181-
try (Formatter formatter = formatterFactory.newFormatter(files, config)) {
182-
process(files, formatter);
183-
}
184-
}
185-
186-
private List<File> collectFiles(FormatterFactory formatterFactory, FormatterConfig config) throws MojoExecutionException {
205+
private List<File> collectFiles(FormatterFactory formatterFactory, FormatterConfig config) {
187206
Optional<String> ratchetFrom = formatterFactory.ratchetFrom(config);
188207
try {
189208
final List<File> files;
@@ -208,11 +227,11 @@ private List<File> collectFiles(FormatterFactory formatterFactory, FormatterConf
208227
.filter(shouldInclude)
209228
.collect(toList());
210229
} catch (IOException e) {
211-
throw new MojoExecutionException("Unable to scan file tree rooted at " + baseDir, e);
230+
throw new PluginException("Unable to scan file tree rooted at " + baseDir, e);
212231
}
213232
}
214233

215-
private List<File> collectFilesFromGit(FormatterFactory formatterFactory, String ratchetFrom) throws MojoExecutionException {
234+
private List<File> collectFilesFromGit(FormatterFactory formatterFactory, String ratchetFrom) {
216235
MatchPatterns includePatterns = MatchPatterns.from(
217236
withNormalizedFileSeparators(getIncludes(formatterFactory)));
218237
MatchPatterns excludePatterns = MatchPatterns.from(
@@ -223,7 +242,7 @@ private List<File> collectFilesFromGit(FormatterFactory formatterFactory, String
223242
dirtyFiles = GitRatchetMaven
224243
.instance().getDirtyFiles(baseDir, ratchetFrom);
225244
} catch (IOException e) {
226-
throw new MojoExecutionException("Unable to scan file tree rooted at " + baseDir, e);
245+
throw new PluginException("Unable to scan file tree rooted at " + baseDir, e);
227246
}
228247

229248
List<File> result = new ArrayList<>();
@@ -237,8 +256,7 @@ private List<File> collectFilesFromGit(FormatterFactory formatterFactory, String
237256
return result;
238257
}
239258

240-
private List<File> collectFilesFromFormatterFactory(FormatterFactory formatterFactory)
241-
throws MojoExecutionException, IOException {
259+
private List<File> collectFilesFromFormatterFactory(FormatterFactory formatterFactory) throws IOException {
242260
String includesString = String.join(",", getIncludes(formatterFactory));
243261
String excludesString = String.join(",", getExcludes(formatterFactory));
244262

@@ -256,11 +274,11 @@ private static String withTrailingSeparator(String path) {
256274
return path.endsWith(File.separator) ? path : path + File.separator;
257275
}
258276

259-
private Set<String> getIncludes(FormatterFactory formatterFactory) throws MojoExecutionException {
277+
private Set<String> getIncludes(FormatterFactory formatterFactory) {
260278
Set<String> configuredIncludes = formatterFactory.includes();
261279
Set<String> includes = configuredIncludes.isEmpty() ? formatterFactory.defaultIncludes() : configuredIncludes;
262280
if (includes.isEmpty()) {
263-
throw new MojoExecutionException("You must specify some files to include, such as '<includes><include>src/**</include></includes>'");
281+
throw new PluginException("You must specify some files to include, such as '<includes><include>src/**</include></includes>'");
264282
}
265283
return includes;
266284
}
@@ -300,4 +318,12 @@ private List<FormatterStepFactory> getFormatterStepFactories() {
300318
.filter(Objects::nonNull)
301319
.collect(toList());
302320
}
321+
322+
private UpToDateChecker createUpToDateChecker(Iterable<Formatter> formatters) {
323+
if (upToDateChecking != null && upToDateChecking.isEnabled()) {
324+
getLog().info("Up-to-date checking enabled");
325+
return UpToDateChecker.forProject(project, formatters, getLog());
326+
}
327+
return UpToDateChecker.noop(project, getLog());
328+
}
303329
}

plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Objects;
2525
import java.util.Optional;
2626
import java.util.Set;
27+
import java.util.function.Supplier;
2728
import java.util.stream.Collectors;
2829

2930
import org.apache.maven.plugins.annotations.Parameter;
@@ -71,10 +72,10 @@ public final Set<String> excludes() {
7172
return excludes == null ? emptySet() : Sets.newHashSet(excludes);
7273
}
7374

74-
public final Formatter newFormatter(List<File> filesToFormat, FormatterConfig config) {
75+
public final Formatter newFormatter(Supplier<Iterable<File>> filesToFormat, FormatterConfig config) {
7576
Charset formatterEncoding = encoding(config);
7677
LineEnding formatterLineEndings = lineEndings(config);
77-
LineEnding.Policy formatterLineEndingPolicy = formatterLineEndings.createPolicy(config.getFileLocator().getBaseDir(), () -> filesToFormat);
78+
LineEnding.Policy formatterLineEndingPolicy = formatterLineEndings.createPolicy(config.getFileLocator().getBaseDir(), filesToFormat);
7879

7980
FormatterStepConfig stepConfig = stepConfig(formatterEncoding, config);
8081
List<FormatterStepFactory> factories = gatherStepFactories(config.getGlobalStepFactories(), stepFactories);
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2021 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.maven;
17+
18+
import java.io.File;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
import java.util.Map.Entry;
22+
import java.util.Set;
23+
import java.util.function.Supplier;
24+
25+
import com.diffplug.spotless.Formatter;
26+
27+
class FormattersHolder implements AutoCloseable {
28+
29+
private final Map<Formatter, Supplier<Iterable<File>>> formatterToFiles;
30+
31+
FormattersHolder(Map<Formatter, Supplier<Iterable<File>>> formatterToFiles) {
32+
this.formatterToFiles = formatterToFiles;
33+
}
34+
35+
static FormattersHolder create(Map<FormatterFactory, Supplier<Iterable<File>>> formatterFactoryToFiles, FormatterConfig config) {
36+
Map<Formatter, Supplier<Iterable<File>>> formatterToFiles = new HashMap<>();
37+
try {
38+
for (Entry<FormatterFactory, Supplier<Iterable<File>>> entry : formatterFactoryToFiles.entrySet()) {
39+
FormatterFactory formatterFactory = entry.getKey();
40+
Supplier<Iterable<File>> files = entry.getValue();
41+
42+
Formatter formatter = formatterFactory.newFormatter(files, config);
43+
formatterToFiles.put(formatter, files);
44+
}
45+
} catch (RuntimeException openError) {
46+
try {
47+
close(formatterToFiles.keySet());
48+
} catch (Exception closeError) {
49+
openError.addSuppressed(closeError);
50+
}
51+
throw openError;
52+
}
53+
54+
return new FormattersHolder(formatterToFiles);
55+
}
56+
57+
Iterable<Formatter> getFormatters() {
58+
return formatterToFiles.keySet();
59+
}
60+
61+
Map<Formatter, Supplier<Iterable<File>>> getFormattersWithFiles() {
62+
return formatterToFiles;
63+
}
64+
65+
@Override
66+
public void close() {
67+
try {
68+
close(formatterToFiles.keySet());
69+
} catch (Exception e) {
70+
throw new RuntimeException("Unable to close formatters", e);
71+
}
72+
}
73+
74+
private static void close(Set<Formatter> formatters) throws Exception {
75+
Exception error = null;
76+
for (Formatter formatter : formatters) {
77+
try {
78+
formatter.close();
79+
} catch (Exception e) {
80+
if (error == null) {
81+
error = e;
82+
} else {
83+
error.addSuppressed(e);
84+
}
85+
}
86+
}
87+
if (error != null) {
88+
throw error;
89+
}
90+
}
91+
}

0 commit comments

Comments
 (0)