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