comparison src/org/tmatesoft/hg/core/HgLogCommand.java @ 518:0d5e1ea7955e

Tests for HgLogCommand#execute(HgChangesetHandler) with various combination of follow renames and ancestry
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Thu, 20 Dec 2012 19:55:45 +0100
parents 9922d1f7cb2a
children 1ee452f31187
comparison
equal deleted inserted replaced
517:9922d1f7cb2a 518:0d5e1ea7955e
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.core; 17 package org.tmatesoft.hg.core;
18 18
19 import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION;
19 import static org.tmatesoft.hg.repo.HgRepository.TIP; 20 import static org.tmatesoft.hg.repo.HgRepository.TIP;
20 import static org.tmatesoft.hg.util.LogFacility.Severity.Error; 21 import static org.tmatesoft.hg.util.LogFacility.Severity.Error;
21 22
22 import java.util.ArrayList; 23 import java.util.ArrayList;
23 import java.util.Arrays; 24 import java.util.Arrays;
33 import java.util.Set; 34 import java.util.Set;
34 import java.util.TreeSet; 35 import java.util.TreeSet;
35 36
36 import org.tmatesoft.hg.internal.IntMap; 37 import org.tmatesoft.hg.internal.IntMap;
37 import org.tmatesoft.hg.internal.IntVector; 38 import org.tmatesoft.hg.internal.IntVector;
39 import org.tmatesoft.hg.internal.Lifecycle;
38 import org.tmatesoft.hg.repo.HgChangelog; 40 import org.tmatesoft.hg.repo.HgChangelog;
39 import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; 41 import org.tmatesoft.hg.repo.HgChangelog.RawChangeset;
40 import org.tmatesoft.hg.repo.HgDataFile; 42 import org.tmatesoft.hg.repo.HgDataFile;
41 import org.tmatesoft.hg.repo.HgInvalidControlFileException; 43 import org.tmatesoft.hg.repo.HgInvalidControlFileException;
42 import org.tmatesoft.hg.repo.HgInvalidRevisionException; 44 import org.tmatesoft.hg.repo.HgInvalidRevisionException;
63 * Not thread-safe (each thread has to use own {@link HgLogCommand} instance). 65 * Not thread-safe (each thread has to use own {@link HgLogCommand} instance).
64 * 66 *
65 * @author Artem Tikhomirov 67 * @author Artem Tikhomirov
66 * @author TMate Software Ltd. 68 * @author TMate Software Ltd.
67 */ 69 */
68 public class HgLogCommand extends HgAbstractCommand<HgLogCommand> implements HgChangelog.Inspector { 70 public class HgLogCommand extends HgAbstractCommand<HgLogCommand> {
69 71
70 private final HgRepository repo; 72 private final HgRepository repo;
71 private Set<String> users; 73 private Set<String> users;
72 private Set<String> branches; 74 private Set<String> branches;
73 private int limit = 0, count = 0; 75 private int limit = 0, count = 0;
274 throw new IllegalArgumentException(); 276 throw new IllegalArgumentException();
275 } 277 }
276 if (csetTransform != null) { 278 if (csetTransform != null) {
277 throw new ConcurrentModificationException(); 279 throw new ConcurrentModificationException();
278 } 280 }
281 final int lastCset = endRev == TIP ? repo.getChangelog().getLastRevision() : endRev;
282 // XXX pretty much like HgInternals.checkRevlogRange
283 if (lastCset < 0 || lastCset > repo.getChangelog().getLastRevision()) {
284 throw new HgBadArgumentException(String.format("Bad value %d for end revision", endRev), null);
285 }
286 if (startRev < 0 || startRev > lastCset) {
287 throw new HgBadArgumentException(String.format("Bad value %d for start revision for range [%1$d..%d]", startRev, lastCset), null);
288 }
279 final ProgressSupport progressHelper = getProgressSupport(handler); 289 final ProgressSupport progressHelper = getProgressSupport(handler);
280 try { 290 try {
281 count = 0; 291 count = 0;
282 HgParentChildMap<HgChangelog> pw = getParentHelper(file == null); // leave it uninitialized unless we iterate whole repo 292 HgParentChildMap<HgChangelog> pw = getParentHelper(file == null); // leave it uninitialized unless we iterate whole repo
283 // ChangesetTransfrom creates a blank PathPool, and #file(String, boolean) above 293 // ChangesetTransfrom creates a blank PathPool, and #file(String, boolean) above
284 // may utilize it as well. CommandContext? How about StatusCollector there as well? 294 // may utilize it as well. CommandContext? How about StatusCollector there as well?
285 csetTransform = new ChangesetTransformer(repo, handler, pw, progressHelper, getCancelSupport(handler, true)); 295 csetTransform = new ChangesetTransformer(repo, handler, pw, progressHelper, getCancelSupport(handler, true));
296 FilteringInspector filterInsp = new FilteringInspector();
297 filterInsp.changesets(startRev, lastCset);
286 if (file == null) { 298 if (file == null) {
287 progressHelper.start(endRev - startRev + 1); 299 progressHelper.start(endRev - startRev + 1);
288 repo.getChangelog().range(startRev, endRev, this); 300 repo.getChangelog().range(startRev, endRev, filterInsp);
289 csetTransform.checkFailure(); 301 csetTransform.checkFailure();
290 } else { 302 } else {
303 final HgFileRenameHandlerMixin withCopyHandler = Adaptable.Factory.getAdapter(handler, HgFileRenameHandlerMixin.class, null);
304 List<Pair<HgDataFile, Nodeid>> fileRenames = buildFileRenamesQueue();
291 progressHelper.start(-1/*XXX enum const, or a dedicated method startUnspecified(). How about startAtLeast(int)?*/); 305 progressHelper.start(-1/*XXX enum const, or a dedicated method startUnspecified(). How about startAtLeast(int)?*/);
292 HgDataFile fileNode = repo.getFileNode(file); 306
293 if (!fileNode.exists()) { 307 for (int nameIndex = 0, fileRenamesSize = fileRenames.size(); nameIndex < fileRenamesSize; nameIndex++) {
294 throw new HgPathNotFoundException(String.format("File %s not found in the repository", file), file); 308 Pair<HgDataFile, Nodeid> curRename = fileRenames.get(nameIndex);
295 } 309 HgDataFile fileNode = curRename.first();
296 // FIXME startRev and endRev ARE CHANGESET REVISIONS, not that of FILE!!! 310 if (followAncestry) {
297 fileNode.history(startRev, endRev, this); 311 TreeBuildInspector treeBuilder = new TreeBuildInspector(followAncestry);
298 csetTransform.checkFailure(); 312 List<HistoryNode> fileAncestry = treeBuilder.go(fileNode, curRename.second());
299 final HgFileRenameHandlerMixin withCopyHandler = Adaptable.Factory.getAdapter(handler, HgFileRenameHandlerMixin.class, null); 313 int[] commitRevisions = narrowChangesetRange(treeBuilder.getCommitRevisions(), startRev, lastCset);
300 if (fileNode.isCopy()) { 314 if (iterateDirection == IterateDirection.FromOldToNew) {
301 // even if we do not follow history, report file rename 315 repo.getChangelog().range(filterInsp, commitRevisions);
302 do { 316 } else {
303 if (withCopyHandler != null) { 317 assert iterateDirection == IterateDirection.FromNewToOld;
304 HgFileRevision src = new HgFileRevision(repo, fileNode.getCopySourceRevision(), null, fileNode.getCopySourceName()); 318 // visit one by one in the opposite direction
305 HgFileRevision dst = new HgFileRevision(repo, fileNode.getRevision(0), null, fileNode.getPath(), src.getPath()); 319 for (int i = commitRevisions.length-1; i >= 0; i--) {
306 withCopyHandler.copy(src, dst); 320 int csetWithFileChange = commitRevisions[i];
321 repo.getChangelog().range(csetWithFileChange, csetWithFileChange, filterInsp);
322 }
307 } 323 }
308 if (limit > 0 && count >= limit) { 324 } else {
309 // if limit reach, follow is useless. 325 // report complete file history (XXX may narrow range with [startRev, endRev], but need to go from file rev to link rev)
310 break; 326 int fileStartRev = 0; //fileNode.getChangesetRevisionIndex(0) >= startRev
327 int fileEndRev = fileNode.getLastRevision();
328 fileNode.history(fileStartRev, fileEndRev, filterInsp);
329 csetTransform.checkFailure();
330 }
331 if (followRenames && withCopyHandler != null && nameIndex + 1 < fileRenamesSize) {
332 Pair<HgDataFile, Nodeid> nextRename = fileRenames.get(nameIndex+1);
333 HgFileRevision src, dst;
334 // A -> B
335 if (iterateDirection == IterateDirection.FromOldToNew) {
336 // curRename: A, nextRename: B
337 src = new HgFileRevision(fileNode, curRename.second(), null);
338 dst = new HgFileRevision(nextRename.first(), nextRename.first().getRevision(0), src.getPath());
339 } else {
340 assert iterateDirection == IterateDirection.FromNewToOld;
341 // curRename: B, nextRename: A
342 src = new HgFileRevision(nextRename.first(), nextRename.second(), null);
343 dst = new HgFileRevision(fileNode, fileNode.getRevision(0), src.getPath());
311 } 344 }
312 if (followRenames) { 345 withCopyHandler.copy(src, dst);
313 fileNode = repo.getFileNode(fileNode.getCopySourceName()); 346 }
314 fileNode.history(this); 347 } // for renames
315 csetTransform.checkFailure(); 348 } // file != null
316 }
317 } while (followRenames && fileNode.isCopy());
318 }
319 }
320 } catch (HgRuntimeException ex) { 349 } catch (HgRuntimeException ex) {
321 throw new HgLibraryFailureException(ex); 350 throw new HgLibraryFailureException(ex);
322 } finally { 351 } finally {
323 csetTransform = null; 352 csetTransform = null;
324 progressHelper.done(); 353 progressHelper.done();
325 } 354 }
355 }
356
357 // public static void main(String[] args) {
358 // int[] r = new int[] {17, 19, 21, 23, 25, 29};
359 // System.out.println(Arrays.toString(narrowChangesetRange(r, 0, 45)));
360 // System.out.println(Arrays.toString(narrowChangesetRange(r, 0, 25)));
361 // System.out.println(Arrays.toString(narrowChangesetRange(r, 5, 26)));
362 // System.out.println(Arrays.toString(narrowChangesetRange(r, 20, 26)));
363 // System.out.println(Arrays.toString(narrowChangesetRange(r, 26, 28)));
364 // }
365
366 private static int[] narrowChangesetRange(int[] csetRange, int startCset, int endCset) {
367 int lastInRange = csetRange[csetRange.length-1];
368 assert csetRange.length < 2 || csetRange[0] < lastInRange; // sorted
369 assert startCset >= 0 && startCset <= endCset;
370 if (csetRange[0] >= startCset && lastInRange <= endCset) {
371 // completely fits in
372 return csetRange;
373 }
374 if (csetRange[0] > endCset || lastInRange < startCset) {
375 return new int[0]; // trivial
376 }
377 int i = 0;
378 while (i < csetRange.length && csetRange[i] < startCset) {
379 i++;
380 }
381 int j = csetRange.length - 1;
382 while (j > i && csetRange[j] > endCset) {
383 j--;
384 }
385 if (i == j) {
386 // no values in csetRange fit into [startCset, endCset]
387 return new int[0];
388 }
389 int[] rv = new int[j-i+1];
390 System.arraycopy(csetRange, i, rv, 0, rv.length);
391 return rv;
326 } 392 }
327 393
328 /** 394 /**
329 * Tree-wise iteration of a file history, with handy access to parent-child relations between changesets. 395 * Tree-wise iteration of a file history, with handy access to parent-child relations between changesets.
330 * When file history is being followed, handler may additionally implement {@link HgFileRenameHandlerMixin} 396 * When file history is being followed, handler may additionally implement {@link HgFileRenameHandlerMixin}
421 * TODO may use HgFileRevision (after some refactoring to accept HgDataFile and Nodeid) instead of Pair 487 * TODO may use HgFileRevision (after some refactoring to accept HgDataFile and Nodeid) instead of Pair
422 * and possibly reuse this functionality 488 * and possibly reuse this functionality
423 * 489 *
424 * @return list of file renames, ordered with respect to {@link #iterateDirection} 490 * @return list of file renames, ordered with respect to {@link #iterateDirection}
425 */ 491 */
426 private List<Pair<HgDataFile, Nodeid>> buildFileRenamesQueue() { 492 private List<Pair<HgDataFile, Nodeid>> buildFileRenamesQueue() throws HgPathNotFoundException {
427 LinkedList<Pair<HgDataFile, Nodeid>> rv = new LinkedList<Pair<HgDataFile, Nodeid>>(); 493 LinkedList<Pair<HgDataFile, Nodeid>> rv = new LinkedList<Pair<HgDataFile, Nodeid>>();
428 Nodeid startRev = null; 494 Nodeid startRev = null;
429 HgDataFile fileNode = repo.getFileNode(file); 495 HgDataFile fileNode = repo.getFileNode(file);
496 if (!fileNode.exists()) {
497 throw new HgPathNotFoundException(String.format("File %s not found in the repository", file), file);
498 }
430 if (followAncestry) { 499 if (followAncestry) {
431 // TODO subject to dedicated method either in HgRepository (getWorkingCopyParentRevisionIndex) 500 // TODO subject to dedicated method either in HgRepository (getWorkingCopyParentRevisionIndex)
432 // or in the HgDataFile (getWorkingCopyOriginRevision) 501 // or in the HgDataFile (getWorkingCopyOriginRevision)
433 Nodeid wdParentChangeset = repo.getWorkingCopyParents().first(); 502 Nodeid wdParentChangeset = repo.getWorkingCopyParents().first();
434 if (!wdParentChangeset.isNull()) { 503 if (!wdParentChangeset.isNull()) {
749 } 818 }
750 819
751 820
752 // 821 //
753 822
754 public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { 823 private class FilteringInspector implements HgChangelog.Inspector, Lifecycle {
755 if (limit > 0 && count >= limit) { 824
756 return; 825 private Callback lifecycle;
757 } 826 private int firstCset = BAD_REVISION, lastCset = BAD_REVISION;
758 if (branches != null && !branches.contains(cset.branch())) { 827
759 return; 828 // limit to changesets in this range only
760 } 829 public void changesets(int start, int end) {
761 if (users != null) { 830 firstCset = start;
762 String csetUser = cset.user().toLowerCase(); 831 lastCset = end;
763 boolean found = false; 832 }
764 for (String u : users) { 833
765 if (csetUser.indexOf(u) != -1) { 834 public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) {
766 found = true; 835 if (limit > 0 && count >= limit) {
767 break;
768 }
769 }
770 if (!found) {
771 return; 836 return;
772 } 837 }
773 } 838 // XXX may benefit from optional interface with #isInterested(int csetRev) - to avoid
774 if (date != null) { 839 // RawChangeset instantiation
775 // TODO post-1.0 implement date support for log 840 if (firstCset != BAD_REVISION && revisionNumber < firstCset) {
776 } 841 return;
777 count++; 842 }
778 csetTransform.next(revisionNumber, nodeid, cset); 843 if (lastCset != BAD_REVISION && revisionNumber > lastCset) {
844 return;
845 }
846 if (branches != null && !branches.contains(cset.branch())) {
847 return;
848 }
849 if (users != null) {
850 String csetUser = cset.user().toLowerCase();
851 boolean found = false;
852 for (String u : users) {
853 if (csetUser.indexOf(u) != -1) {
854 found = true;
855 break;
856 }
857 }
858 if (!found) {
859 return;
860 }
861 }
862 if (date != null) {
863 // TODO post-1.0 implement date support for log
864 }
865 csetTransform.next(revisionNumber, nodeid, cset);
866 if (++count >= limit) {
867 if (lifecycle != null) { // FIXME support Lifecycle delegation
868 lifecycle.stop();
869 }
870 }
871 }
872
873 public void start(int count, Callback callback, Object token) {
874 lifecycle = callback;
875 }
876
877 public void finish(Object token) {
878 }
779 } 879 }
780 880
781 private HgParentChildMap<HgChangelog> getParentHelper(boolean create) throws HgInvalidControlFileException { 881 private HgParentChildMap<HgChangelog> getParentHelper(boolean create) throws HgInvalidControlFileException {
782 if (parentHelper == null && create) { 882 if (parentHelper == null && create) {
783 parentHelper = new HgParentChildMap<HgChangelog>(repo.getChangelog()); 883 parentHelper = new HgParentChildMap<HgChangelog>(repo.getChangelog());