Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the the GitAttributesLineEndingsPolicy relocatable across machines #621

Merged
merged 14 commits into from
Jun 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ This document is intended for Spotless developers.
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).

## [Unreleased]
### Changed
* `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))
### Added
* `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))

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016 DiffPlug
* Copyright 2016-2020 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,19 +22,13 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.Nullable;

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

import com.googlecode.concurrenttrees.radix.ConcurrentRadixTree;
import com.googlecode.concurrenttrees.radix.node.Node;
import com.googlecode.concurrenttrees.radix.node.concrete.DefaultCharSequenceNodeFactory;

import com.diffplug.common.base.Errors;
import com.diffplug.common.tree.TreeStream;
import com.diffplug.spotless.FileSignature;
import com.diffplug.spotless.LazyForwardingEquality;
import com.diffplug.spotless.LineEnding;
Expand All @@ -72,62 +64,82 @@ public final class GitAttributesLineEndings {
// prevent direct instantiation
private GitAttributesLineEndings() {}

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

static class Policy extends LazyForwardingEquality<FileState> implements LineEnding.Policy {
private static final long serialVersionUID = 1L;
static class RelocatablePolicy extends LazyForwardingEquality<CachedEndings> implements LineEnding.Policy {
private static final long serialVersionUID = 5868522122123693015L;

final transient File projectDir;
final transient Supplier<Iterable<File>> toFormat;

Policy(File projectDir, Supplier<Iterable<File>> toFormat) {
RelocatablePolicy(File projectDir, Supplier<Iterable<File>> toFormat) {
this.projectDir = Objects.requireNonNull(projectDir, "projectDir");
this.toFormat = Objects.requireNonNull(toFormat, "toFormat");
}

@Override
protected FileState calculateState() throws Exception {
return new FileState(projectDir, toFormat.get());
protected CachedEndings calculateState() throws Exception {
Runtime runtime = new RuntimeInit(projectDir, toFormat.get()).atRuntime();
return new CachedEndings(projectDir, runtime, toFormat.get());
}

/**
* Initializing the state() for up-to-date checking is faster than the full initialization
* needed to actually do the formatting. We load the Runtime lazily from the state().
*/
transient Runtime runtime;

@Override
public String getEndingFor(File file) {
if (runtime == null) {
runtime = state().atRuntime();
}
return runtime.getEndingFor(file);
return state().endingFor(file);
}
}

@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
static class FileState implements Serializable {
private static final long serialVersionUID = 1L;
static class CachedEndings implements Serializable {
private static final long serialVersionUID = -2534772773057900619L;

/** this is transient, to simulate PathSensitive.RELATIVE */
transient final String rootDir;
/** the line ending used for most files */
final String defaultEnding;
/** any exceptions to that default, in terms of relative path from rootDir */
final ConcurrentRadixTree<String> hasNonDefaultEnding = new ConcurrentRadixTree<>(new DefaultCharSequenceNodeFactory());

CachedEndings(File projectDir, Runtime runtime, Iterable<File> toFormat) {
rootDir = FileSignature.pathNativeToUnix(projectDir.getAbsolutePath()) + "/";
defaultEnding = runtime.defaultEnding;
for (File file : toFormat) {
String ending = runtime.getEndingFor(file);
if (!ending.equals(defaultEnding)) {
String path = FileSignature.pathNativeToUnix(file.getAbsolutePath());
hasNonDefaultEnding.put(path, ending);
}
}
}

/** Returns the line ending appropriate for the given file. */
public String endingFor(File file) {
String path = FileSignature.pathNativeToUnix(file.getAbsolutePath());
String subpath = FileSignature.subpath(rootDir, path);
String ending = hasNonDefaultEnding.getValueForExactKey(subpath);
return ending == null ? defaultEnding : ending;
}
}

static class RuntimeInit {
/** /etc/gitconfig (system-global), ~/.gitconfig, project/.git/config (each might-not exist). */
transient final FileBasedConfig systemConfig, userConfig, repoConfig;
final FileBasedConfig systemConfig, userConfig, repoConfig;

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

/** git worktree root, might not exist if we're not in a git repo. */
transient final @Nullable File workTree;

/** All the .gitattributes files in the work tree that we're formatting. */
transient final List<File> gitattributes;

/** The signature of *all* of the files below. */
final FileSignature signature;
final @Nullable File workTree;

@SuppressFBWarnings("SIC_INNER_SHOULD_BE_STATIC_ANON")
FileState(File projectDir, Iterable<File> toFormat) throws IOException {
RuntimeInit(File projectDir, Iterable<File> toFormat) throws IOException {
requireElementsNonNull(toFormat);
/////////////////////////////////
// USER AND SYSTEM-WIDE VALUES //
Expand Down Expand Up @@ -178,56 +190,6 @@ public boolean isOutdated() {
repoAttributesFile = null;
}
Errors.log().run(repoConfig::load);

// The .gitattributes files which apply to the files we are formatting
gitattributes = gitAttributes(toFormat);

// find every actual File which exists above
Stream<File> misc = Stream.of(systemConfig.getFile(), userConfig.getFile(), repoConfig.getFile(), globalAttributesFile, repoAttributesFile);
List<File> toSign = Stream.concat(gitattributes.stream(), misc)
.filter(file -> file != null && file.exists() && file.isFile())
.collect(Collectors.toList());
// sign it for up-to-date checking
signature = FileSignature.signAsSet(toSign);
}

/** Returns all of the .gitattributes files which affect the given files. */
static List<File> gitAttributes(Iterable<File> files) {
// build a radix tree out of all the parent folders in these files
ConcurrentRadixTree<String> tree = new ConcurrentRadixTree<>(new DefaultCharSequenceNodeFactory());
for (File file : files) {
String parentPath = file.getParent() + File.separator;
tree.putIfAbsent(parentPath, parentPath);
}
// traverse the edge nodes to find the outermost folders
List<File> edgeFolders = TreeStream.depthFirst(Node::getOutgoingEdges, tree.getNode())
.filter(node -> node.getOutgoingEdges().isEmpty() && node.getValue() != null)
.map(node -> new File((String) node.getValue()))
.collect(Collectors.toList());

List<File> gitAttrFiles = new ArrayList<>();
Set<File> visitedFolders = new HashSet<>();
for (File edgeFolder : edgeFolders) {
gitAttrAddWithParents(edgeFolder, visitedFolders, gitAttrFiles);
}
return gitAttrFiles;
}

/** Searches folder and all its parents for gitattributes files. */
private static void gitAttrAddWithParents(File folder, Set<File> visitedFolders, Collection<File> gitAttrFiles) {
if (!visitedFolders.add(folder)) {
// bail if we already visited this folder
return;
}

File gitAttr = new File(folder, Constants.DOT_GIT_ATTRIBUTES);
if (gitAttr.exists() && gitAttr.isFile()) {
gitAttrFiles.add(gitAttr);
}
File parentFile = folder.getParentFile();
if (parentFile != null) {
gitAttrAddWithParents(folder.getParentFile(), visitedFolders, gitAttrFiles);
}
}

private Runtime atRuntime() {
Expand Down
9 changes: 9 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/FileSignature.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,13 @@ public static String pathNativeToUnix(String pathNative) {
public static String pathUnixToNative(String pathUnix) {
return LineEnding.nativeIsWin() ? pathUnix.replace('/', '\\') : pathUnix;
}

/** Asserts that child is a subpath of root. and returns the subpath. */
public static String subpath(String root, String child) {
if (child.startsWith(root)) {
return child.substring(root.length());
} else {
throw new IllegalArgumentException("Expected '" + child + "' to start with '" + root + "'");
}
}
}
2 changes: 2 additions & 0 deletions plugin-gradle/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
### Added
* `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))
### Fixed
* 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))

## [4.4.0] - 2020-06-19
### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -669,13 +669,10 @@ public EclipseWtpConfig eclipseWtp(EclipseWtpFormatterStep type, String version)
protected void setupTask(SpotlessTask task) {
task.setEncoding(getEncoding().name());
task.setExceptionPolicy(exceptionPolicy);
if (targetExclude == null) {
task.setTarget(target);
} else {
task.setTarget(target.minus(targetExclude));
}
FileCollection totalTarget = targetExclude == null ? target : target.minus(targetExclude);
task.setTarget(totalTarget);
task.setSteps(steps);
task.setLineEndingsPolicy(getLineEndings().createPolicy(getProject().getProjectDir(), () -> task.target));
task.setLineEndingsPolicy(getLineEndings().createPolicy(getProject().getProjectDir(), () -> totalTarget));
if (spotless.project != spotless.project.getRootProject()) {
spotless.getRegisterDependenciesTask().hookSubprojectTask(task);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public void setEncoding(String encoding) {
this.encoding = Objects.requireNonNull(encoding);
}

protected LineEnding.Policy lineEndingsPolicy = LineEnding.UNIX.createPolicy();
protected LineEnding.Policy lineEndingsPolicy;

@Input
public LineEnding.Policy getLineEndingsPolicy() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016 DiffPlug
* Copyright 2016-2020 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -37,14 +37,14 @@ public class FormatTaskTest extends ResourceHarness {
public void createTask() throws IOException {
Project project = TestProvisioner.gradleProject(rootFolder());
spotlessTask = project.getTasks().create("spotlessTaskUnderTest", SpotlessTask.class);
spotlessTask.setLineEndingsPolicy(LineEnding.UNIX.createPolicy());
}

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

spotlessTask.setLineEndingsPolicy(LineEnding.UNIX.createPolicy());
spotlessTask.setTarget(Collections.singleton(testFile));
execute(spotlessTask);

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

@Test
public void testSubpath() {
assertThat(FileSignature.subpath("root/", "root/child")).isEqualTo("child");
}
}