Skip to content

Commit 3ceeafa

Browse files
authored
Merge pull request #621 from diffplug/feat/git-attributes-relocatable
Make the the GitAttributesLineEndingsPolicy relocatable across machines
2 parents 5e40de5 + 59b07c7 commit 3ceeafa

File tree

8 files changed

+74
-98
lines changed

8 files changed

+74
-98
lines changed

CHANGES.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This document is intended for Spotless developers.
1010
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
1111

1212
## [Unreleased]
13+
### Changed
14+
* `LineEnding.GIT_ATTRIBUTES` now creates a policy whose serialized state can be relocated from one machine to another. No user-visible change, but paves the way for remote build cache support in Gradle. ([#621](https://github.com/diffplug/spotless/pull/621))
1315
### Added
1416
* `prettier` will now autodetect the parser (and formatter) to use based on the filename, unless you override this using `config` or `configFile` with the option `parser` or `filepath`. ([#620](https://github.com/diffplug/spotless/pull/620))
1517

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

+50-88
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016 DiffPlug
2+
* Copyright 2016-2020 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,19 +22,13 @@
2222
import java.io.IOException;
2323
import java.io.InputStream;
2424
import java.io.Serializable;
25-
import java.util.ArrayList;
26-
import java.util.Collection;
2725
import java.util.Collections;
2826
import java.util.HashMap;
29-
import java.util.HashSet;
3027
import java.util.List;
3128
import java.util.Locale;
3229
import java.util.Map;
3330
import java.util.Objects;
34-
import java.util.Set;
3531
import java.util.function.Supplier;
36-
import java.util.stream.Collectors;
37-
import java.util.stream.Stream;
3832

3933
import javax.annotation.Nullable;
4034

@@ -51,11 +45,9 @@
5145
import org.eclipse.jgit.util.SystemReader;
5246

5347
import com.googlecode.concurrenttrees.radix.ConcurrentRadixTree;
54-
import com.googlecode.concurrenttrees.radix.node.Node;
5548
import com.googlecode.concurrenttrees.radix.node.concrete.DefaultCharSequenceNodeFactory;
5649

5750
import com.diffplug.common.base.Errors;
58-
import com.diffplug.common.tree.TreeStream;
5951
import com.diffplug.spotless.FileSignature;
6052
import com.diffplug.spotless.LazyForwardingEquality;
6153
import com.diffplug.spotless.LineEnding;
@@ -72,62 +64,82 @@ public final class GitAttributesLineEndings {
7264
// prevent direct instantiation
7365
private GitAttributesLineEndings() {}
7466

75-
public static Policy create(File projectDir, Supplier<Iterable<File>> toFormat) {
76-
return new Policy(projectDir, toFormat);
67+
/**
68+
* Creates a line-endings policy whose serialized state is relativized against projectDir,
69+
* at the cost of eagerly evaluating the line-ending state of every target file when the
70+
* policy is checked for equality with another policy.
71+
*/
72+
public static LineEnding.Policy create(File projectDir, Supplier<Iterable<File>> toFormat) {
73+
return new RelocatablePolicy(projectDir, toFormat);
7774
}
7875

79-
static class Policy extends LazyForwardingEquality<FileState> implements LineEnding.Policy {
80-
private static final long serialVersionUID = 1L;
76+
static class RelocatablePolicy extends LazyForwardingEquality<CachedEndings> implements LineEnding.Policy {
77+
private static final long serialVersionUID = 5868522122123693015L;
8178

8279
final transient File projectDir;
8380
final transient Supplier<Iterable<File>> toFormat;
8481

85-
Policy(File projectDir, Supplier<Iterable<File>> toFormat) {
82+
RelocatablePolicy(File projectDir, Supplier<Iterable<File>> toFormat) {
8683
this.projectDir = Objects.requireNonNull(projectDir, "projectDir");
8784
this.toFormat = Objects.requireNonNull(toFormat, "toFormat");
8885
}
8986

9087
@Override
91-
protected FileState calculateState() throws Exception {
92-
return new FileState(projectDir, toFormat.get());
88+
protected CachedEndings calculateState() throws Exception {
89+
Runtime runtime = new RuntimeInit(projectDir, toFormat.get()).atRuntime();
90+
return new CachedEndings(projectDir, runtime, toFormat.get());
9391
}
9492

95-
/**
96-
* Initializing the state() for up-to-date checking is faster than the full initialization
97-
* needed to actually do the formatting. We load the Runtime lazily from the state().
98-
*/
99-
transient Runtime runtime;
100-
10193
@Override
10294
public String getEndingFor(File file) {
103-
if (runtime == null) {
104-
runtime = state().atRuntime();
105-
}
106-
return runtime.getEndingFor(file);
95+
return state().endingFor(file);
10796
}
10897
}
10998

11099
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
111-
static class FileState implements Serializable {
112-
private static final long serialVersionUID = 1L;
100+
static class CachedEndings implements Serializable {
101+
private static final long serialVersionUID = -2534772773057900619L;
102+
103+
/** this is transient, to simulate PathSensitive.RELATIVE */
104+
transient final String rootDir;
105+
/** the line ending used for most files */
106+
final String defaultEnding;
107+
/** any exceptions to that default, in terms of relative path from rootDir */
108+
final ConcurrentRadixTree<String> hasNonDefaultEnding = new ConcurrentRadixTree<>(new DefaultCharSequenceNodeFactory());
109+
110+
CachedEndings(File projectDir, Runtime runtime, Iterable<File> toFormat) {
111+
rootDir = FileSignature.pathNativeToUnix(projectDir.getAbsolutePath()) + "/";
112+
defaultEnding = runtime.defaultEnding;
113+
for (File file : toFormat) {
114+
String ending = runtime.getEndingFor(file);
115+
if (!ending.equals(defaultEnding)) {
116+
String path = FileSignature.pathNativeToUnix(file.getAbsolutePath());
117+
hasNonDefaultEnding.put(path, ending);
118+
}
119+
}
120+
}
113121

122+
/** Returns the line ending appropriate for the given file. */
123+
public String endingFor(File file) {
124+
String path = FileSignature.pathNativeToUnix(file.getAbsolutePath());
125+
String subpath = FileSignature.subpath(rootDir, path);
126+
String ending = hasNonDefaultEnding.getValueForExactKey(subpath);
127+
return ending == null ? defaultEnding : ending;
128+
}
129+
}
130+
131+
static class RuntimeInit {
114132
/** /etc/gitconfig (system-global), ~/.gitconfig, project/.git/config (each might-not exist). */
115-
transient final FileBasedConfig systemConfig, userConfig, repoConfig;
133+
final FileBasedConfig systemConfig, userConfig, repoConfig;
116134

117135
/** Global .gitattributes file pointed at by systemConfig or userConfig, and the file in the repo. */
118-
transient final @Nullable File globalAttributesFile, repoAttributesFile;
136+
final @Nullable File globalAttributesFile, repoAttributesFile;
119137

120138
/** git worktree root, might not exist if we're not in a git repo. */
121-
transient final @Nullable File workTree;
122-
123-
/** All the .gitattributes files in the work tree that we're formatting. */
124-
transient final List<File> gitattributes;
125-
126-
/** The signature of *all* of the files below. */
127-
final FileSignature signature;
139+
final @Nullable File workTree;
128140

129141
@SuppressFBWarnings("SIC_INNER_SHOULD_BE_STATIC_ANON")
130-
FileState(File projectDir, Iterable<File> toFormat) throws IOException {
142+
RuntimeInit(File projectDir, Iterable<File> toFormat) throws IOException {
131143
requireElementsNonNull(toFormat);
132144
/////////////////////////////////
133145
// USER AND SYSTEM-WIDE VALUES //
@@ -178,56 +190,6 @@ public boolean isOutdated() {
178190
repoAttributesFile = null;
179191
}
180192
Errors.log().run(repoConfig::load);
181-
182-
// The .gitattributes files which apply to the files we are formatting
183-
gitattributes = gitAttributes(toFormat);
184-
185-
// find every actual File which exists above
186-
Stream<File> misc = Stream.of(systemConfig.getFile(), userConfig.getFile(), repoConfig.getFile(), globalAttributesFile, repoAttributesFile);
187-
List<File> toSign = Stream.concat(gitattributes.stream(), misc)
188-
.filter(file -> file != null && file.exists() && file.isFile())
189-
.collect(Collectors.toList());
190-
// sign it for up-to-date checking
191-
signature = FileSignature.signAsSet(toSign);
192-
}
193-
194-
/** Returns all of the .gitattributes files which affect the given files. */
195-
static List<File> gitAttributes(Iterable<File> files) {
196-
// build a radix tree out of all the parent folders in these files
197-
ConcurrentRadixTree<String> tree = new ConcurrentRadixTree<>(new DefaultCharSequenceNodeFactory());
198-
for (File file : files) {
199-
String parentPath = file.getParent() + File.separator;
200-
tree.putIfAbsent(parentPath, parentPath);
201-
}
202-
// traverse the edge nodes to find the outermost folders
203-
List<File> edgeFolders = TreeStream.depthFirst(Node::getOutgoingEdges, tree.getNode())
204-
.filter(node -> node.getOutgoingEdges().isEmpty() && node.getValue() != null)
205-
.map(node -> new File((String) node.getValue()))
206-
.collect(Collectors.toList());
207-
208-
List<File> gitAttrFiles = new ArrayList<>();
209-
Set<File> visitedFolders = new HashSet<>();
210-
for (File edgeFolder : edgeFolders) {
211-
gitAttrAddWithParents(edgeFolder, visitedFolders, gitAttrFiles);
212-
}
213-
return gitAttrFiles;
214-
}
215-
216-
/** Searches folder and all its parents for gitattributes files. */
217-
private static void gitAttrAddWithParents(File folder, Set<File> visitedFolders, Collection<File> gitAttrFiles) {
218-
if (!visitedFolders.add(folder)) {
219-
// bail if we already visited this folder
220-
return;
221-
}
222-
223-
File gitAttr = new File(folder, Constants.DOT_GIT_ATTRIBUTES);
224-
if (gitAttr.exists() && gitAttr.isFile()) {
225-
gitAttrFiles.add(gitAttr);
226-
}
227-
File parentFile = folder.getParentFile();
228-
if (parentFile != null) {
229-
gitAttrAddWithParents(folder.getParentFile(), visitedFolders, gitAttrFiles);
230-
}
231193
}
232194

233195
private Runtime atRuntime() {

lib/src/main/java/com/diffplug/spotless/FileSignature.java

+9
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,13 @@ public static String pathNativeToUnix(String pathNative) {
119119
public static String pathUnixToNative(String pathUnix) {
120120
return LineEnding.nativeIsWin() ? pathUnix.replace('/', '\\') : pathUnix;
121121
}
122+
123+
/** Asserts that child is a subpath of root. and returns the subpath. */
124+
public static String subpath(String root, String child) {
125+
if (child.startsWith(root)) {
126+
return child.substring(root.length());
127+
} else {
128+
throw new IllegalArgumentException("Expected '" + child + "' to start with '" + root + "'");
129+
}
130+
}
122131
}

plugin-gradle/CHANGES.md

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
77
## [Unreleased]
88
### Added
99
* `prettier` will now autodetect the parser (and formatter) to use based on the filename, unless you override this using `config()` or `configFile()` with the option `parser` or `filepath`. ([#620](https://github.com/diffplug/spotless/pull/620))
10+
### Fixed
11+
* LineEndings.GIT_ATTRIBUTES is now a bit more efficient, and paves the way for remote build cache support in Gradle. ([#621](https://github.com/diffplug/spotless/pull/621))
1012

1113
## [4.4.0] - 2020-06-19
1214
### Added

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java

+3-6
Original file line numberDiff line numberDiff line change
@@ -669,13 +669,10 @@ public EclipseWtpConfig eclipseWtp(EclipseWtpFormatterStep type, String version)
669669
protected void setupTask(SpotlessTask task) {
670670
task.setEncoding(getEncoding().name());
671671
task.setExceptionPolicy(exceptionPolicy);
672-
if (targetExclude == null) {
673-
task.setTarget(target);
674-
} else {
675-
task.setTarget(target.minus(targetExclude));
676-
}
672+
FileCollection totalTarget = targetExclude == null ? target : target.minus(targetExclude);
673+
task.setTarget(totalTarget);
677674
task.setSteps(steps);
678-
task.setLineEndingsPolicy(getLineEndings().createPolicy(getProject().getProjectDir(), () -> task.target));
675+
task.setLineEndingsPolicy(getLineEndings().createPolicy(getProject().getProjectDir(), () -> totalTarget));
679676
if (spotless.project != spotless.project.getRootProject()) {
680677
spotless.getRegisterDependenciesTask().hookSubprojectTask(task);
681678
}

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskBase.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public void setEncoding(String encoding) {
6363
this.encoding = Objects.requireNonNull(encoding);
6464
}
6565

66-
protected LineEnding.Policy lineEndingsPolicy = LineEnding.UNIX.createPolicy();
66+
protected LineEnding.Policy lineEndingsPolicy;
6767

6868
@Input
6969
public LineEnding.Policy getLineEndingsPolicy() {

plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FormatTaskTest.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016 DiffPlug
2+
* Copyright 2016-2020 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,14 +37,14 @@ public class FormatTaskTest extends ResourceHarness {
3737
public void createTask() throws IOException {
3838
Project project = TestProvisioner.gradleProject(rootFolder());
3939
spotlessTask = project.getTasks().create("spotlessTaskUnderTest", SpotlessTask.class);
40+
spotlessTask.setLineEndingsPolicy(LineEnding.UNIX.createPolicy());
4041
}
4142

4243
@Test
4344
public void testLineEndings() throws Exception {
4445
File testFile = setFile("testFile").toContent("\r\n");
4546
File outputFile = new File(spotlessTask.getOutputDirectory(), "testFile");
4647

47-
spotlessTask.setLineEndingsPolicy(LineEnding.UNIX.createPolicy());
4848
spotlessTask.setTarget(Collections.singleton(testFile));
4949
execute(spotlessTask);
5050

testlib/src/test/java/com/diffplug/spotless/FileSignatureTest.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016 DiffPlug
2+
* Copyright 2016-2020 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -56,4 +56,8 @@ private List<File> getTestFiles(final String[] paths) throws IOException {
5656
return result;
5757
}
5858

59+
@Test
60+
public void testSubpath() {
61+
assertThat(FileSignature.subpath("root/", "root/child")).isEqualTo("child");
62+
}
5963
}

0 commit comments

Comments
 (0)