comparison src/org/tmatesoft/hg/repo/HgBlameFacility.java @ 596:43cfa08ff3fd

HgBlameFacility refactoring: extract code to build file history that spans renames
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Thu, 02 May 2013 19:23:53 +0200
parents e49f9d9513fa
children
comparison
equal deleted inserted replaced
595:92c3ad9c2a51 596:43cfa08ff3fd
14 * the terms of a license other than GNU General Public License 14 * the terms of a license other than GNU General Public License
15 * contact TMate Software at support@hg4j.com 15 * contact TMate Software at support@hg4j.com
16 */ 16 */
17 package org.tmatesoft.hg.repo; 17 package org.tmatesoft.hg.repo;
18 18
19 import static org.tmatesoft.hg.core.HgIterateDirection.NewToOld;
20 import static org.tmatesoft.hg.core.HgIterateDirection.OldToNew; 19 import static org.tmatesoft.hg.core.HgIterateDirection.OldToNew;
21 import static org.tmatesoft.hg.repo.HgInternals.wrongRevisionIndex; 20 import static org.tmatesoft.hg.repo.HgInternals.wrongRevisionIndex;
22 import static org.tmatesoft.hg.repo.HgRepository.*; 21 import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION;
23 22 import static org.tmatesoft.hg.repo.HgRepository.TIP;
24 import java.util.Arrays;
25 import java.util.BitSet;
26 import java.util.Collections;
27 import java.util.LinkedList;
28 23
29 import org.tmatesoft.hg.core.HgCallbackTargetException; 24 import org.tmatesoft.hg.core.HgCallbackTargetException;
30 import org.tmatesoft.hg.core.HgIterateDirection; 25 import org.tmatesoft.hg.core.HgIterateDirection;
31 import org.tmatesoft.hg.core.Nodeid; 26 import org.tmatesoft.hg.core.Nodeid;
32 import org.tmatesoft.hg.internal.BlameHelper; 27 import org.tmatesoft.hg.internal.BlameHelper;
33 import org.tmatesoft.hg.internal.Callback; 28 import org.tmatesoft.hg.internal.Callback;
34 import org.tmatesoft.hg.internal.Experimental; 29 import org.tmatesoft.hg.internal.Experimental;
35 import org.tmatesoft.hg.internal.IntVector; 30 import org.tmatesoft.hg.internal.FileHistory;
31 import org.tmatesoft.hg.internal.FileRevisionHistoryChunk;
36 import org.tmatesoft.hg.util.Adaptable; 32 import org.tmatesoft.hg.util.Adaptable;
37 33
38 /** 34 /**
39 * Facility with diff/annotate functionality. 35 * Facility with diff/annotate functionality.
40 * 36 *
87 } 83 }
88 HgInternals.checkRevlogRange(changelogRevIndexStart, changelogRevIndexEnd, lastRevision); 84 HgInternals.checkRevlogRange(changelogRevIndexStart, changelogRevIndexEnd, lastRevision);
89 if (!df.exists()) { 85 if (!df.exists()) {
90 return; 86 return;
91 } 87 }
88 FileHistory fileHistory = new FileHistory(df, changelogRevIndexStart, changelogRevIndexEnd);
89 fileHistory.build();
92 BlameHelper bh = new BlameHelper(insp, 10); 90 BlameHelper bh = new BlameHelper(insp, 10);
93 HgDataFile currentFile = df; 91 for (FileRevisionHistoryChunk fhc : fileHistory.iterate(OldToNew)) {
94 int fileLastClogRevIndex = changelogRevIndexEnd; 92 // iteration order is not important here
95 FileRevisionHistoryChunk nextChunk = null; 93 bh.useFileUpTo(fhc.getFile(), fhc.getEndChangeset());
96 LinkedList<FileRevisionHistoryChunk> fileCompleteHistory = new LinkedList<FileRevisionHistoryChunk>(); 94 }
97 do {
98 FileRevisionHistoryChunk fileHistory = new FileRevisionHistoryChunk(currentFile);
99 fileHistory.init(fileLastClogRevIndex);
100 fileHistory.linkTo(nextChunk);
101 fileCompleteHistory.addFirst(fileHistory); // to get the list in old-to-new order
102 nextChunk = fileHistory;
103 bh.useFileUpTo(currentFile, fileLastClogRevIndex);
104 if (fileHistory.changeset(0) > changelogRevIndexStart && currentFile.isCopy()) {
105 // fileHistory.changeset(0) is the earliest revision we know about so far,
106 // once we get to revisions earlier than the requested start, stop digging.
107 // The reason there's NO == (i.e. not >=) because:
108 // (easy): once it's equal, we've reached our intended start
109 // (hard): if changelogRevIndexStart happens to be exact start of one of renames in the
110 // chain of renames (test-annotate2 repository, file1->file1a->file1b, i.e. points
111 // to the very start of file1a or file1 history), presence of == would get us to the next
112 // chunk and hence changed parents of present chunk's first element. Our annotate alg
113 // relies on parents only (i.e. knows nothing about 'last iteration element') to find out
114 // what to compare, and hence won't report all lines of 'last iteration element' (which is the
115 // first revision of the renamed file) as "added in this revision", leaving gaps in annotate
116 HgRepository repo = currentFile.getRepo();
117 Nodeid originLastRev = currentFile.getCopySourceRevision();
118 currentFile = repo.getFileNode(currentFile.getCopySourceName());
119 fileLastClogRevIndex = currentFile.getChangesetRevisionIndex(currentFile.getRevisionIndex(originLastRev));
120 // XXX perhaps, shall fail with meaningful exception if new file doesn't exist (.i/.d not found for whatever reason)
121 // or source revision is missing?
122 } else {
123 fileHistory.chopAtChangeset(changelogRevIndexStart);
124 currentFile = null; // stop iterating
125 }
126 } while (currentFile != null && fileLastClogRevIndex > changelogRevIndexStart);
127 // fileCompleteHistory is in (origin, intermediate target, ultimate target) order
128
129 int[] fileClogParentRevs = new int[2]; 95 int[] fileClogParentRevs = new int[2];
130 int[] fileParentRevs = new int[2]; 96 int[] fileParentRevs = new int[2];
131 if (iterateOrder == NewToOld) { 97 for (FileRevisionHistoryChunk fhc : fileHistory.iterate(iterateOrder)) {
132 Collections.reverse(fileCompleteHistory); 98 for (int fri : fhc.fileRevisions(iterateOrder)) {
133 } 99 int clogRevIndex = fhc.changeset(fri);
134 boolean shallFilterStart = changelogRevIndexStart != 0; // no reason if complete history is walked 100 // the way we built fileHistory ensures we won't walk past [changelogRevIndexStart..changelogRevIndexEnd]
135 for (FileRevisionHistoryChunk fileHistory : fileCompleteHistory) { 101 assert clogRevIndex >= changelogRevIndexStart;
136 for (int fri : fileHistory.fileRevisions(iterateOrder)) { 102 assert clogRevIndex <= changelogRevIndexEnd;
137 int clogRevIndex = fileHistory.changeset(fri); 103 fhc.fillFileParents(fri, fileParentRevs);
138 if (shallFilterStart) { 104 fhc.fillCsetParents(fri, fileClogParentRevs);
139 if (iterateOrder == NewToOld) {
140 // clogRevIndex decreases
141 if (clogRevIndex < changelogRevIndexStart) {
142 break;
143 }
144 // fall-through, clogRevIndex is in the [start..end] range
145 } else { // old to new
146 // the way we built fileHistory ensures we won't walk past changelogRevIndexEnd
147 // here we ensure we start from the right one, the one indicated with changelogRevIndexStart
148 if (clogRevIndex < changelogRevIndexStart) {
149 continue;
150 } else {
151 shallFilterStart = false; // once boundary is crossed, no need to check
152 // fall-through
153 }
154 }
155 }
156 fileHistory.fillFileParents(fri, fileParentRevs);
157 fileHistory.fillCsetParents(fri, fileClogParentRevs);
158 bh.annotateChange(fri, clogRevIndex, fileParentRevs, fileClogParentRevs); 105 bh.annotateChange(fri, clogRevIndex, fileParentRevs, fileClogParentRevs);
159 } 106 }
160 } 107 }
161 } 108 }
162 109
193 public interface Inspector { 140 public interface Inspector {
194 void same(EqualBlock block) throws HgCallbackTargetException; 141 void same(EqualBlock block) throws HgCallbackTargetException;
195 void added(AddBlock block) throws HgCallbackTargetException; 142 void added(AddBlock block) throws HgCallbackTargetException;
196 void changed(ChangeBlock block) throws HgCallbackTargetException; 143 void changed(ChangeBlock block) throws HgCallbackTargetException;
197 void deleted(DeleteBlock block) throws HgCallbackTargetException; 144 void deleted(DeleteBlock block) throws HgCallbackTargetException;
198 }
199
200 /**
201 * No need to keep "Block" prefix as long as there's only one {@link Inspector}
202 */
203 @Deprecated
204 public interface BlockInspector extends Inspector {
205 } 145 }
206 146
207 /** 147 /**
208 * Represents content of a block, either as a sequence of bytes or a 148 * Represents content of a block, either as a sequence of bytes or a
209 * sequence of smaller blocks (lines), if appropriate (according to usage context). 149 * sequence of smaller blocks (lines), if appropriate (according to usage context).
351 291
352 private static int fileRevIndex(HgDataFile df, int csetRevIndex) { 292 private static int fileRevIndex(HgDataFile df, int csetRevIndex) {
353 Nodeid fileRev = df.getRepo().getManifest().getFileRevision(csetRevIndex, df.getPath()); 293 Nodeid fileRev = df.getRepo().getManifest().getFileRevision(csetRevIndex, df.getPath());
354 return df.getRevisionIndex(fileRev); 294 return df.getRevisionIndex(fileRev);
355 } 295 }
356
357 private static class FileRevisionHistoryChunk {
358 private final HgDataFile df;
359 // change ancestry, sequence of file revisions
360 private IntVector fileRevsToVisit;
361 // parent pairs of complete file history
362 private IntVector fileParentRevs;
363 // map file revision to changelog revision (sparse array, only file revisions to visit are set)
364 private int[] file2changelog;
365 private int originChangelogRev = BAD_REVISION, originFileRev = BAD_REVISION;
366
367 public FileRevisionHistoryChunk(HgDataFile file) {
368 df = file;
369 }
370
371 public void init(int changelogRevisionIndex) {
372 // XXX df.indexWalk(0, fileRevIndex, ) might be more effective
373 int fileRevIndex = fileRevIndex(df, changelogRevisionIndex);
374 int[] fileRevParents = new int[2];
375 fileParentRevs = new IntVector((fileRevIndex+1) * 2, 0);
376 fileParentRevs.add(NO_REVISION, NO_REVISION); // parents of fileRevIndex == 0
377 for (int i = 1; i <= fileRevIndex; i++) {
378 df.parents(i, fileRevParents, null, null);
379 fileParentRevs.add(fileRevParents[0], fileRevParents[1]);
380 }
381 // fileRevsToVisit keep file change ancestry from new to old
382 fileRevsToVisit = new IntVector(fileRevIndex + 1, 0);
383 // keep map of file revision to changelog revision
384 file2changelog = new int[fileRevIndex+1];
385 // only elements worth visit would get mapped, so there would be unfilled areas in the file2changelog,
386 // prevent from error (make it explicit) by bad value
387 Arrays.fill(file2changelog, BAD_REVISION);
388 LinkedList<Integer> queue = new LinkedList<Integer>();
389 BitSet seen = new BitSet(fileRevIndex + 1);
390 queue.add(fileRevIndex);
391 do {
392 int x = queue.removeFirst();
393 if (seen.get(x)) {
394 continue;
395 }
396 seen.set(x);
397 fileRevsToVisit.add(x);
398 file2changelog[x] = df.getChangesetRevisionIndex(x);
399 int p1 = fileParentRevs.get(2*x);
400 int p2 = fileParentRevs.get(2*x + 1);
401 if (p1 != NO_REVISION) {
402 queue.addLast(p1);
403 }
404 if (p2 != NO_REVISION) {
405 queue.addLast(p2);
406 }
407 } while (!queue.isEmpty());
408 // make sure no child is processed before we handled all (grand-)parents of the element
409 fileRevsToVisit.sort(false);
410 }
411
412 public void linkTo(FileRevisionHistoryChunk target) {
413 // assume that target.init() has been called already
414 if (target == null) {
415 return;
416 }
417 target.originFileRev = fileRevsToVisit.get(0); // files to visit are new to old
418 target.originChangelogRev = changeset(target.originFileRev);
419 }
420
421 /**
422 * Mark revision closest(ceil) to specified as the very first one (no parents)
423 */
424 public void chopAtChangeset(int firstChangelogRevOfInterest) {
425 if (firstChangelogRevOfInterest == 0) {
426 return; // nothing to do
427 }
428 int i = 0, x = fileRevsToVisit.size(), fileRev = BAD_REVISION;
429 // fileRevsToVisit is new to old, greater numbers to smaller
430 while (i < x && changeset(fileRev = fileRevsToVisit.get(i)) >= firstChangelogRevOfInterest) {
431 i++;
432 }
433 assert fileRev != BAD_REVISION; // there's at least 1 revision in fileRevsToVisit
434 if (i == x && changeset(fileRev) != firstChangelogRevOfInterest) {
435 assert false : "Requested changeset shall belong to the chunk";
436 return;
437 }
438 fileRevsToVisit.trimTo(i); // no need to iterate more
439 // pretend fileRev got no parents
440 fileParentRevs.set(fileRev * 2, NO_REVISION);
441 fileParentRevs.set(fileRev, NO_REVISION);
442 }
443
444 public int[] fileRevisions(HgIterateDirection iterateOrder) {
445 // fileRevsToVisit is { r10, r7, r6, r5, r0 }, new to old
446 int[] rv = fileRevsToVisit.toArray();
447 if (iterateOrder == OldToNew) {
448 // reverse return value
449 for (int a = 0, b = rv.length-1; a < b; a++, b--) {
450 int t = rv[b];
451 rv[b] = rv[a];
452 rv[a] = t;
453 }
454 }
455 return rv;
456 }
457
458 public int changeset(int fileRevIndex) {
459 return file2changelog[fileRevIndex];
460 }
461
462 public void fillFileParents(int fileRevIndex, int[] fileParents) {
463 if (fileRevIndex == 0 && originFileRev != BAD_REVISION) {
464 // this chunk continues another file
465 assert originFileRev != NO_REVISION;
466 fileParents[0] = originFileRev;
467 fileParents[1] = NO_REVISION;
468 return;
469 }
470 fileParents[0] = fileParentRevs.get(fileRevIndex * 2);
471 fileParents[1] = fileParentRevs.get(fileRevIndex * 2 + 1);
472 }
473
474 public void fillCsetParents(int fileRevIndex, int[] csetParents) {
475 if (fileRevIndex == 0 && originFileRev != BAD_REVISION) {
476 assert originFileRev != NO_REVISION;
477 csetParents[0] = originChangelogRev;
478 csetParents[1] = NO_REVISION; // I wonder if possible to start a copy with two parents?
479 return;
480 }
481 int fp1 = fileParentRevs.get(fileRevIndex * 2);
482 int fp2 = fileParentRevs.get(fileRevIndex * 2 + 1);
483 csetParents[0] = fp1 == NO_REVISION ? NO_REVISION : changeset(fp1);
484 csetParents[1] = fp2 == NO_REVISION ? NO_REVISION : changeset(fp2);
485 }
486 }
487 } 296 }