Skip to content

Commit c5608cf

Browse files
Git binary diff (#6292)
* WIP * Use new binary patch output format * Fix incubating version value * Fully integrate binary patch support * Revert back to latest.release for jgit * Enable being able to specify the remote charset, but when absent fallback to ISO-8859-1 --------- Co-authored-by: Jammy Louie <[email protected]>
1 parent 9d51039 commit c5608cf

File tree

7 files changed

+405
-36
lines changed

7 files changed

+405
-36
lines changed

rewrite-core/src/main/java/org/openrewrite/Result.java

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -208,21 +208,11 @@ public String diff(@Nullable Path relativeTo, PrintOutputCapture.@Nullable Marke
208208

209209
@Incubating(since = "7.34.0")
210210
public String diff(@Nullable Path relativeTo, PrintOutputCapture.@Nullable MarkerPrinter markerPrinter, @Nullable Boolean ignoreAllWhitespace) {
211-
Path beforePath = before == null ? null : before.getSourcePath();
212-
Path afterPath = null;
213-
if (before == null && after == null) {
214-
afterPath = (relativeTo == null ? Paths.get(".") : relativeTo).resolve("partial-" + System.nanoTime());
215-
} else if (after != null) {
216-
afterPath = after.getSourcePath();
217-
}
218-
219-
PrintOutputCapture<Integer> out = markerPrinter == null ?
220-
new PrintOutputCapture<>(0) :
221-
new PrintOutputCapture<>(0, markerPrinter);
222-
223-
FileMode beforeMode = before != null && before.getFileAttributes() != null && before.getFileAttributes().isExecutable() ? FileMode.EXECUTABLE_FILE : FileMode.REGULAR_FILE;
224-
FileMode afterMode = after != null && after.getFileAttributes() != null && after.getFileAttributes().isExecutable() ? FileMode.EXECUTABLE_FILE : FileMode.REGULAR_FILE;
211+
return diff(relativeTo, markerPrinter, ignoreAllWhitespace, false);
212+
}
225213

214+
@Incubating(since = "8.69.0")
215+
public String diff(@Nullable Path relativeTo, PrintOutputCapture.@Nullable MarkerPrinter markerPrinter, @Nullable Boolean ignoreAllWhitespace, boolean binaryPatch) {
226216
Set<Recipe> recipeSet = new HashSet<>(recipes.size());
227217
for (List<Recipe> rs : recipes) {
228218
if (!rs.isEmpty()) {
@@ -231,14 +221,12 @@ public String diff(@Nullable Path relativeTo, PrintOutputCapture.@Nullable Marke
231221
}
232222

233223
try (InMemoryDiffEntry diffEntry = new InMemoryDiffEntry(
234-
beforePath,
235-
afterPath,
224+
before,
225+
after,
236226
relativeTo,
237-
before == null ? "" : before.printAll(out),
238-
after == null ? "" : after.printAll(out.clone()),
227+
markerPrinter,
239228
recipeSet,
240-
beforeMode,
241-
afterMode
229+
binaryPatch
242230
)) {
243231
return diffEntry.getDiff(ignoreAllWhitespace);
244232
}

rewrite-core/src/main/java/org/openrewrite/internal/InMemoryDiffEntry.java

Lines changed: 235 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,29 @@
1515
*/
1616
package org.openrewrite.internal;
1717

18+
import lombok.EqualsAndHashCode;
19+
import lombok.Value;
20+
import lombok.experimental.NonFinal;
1821
import org.jspecify.annotations.Nullable;
22+
import org.openrewrite.Incubating;
23+
import org.openrewrite.PrintOutputCapture;
1924
import org.openrewrite.Recipe;
25+
import org.openrewrite.SourceFile;
26+
import org.openrewrite.jgit.attributes.AttributesNodeProvider;
2027
import org.openrewrite.jgit.diff.DiffEntry;
2128
import org.openrewrite.jgit.diff.DiffFormatter;
2229
import org.openrewrite.jgit.diff.RawTextComparator;
23-
import org.openrewrite.jgit.internal.storage.dfs.DfsRepositoryDescription;
24-
import org.openrewrite.jgit.internal.storage.dfs.InMemoryRepository;
30+
import org.openrewrite.jgit.internal.storage.dfs.*;
2531
import org.openrewrite.jgit.lib.*;
32+
import org.openrewrite.marker.GitTreeEntry;
33+
import org.openrewrite.quark.Quark;
2634

2735
import java.io.ByteArrayOutputStream;
2836
import java.io.IOException;
2937
import java.io.UncheckedIOException;
3038
import java.nio.charset.StandardCharsets;
3139
import java.nio.file.Path;
32-
import java.util.Arrays;
33-
import java.util.LinkedHashSet;
34-
import java.util.Set;
35-
import java.util.StringJoiner;
40+
import java.util.*;
3641
import java.util.concurrent.atomic.AtomicBoolean;
3742

3843
import static java.util.stream.Collectors.joining;
@@ -42,28 +47,105 @@ public class InMemoryDiffEntry extends DiffEntry implements AutoCloseable {
4247
static final AbbreviatedObjectId A_ZERO = AbbreviatedObjectId
4348
.fromObjectId(ObjectId.zeroId());
4449

45-
private final InMemoryRepository repo;
50+
private final VirtualInMemoryRepository repo;
4651
private final Set<Recipe> recipesThatMadeChanges;
4752

53+
private final boolean binaryPatch;
54+
4855
public InMemoryDiffEntry(@Nullable Path originalFilePath, @Nullable Path filePath, @Nullable Path relativeTo, String oldSource,
4956
String newSource, Set<Recipe> recipesThatMadeChanges) {
5057
this(originalFilePath, filePath, relativeTo, oldSource, newSource, recipesThatMadeChanges, FileMode.REGULAR_FILE, FileMode.REGULAR_FILE);
5158
}
5259

5360
public InMemoryDiffEntry(@Nullable Path originalFilePath, @Nullable Path filePath, @Nullable Path relativeTo, String oldSource,
5461
String newSource, Set<Recipe> recipesThatMadeChanges, FileMode oldMode, FileMode newMode) {
62+
this(originalFilePath, filePath, relativeTo, oldSource.getBytes(StandardCharsets.UTF_8), newSource.getBytes(StandardCharsets.UTF_8), recipesThatMadeChanges, oldMode, newMode, false);
63+
}
64+
65+
@Incubating(since = "8.69.0")
66+
public InMemoryDiffEntry(@Nullable SourceFile before, @Nullable SourceFile after, @Nullable Path relativeTo, PrintOutputCapture.@Nullable MarkerPrinter markerPrinter, Set<Recipe> recipesThatMadeChanges, boolean binaryPatch) {
67+
if (before == null && after == null) {
68+
throw new NullPointerException("before and after can't be null");
69+
}
5570

5671
this.recipesThatMadeChanges = recipesThatMadeChanges;
72+
this.binaryPatch = binaryPatch;
5773

5874
try {
59-
this.repo = new InMemoryRepository.Builder()
60-
.setRepositoryDescription(new DfsRepositoryDescription())
61-
.build();
75+
this.repo = new VirtualInMemoryRepository(new InMemoryRepository.Builder()
76+
.setRepositoryDescription(new DfsRepositoryDescription()));
77+
78+
try (VirtualObjectDatabase database = repo.getObjectDatabase()) {
79+
try (ObjectInserter inserter = repo.getObjectDatabase().newInserter()) {
80+
if (before == null) {
81+
this.oldId = A_ZERO;
82+
this.oldMode = FileMode.MISSING;
83+
this.oldPath = DEV_NULL;
84+
} else {
85+
Optional<GitTreeEntry> maybeGitTreeEntry = before.getMarkers().findFirst(GitTreeEntry.class);
86+
if (maybeGitTreeEntry.isPresent()) {
87+
GitTreeEntry entry = maybeGitTreeEntry.get();
88+
this.oldId = database.insertVirtual(ObjectId.fromString(entry.getObjectId()), printAllAsBytes(before, markerPrinter)).abbreviate(40);
89+
this.oldMode = FileMode.fromBits(entry.getFileMode());
90+
} else {
91+
this.oldId = inserter.insert(Constants.OBJ_BLOB, printAllAsBytes(before, markerPrinter)).abbreviate(40);
92+
this.oldMode = before.getFileAttributes() != null && before.getFileAttributes().isExecutable() ? FileMode.EXECUTABLE_FILE : FileMode.REGULAR_FILE;
93+
}
94+
this.oldPath = (relativeTo == null ? before.getSourcePath() : relativeTo.relativize(before.getSourcePath())).toString().replace("\\", "/");
95+
}
96+
97+
if (after == null) {
98+
this.newId = A_ZERO;
99+
this.newMode = FileMode.MISSING;
100+
this.newPath = DEV_NULL;
101+
} else {
102+
this.newId = inserter.insert(Constants.OBJ_BLOB, printAllAsBytes(after, markerPrinter)).abbreviate(40);
103+
this.newMode = after.getMarkers().findFirst(GitTreeEntry.class)
104+
.map(entry -> FileMode.fromBits(entry.getFileMode()))
105+
.orElseGet(() -> after.getFileAttributes() != null && after.getFileAttributes().isExecutable() ? FileMode.EXECUTABLE_FILE : FileMode.REGULAR_FILE);
106+
this.newPath = (relativeTo == null ? after.getSourcePath() : relativeTo.relativize(after.getSourcePath())).toString().replace("\\", "/");
107+
}
108+
inserter.flush();
109+
}
110+
}
111+
} catch (IOException e) {
112+
throw new UncheckedIOException(e);
113+
}
114+
115+
if (this.oldMode == FileMode.MISSING && this.newMode != FileMode.MISSING) {
116+
this.changeType = ChangeType.ADD;
117+
} else if (this.oldMode != FileMode.MISSING && this.newMode == FileMode.MISSING) {
118+
this.changeType = ChangeType.DELETE;
119+
} else if (!oldPath.equals(newPath)) {
120+
this.changeType = ChangeType.RENAME;
121+
} else {
122+
this.changeType = ChangeType.MODIFY;
123+
}
124+
}
125+
126+
public InMemoryDiffEntry(
127+
@Nullable Path originalFilePath,
128+
@Nullable Path filePath,
129+
@Nullable Path relativeTo,
130+
byte[] oldSource,
131+
byte[] newSource,
132+
Set<Recipe> recipesThatMadeChanges,
133+
FileMode oldMode,
134+
FileMode newMode,
135+
boolean binaryPatch
136+
) {
137+
138+
this.recipesThatMadeChanges = recipesThatMadeChanges;
139+
this.binaryPatch = binaryPatch;
140+
141+
try {
142+
this.repo = new VirtualInMemoryRepository(new InMemoryRepository.Builder()
143+
.setRepositoryDescription(new DfsRepositoryDescription()));
62144

63145
try (ObjectInserter inserter = repo.getObjectDatabase().newInserter()) {
64146

65147
if (originalFilePath != null) {
66-
this.oldId = inserter.insert(Constants.OBJ_BLOB, oldSource.getBytes(StandardCharsets.UTF_8)).abbreviate(40);
148+
this.oldId = inserter.insert(Constants.OBJ_BLOB, oldSource).abbreviate(40);
67149
this.oldMode = oldMode;
68150
this.oldPath = (relativeTo == null ? originalFilePath : relativeTo.relativize(originalFilePath)).toString().replace("\\", "/");
69151
} else {
@@ -73,7 +155,7 @@ public InMemoryDiffEntry(@Nullable Path originalFilePath, @Nullable Path filePat
73155
}
74156

75157
if (filePath != null) {
76-
this.newId = inserter.insert(Constants.OBJ_BLOB, newSource.getBytes(StandardCharsets.UTF_8)).abbreviate(40);
158+
this.newId = inserter.insert(Constants.OBJ_BLOB, newSource).abbreviate(40);
77159
this.newMode = newMode;
78160
this.newPath = (relativeTo == null ? filePath : relativeTo.relativize(filePath)).toString().replace("\\", "/");
79161
} else {
@@ -115,6 +197,7 @@ public String getDiff(@Nullable Boolean ignoreAllWhitespace) {
115197
try (DiffFormatter formatter = new DiffFormatter(patch)) {
116198
formatter.setDiffComparator(ignoreAllWhitespace ? RawTextComparator.WS_IGNORE_ALL : RawTextComparator.DEFAULT);
117199
formatter.setRepository(repo);
200+
formatter.setBinary(binaryPatch);
118201
formatter.format(this);
119202
} catch (IOException e) {
120203
throw new UncheckedIOException(e);
@@ -124,7 +207,8 @@ public String getDiff(@Nullable Boolean ignoreAllWhitespace) {
124207

125208
AtomicBoolean addedComment = new AtomicBoolean(false);
126209
// NOTE: String.lines() would remove empty lines which we don't want
127-
return Arrays.stream(diff.split("\n"))
210+
// Use split with limit -1 to preserve trailing empty strings (important for binary patches)
211+
return Arrays.stream(diff.split("\n", -1))
128212
.map(l -> {
129213
if (!addedComment.get() && l.startsWith("@@") && l.endsWith("@@")) {
130214
addedComment.set(true);
@@ -142,11 +226,148 @@ public String getDiff(@Nullable Boolean ignoreAllWhitespace) {
142226
}
143227
return l;
144228
})
145-
.collect(joining("\n")) + "\n";
229+
.collect(joining("\n"));
146230
}
147231

148232
@Override
149233
public void close() {
150234
this.repo.close();
151235
}
236+
237+
private static byte[] printAllAsBytes(@Nullable SourceFile sourceFile, PrintOutputCapture.@Nullable MarkerPrinter markerPrinter) {
238+
if (sourceFile == null || sourceFile instanceof Quark) {
239+
return new byte[0];
240+
}
241+
242+
PrintOutputCapture<Integer> out = markerPrinter == null ?
243+
new PrintOutputCapture<>(0) :
244+
new PrintOutputCapture<>(0, markerPrinter);
245+
246+
return sourceFile.printAllAsBytes(out);
247+
}
248+
249+
@Value
250+
@EqualsAndHashCode(callSuper = false)
251+
private static class VirtualInMemoryRepository extends Repository {
252+
InMemoryRepository repo;
253+
VirtualObjectDatabase objdb;
254+
255+
public VirtualInMemoryRepository(InMemoryRepository.Builder builder) throws IOException {
256+
super(builder);
257+
repo = builder.build();
258+
objdb = new VirtualObjectDatabase(repo.getObjectDatabase());
259+
}
260+
261+
@Override
262+
public void create(boolean bare) throws IOException {
263+
repo.create(bare);
264+
}
265+
266+
@Override
267+
public String getIdentifier() {
268+
return repo.getIdentifier();
269+
}
270+
271+
@Override
272+
public VirtualObjectDatabase getObjectDatabase() {
273+
return objdb;
274+
}
275+
276+
@Override
277+
public RefDatabase getRefDatabase() {
278+
return repo.getRefDatabase();
279+
}
280+
281+
@Override
282+
public StoredConfig getConfig() {
283+
return repo.getConfig();
284+
}
285+
286+
@Override
287+
public AttributesNodeProvider createAttributesNodeProvider() {
288+
return repo.createAttributesNodeProvider();
289+
}
290+
291+
@Override
292+
public void scanForRepoChanges() throws IOException {
293+
repo.scanForRepoChanges();
294+
}
295+
296+
@Override
297+
public void notifyIndexChanged(boolean internal) {
298+
repo.notifyIndexChanged(internal);
299+
}
300+
301+
@Override
302+
public ReflogReader getReflogReader(String refName) throws IOException {
303+
return repo.getReflogReader(refName);
304+
}
305+
}
306+
307+
@Value
308+
@EqualsAndHashCode(callSuper = false)
309+
private static class VirtualObjectDatabase extends ObjectDatabase implements AutoCloseable {
310+
ObjectDatabase delegate;
311+
Map<ObjectId, byte[]> virtualObjects = new HashMap<>();
312+
313+
public ObjectId insertVirtual(ObjectId id, byte[] content) {
314+
virtualObjects.put(id, content);
315+
return id;
316+
}
317+
318+
@Override
319+
public ObjectInserter newInserter() {
320+
return delegate.newInserter();
321+
}
322+
323+
@Override
324+
public VirtualObjectReader newReader() {
325+
return new VirtualObjectReader(virtualObjects, delegate.newReader());
326+
}
327+
328+
@Override
329+
public void close() {
330+
delegate.close();
331+
}
332+
}
333+
334+
@Value
335+
@EqualsAndHashCode(callSuper = false)
336+
private static class VirtualObjectReader extends ObjectReader {
337+
Map<ObjectId, byte[]> virtualObjects;
338+
ObjectReader delegate;
339+
340+
@Override
341+
public ObjectReader newReader() {
342+
return delegate.newReader();
343+
}
344+
345+
@Override
346+
public Collection<ObjectId> resolve(AbbreviatedObjectId id) throws IOException {
347+
return delegate.resolve(id);
348+
}
349+
350+
@Override
351+
public ObjectLoader open(AnyObjectId objectId, int typeHint) throws IOException {
352+
byte[] virtual = virtualObjects.get(objectId.toObjectId());
353+
if (virtual != null) {
354+
return new ObjectLoader.SmallObject(typeHint, virtual);
355+
}
356+
return delegate.open(objectId, typeHint);
357+
}
358+
359+
@Override
360+
public Set<ObjectId> getShallowCommits() {
361+
try {
362+
return delegate.getShallowCommits();
363+
} catch (IOException e) {
364+
throw new UncheckedIOException(e);
365+
}
366+
}
367+
368+
@Override
369+
public void close() {
370+
delegate.close();
371+
}
372+
}
152373
}

rewrite-core/src/main/java/org/openrewrite/remote/Remote.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ default <T extends SourceFile> T withChecksum(@Nullable Checksum checksum) {
7373
default <P> String printAll(P p) {
7474
ExecutionContext ctx = p instanceof ExecutionContext ? (ExecutionContext) p :
7575
new InMemoryExecutionContext();
76-
return StringUtils.readFully(getInputStream(ctx), StandardCharsets.UTF_8);
76+
return StringUtils.readFully(getInputStream(ctx), getCharset() != null ? getCharset() : StandardCharsets.ISO_8859_1);
7777
}
7878

7979
@Override
@@ -108,7 +108,7 @@ public Tree visit(@Nullable Tree tree, PrintOutputCapture<P> p) {
108108
SourceFile sourceFile = (SourceFile) requireNonNull(tree);
109109
ExecutionContext ctx = p.getContext() instanceof ExecutionContext ? (ExecutionContext) p.getContext() :
110110
new InMemoryExecutionContext();
111-
p.append(StringUtils.readFully(getInputStream(ctx), StandardCharsets.UTF_8));
111+
p.append(StringUtils.readFully(getInputStream(ctx), getCharset() != null ? getCharset() : StandardCharsets.ISO_8859_1));
112112
return sourceFile;
113113
}
114114
};

0 commit comments

Comments
 (0)