Skip to content

Commit 2e96bde

Browse files
authored
(plugin-maven) when ratcheting, check dirty files from git (#706)
2 parents fe207a2 + 13c7b2b commit 2e96bde

File tree

4 files changed

+130
-42
lines changed

4 files changed

+130
-42
lines changed

lib-extra/src/main/java/com/diffplug/spotless/extra/GitRatchet.java

+11-5
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,21 @@
5656
* - If you have up-to-date checking and want the best possible performance, use {@link #subtreeShaOf(Object, ObjectId)} to optimize up-to-date checks on a per-project basis.
5757
*/
5858
public abstract class GitRatchet<Project> implements AutoCloseable {
59+
60+
public boolean isClean(Project project, ObjectId treeSha, File file) throws IOException {
61+
Repository repo = repositoryFor(project);
62+
String relativePath = FileSignature.pathNativeToUnix(repo.getWorkTree().toPath().relativize(file.toPath()).toString());
63+
return isClean(project, treeSha, relativePath);
64+
}
65+
5966
/**
6067
* This is the highest-level method, which all the others serve. Given the sha
6168
* of a git tree (not a commit!), and the file in question, this method returns
6269
* true if that file is clean relative to that tree. A naive implementation of this
6370
* could be verrrry slow, so the rest of this is about speeding this up.
6471
*/
65-
public boolean isClean(Project project, ObjectId treeSha, File file) throws IOException {
72+
public boolean isClean(Project project, ObjectId treeSha, String relativePathUnix) throws IOException {
6673
Repository repo = repositoryFor(project);
67-
String path = FileSignature.pathNativeToUnix(repo.getWorkTree().toPath().relativize(file.toPath()).toString());
6874

6975
// TODO: should be cached-per-repo if it is thread-safe, or per-repo-per-thread if it is not
7076
DirCache dirCache = repo.readDirCache();
@@ -75,7 +81,7 @@ public boolean isClean(Project project, ObjectId treeSha, File file) throws IOEx
7581
treeWalk.addTree(new DirCacheIterator(dirCache));
7682
treeWalk.addTree(new FileTreeIterator(repo));
7783
treeWalk.setFilter(AndTreeFilter.create(
78-
PathFilter.create(path),
84+
PathFilter.create(relativePathUnix),
7985
new IndexDiffFilter(INDEX, WORKDIR)));
8086

8187
if (!treeWalk.next()) {
@@ -102,7 +108,7 @@ public boolean isClean(Project project, ObjectId treeSha, File file) throws IOEx
102108
} else if (treeEqualsIndex) {
103109
// this means they are all equal to each other, which should never happen
104110
// the IndexDiffFilter should keep those out of the TreeWalk entirely
105-
throw new IllegalStateException("Index status for " + file + " against treeSha " + treeSha + " is invalid.");
111+
throw new IllegalStateException("Index status for " + relativePathUnix + " against treeSha " + treeSha + " is invalid.");
106112
} else {
107113
// they are all unique
108114
// we have to check manually
@@ -135,7 +141,7 @@ private static boolean worktreeIsCleanCheckout(TreeWalk treeWalk) {
135141
* builds and submodules, it's quite possible that a single Gradle project will span across multiple git repositories.
136142
* We cache the Repository for every Project in `gitRoots`, and use dynamic programming to populate it.
137143
*/
138-
private Repository repositoryFor(Project project) throws IOException {
144+
protected Repository repositoryFor(Project project) throws IOException {
139145
Repository repo = gitRoots.get(project);
140146
if (repo == null) {
141147
if (isGitRoot(getDir(project))) {

plugin-maven/CHANGES.md

+2
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+
### Fixed
7+
Improve speed of plugin when using `<ratchetFrom>` ([#701](https://github.com/diffplug/spotless/pull/706)).
68

79
## [2.4.1] - 2020-09-18
810
### Fixed

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

+72-27
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import java.io.File;
2121
import java.io.IOException;
22+
import java.util.ArrayList;
2223
import java.util.Arrays;
2324
import java.util.Collections;
2425
import java.util.HashSet;
@@ -30,6 +31,7 @@
3031
import java.util.regex.Pattern;
3132
import java.util.stream.Collectors;
3233
import java.util.stream.Stream;
34+
import java.util.stream.StreamSupport;
3335

3436
import org.apache.maven.plugin.AbstractMojo;
3537
import org.apache.maven.plugin.MojoExecutionException;
@@ -38,11 +40,11 @@
3840
import org.codehaus.plexus.resource.ResourceManager;
3941
import org.codehaus.plexus.resource.loader.FileResourceLoader;
4042
import org.codehaus.plexus.util.FileUtils;
43+
import org.codehaus.plexus.util.MatchPatterns;
4144
import org.eclipse.aether.RepositorySystem;
4245
import org.eclipse.aether.RepositorySystemSession;
4346
import org.eclipse.aether.repository.RemoteRepository;
4447

45-
import com.diffplug.common.collect.Iterables;
4648
import com.diffplug.spotless.Formatter;
4749
import com.diffplug.spotless.LineEnding;
4850
import com.diffplug.spotless.Provisioner;
@@ -137,38 +139,23 @@ public final void execute() throws MojoExecutionException {
137139
}
138140

139141
private void execute(FormatterFactory formatterFactory) throws MojoExecutionException {
140-
List<File> files = collectFiles(formatterFactory);
141142
FormatterConfig config = getFormatterConfig();
142-
Optional<String> ratchetFrom = formatterFactory.ratchetFrom(config);
143-
Iterable<File> toFormat;
144-
if (!ratchetFrom.isPresent()) {
145-
toFormat = files;
146-
} else {
147-
toFormat = Iterables.filter(files, GitRatchetMaven.instance().isGitDirty(baseDir, ratchetFrom.get()));
148-
}
143+
List<File> files = collectFiles(formatterFactory, config);
144+
149145
try (Formatter formatter = formatterFactory.newFormatter(files, config)) {
150-
process(toFormat, formatter);
146+
process(files, formatter);
151147
}
152148
}
153149

154-
private List<File> collectFiles(FormatterFactory formatterFactory) throws MojoExecutionException {
155-
Set<String> configuredIncludes = formatterFactory.includes();
156-
Set<String> configuredExcludes = formatterFactory.excludes();
157-
158-
Set<String> includes = configuredIncludes.isEmpty() ? formatterFactory.defaultIncludes() : configuredIncludes;
159-
if (includes.isEmpty()) {
160-
throw new MojoExecutionException("You must specify some files to include, such as '<includes><include>src/**</include></includes>'");
161-
}
162-
163-
Set<String> excludes = new HashSet<>(FileUtils.getDefaultExcludesAsList());
164-
excludes.add(withTrailingSeparator(buildDir.toString()));
165-
excludes.addAll(configuredExcludes);
166-
167-
String includesString = String.join(",", includes);
168-
String excludesString = String.join(",", excludes);
169-
150+
private List<File> collectFiles(FormatterFactory formatterFactory, FormatterConfig config) throws MojoExecutionException {
151+
Optional<String> ratchetFrom = formatterFactory.ratchetFrom(config);
170152
try {
171-
final List<File> files = FileUtils.getFiles(baseDir, includesString, excludesString);
153+
final List<File> files;
154+
if (ratchetFrom.isPresent()) {
155+
files = collectFilesFromGit(formatterFactory, ratchetFrom.get());
156+
} else {
157+
files = collectFilesFromFormatterFactory(formatterFactory);
158+
}
172159
if (filePatterns == null || filePatterns.isEmpty()) {
173160
return files;
174161
}
@@ -189,10 +176,68 @@ private List<File> collectFiles(FormatterFactory formatterFactory) throws MojoEx
189176
}
190177
}
191178

179+
private List<File> collectFilesFromGit(FormatterFactory formatterFactory, String ratchetFrom) throws MojoExecutionException {
180+
MatchPatterns includePatterns = MatchPatterns.from(
181+
withNormalizedFileSeparators(getIncludes(formatterFactory)));
182+
MatchPatterns excludePatterns = MatchPatterns.from(
183+
withNormalizedFileSeparators(getExcludes(formatterFactory)));
184+
185+
Iterable<String> dirtyFiles;
186+
try {
187+
dirtyFiles = GitRatchetMaven
188+
.instance().getDirtyFiles(baseDir, ratchetFrom);
189+
} catch (IOException e) {
190+
throw new MojoExecutionException("Unable to scan file tree rooted at " + baseDir, e);
191+
}
192+
193+
List<File> result = new ArrayList<>();
194+
for (String file : withNormalizedFileSeparators(dirtyFiles)) {
195+
if (includePatterns.matches(file, true)) {
196+
if (!excludePatterns.matches(file, true)) {
197+
result.add(new File(baseDir.getPath(), file));
198+
}
199+
}
200+
}
201+
return result;
202+
}
203+
204+
private List<File> collectFilesFromFormatterFactory(FormatterFactory formatterFactory)
205+
throws MojoExecutionException, IOException {
206+
String includesString = String.join(",", getIncludes(formatterFactory));
207+
String excludesString = String.join(",", getExcludes(formatterFactory));
208+
209+
return FileUtils.getFiles(baseDir, includesString, excludesString);
210+
}
211+
212+
private Iterable<String> withNormalizedFileSeparators(Iterable<String> patterns) {
213+
return StreamSupport.stream(patterns.spliterator(), true)
214+
.map(pattern -> pattern.replace('/', File.separatorChar))
215+
.map(pattern -> pattern.replace('\\', File.separatorChar))
216+
.collect(Collectors.toSet());
217+
}
218+
192219
private static String withTrailingSeparator(String path) {
193220
return path.endsWith(File.separator) ? path : path + File.separator;
194221
}
195222

223+
private Set<String> getIncludes(FormatterFactory formatterFactory) throws MojoExecutionException {
224+
Set<String> configuredIncludes = formatterFactory.includes();
225+
Set<String> includes = configuredIncludes.isEmpty() ? formatterFactory.defaultIncludes() : configuredIncludes;
226+
if (includes.isEmpty()) {
227+
throw new MojoExecutionException("You must specify some files to include, such as '<includes><include>src/**</include></includes>'");
228+
}
229+
return includes;
230+
}
231+
232+
private Set<String> getExcludes(FormatterFactory formatterFactory) {
233+
Set<String> configuredExcludes = formatterFactory.excludes();
234+
235+
Set<String> excludes = new HashSet<>(FileUtils.getDefaultExcludesAsList());
236+
excludes.add(withTrailingSeparator(buildDir.toString()));
237+
excludes.addAll(configuredExcludes);
238+
return excludes;
239+
}
240+
196241
private FormatterConfig getFormatterConfig() {
197242
ArtifactResolver resolver = new ArtifactResolver(repositorySystem, repositorySystemSession, repositories, getLog());
198243
Provisioner provisioner = MavenProvisioner.create(resolver);

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

+45-10
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,17 @@
1717

1818
import java.io.File;
1919
import java.io.IOException;
20-
import java.util.function.Predicate;
20+
import java.nio.file.Path;
21+
import java.nio.file.Paths;
22+
import java.util.HashSet;
23+
import java.util.Set;
24+
import java.util.stream.Collectors;
2125

26+
import org.eclipse.jgit.lib.IndexDiff;
2227
import org.eclipse.jgit.lib.ObjectId;
28+
import org.eclipse.jgit.lib.Repository;
29+
import org.eclipse.jgit.treewalk.FileTreeIterator;
2330

24-
import com.diffplug.common.base.Errors;
2531
import com.diffplug.spotless.extra.GitRatchet;
2632

2733
final class GitRatchetMaven extends GitRatchet<File> {
@@ -50,15 +56,44 @@ static GitRatchetMaven instance() {
5056
return instance;
5157
}
5258

53-
/** A predicate which returns only the "git dirty" files. */
54-
Predicate<File> isGitDirty(File baseDir, String ratchetFrom) {
59+
Iterable<String> getDirtyFiles(File baseDir, String ratchetFrom) throws IOException {
60+
Repository repository = repositoryFor(baseDir);
5561
ObjectId sha = rootTreeShaOf(baseDir, ratchetFrom);
56-
return file -> {
57-
try {
58-
return !isClean(baseDir, sha, file);
59-
} catch (IOException e) {
60-
throw Errors.asRuntime(e);
62+
63+
IndexDiff indexDiff = new IndexDiff(repository, sha, new FileTreeIterator(repository));
64+
indexDiff.diff();
65+
66+
String workTreePath = repository.getWorkTree().getPath();
67+
Path baseDirPath = Paths.get(baseDir.getPath());
68+
69+
Set<String> dirtyPaths = new HashSet<>(indexDiff.getChanged());
70+
dirtyPaths.addAll(indexDiff.getAdded());
71+
dirtyPaths.addAll(indexDiff.getConflicting());
72+
dirtyPaths.addAll(indexDiff.getUntracked());
73+
74+
for (String path : indexDiff.getModified()) {
75+
if (!dirtyPaths.add(path)) {
76+
// File differs to index both in working tree and local repository,
77+
// which means the working tree and local repository versions may be equal
78+
if (isClean(baseDir, sha, path)) {
79+
dirtyPaths.remove(path);
80+
}
6181
}
62-
};
82+
}
83+
for (String path : indexDiff.getRemoved()) {
84+
if (dirtyPaths.contains(path)) {
85+
// A removed file can also be untracked, if a new file with the same name has been created.
86+
// This file may be identical to the one in the local repository.
87+
if (isClean(baseDir, sha, path)) {
88+
dirtyPaths.remove(path);
89+
}
90+
}
91+
}
92+
// A file can be modified in the index but removed in the tree
93+
dirtyPaths.removeAll(indexDiff.getMissing());
94+
95+
return dirtyPaths.stream()
96+
.map(path -> baseDirPath.relativize(Paths.get(workTreePath, path)).toString())
97+
.collect(Collectors.toList());
6398
}
6499
}

0 commit comments

Comments
 (0)