Skip to content

Commit 41ed32d

Browse files
authored
Merge pull request #603 from diffplug/feat/ratchetfrom-maven
Add `ratchetFrom` to maven
2 parents 07d5ab0 + a5d1453 commit 41ed32d

File tree

15 files changed

+339
-35
lines changed

15 files changed

+339
-35
lines changed

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GitRatchet.java renamed to lib-extra/src/main/java/com/diffplug/spotless/extra/GitRatchet.java

+21-11
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package com.diffplug.gradle.spotless;
16+
package com.diffplug.spotless.extra;
1717

1818
import java.io.File;
1919
import java.io.IOException;
@@ -40,14 +40,20 @@
4040
import org.eclipse.jgit.treewalk.filter.IndexDiffFilter;
4141
import org.eclipse.jgit.treewalk.filter.PathFilter;
4242
import org.eclipse.jgit.util.FS;
43-
import org.gradle.api.Project;
4443

4544
import com.diffplug.common.base.Errors;
4645
import com.diffplug.common.collect.HashBasedTable;
4746
import com.diffplug.common.collect.Table;
4847
import com.diffplug.spotless.FileSignature;
4948

50-
class GitRatchet implements AutoCloseable {
49+
/**
50+
* How to use:
51+
* - For best performance, you should have one instance of GitRatchet, shared by all projects.
52+
* - Use {@link #rootTreeShaOf(Object, String)} to turn `origin/master` into the SHA of the tree object at that reference
53+
* - Use {@link #isClean(Object, ObjectId, File)} to see if the given file is "git clean" relative to that tree
54+
* - 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.
55+
*/
56+
public abstract class GitRatchet<Project> implements AutoCloseable {
5157
/**
5258
* This is the highest-level method, which all the others serve. Given the sha
5359
* of a git tree (not a commit!), and the file in question, this method returns
@@ -130,17 +136,17 @@ private static boolean worktreeIsCleanCheckout(TreeWalk treeWalk) {
130136
private Repository repositoryFor(Project project) throws IOException {
131137
Repository repo = gitRoots.get(project);
132138
if (repo == null) {
133-
if (isGitRoot(project.getProjectDir())) {
134-
repo = createRepo(project.getProjectDir());
139+
if (isGitRoot(getDir(project))) {
140+
repo = createRepo(getDir(project));
135141
} else {
136-
Project parentProj = project.getParent();
142+
Project parentProj = getParent(project);
137143
if (parentProj == null) {
138-
repo = traverseParentsUntil(project.getProjectDir().getParentFile(), null);
144+
repo = traverseParentsUntil(getDir(project).getParentFile(), null);
139145
if (repo == null) {
140146
throw new IllegalArgumentException("Cannot find git repository in any parent directory");
141147
}
142148
} else {
143-
repo = traverseParentsUntil(project.getProjectDir().getParentFile(), parentProj.getProjectDir());
149+
repo = traverseParentsUntil(getDir(project).getParentFile(), getDir(parentProj));
144150
if (repo == null) {
145151
repo = repositoryFor(parentProj);
146152
}
@@ -151,6 +157,10 @@ private Repository repositoryFor(Project project) throws IOException {
151157
return repo;
152158
}
153159

160+
protected abstract File getDir(Project project);
161+
162+
protected abstract @Nullable Project getParent(Project project);
163+
154164
private static @Nullable Repository traverseParentsUntil(File startWith, File file) throws IOException {
155165
while (startWith != null) {
156166
if (isGitRoot(startWith)) {
@@ -196,7 +206,7 @@ public synchronized ObjectId rootTreeShaOf(Project project, String reference) {
196206
rootTreeShaCache.put(repo, reference, treeSha);
197207
}
198208
return treeSha;
199-
} catch (Exception e) {
209+
} catch (IOException e) {
200210
throw Errors.asRuntime(e);
201211
}
202212
}
@@ -210,7 +220,7 @@ public synchronized ObjectId subtreeShaOf(Project project, ObjectId rootTreeSha)
210220
ObjectId subtreeSha = subtreeShaCache.get(project);
211221
if (subtreeSha == null) {
212222
Repository repo = repositoryFor(project);
213-
File directory = project.getProjectDir();
223+
File directory = getDir(project);
214224
if (repo.getWorkTree().equals(directory)) {
215225
subtreeSha = rootTreeSha;
216226
} else {
@@ -221,7 +231,7 @@ public synchronized ObjectId subtreeShaOf(Project project, ObjectId rootTreeSha)
221231
subtreeShaCache.put(project, subtreeSha);
222232
}
223233
return subtreeSha;
224-
} catch (Exception e) {
234+
} catch (IOException e) {
225235
throw Errors.asRuntime(e);
226236
}
227237
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2020 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.gradle.spotless;
17+
18+
import java.io.File;
19+
20+
import javax.annotation.Nullable;
21+
22+
import org.gradle.api.Project;
23+
24+
import com.diffplug.spotless.extra.GitRatchet;
25+
26+
/** Gradle implementation of GitRatchet. */
27+
final class GitRatchetGradle extends GitRatchet<Project> {
28+
@Override
29+
protected File getDir(Project project) {
30+
return project.getProjectDir();
31+
}
32+
33+
@Override
34+
protected @Nullable Project getParent(Project project) {
35+
return project.getParent();
36+
}
37+
}

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

+3-3
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.
@@ -106,10 +106,10 @@ public void trivialFunction() throws IOException {
106106
Files.write(Integer.toString(getSteps().size()), unitOutput, StandardCharsets.UTF_8);
107107
}
108108

109-
GitRatchet gitRatchet = new GitRatchet();
109+
GitRatchetGradle gitRatchet = new GitRatchetGradle();
110110

111111
@Internal
112-
GitRatchet getGitRatchet() {
112+
GitRatchetGradle getGitRatchet() {
113113
return gitRatchet;
114114
}
115115
}

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public void setLineEndingsPolicy(LineEnding.Policy lineEndingsPolicy) {
7676

7777
/*** API which performs git up-to-date tasks. */
7878
@Nullable
79-
GitRatchet ratchet;
79+
GitRatchetGradle ratchet;
8080
/** The sha of the tree at repository root, used for determining if an individual *file* is clean according to git. */
8181
ObjectId rootTreeSha;
8282
/**
@@ -86,14 +86,14 @@ public void setLineEndingsPolicy(LineEnding.Policy lineEndingsPolicy) {
8686
*/
8787
private ObjectId subtreeSha = ObjectId.zeroId();
8888

89-
public void setupRatchet(GitRatchet gitRatchet, String ratchetFrom) {
89+
public void setupRatchet(GitRatchetGradle gitRatchet, String ratchetFrom) {
9090
ratchet = gitRatchet;
9191
rootTreeSha = gitRatchet.rootTreeShaOf(getProject(), ratchetFrom);
9292
subtreeSha = gitRatchet.subtreeShaOf(getProject(), rootTreeSha);
9393
}
9494

9595
@Internal
96-
GitRatchet getRatchet() {
96+
GitRatchetGradle getRatchet() {
9797
return ratchet;
9898
}
9999

plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RatchetFromTest.java renamed to plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GitRatchetGradleTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
import org.gradle.testkit.runner.TaskOutcome;
3535
import org.junit.Test;
3636

37-
public class RatchetFromTest extends GradleIntegrationHarness {
37+
public class GitRatchetGradleTest extends GradleIntegrationHarness {
3838
private static final String TEST_PATH = "src/markdown/test.md";
3939

4040
private Git initRepo() throws IllegalStateException, GitAPIException, IOException {

plugin-maven/CHANGES.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
44

55
## [Unreleased]
66
### Added
7-
* `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)).
7+
* 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/main'`. ([#590](https://github.com/diffplug/spotless/pull/590))
88
* Huge speed improvement for multi-module projects thanks to improved cross-project classloader caching ([#571](https://github.com/diffplug/spotless/pull/571), fixes [#559](https://github.com/diffplug/spotless/issues/559)).
9+
* `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)).
910

1011
## [1.31.3] - 2020-06-17
1112
### Changed

plugin-maven/README.md

+21-1
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,25 @@ By default, `spotless:check` is bound to the `verify` phase. You might want to
594594
- If you don't like what spotless did, `git reset --hard`
595595
- 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.
596596

597-
<a name="examples"></a>
597+
<a name="ratchet"></a>
598+
599+
## How can I enforce formatting gradually?
600+
601+
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:
602+
603+
```xml
604+
<configuration>
605+
<ratchetFrom>origin/main</ratchetFrom> <!-- only format files which have changed since origin/main -->
606+
<!-- ... define formats ... -->
607+
</configuration>
608+
```
609+
610+
In this mode, Spotless will apply only to files which have changed since `origin/main`. 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`. You can also set `ratchetFrom` per-format if you prefer (e.g. `<configuration><java><ratchetFrom>...`).
611+
612+
However, we strongly recommend that you use a non-local branch, such as a tag or `origin/main`. 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/main` 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.
613+
614+
This is especially helpful for injecting accurate copyright dates using the [license step](#license-header).
615+
598616

599617
## Can I apply Spotless to specific files?
600618

@@ -606,6 +624,8 @@ cmd> mvn spotless:apply -DspotlessFiles=my/file/pattern.java,more/generic/.*-pat
606624

607625
The patterns are matched using `String#matches(String)` against the absolute file path.
608626

627+
<a name="examples"></a>
628+
609629
## Example configurations (from real-world projects)
610630

611631
- [Apache Avro](https://github.com/apache/avro/blob/8026c8ffe4ef67ab419dba73910636bf2c1a691c/lang/java/pom.xml#L307-L334)

plugin-maven/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ dependencies {
7878

7979
implementation "com.diffplug.durian:durian-core:${VER_DURIAN}"
8080
implementation "com.diffplug.durian:durian-collect:${VER_DURIAN}"
81+
implementation "org.eclipse.jgit:org.eclipse.jgit:${VER_JGIT}"
8182

8283
testImplementation project(":testlib")
8384
testImplementation "junit:junit:${VER_JUNIT}"

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

+24-7
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.
@@ -19,7 +19,13 @@
1919

2020
import java.io.File;
2121
import java.io.IOException;
22-
import java.util.*;
22+
import java.util.Arrays;
23+
import java.util.Collections;
24+
import java.util.HashSet;
25+
import java.util.List;
26+
import java.util.Objects;
27+
import java.util.Optional;
28+
import java.util.Set;
2329
import java.util.function.Predicate;
2430
import java.util.regex.Pattern;
2531
import java.util.stream.Collectors;
@@ -36,6 +42,7 @@
3642
import org.eclipse.aether.RepositorySystemSession;
3743
import org.eclipse.aether.repository.RemoteRepository;
3844

45+
import com.diffplug.common.collect.Iterables;
3946
import com.diffplug.spotless.Formatter;
4047
import com.diffplug.spotless.LineEnding;
4148
import com.diffplug.spotless.Provisioner;
@@ -76,6 +83,9 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo {
7683
@Parameter(defaultValue = DEFAULT_LINE_ENDINGS)
7784
private LineEnding lineEndings;
7885

86+
@Parameter
87+
private String ratchetFrom;
88+
7989
@Parameter
8090
private LicenseHeader licenseHeader;
8191

@@ -110,21 +120,28 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo {
110120
@Parameter(property = "spotlessFiles")
111121
private String filePatterns;
112122

113-
protected abstract void process(List<File> files, Formatter formatter) throws MojoExecutionException;
123+
protected abstract void process(Iterable<File> files, Formatter formatter) throws MojoExecutionException;
114124

115125
@Override
116126
public final void execute() throws MojoExecutionException {
117127
List<FormatterFactory> formatterFactories = getFormatterFactories();
118-
119128
for (FormatterFactory formatterFactory : formatterFactories) {
120129
execute(formatterFactory);
121130
}
122131
}
123132

124133
private void execute(FormatterFactory formatterFactory) throws MojoExecutionException {
125134
List<File> files = collectFiles(formatterFactory);
126-
try (Formatter formatter = formatterFactory.newFormatter(files, getFormatterConfig())) {
127-
process(files, formatter);
135+
FormatterConfig config = getFormatterConfig();
136+
Optional<String> ratchetFrom = formatterFactory.ratchetFrom(config);
137+
Iterable<File> toFormat;
138+
if (!ratchetFrom.isPresent()) {
139+
toFormat = files;
140+
} else {
141+
toFormat = Iterables.filter(files, GitRatchetMaven.instance().isGitDirty(baseDir, ratchetFrom.get()));
142+
}
143+
try (Formatter formatter = formatterFactory.newFormatter(files, config)) {
144+
process(toFormat, formatter);
128145
}
129146
}
130147

@@ -172,7 +189,7 @@ private FormatterConfig getFormatterConfig() {
172189
Provisioner provisioner = MavenProvisioner.create(resolver);
173190
List<FormatterStepFactory> formatterStepFactories = getFormatterStepFactories();
174191
FileLocator fileLocator = getFileLocator();
175-
return new FormatterConfig(baseDir, encoding, lineEndings, provisioner, fileLocator, formatterStepFactories);
192+
return new FormatterConfig(baseDir, encoding, lineEndings, Optional.ofNullable(ratchetFrom), provisioner, fileLocator, formatterStepFactories);
176193
}
177194

178195
private FileLocator getFileLocator() {

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

+9-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.
@@ -19,6 +19,7 @@
1919

2020
import java.io.File;
2121
import java.util.List;
22+
import java.util.Optional;
2223

2324
import com.diffplug.spotless.LineEnding;
2425
import com.diffplug.spotless.Provisioner;
@@ -27,14 +28,16 @@ public class FormatterConfig {
2728

2829
private final String encoding;
2930
private final LineEnding lineEndings;
31+
private final Optional<String> ratchetFrom;
3032
private final Provisioner provisioner;
3133
private final FileLocator fileLocator;
3234
private final List<FormatterStepFactory> globalStepFactories;
3335

34-
public FormatterConfig(File baseDir, String encoding, LineEnding lineEndings, Provisioner provisioner,
36+
public FormatterConfig(File baseDir, String encoding, LineEnding lineEndings, Optional<String> ratchetFrom, Provisioner provisioner,
3537
FileLocator fileLocator, List<FormatterStepFactory> globalStepFactories) {
3638
this.encoding = encoding;
3739
this.lineEndings = lineEndings;
40+
this.ratchetFrom = ratchetFrom;
3841
this.provisioner = provisioner;
3942
this.fileLocator = fileLocator;
4043
this.globalStepFactories = globalStepFactories;
@@ -48,6 +51,10 @@ public LineEnding getLineEndings() {
4851
return lineEndings;
4952
}
5053

54+
public Optional<String> getRatchetFrom() {
55+
return ratchetFrom;
56+
}
57+
5158
public Provisioner getProvisioner() {
5259
return provisioner;
5360
}

0 commit comments

Comments
 (0)