1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 package org.eclipse.jgit.diff;
46
47 import static org.eclipse.jgit.diff.DiffEntry.ChangeType.ADD;
48 import static org.eclipse.jgit.diff.DiffEntry.ChangeType.COPY;
49 import static org.eclipse.jgit.diff.DiffEntry.ChangeType.DELETE;
50 import static org.eclipse.jgit.diff.DiffEntry.ChangeType.MODIFY;
51 import static org.eclipse.jgit.diff.DiffEntry.ChangeType.RENAME;
52 import static org.eclipse.jgit.diff.DiffEntry.Side.NEW;
53 import static org.eclipse.jgit.diff.DiffEntry.Side.OLD;
54 import static org.eclipse.jgit.lib.Constants.encode;
55 import static org.eclipse.jgit.lib.Constants.encodeASCII;
56 import static org.eclipse.jgit.lib.FileMode.GITLINK;
57
58 import java.io.ByteArrayOutputStream;
59 import java.io.IOException;
60 import java.io.OutputStream;
61 import java.util.Collection;
62 import java.util.Collections;
63 import java.util.List;
64
65 import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm;
66 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
67 import org.eclipse.jgit.dircache.DirCacheIterator;
68 import org.eclipse.jgit.errors.AmbiguousObjectException;
69 import org.eclipse.jgit.errors.BinaryBlobException;
70 import org.eclipse.jgit.errors.CorruptObjectException;
71 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
72 import org.eclipse.jgit.errors.MissingObjectException;
73 import org.eclipse.jgit.internal.JGitText;
74 import org.eclipse.jgit.lib.AbbreviatedObjectId;
75 import org.eclipse.jgit.lib.AnyObjectId;
76 import org.eclipse.jgit.lib.Config;
77 import org.eclipse.jgit.lib.ConfigConstants;
78 import org.eclipse.jgit.lib.Constants;
79 import org.eclipse.jgit.lib.FileMode;
80 import org.eclipse.jgit.lib.ObjectId;
81 import org.eclipse.jgit.lib.ObjectLoader;
82 import org.eclipse.jgit.lib.ObjectReader;
83 import org.eclipse.jgit.lib.ProgressMonitor;
84 import org.eclipse.jgit.lib.Repository;
85 import org.eclipse.jgit.patch.FileHeader;
86 import org.eclipse.jgit.patch.FileHeader.PatchType;
87 import org.eclipse.jgit.revwalk.FollowFilter;
88 import org.eclipse.jgit.revwalk.RevTree;
89 import org.eclipse.jgit.revwalk.RevWalk;
90 import org.eclipse.jgit.storage.pack.PackConfig;
91 import org.eclipse.jgit.treewalk.AbstractTreeIterator;
92 import org.eclipse.jgit.treewalk.CanonicalTreeParser;
93 import org.eclipse.jgit.treewalk.EmptyTreeIterator;
94 import org.eclipse.jgit.treewalk.TreeWalk;
95 import org.eclipse.jgit.treewalk.WorkingTreeIterator;
96 import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
97 import org.eclipse.jgit.treewalk.filter.IndexDiffFilter;
98 import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
99 import org.eclipse.jgit.treewalk.filter.PathFilter;
100 import org.eclipse.jgit.treewalk.filter.TreeFilter;
101 import org.eclipse.jgit.util.LfsFactory;
102 import org.eclipse.jgit.util.QuotedString;
103
104
105
106
107 public class DiffFormatter implements AutoCloseable {
108 private static final int DEFAULT_BINARY_FILE_THRESHOLD = PackConfig.DEFAULT_BIG_FILE_THRESHOLD;
109
110 private static final byte[] noNewLine = encodeASCII("\\ No newline at end of file\n");
111
112
113 private static final byte[] EMPTY = new byte[] {};
114
115 private final OutputStream out;
116
117 private ObjectReader reader;
118
119 private boolean closeReader;
120
121 private DiffConfig diffCfg;
122
123 private int context = 3;
124
125 private int abbreviationLength = 7;
126
127 private DiffAlgorithm diffAlgorithm;
128
129 private RawTextComparator comparator = RawTextComparator.DEFAULT;
130
131 private int binaryFileThreshold = DEFAULT_BINARY_FILE_THRESHOLD;
132
133 private String oldPrefix = "a/";
134
135 private String newPrefix = "b/";
136
137 private TreeFilter pathFilter = TreeFilter.ALL;
138
139 private RenameDetector renameDetector;
140
141 private ProgressMonitor progressMonitor;
142
143 private ContentSource.Pair source;
144
145 private Repository repository;
146
147
148
149
150
151
152
153
154
155 public DiffFormatter(OutputStream out) {
156 this.out = out;
157 }
158
159
160
161
162
163
164 protected OutputStream getOutputStream() {
165 return out;
166 }
167
168
169
170
171
172
173
174
175
176
177 public void setRepository(Repository repository) {
178 this.repository = repository;
179 setReader(repository.newObjectReader(), repository.getConfig(), true);
180 }
181
182
183
184
185
186
187
188
189
190
191
192 public void setReader(ObjectReader reader, Config cfg) {
193 setReader(reader, cfg, false);
194 }
195
196 private void setReader(ObjectReader reader, Config cfg, boolean closeReader) {
197 close();
198 this.closeReader = closeReader;
199 this.reader = reader;
200 this.diffCfg = cfg.get(DiffConfig.KEY);
201
202 ContentSource cs = ContentSource.create(reader);
203 source = new ContentSource.Pair(cs, cs);
204
205 if (diffCfg.isNoPrefix()) {
206 setOldPrefix("");
207 setNewPrefix("");
208 }
209 setDetectRenames(diffCfg.isRenameDetectionEnabled());
210
211 diffAlgorithm = DiffAlgorithm.getAlgorithm(cfg.getEnum(
212 ConfigConstants.CONFIG_DIFF_SECTION, null,
213 ConfigConstants.CONFIG_KEY_ALGORITHM,
214 SupportedAlgorithm.HISTOGRAM));
215 }
216
217
218
219
220
221
222
223
224
225 public void setContext(int lineCount) {
226 if (lineCount < 0)
227 throw new IllegalArgumentException(
228 JGitText.get().contextMustBeNonNegative);
229 context = lineCount;
230 }
231
232
233
234
235
236
237
238 public void setAbbreviationLength(int count) {
239 if (count < 0)
240 throw new IllegalArgumentException(
241 JGitText.get().abbreviationLengthMustBeNonNegative);
242 abbreviationLength = count;
243 }
244
245
246
247
248
249
250
251
252 public void setDiffAlgorithm(DiffAlgorithm alg) {
253 diffAlgorithm = alg;
254 }
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269 public void setDiffComparator(RawTextComparator cmp) {
270 comparator = cmp;
271 }
272
273
274
275
276
277
278
279
280
281
282
283 public void setBinaryFileThreshold(int threshold) {
284 this.binaryFileThreshold = threshold;
285 }
286
287
288
289
290
291
292
293
294
295
296 public void setOldPrefix(String prefix) {
297 oldPrefix = prefix;
298 }
299
300
301
302
303
304
305
306 public String getOldPrefix() {
307 return this.oldPrefix;
308 }
309
310
311
312
313
314
315
316
317
318
319 public void setNewPrefix(String prefix) {
320 newPrefix = prefix;
321 }
322
323
324
325
326
327
328
329 public String getNewPrefix() {
330 return this.newPrefix;
331 }
332
333
334
335
336
337
338 public boolean isDetectRenames() {
339 return renameDetector != null;
340 }
341
342
343
344
345
346
347
348
349
350
351
352
353 public void setDetectRenames(boolean on) {
354 if (on && renameDetector == null) {
355 assertHaveReader();
356 renameDetector = new RenameDetector(reader, diffCfg);
357 } else if (!on)
358 renameDetector = null;
359 }
360
361
362
363
364
365
366 public RenameDetector getRenameDetector() {
367 return renameDetector;
368 }
369
370
371
372
373
374
375
376 public void setProgressMonitor(ProgressMonitor pm) {
377 progressMonitor = pm;
378 }
379
380
381
382
383
384
385
386
387
388
389
390
391 public void setPathFilter(TreeFilter filter) {
392 pathFilter = filter != null ? filter : TreeFilter.ALL;
393 }
394
395
396
397
398
399
400 public TreeFilter getPathFilter() {
401 return pathFilter;
402 }
403
404
405
406
407
408
409
410 public void flush() throws IOException {
411 out.flush();
412 }
413
414
415
416
417
418
419
420
421 @Override
422 public void close() {
423 if (reader != null && closeReader) {
424 reader.close();
425 }
426 }
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447 public List<DiffEntry> scan(AnyObjectId a, AnyObjectId b)
448 throws IOException {
449 assertHaveReader();
450
451 try (RevWalk rw = new RevWalk(reader)) {
452 RevTree aTree = a != null ? rw.parseTree(a) : null;
453 RevTree bTree = b != null ? rw.parseTree(b) : null;
454 return scan(aTree, bTree);
455 }
456 }
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477 public List<DiffEntry> scan(RevTree a, RevTree b) throws IOException {
478 assertHaveReader();
479
480 AbstractTreeIterator aIterator = makeIteratorFromTreeOrNull(a);
481 AbstractTreeIterator bIterator = makeIteratorFromTreeOrNull(b);
482 return scan(aIterator, bIterator);
483 }
484
485 private AbstractTreeIterator makeIteratorFromTreeOrNull(RevTree tree)
486 throws IncorrectObjectTypeException, IOException {
487 if (tree != null) {
488 CanonicalTreeParser parser = new CanonicalTreeParser();
489 parser.reset(reader, tree);
490 return parser;
491 } else
492 return new EmptyTreeIterator();
493 }
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511 public List<DiffEntry> scan(AbstractTreeIterator a, AbstractTreeIterator b)
512 throws IOException {
513 assertHaveReader();
514
515 TreeWalk walk = new TreeWalk(reader);
516 walk.addTree(a);
517 walk.addTree(b);
518 walk.setRecursive(true);
519
520 TreeFilter filter = getDiffTreeFilterFor(a, b);
521 if (pathFilter instanceof FollowFilter) {
522 walk.setFilter(AndTreeFilter.create(
523 PathFilter.create(((FollowFilter) pathFilter).getPath()),
524 filter));
525 } else {
526 walk.setFilter(AndTreeFilter.create(pathFilter, filter));
527 }
528
529 source = new ContentSource.Pair(source(a), source(b));
530
531 List<DiffEntry> files = DiffEntry.scan(walk);
532 if (pathFilter instanceof FollowFilter && isAdd(files)) {
533
534
535
536
537 a.reset();
538 b.reset();
539 walk.reset();
540 walk.addTree(a);
541 walk.addTree(b);
542 walk.setFilter(filter);
543
544 if (renameDetector == null)
545 setDetectRenames(true);
546 files = updateFollowFilter(detectRenames(DiffEntry.scan(walk)));
547
548 } else if (renameDetector != null)
549 files = detectRenames(files);
550
551 return files;
552 }
553
554 private static TreeFilter getDiffTreeFilterFor(AbstractTreeIterator a,
555 AbstractTreeIterator b) {
556 if (a instanceof DirCacheIterator && b instanceof WorkingTreeIterator)
557 return new IndexDiffFilter(0, 1);
558
559 if (a instanceof WorkingTreeIterator && b instanceof DirCacheIterator)
560 return new IndexDiffFilter(1, 0);
561
562 TreeFilter filter = TreeFilter.ANY_DIFF;
563 if (a instanceof WorkingTreeIterator)
564 filter = AndTreeFilter.create(new NotIgnoredFilter(0), filter);
565 if (b instanceof WorkingTreeIterator)
566 filter = AndTreeFilter.create(new NotIgnoredFilter(1), filter);
567 return filter;
568 }
569
570 private ContentSource source(AbstractTreeIterator iterator) {
571 if (iterator instanceof WorkingTreeIterator)
572 return ContentSource.create((WorkingTreeIterator) iterator);
573 return ContentSource.create(reader);
574 }
575
576 private List<DiffEntry> detectRenames(List<DiffEntry> files)
577 throws IOException {
578 renameDetector.reset();
579 renameDetector.addAll(files);
580 return renameDetector.compute(reader, progressMonitor);
581 }
582
583 private boolean isAdd(List<DiffEntry> files) {
584 String oldPath = ((FollowFilter) pathFilter).getPath();
585 for (DiffEntry ent : files) {
586 if (ent.getChangeType() == ADD && ent.getNewPath().equals(oldPath))
587 return true;
588 }
589 return false;
590 }
591
592 private List<DiffEntry> updateFollowFilter(List<DiffEntry> files) {
593 String oldPath = ((FollowFilter) pathFilter).getPath();
594 for (DiffEntry ent : files) {
595 if (isRename(ent) && ent.getNewPath().equals(oldPath)) {
596 pathFilter = FollowFilter.create(ent.getOldPath(), diffCfg);
597 return Collections.singletonList(ent);
598 }
599 }
600 return Collections.emptyList();
601 }
602
603 private static boolean isRename(DiffEntry ent) {
604 return ent.getChangeType() == RENAME || ent.getChangeType() == COPY;
605 }
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624 public void format(AnyObjectId a, AnyObjectId b) throws IOException {
625 format(scan(a, b));
626 }
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646 public void format(RevTree a, RevTree b) throws IOException {
647 format(scan(a, b));
648 }
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667 public void format(AbstractTreeIterator a, AbstractTreeIterator b)
668 throws IOException {
669 format(scan(a, b));
670 }
671
672
673
674
675
676
677
678
679
680
681
682
683 public void format(List<? extends DiffEntry> entries) throws IOException {
684 for (DiffEntry ent : entries)
685 format(ent);
686 }
687
688
689
690
691
692
693
694
695
696
697 public void format(DiffEntry ent) throws IOException {
698 FormatResult res = createFormatResult(ent);
699 format(res.header, res.a, res.b);
700 }
701
702 private static byte[] writeGitLinkText(AbbreviatedObjectId id) {
703 if (ObjectId.zeroId().equals(id.toObjectId())) {
704 return EMPTY;
705 }
706 return encodeASCII("Subproject commit " + id.name()
707 + "\n");
708 }
709
710 private String format(AbbreviatedObjectId id) {
711 if (id.isComplete() && reader != null) {
712 try {
713 id = reader.abbreviate(id.toObjectId(), abbreviationLength);
714 } catch (IOException cannotAbbreviate) {
715
716 }
717 }
718 return id.name();
719 }
720
721 private static String quotePath(String name) {
722 return QuotedString.GIT_PATH.quote(name);
723 }
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745 public void format(FileHeader head, RawText a, RawText b)
746 throws IOException {
747
748
749
750
751 final int start = head.getStartOffset();
752 int end = head.getEndOffset();
753 if (!head.getHunks().isEmpty())
754 end = head.getHunks().get(0).getStartOffset();
755 out.write(head.getBuffer(), start, end - start);
756 if (head.getPatchType() == PatchType.UNIFIED)
757 format(head.toEditList(), a, b);
758 }
759
760
761
762
763
764
765
766
767
768
769
770
771 public void format(EditList edits, RawText a, RawText b)
772 throws IOException {
773 for (int curIdx = 0; curIdx < edits.size();) {
774 Edit curEdit = edits.get(curIdx);
775 final int endIdx = findCombinedEnd(edits, curIdx);
776 final Edit endEdit = edits.get(endIdx);
777
778 int aCur = (int) Math.max(0, (long) curEdit.getBeginA() - context);
779 int bCur = (int) Math.max(0, (long) curEdit.getBeginB() - context);
780 final int aEnd = (int) Math.min(a.size(), (long) endEdit.getEndA() + context);
781 final int bEnd = (int) Math.min(b.size(), (long) endEdit.getEndB() + context);
782
783 writeHunkHeader(aCur, aEnd, bCur, bEnd);
784
785 while (aCur < aEnd || bCur < bEnd) {
786 if (aCur < curEdit.getBeginA() || endIdx + 1 < curIdx) {
787 writeContextLine(a, aCur);
788 if (isEndOfLineMissing(a, aCur))
789 out.write(noNewLine);
790 aCur++;
791 bCur++;
792 } else if (aCur < curEdit.getEndA()) {
793 writeRemovedLine(a, aCur);
794 if (isEndOfLineMissing(a, aCur))
795 out.write(noNewLine);
796 aCur++;
797 } else if (bCur < curEdit.getEndB()) {
798 writeAddedLine(b, bCur);
799 if (isEndOfLineMissing(b, bCur))
800 out.write(noNewLine);
801 bCur++;
802 }
803
804 if (end(curEdit, aCur, bCur) && ++curIdx < edits.size())
805 curEdit = edits.get(curIdx);
806 }
807 }
808 }
809
810
811
812
813
814
815
816
817
818
819 protected void writeContextLine(RawText text, int line)
820 throws IOException {
821 writeLine(' ', text, line);
822 }
823
824 private static boolean isEndOfLineMissing(RawText text, int line) {
825 return line + 1 == text.size() && text.isMissingNewlineAtEnd();
826 }
827
828
829
830
831
832
833
834
835
836
837 protected void writeAddedLine(RawText text, int line)
838 throws IOException {
839 writeLine('+', text, line);
840 }
841
842
843
844
845
846
847
848
849
850
851 protected void writeRemovedLine(RawText text, int line)
852 throws IOException {
853 writeLine('-', text, line);
854 }
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869 protected void writeHunkHeader(int aStartLine, int aEndLine,
870 int bStartLine, int bEndLine) throws IOException {
871 out.write('@');
872 out.write('@');
873 writeRange('-', aStartLine + 1, aEndLine - aStartLine);
874 writeRange('+', bStartLine + 1, bEndLine - bStartLine);
875 out.write(' ');
876 out.write('@');
877 out.write('@');
878 out.write('\n');
879 }
880
881 private void writeRange(char prefix, int begin, int cnt)
882 throws IOException {
883 out.write(' ');
884 out.write(prefix);
885 switch (cnt) {
886 case 0:
887
888
889
890
891
892 out.write(encodeASCII(begin - 1));
893 out.write(',');
894 out.write('0');
895 break;
896
897 case 1:
898
899
900 out.write(encodeASCII(begin));
901 break;
902
903 default:
904 out.write(encodeASCII(begin));
905 out.write(',');
906 out.write(encodeASCII(cnt));
907 break;
908 }
909 }
910
911
912
913
914
915
916
917
918
919
920
921
922
923 protected void writeLine(final char prefix, final RawText text,
924 final int cur) throws IOException {
925 out.write(prefix);
926 text.writeLine(out, cur);
927 out.write('\n');
928 }
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953 public FileHeader toFileHeader(DiffEntry ent) throws IOException,
954 CorruptObjectException, MissingObjectException {
955 return createFormatResult(ent).header;
956 }
957
958 private static class FormatResult {
959 FileHeader header;
960
961 RawText a;
962
963 RawText b;
964 }
965
966 private FormatResult createFormatResult(DiffEntry ent) throws IOException,
967 CorruptObjectException, MissingObjectException {
968 final FormatResult res = new FormatResult();
969 ByteArrayOutputStream buf = new ByteArrayOutputStream();
970 final EditList editList;
971 final FileHeader.PatchType type;
972
973 formatHeader(buf, ent);
974
975 if (ent.getOldId() == null || ent.getNewId() == null) {
976
977 editList = new EditList();
978 type = PatchType.UNIFIED;
979 res.header = new FileHeader(buf.toByteArray(), editList, type);
980 return res;
981 }
982
983 assertHaveReader();
984
985 RawText aRaw = null;
986 RawText bRaw = null;
987 if (ent.getOldMode() == GITLINK || ent.getNewMode() == GITLINK) {
988 aRaw = new RawText(writeGitLinkText(ent.getOldId()));
989 bRaw = new RawText(writeGitLinkText(ent.getNewId()));
990 } else {
991 try {
992 aRaw = open(OLD, ent);
993 bRaw = open(NEW, ent);
994 } catch (BinaryBlobException e) {
995
996 formatOldNewPaths(buf, ent);
997 buf.write(encodeASCII("Binary files differ\n"));
998 editList = new EditList();
999 type = PatchType.BINARY;
1000 res.header = new FileHeader(buf.toByteArray(), editList, type);
1001 return res;
1002 }
1003 }
1004
1005 res.a = aRaw;
1006 res.b = bRaw;
1007 editList = diff(res.a, res.b);
1008 type = PatchType.UNIFIED;
1009
1010 switch (ent.getChangeType()) {
1011 case RENAME:
1012 case COPY:
1013 if (!editList.isEmpty())
1014 formatOldNewPaths(buf, ent);
1015 break;
1016
1017 default:
1018 formatOldNewPaths(buf, ent);
1019 break;
1020 }
1021
1022
1023 res.header = new FileHeader(buf.toByteArray(), editList, type);
1024 return res;
1025 }
1026
1027 private EditList diff(RawText a, RawText b) {
1028 return diffAlgorithm.diff(comparator, a, b);
1029 }
1030
1031 private void assertHaveReader() {
1032 if (reader == null) {
1033 throw new IllegalStateException(JGitText.get().readerIsRequired);
1034 }
1035 }
1036
1037 private RawText open(DiffEntry.Side side, DiffEntry entry)
1038 throws IOException, BinaryBlobException {
1039 if (entry.getMode(side) == FileMode.MISSING)
1040 return RawText.EMPTY_TEXT;
1041
1042 if (entry.getMode(side).getObjectType() != Constants.OBJ_BLOB)
1043 return RawText.EMPTY_TEXT;
1044
1045 AbbreviatedObjectId id = entry.getId(side);
1046 if (!id.isComplete()) {
1047 Collection<ObjectId> ids = reader.resolve(id);
1048 if (ids.size() == 1) {
1049 id = AbbreviatedObjectId.fromObjectId(ids.iterator().next());
1050 switch (side) {
1051 case OLD:
1052 entry.oldId = id;
1053 break;
1054 case NEW:
1055 entry.newId = id;
1056 break;
1057 }
1058 } else if (ids.size() == 0)
1059 throw new MissingObjectException(id, Constants.OBJ_BLOB);
1060 else
1061 throw new AmbiguousObjectException(id, ids);
1062 }
1063
1064 ObjectLoader ldr = LfsFactory.getInstance().applySmudgeFilter(repository,
1065 source.open(side, entry), entry.getDiffAttribute());
1066 return RawText.load(ldr, binaryFileThreshold);
1067 }
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083 protected void formatGitDiffFirstHeaderLine(ByteArrayOutputStream o,
1084 final ChangeType type, final String oldPath, final String newPath)
1085 throws IOException {
1086 o.write(encodeASCII("diff --git "));
1087 o.write(encode(quotePath(oldPrefix + (type == ADD ? newPath : oldPath))));
1088 o.write(' ');
1089 o.write(encode(quotePath(newPrefix
1090 + (type == DELETE ? oldPath : newPath))));
1091 o.write('\n');
1092 }
1093
1094 private void formatHeader(ByteArrayOutputStream o, DiffEntry ent)
1095 throws IOException {
1096 final ChangeType type = ent.getChangeType();
1097 final String oldp = ent.getOldPath();
1098 final String newp = ent.getNewPath();
1099 final FileMode oldMode = ent.getOldMode();
1100 final FileMode newMode = ent.getNewMode();
1101
1102 formatGitDiffFirstHeaderLine(o, type, oldp, newp);
1103
1104 if ((type == MODIFY || type == COPY || type == RENAME)
1105 && !oldMode.equals(newMode)) {
1106 o.write(encodeASCII("old mode "));
1107 oldMode.copyTo(o);
1108 o.write('\n');
1109
1110 o.write(encodeASCII("new mode "));
1111 newMode.copyTo(o);
1112 o.write('\n');
1113 }
1114
1115 switch (type) {
1116 case ADD:
1117 o.write(encodeASCII("new file mode "));
1118 newMode.copyTo(o);
1119 o.write('\n');
1120 break;
1121
1122 case DELETE:
1123 o.write(encodeASCII("deleted file mode "));
1124 oldMode.copyTo(o);
1125 o.write('\n');
1126 break;
1127
1128 case RENAME:
1129 o.write(encodeASCII("similarity index " + ent.getScore() + "%"));
1130 o.write('\n');
1131
1132 o.write(encode("rename from " + quotePath(oldp)));
1133 o.write('\n');
1134
1135 o.write(encode("rename to " + quotePath(newp)));
1136 o.write('\n');
1137 break;
1138
1139 case COPY:
1140 o.write(encodeASCII("similarity index " + ent.getScore() + "%"));
1141 o.write('\n');
1142
1143 o.write(encode("copy from " + quotePath(oldp)));
1144 o.write('\n');
1145
1146 o.write(encode("copy to " + quotePath(newp)));
1147 o.write('\n');
1148 break;
1149
1150 case MODIFY:
1151 if (0 < ent.getScore()) {
1152 o.write(encodeASCII("dissimilarity index "
1153 + (100 - ent.getScore()) + "%"));
1154 o.write('\n');
1155 }
1156 break;
1157 }
1158
1159 if (ent.getOldId() != null && !ent.getOldId().equals(ent.getNewId())) {
1160 formatIndexLine(o, ent);
1161 }
1162 }
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174 protected void formatIndexLine(OutputStream o, DiffEntry ent)
1175 throws IOException {
1176 o.write(encodeASCII("index "
1177 + format(ent.getOldId())
1178 + ".."
1179 + format(ent.getNewId())));
1180 if (ent.getOldMode().equals(ent.getNewMode())) {
1181 o.write(' ');
1182 ent.getNewMode().copyTo(o);
1183 }
1184 o.write('\n');
1185 }
1186
1187 private void formatOldNewPaths(ByteArrayOutputStream o, DiffEntry ent)
1188 throws IOException {
1189 if (ent.oldId.equals(ent.newId))
1190 return;
1191
1192 final String oldp;
1193 final String newp;
1194
1195 switch (ent.getChangeType()) {
1196 case ADD:
1197 oldp = DiffEntry.DEV_NULL;
1198 newp = quotePath(newPrefix + ent.getNewPath());
1199 break;
1200
1201 case DELETE:
1202 oldp = quotePath(oldPrefix + ent.getOldPath());
1203 newp = DiffEntry.DEV_NULL;
1204 break;
1205
1206 default:
1207 oldp = quotePath(oldPrefix + ent.getOldPath());
1208 newp = quotePath(newPrefix + ent.getNewPath());
1209 break;
1210 }
1211
1212 o.write(encode("--- " + oldp + "\n"));
1213 o.write(encode("+++ " + newp + "\n"));
1214 }
1215
1216 private int findCombinedEnd(List<Edit> edits, int i) {
1217 int end = i + 1;
1218 while (end < edits.size()
1219 && (combineA(edits, end) || combineB(edits, end)))
1220 end++;
1221 return end - 1;
1222 }
1223
1224 private boolean combineA(List<Edit> e, int i) {
1225 return e.get(i).getBeginA() - e.get(i - 1).getEndA() <= 2 * context;
1226 }
1227
1228 private boolean combineB(List<Edit> e, int i) {
1229 return e.get(i).getBeginB() - e.get(i - 1).getEndB() <= 2 * context;
1230 }
1231
1232 private static boolean end(Edit edit, int a, int b) {
1233 return edit.getEndA() <= a && edit.getEndB() <= b;
1234 }
1235 }