1
2
3
4
5
6
7
8
9
10
11 package org.eclipse.jgit.blame;
12
13 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
14 import static org.eclipse.jgit.lib.FileMode.TYPE_FILE;
15 import static org.eclipse.jgit.lib.FileMode.TYPE_MASK;
16
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.text.MessageFormat;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.List;
24
25 import org.eclipse.jgit.annotations.Nullable;
26 import org.eclipse.jgit.api.errors.NoHeadException;
27 import org.eclipse.jgit.blame.Candidate.BlobCandidate;
28 import org.eclipse.jgit.blame.Candidate.HeadCandidate;
29 import org.eclipse.jgit.blame.Candidate.ReverseCandidate;
30 import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit;
31 import org.eclipse.jgit.diff.DiffAlgorithm;
32 import org.eclipse.jgit.diff.DiffEntry;
33 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
34 import org.eclipse.jgit.diff.EditList;
35 import org.eclipse.jgit.diff.HistogramDiff;
36 import org.eclipse.jgit.diff.RawText;
37 import org.eclipse.jgit.diff.RawTextComparator;
38 import org.eclipse.jgit.diff.RenameDetector;
39 import org.eclipse.jgit.dircache.DirCache;
40 import org.eclipse.jgit.dircache.DirCacheEntry;
41 import org.eclipse.jgit.dircache.DirCacheIterator;
42 import org.eclipse.jgit.errors.NoWorkTreeException;
43 import org.eclipse.jgit.internal.JGitText;
44 import org.eclipse.jgit.internal.diff.FilteredRenameDetector;
45 import org.eclipse.jgit.lib.AnyObjectId;
46 import org.eclipse.jgit.lib.Constants;
47 import org.eclipse.jgit.lib.MutableObjectId;
48 import org.eclipse.jgit.lib.ObjectId;
49 import org.eclipse.jgit.lib.ObjectLoader;
50 import org.eclipse.jgit.lib.ObjectReader;
51 import org.eclipse.jgit.lib.PersonIdent;
52 import org.eclipse.jgit.lib.Repository;
53 import org.eclipse.jgit.revwalk.RevCommit;
54 import org.eclipse.jgit.revwalk.RevFlag;
55 import org.eclipse.jgit.revwalk.RevWalk;
56 import org.eclipse.jgit.treewalk.FileTreeIterator;
57 import org.eclipse.jgit.treewalk.TreeWalk;
58 import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
59 import org.eclipse.jgit.treewalk.filter.PathFilter;
60 import org.eclipse.jgit.treewalk.filter.TreeFilter;
61 import org.eclipse.jgit.util.IO;
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101 public class BlameGenerator implements AutoCloseable {
102 private final Repository repository;
103
104 private final PathFilter resultPath;
105
106 private final MutableObjectId idBuf;
107
108
109 private RevWalk revPool;
110
111
112 private RevFlag SEEN;
113
114 private ObjectReader reader;
115
116 private TreeWalk treeWalk;
117
118 private DiffAlgorithm diffAlgorithm = new HistogramDiff();
119
120 private RawTextComparator textComparator = RawTextComparator.DEFAULT;
121
122 private RenameDetector renameDetector;
123
124
125 private Candidate queue;
126
127
128 private int remaining;
129
130
131 private Candidate outCandidate;
132 private Region outRegion;
133
134
135
136
137
138
139
140
141
142
143
144 public BlameGenerator(Repository repository, String path) {
145 this.repository = repository;
146 this.resultPath = PathFilter.create(path);
147
148 idBuf = new MutableObjectId();
149 setFollowFileRenames(true);
150 initRevPool(false);
151
152 remaining = -1;
153 }
154
155 private void initRevPool(boolean reverse) {
156 if (queue != null)
157 throw new IllegalStateException();
158
159 if (revPool != null)
160 revPool.close();
161
162 if (reverse)
163 revPool = new ReverseWalk(getRepository());
164 else
165 revPool = new RevWalk(getRepository());
166
167 SEEN = revPool.newFlag("SEEN");
168 reader = revPool.getObjectReader();
169 treeWalk = new TreeWalk(reader);
170 treeWalk.setRecursive(true);
171 }
172
173
174
175
176
177
178 public Repository getRepository() {
179 return repository;
180 }
181
182
183
184
185
186
187 public String getResultPath() {
188 return resultPath.getPath();
189 }
190
191
192
193
194
195
196
197
198 public BlameGenerator setDiffAlgorithm(DiffAlgorithm algorithm) {
199 diffAlgorithm = algorithm;
200 return this;
201 }
202
203
204
205
206
207
208
209
210 public BlameGenerator setTextComparator(RawTextComparator comparator) {
211 textComparator = comparator;
212 return this;
213 }
214
215
216
217
218
219
220
221
222
223
224
225
226
227 public BlameGenerator setFollowFileRenames(boolean follow) {
228 if (follow)
229 renameDetector = new RenameDetector(getRepository());
230 else
231 renameDetector = null;
232 return this;
233 }
234
235
236
237
238
239
240
241
242 @Nullable
243 public RenameDetector getRenameDetector() {
244 return renameDetector;
245 }
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263 public BlameGenerator push(String description, byte[] contents)
264 throws IOException {
265 return push(description, new RawText(contents));
266 }
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284 public BlameGenerator push(String description, RawText contents)
285 throws IOException {
286 if (description == null)
287 description = JGitText.get().blameNotCommittedYet;
288 BlobCandidate c = new BlobCandidate(getRepository(), description,
289 resultPath);
290 c.sourceText = contents;
291 c.regionList = new Region(0, 0, contents.size());
292 remaining = contents.size();
293 push(c);
294 return this;
295 }
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310 public BlameGenerator prepareHead() throws NoHeadException, IOException {
311 Repository repo = getRepository();
312 ObjectId head = repo.resolve(Constants.HEAD);
313 if (head == null) {
314 throw new NoHeadException(MessageFormat
315 .format(JGitText.get().noSuchRefKnown, Constants.HEAD));
316 }
317 if (repo.isBare()) {
318 return push(null, head);
319 }
320 DirCache dc = repo.readDirCache();
321 try (TreeWalk walk = new TreeWalk(repo)) {
322 walk.setOperationType(OperationType.CHECKIN_OP);
323 FileTreeIterator iter = new FileTreeIterator(repo);
324 int fileTree = walk.addTree(iter);
325 int indexTree = walk.addTree(new DirCacheIterator(dc));
326 iter.setDirCacheIterator(walk, indexTree);
327 walk.setFilter(resultPath);
328 walk.setRecursive(true);
329 if (!walk.next()) {
330 return this;
331 }
332 DirCacheIterator dcIter = walk.getTree(indexTree,
333 DirCacheIterator.class);
334 if (dcIter == null) {
335
336 return this;
337 }
338 iter = walk.getTree(fileTree, FileTreeIterator.class);
339 if (iter == null || !isFile(iter.getEntryRawMode())) {
340 return this;
341 }
342 RawText inTree;
343 long filteredLength = iter.getEntryContentLength();
344 try (InputStream stream = iter.openEntryStream()) {
345 inTree = new RawText(getBytes(iter.getEntryFile().getPath(),
346 stream, filteredLength));
347 }
348 DirCacheEntry indexEntry = dcIter.getDirCacheEntry();
349 if (indexEntry.getStage() == DirCacheEntry.STAGE_0) {
350 push(null, head);
351 push(null, indexEntry.getObjectId());
352 push(null, inTree);
353 } else {
354
355
356 HeadCandidate c = new HeadCandidate(getRepository(), resultPath,
357 getHeads(repo, head));
358 c.sourceText = inTree;
359 c.regionList = new Region(0, 0, inTree.size());
360 remaining = inTree.size();
361 push(c);
362 }
363 }
364 return this;
365 }
366
367 private List<RevCommit> getHeads(Repository repo, ObjectId head)
368 throws NoWorkTreeException, IOException {
369 List<ObjectId> mergeIds = repo.readMergeHeads();
370 if (mergeIds == null || mergeIds.isEmpty()) {
371 return Collections.singletonList(revPool.parseCommit(head));
372 }
373 List<RevCommit> heads = new ArrayList<>(mergeIds.size() + 1);
374 heads.add(revPool.parseCommit(head));
375 for (ObjectId id : mergeIds) {
376 heads.add(revPool.parseCommit(id));
377 }
378 return heads;
379 }
380
381 private static byte[] getBytes(String path, InputStream in, long maxLength)
382 throws IOException {
383 if (maxLength > Integer.MAX_VALUE) {
384 throw new IOException(
385 MessageFormat.format(JGitText.get().fileIsTooLarge, path));
386 }
387 int max = (int) maxLength;
388 byte[] buffer = new byte[max];
389 int read = IO.readFully(in, buffer, 0);
390 if (read == max) {
391 return buffer;
392 }
393 byte[] copy = new byte[read];
394 System.arraycopy(buffer, 0, copy, 0, read);
395 return copy;
396 }
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414 public BlameGenerator push(String description, AnyObjectId id)
415 throws IOException {
416 ObjectLoader ldr = reader.open(id);
417 if (ldr.getType() == OBJ_BLOB) {
418 if (description == null)
419 description = JGitText.get().blameNotCommittedYet;
420 BlobCandidate c = new BlobCandidate(getRepository(), description,
421 resultPath);
422 c.sourceBlob = id.toObjectId();
423 c.sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE));
424 c.regionList = new Region(0, 0, c.sourceText.size());
425 remaining = c.sourceText.size();
426 push(c);
427 return this;
428 }
429
430 RevCommit commit = revPool.parseCommit(id);
431 if (!find(commit, resultPath))
432 return this;
433
434 Candidate c = new Candidate(getRepository(), commit, resultPath);
435 c.sourceBlob = idBuf.toObjectId();
436 c.loadText(reader);
437 c.regionList = new Region(0, 0, c.sourceText.size());
438 remaining = c.sourceText.size();
439 push(c);
440 return this;
441 }
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470 public BlameGenerator reverse(AnyObjectId start, AnyObjectId end)
471 throws IOException {
472 return reverse(start, Collections.singleton(end.toObjectId()));
473 }
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502 public BlameGenerator reverse(AnyObjectId start,
503 Collection<? extends ObjectId> end) throws IOException {
504 initRevPool(true);
505
506 ReverseCommit result = (ReverseCommit) revPool.parseCommit(start);
507 if (!find(result, resultPath))
508 return this;
509
510 revPool.markUninteresting(result);
511 for (ObjectId id : end)
512 revPool.markStart(revPool.parseCommit(id));
513
514 while (revPool.next() != null) {
515
516 }
517
518 ReverseCandidate c = new ReverseCandidate(getRepository(), result,
519 resultPath);
520 c.sourceBlob = idBuf.toObjectId();
521 c.loadText(reader);
522 c.regionList = new Region(0, 0, c.sourceText.size());
523 remaining = c.sourceText.size();
524 push(c);
525 return this;
526 }
527
528
529
530
531
532
533
534
535
536 public RevFlag newFlag(String name) {
537 return revPool.newFlag(name);
538 }
539
540
541
542
543
544
545
546
547 public BlameResult computeBlameResult() throws IOException {
548 try {
549 BlameResult r = BlameResult.create(this);
550 if (r != null)
551 r.computeAll();
552 return r;
553 } finally {
554 close();
555 }
556 }
557
558
559
560
561
562
563
564
565
566
567
568 public boolean next() throws IOException {
569
570 if (outRegion != null) {
571 Region r = outRegion;
572 remaining -= r.length;
573 if (r.next != null) {
574 outRegion = r.next;
575 return true;
576 }
577
578 if (outCandidate.queueNext != null)
579 return result(outCandidate.queueNext);
580
581 outCandidate = null;
582 outRegion = null;
583 }
584
585
586
587 if (remaining == 0)
588 return done();
589
590 for (;;) {
591 Candidate n = pop();
592 if (n == null)
593 return done();
594
595 int pCnt = n.getParentCount();
596 if (pCnt == 1) {
597 if (processOne(n))
598 return true;
599
600 } else if (1 < pCnt) {
601 if (processMerge(n))
602 return true;
603
604 } else if (n instanceof ReverseCandidate) {
605
606
607
608 } else {
609
610
611 return result(n);
612 }
613 }
614 }
615
616 private boolean done() {
617 close();
618 return false;
619 }
620
621 private boolean result(Candidate n) throws IOException {
622 n.beginResult(revPool);
623 outCandidate = n;
624 outRegion = n.regionList;
625 return outRegion != null;
626 }
627
628 private boolean reverseResult(Candidate parent, Candidate source)
629 throws IOException {
630
631
632
633 Candidate res = parent.copy(parent.sourceCommit);
634 res.regionList = source.regionList;
635 return result(res);
636 }
637
638 private Candidate pop() {
639 Candidate n = queue;
640 if (n != null) {
641 queue = n.queueNext;
642 n.queueNext = null;
643 }
644 return n;
645 }
646
647 private void push(BlobCandidate toInsert) {
648 Candidate c = queue;
649 if (c != null) {
650 c.remove(SEEN);
651 c.regionList = null;
652 toInsert.parent = c;
653 }
654 queue = toInsert;
655 }
656
657 private void push(Candidate toInsert) {
658 if (toInsert.has(SEEN)) {
659
660
661
662
663
664
665
666
667
668
669 for (Candidate p = queue; p != null; p = p.queueNext) {
670 if (p.canMergeRegions(toInsert)) {
671 p.mergeRegions(toInsert);
672 return;
673 }
674 }
675 }
676 toInsert.add(SEEN);
677
678
679
680 int time = toInsert.getTime();
681 Candidate n = queue;
682 if (n == null || time >= n.getTime()) {
683 toInsert.queueNext = n;
684 queue = toInsert;
685 return;
686 }
687
688 for (Candidate p = n;; p = n) {
689 n = p.queueNext;
690 if (n == null || time >= n.getTime()) {
691 toInsert.queueNext = n;
692 p.queueNext = toInsert;
693 return;
694 }
695 }
696 }
697
698 private boolean processOne(Candidate n) throws IOException {
699 RevCommit parent = n.getParent(0);
700 if (parent == null)
701 return split(n.getNextCandidate(0), n);
702 revPool.parseHeaders(parent);
703
704 if (find(parent, n.sourcePath)) {
705 if (idBuf.equals(n.sourceBlob))
706 return blameEntireRegionOnParent(n, parent);
707 return splitBlameWithParent(n, parent);
708 }
709
710 if (n.sourceCommit == null)
711 return result(n);
712
713 DiffEntry r = findRename(parent, n.sourceCommit, n.sourcePath);
714 if (r == null)
715 return result(n);
716
717 if (0 == r.getOldId().prefixCompare(n.sourceBlob)) {
718
719
720 n.sourceCommit = parent;
721 n.sourcePath = PathFilter.create(r.getOldPath());
722 push(n);
723 return false;
724 }
725
726 Candidate next = n.create(getRepository(), parent,
727 PathFilter.create(r.getOldPath()));
728 next.sourceBlob = r.getOldId().toObjectId();
729 next.renameScore = r.getScore();
730 next.loadText(reader);
731 return split(next, n);
732 }
733
734 private boolean blameEntireRegionOnParent(Candidate n, RevCommit parent) {
735
736 n.sourceCommit = parent;
737 push(n);
738 return false;
739 }
740
741 private boolean splitBlameWithParent(Candidate n, RevCommit parent)
742 throws IOException {
743 Candidate next = n.create(getRepository(), parent, n.sourcePath);
744 next.sourceBlob = idBuf.toObjectId();
745 next.loadText(reader);
746 return split(next, n);
747 }
748
749 private boolean split(Candidate parent, Candidate source)
750 throws IOException {
751 EditList editList = diffAlgorithm.diff(textComparator,
752 parent.sourceText, source.sourceText);
753 if (editList.isEmpty()) {
754
755
756
757 parent.regionList = source.regionList;
758 push(parent);
759 return false;
760 }
761
762 parent.takeBlame(editList, source);
763 if (parent.regionList != null)
764 push(parent);
765 if (source.regionList != null) {
766 if (source instanceof ReverseCandidate)
767 return reverseResult(parent, source);
768 return result(source);
769 }
770 return false;
771 }
772
773 private boolean processMerge(Candidate n) throws IOException {
774 int pCnt = n.getParentCount();
775
776
777
778 ObjectId[] ids = null;
779 for (int pIdx = 0; pIdx < pCnt; pIdx++) {
780 RevCommit parent = n.getParent(pIdx);
781 revPool.parseHeaders(parent);
782 if (!find(parent, n.sourcePath))
783 continue;
784 if (!(n instanceof ReverseCandidate) && idBuf.equals(n.sourceBlob))
785 return blameEntireRegionOnParent(n, parent);
786 if (ids == null)
787 ids = new ObjectId[pCnt];
788 ids[pIdx] = idBuf.toObjectId();
789 }
790
791
792 DiffEntry[] renames = null;
793 if (renameDetector != null) {
794 renames = new DiffEntry[pCnt];
795 for (int pIdx = 0; pIdx < pCnt; pIdx++) {
796 RevCommit parent = n.getParent(pIdx);
797 if (ids != null && ids[pIdx] != null)
798 continue;
799
800 DiffEntry r = findRename(parent, n.sourceCommit, n.sourcePath);
801 if (r == null)
802 continue;
803
804 if (n instanceof ReverseCandidate) {
805 if (ids == null)
806 ids = new ObjectId[pCnt];
807 ids[pCnt] = r.getOldId().toObjectId();
808 } else if (0 == r.getOldId().prefixCompare(n.sourceBlob)) {
809
810
811
812
813
814
815 n.sourcePath = PathFilter.create(r.getOldPath());
816 return blameEntireRegionOnParent(n, parent);
817 }
818
819 renames[pIdx] = r;
820 }
821 }
822
823
824 Candidate[] parents = new Candidate[pCnt];
825 for (int pIdx = 0; pIdx < pCnt; pIdx++) {
826 RevCommit parent = n.getParent(pIdx);
827
828 Candidate p;
829 if (renames != null && renames[pIdx] != null) {
830 p = n.create(getRepository(), parent,
831 PathFilter.create(renames[pIdx].getOldPath()));
832 p.renameScore = renames[pIdx].getScore();
833 p.sourceBlob = renames[pIdx].getOldId().toObjectId();
834 } else if (ids != null && ids[pIdx] != null) {
835 p = n.create(getRepository(), parent, n.sourcePath);
836 p.sourceBlob = ids[pIdx];
837 } else {
838 continue;
839 }
840
841 EditList editList;
842 if (n instanceof ReverseCandidate
843 && p.sourceBlob.equals(n.sourceBlob)) {
844
845 p.sourceText = n.sourceText;
846 editList = new EditList(0);
847 } else {
848 p.loadText(reader);
849 editList = diffAlgorithm.diff(textComparator,
850 p.sourceText, n.sourceText);
851 }
852
853 if (editList.isEmpty()) {
854
855
856
857 if (n instanceof ReverseCandidate) {
858 parents[pIdx] = p;
859 continue;
860 }
861
862 p.regionList = n.regionList;
863 n.regionList = null;
864 parents[pIdx] = p;
865 break;
866 }
867
868 p.takeBlame(editList, n);
869
870
871
872 if (p.regionList != null) {
873
874
875
876
877 if (n instanceof ReverseCandidate) {
878 Region r = p.regionList;
879 p.regionList = n.regionList;
880 n.regionList = r;
881 }
882
883 parents[pIdx] = p;
884 }
885 }
886
887 if (n instanceof ReverseCandidate) {
888
889
890 Candidate resultHead = null;
891 Candidate resultTail = null;
892
893 for (int pIdx = 0; pIdx < pCnt; pIdx++) {
894 Candidate p = parents[pIdx];
895 if (p == null)
896 continue;
897
898 if (p.regionList != null) {
899 Candidate r = p.copy(p.sourceCommit);
900 if (resultTail != null) {
901 resultTail.queueNext = r;
902 resultTail = r;
903 } else {
904 resultHead = r;
905 resultTail = r;
906 }
907 }
908
909 if (n.regionList != null) {
910 p.regionList = n.regionList.deepCopy();
911 push(p);
912 }
913 }
914
915 if (resultHead != null)
916 return result(resultHead);
917 return false;
918 }
919
920
921 for (int pIdx = 0; pIdx < pCnt; pIdx++) {
922 if (parents[pIdx] != null)
923 push(parents[pIdx]);
924 }
925
926 if (n.regionList != null)
927 return result(n);
928 return false;
929 }
930
931
932
933
934
935
936
937
938
939
940 public RevCommit getSourceCommit() {
941 return outCandidate.sourceCommit;
942 }
943
944
945
946
947
948
949 public PersonIdent getSourceAuthor() {
950 return outCandidate.getAuthor();
951 }
952
953
954
955
956
957
958 public PersonIdent getSourceCommitter() {
959 RevCommit c = getSourceCommit();
960 return c != null ? c.getCommitterIdent() : null;
961 }
962
963
964
965
966
967
968 public String getSourcePath() {
969 return outCandidate.sourcePath.getPath();
970 }
971
972
973
974
975
976
977 public int getRenameScore() {
978 return outCandidate.renameScore;
979 }
980
981
982
983
984
985
986
987
988
989
990 public int getSourceStart() {
991 return outRegion.sourceStart;
992 }
993
994
995
996
997
998
999
1000
1001
1002
1003 public int getSourceEnd() {
1004 Region r = outRegion;
1005 return r.sourceStart + r.length;
1006 }
1007
1008
1009
1010
1011
1012
1013
1014
1015 public int getResultStart() {
1016 return outRegion.resultStart;
1017 }
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029 public int getResultEnd() {
1030 Region r = outRegion;
1031 return r.resultStart + r.length;
1032 }
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043 public int getRegionLength() {
1044 return outRegion.length;
1045 }
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057 public RawText getSourceContents() {
1058 return outCandidate.sourceText;
1059 }
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073 public RawText getResultContents() throws IOException {
1074 return queue != null ? queue.sourceText : null;
1075 }
1076
1077
1078
1079
1080
1081
1082
1083
1084 @Override
1085 public void close() {
1086 revPool.close();
1087 queue = null;
1088 outCandidate = null;
1089 outRegion = null;
1090 }
1091
1092 private boolean find(RevCommit commit, PathFilter path) throws IOException {
1093 treeWalk.setFilter(path);
1094 treeWalk.reset(commit.getTree());
1095 if (treeWalk.next() && isFile(treeWalk.getRawMode(0))) {
1096 treeWalk.getObjectId(idBuf, 0);
1097 return true;
1098 }
1099 return false;
1100 }
1101
1102 private static final boolean isFile(int rawMode) {
1103 return (rawMode & TYPE_MASK) == TYPE_FILE;
1104 }
1105
1106 private DiffEntry findRename(RevCommit parent, RevCommit commit,
1107 PathFilter path) throws IOException {
1108 if (renameDetector == null)
1109 return null;
1110
1111 treeWalk.setFilter(TreeFilter.ANY_DIFF);
1112 treeWalk.reset(parent.getTree(), commit.getTree());
1113 List<DiffEntry> diffs = DiffEntry.scan(treeWalk);
1114 FilteredRenameDetector filteredRenameDetector = new FilteredRenameDetector(
1115 renameDetector);
1116 for (DiffEntry ent : filteredRenameDetector.compute(diffs, path)) {
1117 if (isRename(ent) && ent.getNewPath().equals(path.getPath()))
1118 return ent;
1119 }
1120 return null;
1121 }
1122
1123 private static boolean isRename(DiffEntry ent) {
1124 return ent.getChangeType() == ChangeType.RENAME
1125 || ent.getChangeType() == ChangeType.COPY;
1126 }
1127 }