1515 */
1616package org .openrewrite .internal ;
1717
18+ import lombok .EqualsAndHashCode ;
19+ import lombok .Value ;
20+ import lombok .experimental .NonFinal ;
1821import org .jspecify .annotations .Nullable ;
22+ import org .openrewrite .Incubating ;
23+ import org .openrewrite .PrintOutputCapture ;
1924import org .openrewrite .Recipe ;
25+ import org .openrewrite .SourceFile ;
26+ import org .openrewrite .jgit .attributes .AttributesNodeProvider ;
2027import org .openrewrite .jgit .diff .DiffEntry ;
2128import org .openrewrite .jgit .diff .DiffFormatter ;
2229import 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 .*;
2531import org .openrewrite .jgit .lib .*;
32+ import org .openrewrite .marker .GitTreeEntry ;
33+ import org .openrewrite .quark .Quark ;
2634
2735import java .io .ByteArrayOutputStream ;
2836import java .io .IOException ;
2937import java .io .UncheckedIOException ;
3038import java .nio .charset .StandardCharsets ;
3139import 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 .*;
3641import java .util .concurrent .atomic .AtomicBoolean ;
3742
3843import 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}
0 commit comments