diff --git a/gradle.properties b/gradle.properties
index 2d4b0cd9bb..1333215701 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -22,6 +22,7 @@ VER_SLF4J=[1.6,2.0[
# Used in multiple places
VER_DURIAN=1.2.0
+VER_JGIT=5.7.0.202003110725-r
VER_JUNIT=4.13
VER_ASSERTJ=3.15.0
VER_MOCKITO=3.3.3
diff --git a/lib-extra/build.gradle b/lib-extra/build.gradle
index 89ad286633..781a12a6d9 100644
--- a/lib-extra/build.gradle
+++ b/lib-extra/build.gradle
@@ -12,7 +12,7 @@ dependencies {
implementation "com.diffplug.durian:durian-core:${VER_DURIAN}"
implementation "com.diffplug.durian:durian-collect:${VER_DURIAN}"
// needed by GitAttributesLineEndings
- implementation "org.eclipse.jgit:org.eclipse.jgit:5.7.0.202003110725-r"
+ implementation "org.eclipse.jgit:org.eclipse.jgit:${VER_JGIT}"
implementation "com.googlecode.concurrent-trees:concurrent-trees:2.6.1"
// used for xml parsing in EclipseFormatter
implementation "org.codehaus.groovy:groovy-xml:3.0.3"
diff --git a/lib/src/main/java/com/diffplug/spotless/PaddedCell.java b/lib/src/main/java/com/diffplug/spotless/PaddedCell.java
index e3283ad11c..1ae6bd5277 100644
--- a/lib/src/main/java/com/diffplug/spotless/PaddedCell.java
+++ b/lib/src/main/java/com/diffplug/spotless/PaddedCell.java
@@ -270,6 +270,11 @@ public void writeCanonicalTo(OutputStream out) throws IOException {
}
}
+ /** Returns the DirtyState which corresponds to `isClean()`. */
+ public static DirtyState isClean() {
+ return isClean;
+ }
+
private static final DirtyState didNotConverge = new DirtyState(null);
private static final DirtyState isClean = new DirtyState(null);
}
diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md
index 0a1f8c194d..017bbdbefd 100644
--- a/plugin-gradle/CHANGES.md
+++ b/plugin-gradle/CHANGES.md
@@ -4,6 +4,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
### Added
+* You can now ratchet a project's style by limiting Spotless only to files which have changed since a given [git reference](https://javadoc.io/static/org.eclipse.jgit/org.eclipse.jgit/5.6.1.202002131546-r/org/eclipse/jgit/lib/Repository.html#resolve-java.lang.String-), e.g. `ratchetFrom 'origin/master'`. ([#590](https://github.com/diffplug/spotless/pull/590))
* Support for ktfmt in KotlinGradleExtension. ([#583](https://github.com/diffplug/spotless/pull/583))
### Fixed
* Users can now run `spotlessCheck` and `spotlessApply` in the same build. ([#584](https://github.com/diffplug/spotless/pull/584))
diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md
index 635d17f51f..aeca564c81 100644
--- a/plugin-gradle/README.md
+++ b/plugin-gradle/README.md
@@ -543,20 +543,9 @@ to true.
## License header options
-If the license header (specified with `licenseHeader` or `licenseHeaderFile`) contains `$YEAR` or `$today.year`, then that token will be replaced with the current 4-digit year.
+If the license header (specified with `licenseHeader` or `licenseHeaderFile`) contains `$YEAR` or `$today.year`, then that token will be replaced with the current 4-digit year. For example, if Spotless is launched in 2017, then `/* Licensed under Apache-2.0 $YEAR. */` will produce `/* Licensed under Apache-2.0 2017. */`
-For example:
-```
-/* Licensed under Apache-2.0 $YEAR. */
-```
-will produce
-```
-/* Licensed under Apache-2.0 2017. */
-```
-if Spotless is launched in 2017
-
-
-The `licenseHeader` and `licenseHeaderFile` steps will generate license headers with automatic years from the base license header according to the following rules:
+The `licenseHeader` and `licenseHeaderFile` steps will generate license headers with automatic years according to the following rules:
* A generated license header will be updated with the current year when
* the generated license header is missing
* the generated license header is not formatted correctly
@@ -579,6 +568,8 @@ spotless {
}
```
+To update the copyright notice only for changed files, use the [`ratchetFrom` functionality](#ratchet).
+
## Custom rules
@@ -694,10 +685,29 @@ spotless {
- If you don't like what spotless did, `git reset --hard`
- If you'd like to remove the "checkpoint" commit, `git reset --soft head~1` will make the checkpoint commit "disappear" from history, but keeps the changes in your working directory.
-
+
+
+## How can I enforce formatting gradually?
+
+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:
+
+```gradle
+spotless {
+ ratchetFrom 'origin/master'
+ ...
+}
+```
+
+In this mode, Spotless will apply only to files which have changed since `origin/master`. You can ratchet from [any point you want](https://javadoc.io/static/org.eclipse.jgit/org.eclipse.jgit/5.6.1.202002131546-r/org/eclipse/jgit/lib/Repository.html#resolve-java.lang.String-), even `HEAD`.
+
+However, we strongly recommend that you use a non-local branch, such as a tag or `origin/master`. The problem with `HEAD` or any local branch is that as soon as you commit a file, that is now the canonical formatting, even if it was formatted incorrectly. By instead specifying `origin/master` or a tag, your CI server will fail unless every changed file is at least as good or better than it was before the change.
+
+This is especially helpful for injecting accurate copyright dates using the [license step](#license-header).
## Can I apply Spotless to specific files?
+**DEPRECATED: use [`ratchetFrom`]($ratchet) instead. The regex API below is difficult to use correctly, especially for cross-platform (win/unix) builds.**
+
You can target specific files by setting the `spotlessFiles` project property to a comma-separated list of file patterns:
```
@@ -706,6 +716,8 @@ cmd> gradlew spotlessApply -PspotlessFiles=my/file/pattern.java,more/generic/.*-
The patterns are matched using `String#matches(String)` against the absolute file path.
+
+
## Example configurations (from real-world projects)
Spotless is hosted on jcenter and at plugins.gradle.org. [Go here](https://plugins.gradle.org/plugin/com.diffplug.gradle.spotless) if you're not sure how to import the plugin.
diff --git a/plugin-gradle/build.gradle b/plugin-gradle/build.gradle
index 7970c62650..8a31f2c41c 100644
--- a/plugin-gradle/build.gradle
+++ b/plugin-gradle/build.gradle
@@ -19,6 +19,7 @@ dependencies {
implementation "com.diffplug.durian:durian-core:${VER_DURIAN}"
implementation "com.diffplug.durian:durian-io:${VER_DURIAN}"
implementation "com.diffplug.durian:durian-collect:${VER_DURIAN}"
+ implementation "org.eclipse.jgit:org.eclipse.jgit:${VER_JGIT}"
testImplementation project(':testlib')
testImplementation "junit:junit:${VER_JUNIT}"
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java
index d54c56cc7f..7e65fa5371 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java
@@ -605,6 +605,9 @@ protected void setupTask(SpotlessTask task) {
if (root.project != root.project.getRootProject()) {
root.registerDependenciesTask.hookSubprojectTask(task);
}
+ if (root.getRatchetFrom() != null) {
+ task.treeSha = GitRatchet.treeShaOf(root.project, root.getRatchetFrom());
+ }
}
/** Returns the project that this extension is attached to. */
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GitRatchet.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GitRatchet.java
new file mode 100644
index 0000000000..b366f1946a
--- /dev/null
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GitRatchet.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2016 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.gradle.spotless;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Objects;
+import java.util.TreeMap;
+
+import javax.annotation.Nullable;
+
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheIterator;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
+import org.eclipse.jgit.treewalk.AbstractTreeIterator;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.WorkingTreeIterator;
+import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
+import org.eclipse.jgit.treewalk.filter.IndexDiffFilter;
+import org.eclipse.jgit.treewalk.filter.PathFilter;
+import org.eclipse.jgit.util.FS;
+import org.gradle.api.Project;
+
+import com.diffplug.common.base.Errors;
+import com.diffplug.common.collect.HashBasedTable;
+import com.diffplug.common.collect.Table;
+
+class GitRatchet implements AutoCloseable {
+ /** There is a single GitRatchet instance shared across the entire Gradle build, this method helps you get it. */
+ private static GitRatchet instance(Project project) {
+ return project.getPlugins().getPlugin(SpotlessPlugin.class).spotlessExtension.registerDependenciesTask.gitRatchet;
+ }
+
+ /**
+ * This is the highest-level method, which all the others serve. Given the sha
+ * of a git tree (not a commit!), and the file in question, this method returns
+ * true if that file is clean relative to that tree. A naive implementation of this
+ * could be verrrry slow, so the rest of this is about speeding this up.
+ */
+ public static boolean isClean(Project project, ObjectId treeSha, File file) throws IOException {
+ GitRatchet instance = instance(project);
+ Repository repo = instance.repositoryFor(project);
+ String path = repo.getWorkTree().toPath().relativize(file.toPath()).toString();
+
+ // TODO: should be cached-per-repo if it is thread-safe, or per-repo-per-thread if it is not
+ DirCache dirCache = repo.readDirCache();
+
+ try (TreeWalk treeWalk = new TreeWalk(repo)) {
+ treeWalk.addTree(treeSha);
+ treeWalk.addTree(new DirCacheIterator(dirCache));
+ treeWalk.addTree(new FileTreeIterator(repo));
+ treeWalk.setFilter(AndTreeFilter.create(
+ PathFilter.create(path),
+ new IndexDiffFilter(INDEX, WORKDIR)));
+
+ if (!treeWalk.next()) {
+ // the file we care about is git clean
+ return true;
+ } else {
+ AbstractTreeIterator treeIterator = treeWalk.getTree(TREE, AbstractTreeIterator.class);
+ DirCacheIterator dirCacheIterator = treeWalk.getTree(INDEX, DirCacheIterator.class);
+ WorkingTreeIterator workingTreeIterator = treeWalk.getTree(WORKDIR, WorkingTreeIterator.class);
+
+ boolean hasTree = treeIterator != null;
+ boolean hasDirCache = dirCacheIterator != null;
+
+ if (!hasTree) {
+ // it's not in the tree, so it was added
+ return false;
+ } else {
+ if (hasDirCache) {
+ boolean treeEqualsIndex = treeIterator.idEqual(dirCacheIterator) && treeIterator.getEntryRawMode() == dirCacheIterator.getEntryRawMode();
+ boolean indexEqualsWC = !workingTreeIterator.isModified(dirCacheIterator.getDirCacheEntry(), true, treeWalk.getObjectReader());
+ if (treeEqualsIndex != indexEqualsWC) {
+ // if one is equal and the other isn't, then it has definitely changed
+ return false;
+ } else if (treeEqualsIndex) {
+ // this means they are all equal to each other, which should never happen
+ // the IndexDiffFilter should keep those out of the TreeWalk entirely
+ throw new IllegalStateException("Index status for " + file + " against treeSha " + treeSha + " is invalid.");
+ } else {
+ // they are all unique
+ // we have to check manually
+ return worktreeIsCleanCheckout(treeWalk);
+ }
+ } else {
+ // no dirCache, so we will compare the tree to the workdir manually
+ return worktreeIsCleanCheckout(treeWalk);
+ }
+ }
+ }
+ }
+ }
+
+ /** Returns true if the worktree file is a clean checkout of head (possibly smudged). */
+ private static boolean worktreeIsCleanCheckout(TreeWalk treeWalk) {
+ return treeWalk.idEqual(TREE, WORKDIR);
+ }
+
+ private final static int TREE = 0;
+ private final static int INDEX = 1;
+ private final static int WORKDIR = 2;
+
+ TreeMap gitRoots = new TreeMap<>();
+ Table shaCache = HashBasedTable.create();
+
+ /**
+ * The first part of making this fast is finding the appropriate git repository quickly. Because of composite
+ * builds and submodules, it's quite possible that a single Gradle project will span across multiple git repositories.
+ * We cache the Repository for every Project in `gitRoots`, and use dynamic programming to populate it.
+ */
+ private Repository repositoryFor(Project project) throws IOException {
+ Repository repo = gitRoots.get(project);
+ if (repo == null) {
+ if (isGitRoot(project.getProjectDir())) {
+ repo = createRepo(project.getProjectDir());
+ } else {
+ Project parentProj = project.getParent();
+ if (parentProj == null) {
+ repo = traverseParentsUntil(project.getProjectDir().getParentFile(), null);
+ if (repo == null) {
+ throw new IllegalArgumentException("Cannot find git repository in any parent directory");
+ }
+ } else {
+ repo = traverseParentsUntil(project.getProjectDir().getParentFile(), parentProj.getProjectDir());
+ if (repo == null) {
+ repo = repositoryFor(parentProj);
+ }
+ }
+ }
+ gitRoots.put(project, repo);
+ }
+ return repo;
+ }
+
+ private static @Nullable Repository traverseParentsUntil(File startWith, File file) throws IOException {
+ do {
+ if (isGitRoot(startWith)) {
+ return createRepo(startWith);
+ } else {
+ startWith = startWith.getParentFile();
+ }
+ } while (!Objects.equals(startWith, file));
+ return null;
+ }
+
+ private static boolean isGitRoot(File dir) {
+ File dotGit = new File(dir, Constants.DOT_GIT);
+ return dotGit.isDirectory() && RepositoryCache.FileKey.isGitRepository(dotGit, FS.DETECTED);
+ }
+
+ static Repository createRepo(File dir) throws IOException {
+ return FileRepositoryBuilder.create(new File(dir, Constants.DOT_GIT));
+ }
+
+ /**
+ * Fast way to return treeSha of the given ref against the git repository which stores the given project.
+ * Because of parallel project evaluation, there may be races here, so we synchronize on ourselves. However, this method
+ * is the only method which can trigger any changes, and it is only called during project evaluation. That means our state
+ * is final/read-only during task execution, so we don't need any locks during the heavy lifting.
+ */
+ public static ObjectId treeShaOf(Project project, String reference) {
+ GitRatchet instance = instance(project);
+ synchronized (instance) {
+ try {
+ Repository repo = instance.repositoryFor(project);
+ ObjectId treeSha = instance.shaCache.get(repo, reference);
+ if (treeSha == null) {
+ ObjectId commitSha = repo.resolve(reference);
+ try (RevWalk revWalk = new RevWalk(repo)) {
+ RevCommit revCommit = revWalk.parseCommit(commitSha);
+ treeSha = revCommit.getTree();
+ }
+ instance.shaCache.put(repo, reference, treeSha);
+ }
+ return treeSha;
+ } catch (Exception e) {
+ throw Errors.asRuntime(e);
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ gitRoots.values().stream()
+ .distinct()
+ .forEach(Repository::close);
+ }
+}
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java
index eb21fb5547..d154fc5f02 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java
@@ -19,6 +19,8 @@
import java.io.IOException;
import java.nio.file.Files;
+import org.eclipse.jgit.lib.ObjectId;
+
import com.diffplug.common.base.Errors;
import com.diffplug.common.io.ByteStreams;
import com.diffplug.spotless.Formatter;
@@ -29,6 +31,10 @@ class IdeHook {
final static String USE_STD_IN = "spotlessIdeHookUseStdIn";
final static String USE_STD_OUT = "spotlessIdeHookUseStdOut";
+ private static void dumpIsClean() {
+ System.err.println("IS CLEAN");
+ }
+
static void performHook(SpotlessTask spotlessTask) {
String path = (String) spotlessTask.getProject().property(PROPERTY);
File file = new File(path);
@@ -38,6 +44,12 @@ static void performHook(SpotlessTask spotlessTask) {
}
if (spotlessTask.getTarget().contains(file)) {
try (Formatter formatter = spotlessTask.buildFormatter()) {
+ if (!spotlessTask.getRatchetSha().equals(ObjectId.zeroId())) {
+ if (GitRatchet.isClean(spotlessTask.getProject(), spotlessTask.treeSha, file)) {
+ dumpIsClean();
+ return;
+ }
+ }
byte[] bytes;
if (spotlessTask.getProject().hasProperty(USE_STD_IN)) {
bytes = ByteStreams.toByteArray(System.in);
@@ -46,7 +58,7 @@ static void performHook(SpotlessTask spotlessTask) {
}
PaddedCell.DirtyState dirty = PaddedCell.calculateDirtyState(formatter, file, bytes);
if (dirty.isClean()) {
- System.err.println("IS CLEAN");
+ dumpIsClean();
} else if (dirty.didNotConverge()) {
System.err.println("DID NOT CONVERGE");
System.err.println("Run 'spotlessDiagnose' for details https://github.com/diffplug/spotless/blob/master/PADDEDCELL.md");
@@ -58,11 +70,12 @@ static void performHook(SpotlessTask spotlessTask) {
dirty.writeCanonicalTo(file);
}
}
- System.err.close();
- System.out.close();
} catch (IOException e) {
e.printStackTrace(System.err);
throw Errors.asRuntime(e);
+ } finally {
+ System.err.close();
+ System.out.close();
}
}
}
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java
index 1284d7f372..4fd4c46c9a 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java
@@ -32,6 +32,9 @@
import com.diffplug.common.io.Files;
import com.diffplug.spotless.FormatterStep;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import groovy.lang.Closure;
+
/**
* NOT AN END-USER TASK, DO NOT USE FOR ANYTHING!
*
@@ -83,10 +86,18 @@ public GradleProvisioner.RootProvisioner getRootProvisioner() {
return rootProvisioner;
}
+ @SuppressWarnings({"rawtypes", "serial"})
void setup() {
Preconditions.checkArgument(getProject().getRootProject() == getProject(), "Can only be used on the root project");
unitOutput = new File(getProject().getBuildDir(), "tmp/spotless-register-dependencies");
rootProvisioner = new GradleProvisioner.RootProvisioner(getProject());
+ getProject().getGradle().buildFinished(new Closure(null) {
+ @SuppressFBWarnings("UMAC_UNCALLABLE_METHOD_OF_ANONYMOUS_CLASS")
+ public Object doCall() {
+ gitRatchet.close();
+ return null;
+ }
+ });
}
@TaskAction
@@ -94,4 +105,11 @@ public void trivialFunction() throws IOException {
Files.createParentDirs(unitOutput);
Files.write(Integer.toString(getSteps().size()), unitOutput, StandardCharsets.UTF_8);
}
+
+ GitRatchet gitRatchet = new GitRatchet();
+
+ @Internal
+ GitRatchet getGitRatchet() {
+ return gitRatchet;
+ }
}
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java
index 3d683524ad..272b66ff9b 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java
@@ -24,6 +24,8 @@
import java.util.LinkedHashMap;
import java.util.Map;
+import javax.annotation.Nullable;
+
import org.gradle.api.Action;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
@@ -105,6 +107,24 @@ public void encoding(String charset) {
setEncoding(charset);
}
+ private @Nullable String ratchetFrom;
+
+ /**
+ * Limits the target to only the files which have changed since the given git reference,
+ * which is resolved according to [this](https://javadoc.io/static/org.eclipse.jgit/org.eclipse.jgit/5.6.1.202002131546-r/org/eclipse/jgit/lib/Repository.html#resolve-java.lang.String-)
+ */
+ public void setRatchetFrom(String ratchetFrom) {
+ this.ratchetFrom = ratchetFrom;
+ }
+
+ public @Nullable String getRatchetFrom() {
+ return ratchetFrom;
+ }
+
+ public void ratchetFrom(String ratchetFrom) {
+ setRatchetFrom(ratchetFrom);
+ }
+
final Map formats = new LinkedHashMap<>();
/** Configures the special java-specific extension. */
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java
index db099f9cfe..ec60b8d426 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java
@@ -27,10 +27,10 @@
import java.util.List;
import java.util.Locale;
import java.util.Objects;
-import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
+import org.eclipse.jgit.lib.ObjectId;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.file.FileCollection;
@@ -45,7 +45,9 @@
import org.gradle.api.tasks.incremental.IncrementalTaskInputs;
import com.diffplug.common.base.Errors;
+import com.diffplug.common.base.Preconditions;
import com.diffplug.common.base.StringPrinter;
+import com.diffplug.common.base.Throwing;
import com.diffplug.spotless.FormatExceptionPolicy;
import com.diffplug.spotless.FormatExceptionPolicyStrict;
import com.diffplug.spotless.Formatter;
@@ -87,6 +89,13 @@ public void setLineEndingsPolicy(LineEnding.Policy lineEndingsPolicy) {
this.lineEndingsPolicy = Objects.requireNonNull(lineEndingsPolicy);
}
+ ObjectId treeSha = ObjectId.zeroId();
+
+ @Input
+ public ObjectId getRatchetSha() {
+ return treeSha;
+ }
+
@Deprecated
@Internal
public boolean isPaddedCell() {
@@ -180,10 +189,13 @@ public void performAction(IncrementalTaskInputs inputs) throws Exception {
Files.createDirectories(outputDirectory.toPath());
}
- Predicate shouldInclude;
+ Throwing.Specific.Predicate shouldInclude;
if (this.filePatterns.isEmpty()) {
shouldInclude = file -> true;
} else {
+ Preconditions.checkArgument(treeSha == ObjectId.zeroId(),
+ "Cannot use 'ratchetFrom' and '-PspotlessFiles' at the same time");
+
// a list of files has been passed in via project property
final String[] includePatterns = this.filePatterns.split(",");
final List compiledIncludePatterns = Arrays.stream(includePatterns)
@@ -198,24 +210,24 @@ public void performAction(IncrementalTaskInputs inputs) throws Exception {
try (Formatter formatter = buildFormatter()) {
inputs.outOfDate(inputDetails -> {
File input = inputDetails.getFile();
- if (shouldInclude.test(input) && input.isFile()) {
- try {
+ try {
+ if (shouldInclude.test(input) && input.isFile()) {
processInputFile(formatter, input);
- } catch (IOException e) {
- throw Errors.asRuntime(e);
}
+ } catch (IOException e) {
+ throw Errors.asRuntime(e);
}
});
}
inputs.removed(removedDetails -> {
File input = removedDetails.getFile();
- if (shouldInclude.test(input)) {
- try {
+ try {
+ if (shouldInclude.test(input)) {
deletePreviousResult(input);
- } catch (IOException e) {
- throw Errors.asRuntime(e);
}
+ } catch (IOException e) {
+ throw Errors.asRuntime(e);
}
});
}
@@ -223,7 +235,12 @@ public void performAction(IncrementalTaskInputs inputs) throws Exception {
private void processInputFile(Formatter formatter, File input) throws IOException {
File output = getOutputFile(input);
getLogger().debug("Applying format to " + input + " and writing to " + output);
- PaddedCell.DirtyState dirtyState = PaddedCell.calculateDirtyState(formatter, input);
+ PaddedCell.DirtyState dirtyState;
+ if (treeSha != ObjectId.zeroId() && GitRatchet.isClean(getProject(), treeSha, input)) {
+ dirtyState = PaddedCell.isClean();
+ } else {
+ dirtyState = PaddedCell.calculateDirtyState(formatter, input);
+ }
if (dirtyState.isClean()) {
// Remove previous output if it exists
Files.deleteIfExists(output.toPath());
diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RatchetFromTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RatchetFromTest.java
new file mode 100644
index 0000000000..cfcf04bf60
--- /dev/null
+++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RatchetFromTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2016 DiffPlug
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.diffplug.gradle.spotless;
+
+import org.eclipse.jgit.api.Git;
+import org.junit.Test;
+
+public class RatchetFromTest extends GradleIntegrationTest {
+ @Test
+ public void singleProjectExhaustive() throws Exception {
+ Git git = Git.init().setDirectory(rootFolder()).call();
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.gradle.spotless'",
+ "}",
+ "spotless {",
+ " ratchetFrom 'baseline'",
+ " format 'misc', {",
+ " target '*.md'",
+ " custom 'lowercase', { str -> str.toLowerCase() }",
+ " bumpThisNumberIfACustomStepChanges(1)",
+ " }",
+ "}");
+ setFile("test.md").toContent("HELLO");
+ git.add().addFilepattern("test.md").call();
+ git.commit().setMessage("Initial state").call();
+ // tag this initial state as the baseline for spotless to ratchet from
+ git.tag().setName("baseline").call();
+
+ // so at this point we have test.md, and it would normally be dirty,
+ // but because it is unchanged, spotless says it is clean
+ assertClean();
+
+ // but if we change it so that it is not clean, spotless will now say it is dirty
+ setFile("test.md").toContent("HELLO WORLD");
+ assertDirty();
+ gradleRunner().withArguments("spotlessApply").build();
+ assertFile("test.md").hasContent("hello world");
+
+ // but if we make it unchanged again, it goes back to being clean
+ setFile("test.md").toContent("HELLO");
+ assertClean();
+
+ // and if we make the index dirty
+ setFile("test.md").toContent("HELLO WORLD");
+ git.add().addFilepattern("test.md").call();
+ {
+ // and the content dirty in the same way, then it's dirty
+ assertDirty();
+ // if we make the content something else dirty, then it's dirty
+ setFile("test.md").toContent("HELLO MOM");
+ assertDirty();
+ // if we make the content unchanged, even though index it and index are dirty, then it's clean
+ setFile("test.md").toContent("HELLO");
+ assertClean();
+ // if we delete the file, but it's still in the index, then it's clean
+ setFile("test.md").deleted();
+ assertClean();
+ }
+ // if we remove the file from the index
+ git.rm().addFilepattern("test.md").setCached(true).call();
+ {
+ // and it's gone in real life too, then it's clean
+ assertClean();
+ // if the content is there and unchanged, then it's clean
+ setFile("test.md").toContent("HELLO");
+ assertClean();
+ // if the content is dirty, then it's dirty
+ setFile("test.md").toContent("HELLO WORLD");
+ assertDirty();
+ }
+
+ // new files always get checked
+ setFile("new.md").toContent("HELLO");
+ {
+ assertDirty();
+ // even if they are added
+ git.add().addFilepattern("new.md").call();
+ assertDirty();
+ }
+ }
+
+ private void assertClean() throws Exception {
+ gradleRunner().withArguments("spotlessCheck").build();
+ }
+
+ private void assertDirty() throws Exception {
+ gradleRunner().withArguments("spotlessCheck").buildAndFail();
+ }
+}
diff --git a/testlib/src/main/java/com/diffplug/spotless/ResourceHarness.java b/testlib/src/main/java/com/diffplug/spotless/ResourceHarness.java
index 30b7bbc3cf..f436ce16ff 100644
--- a/testlib/src/main/java/com/diffplug/spotless/ResourceHarness.java
+++ b/testlib/src/main/java/com/diffplug/spotless/ResourceHarness.java
@@ -234,5 +234,10 @@ public File toResource(String path) throws IOException {
Files.write(file.toPath(), getTestResource(path).getBytes(StandardCharsets.UTF_8));
return file;
}
+
+ public File deleted() throws IOException {
+ Files.delete(file.toPath());
+ return file;
+ }
}
}