diff --git a/changelog/@unreleased/pr-39.v2.yml b/changelog/@unreleased/pr-39.v2.yml new file mode 100644 index 000000000..e3fee21e8 --- /dev/null +++ b/changelog/@unreleased/pr-39.v2.yml @@ -0,0 +1,6 @@ +type: improvement +improvement: + description: A new `FormatDiffCli` class reads the output of `git diff -U0` and + runs the formatter on modified lines only. + links: + - https://github.com/palantir/palantir-java-format/pull/39 diff --git a/palantir-java-format/src/main/java/com/palantir/javaformat/java/FormatDiffCli.java b/palantir-java-format/src/main/java/com/palantir/javaformat/java/FormatDiffCli.java new file mode 100644 index 000000000..e09b3a98e --- /dev/null +++ b/palantir-java-format/src/main/java/com/palantir/javaformat/java/FormatDiffCli.java @@ -0,0 +1,135 @@ +/* + * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.javaformat.java; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; +import com.google.common.collect.Range; +import com.google.common.collect.RangeSet; +import com.google.common.collect.Streams; +import com.google.common.collect.TreeRangeSet; +import com.google.common.io.ByteStreams; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +public final class FormatDiffCli { + // each section in the git diff output starts like this + private static final Pattern SEPARATOR = Pattern.compile("diff --git"); + + // "+++ b/witchcraft-example-stateless/src/main/java/com/palantir/witchcraftexample/WitchcraftExampleResource.java" + private static final Pattern FILENAME = Pattern.compile("^\\+\\+\\+ (.+?)/(?.+)\\n", Pattern.MULTILINE); + + // "@@ -25,6 +26,19 @@ public final class WitchcraftExampleServer {" + private static final Pattern HUNK = + Pattern.compile("^@@.*\\+(?\\d+)(,(?\\d+))?", Pattern.MULTILINE); + + public static void main(String[] _args) throws IOException, InterruptedException { + Formatter formatter = Formatter.createFormatter( + JavaFormatterOptions.builder().style(JavaFormatterOptions.Style.PALANTIR).build()); + Path cwd = Paths.get("."); + + String gitOutput = gitDiff(cwd); + parseGitDiffOutput(gitOutput) + .filter(diff -> diff.path.toString().endsWith(".java")) + .map(diff -> new SingleFileDiff(cwd.resolve(diff.path), diff.lineRanges)) + .filter(diff -> Files.exists(diff.path)) + .forEach(diff -> format(formatter, diff)); + } + + /** Parses the filenames and edited ranges out of `git diff -U0`. */ + @VisibleForTesting + static Stream parseGitDiffOutput(String gitOutput) { + return Streams.stream(Splitter.on(SEPARATOR).omitEmptyStrings().split(gitOutput)).flatMap(singleFileDiff -> { + Matcher filenameMatcher = FILENAME.matcher(singleFileDiff); + if (!filenameMatcher.find()) { + System.err.println("Failed to find filename"); + return Stream.empty(); + } + Path path = Paths.get(filenameMatcher.group("filename")); + + RangeSet lineRanges = TreeRangeSet.create(); + Matcher hunk = HUNK.matcher(singleFileDiff); + while (hunk.find()) { + int firstLineOfHunk = Integer.parseInt(hunk.group("startLineOneIndexed")) - 1; + int hunkLength = Optional.ofNullable(hunk.group("numLines")).map(Integer::parseInt).orElse(1); + Range rangeZeroIndexed = Range.closedOpen(firstLineOfHunk, firstLineOfHunk + hunkLength); + lineRanges.add(rangeZeroIndexed); + } + + return Stream.of(new SingleFileDiff(path, lineRanges)); + }); + } + + private static void format(Formatter formatter, SingleFileDiff diff) { + String input; + try { + input = new String(Files.readAllBytes(diff.path), UTF_8); + } catch (IOException e) { + System.err.println("Failed to read file " + diff.path); + e.printStackTrace(System.err); + return; + } + + RangeSet charRanges = Formatter.lineRangesToCharRanges(input, diff.lineRanges); + + try { + System.err.println("Formatting " + diff.path); + String output = formatter.formatSource(input, charRanges.asRanges()); + Files.write(diff.path, output.getBytes(UTF_8)); + } catch (IOException | FormatterException e) { + System.err.println("Failed to format file " + diff.path); + e.printStackTrace(System.err); + } + } + + private static String gitDiff(Path cwd) throws IOException, InterruptedException { + // TODO(dfox): this does nothing if working dir is clean - maybe use HEAD^ to format prev commit? + Process process = new ProcessBuilder().command("git", "diff", "-U0", "HEAD").directory(cwd.toFile()).start(); + Preconditions.checkState(process.waitFor(10, TimeUnit.SECONDS), "git diff took too long to terminate"); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ByteStreams.copy(process.getInputStream(), baos); + return new String(baos.toByteArray(), UTF_8); + } + + // TODO(dfox): replace this with immutables + public static class SingleFileDiff { + private final Path path; + private final RangeSet lineRanges; // zero-indexed + + public SingleFileDiff(Path path, RangeSet lineRanges) { + this.path = path; + this.lineRanges = lineRanges; + } + + @Override + public String toString() { + return "SingleFileDiff{path=" + path + ", lineRanges=" + lineRanges + '}'; + } + } +} diff --git a/palantir-java-format/src/test/java/com/palantir/javaformat/java/FormatDiffCliTest.java b/palantir-java-format/src/test/java/com/palantir/javaformat/java/FormatDiffCliTest.java new file mode 100644 index 000000000..291dd4cfc --- /dev/null +++ b/palantir-java-format/src/test/java/com/palantir/javaformat/java/FormatDiffCliTest.java @@ -0,0 +1,49 @@ +/* + * (c) Copyright 2019 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.javaformat.java; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +class FormatDiffCliTest { + + @Test + void parsing_git_diff_output_works() throws IOException { + String example1 = new String( + Files.readAllBytes( + Paths.get("src/test/resources/com/palantir/javaformat/java/FormatDiffCliTest/example1.patch")), + StandardCharsets.UTF_8); + + List strings = FormatDiffCli.parseGitDiffOutput(example1) + .map(FormatDiffCli.SingleFileDiff::toString) + .collect(Collectors.toList()); + assertEquals( + ImmutableList.of( + "SingleFileDiff{path=build.gradle, lineRanges=[[24..25), [29..30)]}", + "SingleFileDiff{path=tracing/src/test/java/com/palantir/tracing/TracersTest.java, " + + "lineRanges=[[659..660), [675..676)]}"), + strings); + } +} diff --git a/palantir-java-format/src/test/resources/com/palantir/javaformat/java/FormatDiffCliTest/example1.patch b/palantir-java-format/src/test/resources/com/palantir/javaformat/java/FormatDiffCliTest/example1.patch new file mode 100644 index 000000000..72e63e7bc --- /dev/null +++ b/palantir-java-format/src/test/resources/com/palantir/javaformat/java/FormatDiffCliTest/example1.patch @@ -0,0 +1,20 @@ +diff --git a/build.gradle b/build.gradle +index 1b1b970..2150904 100644 +--- a/build.gradle ++++ b/build.gradle +@@ -25 +25 @@ buildscript { +- classpath 'com.palantir.gradle.revapi:gradle-revapi:1.0.5' ++ classpath 'com.palantir.gradle.revapi:gradle-revapi:1.0.6' +@@ -30 +30 @@ buildscript { +- classpath 'com.palantir.baseline:gradle-baseline-java:2.19.0' ++ classpath 'com.palantir.baseline:gradle-baseline-java:2.24.0' +diff --git a/tracing/src/test/java/com/palantir/tracing/TracersTest.java b/tracing/src/test/java/com/palantir/tracing/TracersTest.java +index 7341ef6..723d4ba 100644 +--- a/tracing/src/test/java/com/palantir/tracing/TracersTest.java ++++ b/tracing/src/test/java/com/palantir/tracing/TracersTest.java +@@ -660 +660 @@ public final class TracersTest { +- assertThat(trace).hasSize(0); ++ assertThat(trace).isEmpty(); +@@ -676 +676 @@ public final class TracersTest { +- assertThat(trace).hasSize(0); ++ assertThat(trace).isEmpty();