comparison src/org/tmatesoft/hg/core/HgLogCommand.java @ 508:ca5202afea90

Support follow history option when walking file history tree
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Wed, 12 Dec 2012 20:52:10 +0100
parents a6435c1a42d0
children a30e74dca193
comparison
equal deleted inserted replaced
507:a6435c1a42d0 508:ca5202afea90
298 throw new ConcurrentModificationException(); 298 throw new ConcurrentModificationException();
299 } 299 }
300 if (file == null) { 300 if (file == null) {
301 throw new IllegalArgumentException("History tree is supported for files only (at least now), please specify file"); 301 throw new IllegalArgumentException("History tree is supported for files only (at least now), please specify file");
302 } 302 }
303 if (followHistory) {
304 throw new UnsupportedOperationException("Can't follow file history when building tree (yet?)");
305 }
306 class TreeBuildInspector implements HgChangelog.ParentInspector, HgChangelog.RevisionInspector {
307 HistoryNode[] completeHistory;
308 int[] commitRevisions;
309
310 public void next(int revisionNumber, Nodeid revision, int linkedRevision) {
311 commitRevisions[revisionNumber] = linkedRevision;
312 }
313
314 public void next(int revisionNumber, Nodeid revision, int parent1, int parent2, Nodeid nidParent1, Nodeid nidParent2) {
315 HistoryNode p1 = null, p2 = null;
316 if (parent1 != -1) {
317 p1 = completeHistory[parent1];
318 }
319 if (parent2!= -1) {
320 p2 = completeHistory[parent2];
321 }
322 completeHistory[revisionNumber] = new HistoryNode(commitRevisions[revisionNumber], revision, p1, p2);
323 }
324
325 HistoryNode[] go(HgDataFile fileNode) throws HgInvalidControlFileException {
326 completeHistory = new HistoryNode[fileNode.getRevisionCount()];
327 commitRevisions = new int[completeHistory.length];
328 fileNode.indexWalk(0, TIP, this);
329 return completeHistory;
330 }
331 };
332 final ProgressSupport progressHelper = getProgressSupport(handler); 303 final ProgressSupport progressHelper = getProgressSupport(handler);
333 final CancelSupport cancelHelper = getCancelSupport(handler, true); 304 final CancelSupport cancelHelper = getCancelSupport(handler, true);
334 305
335 LinkedList<HgDataFile> fileRenamesQueue = buildFileRenamesQueue(); 306 // builds tree of nodes according to parents in file's revlog
307 final TreeBuildInspector treeBuildInspector = new TreeBuildInspector(followHistory);
308
309 // most recent file is first in the queue
310 LinkedList<Pair<HgDataFile, Nodeid>> fileRenamesQueue = buildFileRenamesQueue();
336 progressHelper.start(4 * fileRenamesQueue.size()); 311 progressHelper.start(4 * fileRenamesQueue.size());
337 do { 312 do {
338 HgDataFile fileNode = fileRenamesQueue.removeLast(); 313 // to maintain iteration order from older to newer, take oldest name (in case of renames) first
314 Pair<HgDataFile, Nodeid> renameInfo = fileRenamesQueue.removeLast();
339 cancelHelper.checkCancelled(); 315 cancelHelper.checkCancelled();
340 // build tree of nodes according to parents in file's revlog 316 HgDataFile fileNode = renameInfo.first();
341 final TreeBuildInspector treeBuildInspector = new TreeBuildInspector(); 317 Nodeid fileLastRevToVisit = null;
342 final HistoryNode[] completeHistory = treeBuildInspector.go(fileNode); 318 if (followHistory) {
319 fileLastRevToVisit = renameInfo.second();
320 if (fileLastRevToVisit == null) {
321 // only recent file name should not have a changeset when rename has happened.
322 assert fileRenamesQueue.isEmpty();
323 // TODO subject to dedicated method either in HgRepository (getWorkingCopyParentRevisionIndex)
324 // or in the HgDataFile (getWorkingCopyOriginRevision)
325 Nodeid wdParentChangeset = repo.getWorkingCopyParents().first();
326 if (!wdParentChangeset.isNull()) {
327 int wdParentRevIndex = repo.getChangelog().getRevisionIndex(wdParentChangeset);
328 fileLastRevToVisit = repo.getManifest().getFileRevision(wdParentRevIndex, fileNode.getPath());
329 }
330 // else fall-through, assume lastRevision() is ok here
331 }
332 }
333 int fileLastRevIndexToVisit = fileLastRevToVisit == null ? fileNode.getLastRevision() : fileNode.getRevisionIndex(fileLastRevToVisit);
334 final HistoryNode[] changeHistory = treeBuildInspector.go(fileNode, fileLastRevIndexToVisit);
335 int[] commitRevisions = treeBuildInspector.getCommitRevisions();
336 assert changeHistory.length == commitRevisions.length;
343 progressHelper.worked(1); 337 progressHelper.worked(1);
344 cancelHelper.checkCancelled(); 338 cancelHelper.checkCancelled();
345 ElementImpl ei = new ElementImpl(treeBuildInspector.commitRevisions.length); 339 ElementImpl ei = new ElementImpl(commitRevisions.length);
346 final ProgressSupport ph2; 340 final ProgressSupport ph2;
347 if (treeBuildInspector.commitRevisions.length < 100 /*XXX is it really worth it? */) { 341 if (commitRevisions.length < 100 /*XXX is it really worth it? */) {
342 // read bunch of changesets at once and cache 'em
348 ei.initTransform(); 343 ei.initTransform();
349 repo.getChangelog().range(ei, treeBuildInspector.commitRevisions); 344 repo.getChangelog().range(ei, commitRevisions);
350 progressHelper.worked(1); 345 progressHelper.worked(1);
351 ph2 = new ProgressSupport.Sub(progressHelper, 2); 346 ph2 = new ProgressSupport.Sub(progressHelper, 2);
352 } else { 347 } else {
353 ph2 = new ProgressSupport.Sub(progressHelper, 3); 348 ph2 = new ProgressSupport.Sub(progressHelper, 3);
354 } 349 }
355 ph2.start(completeHistory.length); 350 ph2.start(changeHistory.length);
356 // XXX shall sort completeHistory according to changeset numbers? 351 // XXX shall sort completeHistory according to changeset numbers?
357 for (int i = 0; i < completeHistory.length; i++ ) { 352 for (int i = 0; i < changeHistory.length; i++ ) {
358 final HistoryNode n = completeHistory[i]; 353 final HistoryNode n = changeHistory[i];
359 handler.treeElement(ei.init(n)); 354 handler.treeElement(ei.init(n));
360 ph2.worked(1); 355 ph2.worked(1);
361 cancelHelper.checkCancelled(); 356 cancelHelper.checkCancelled();
362 } 357 }
363 } while (!fileRenamesQueue.isEmpty()); 358 } while (!fileRenamesQueue.isEmpty());
364 progressHelper.done(); 359 progressHelper.done();
365 } 360 }
366 361
367 /** 362 /**
368 * Follows file renames and build a list of all corresponding file nodes. If {@link #followHistory} is <code>false</code>, 363 * Follows file renames and build a list of all corresponding file nodes and revisions they were
369 * the list contains one element only, file node with the name of the file as it was specified by the user. 364 * copied/renamed/branched at (IOW, their latest revision to look at).
365 *
366 * If {@link #followHistory} is <code>false</code>, the list contains one element only,
367 * file node with the name of the file as it was specified by the user.
368 *
369 * For the most recent file revision is null.
370 *
371 * TODO may use HgFileRevision (after some refactoring to accept HgDataFile and Nodeid) instead of Pair
372 * and possibly reuse this functionality
370 * 373 *
371 * @return list of file renames, with most recent file first 374 * @return list of file renames, with most recent file first
372 */ 375 */
373 private LinkedList<HgDataFile> buildFileRenamesQueue() { 376 private LinkedList<Pair<HgDataFile, Nodeid>> buildFileRenamesQueue() {
374 LinkedList<HgDataFile> rv = new LinkedList<HgDataFile>(); 377 LinkedList<Pair<HgDataFile, Nodeid>> rv = new LinkedList<Pair<HgDataFile, Nodeid>>();
375 if (!followHistory) { 378 if (!followHistory) {
376 rv.add(repo.getFileNode(file)); 379 rv.add(new Pair<HgDataFile, Nodeid>(repo.getFileNode(file), null));
377 return rv; 380 return rv;
378 } 381 }
379 HgDataFile fileNode;
380 Path fp = file; 382 Path fp = file;
383 Nodeid copyRev = null;
381 boolean isCopy; 384 boolean isCopy;
382 do { 385 do {
383 fileNode = repo.getFileNode(fp); 386 HgDataFile fileNode = repo.getFileNode(fp);
384 rv.addLast(fileNode); 387 rv.addLast(new Pair<HgDataFile, Nodeid>(fileNode, copyRev));
385 if (isCopy = fileNode.isCopy()) { 388 if (isCopy = fileNode.isCopy()) {
386 fp = fileNode.getCopySourceName(); 389 fp = fileNode.getCopySourceName();
390 copyRev = fileNode.getCopySourceRevision();
387 } 391 }
388 } while (isCopy); 392 } while (isCopy);
389 return rv; 393 return rv;
390 } 394 }
395
396 private static class TreeBuildInspector implements HgChangelog.ParentInspector, HgChangelog.RevisionInspector {
397 private final boolean followAncestry;
398
399 private HistoryNode[] completeHistory;
400 private int[] commitRevisions;
401
402 TreeBuildInspector(boolean _followAncestry) {
403 followAncestry = _followAncestry;
404 }
405
406 public void next(int revisionNumber, Nodeid revision, int linkedRevision) {
407 commitRevisions[revisionNumber] = linkedRevision;
408 }
409
410 public void next(int revisionNumber, Nodeid revision, int parent1, int parent2, Nodeid nidParent1, Nodeid nidParent2) {
411 HistoryNode p1 = null, p2 = null;
412 if (parent1 != -1) {
413 p1 = completeHistory[parent1];
414 }
415 if (parent2!= -1) {
416 p2 = completeHistory[parent2];
417 }
418 completeHistory[revisionNumber] = new HistoryNode(commitRevisions[revisionNumber], revision, p1, p2);
419 }
420
421 /**
422 * Builds history of file changes (in natural order, from oldest to newest) up to (and including) file revision specified.
423 * If {@link TreeBuildInspector} follows ancestry, only elements that are on the line of ancestry of the revision at
424 * lastRevisionIndex would be included.
425 */
426 HistoryNode[] go(HgDataFile fileNode, int lastRevisionIndex) throws HgInvalidControlFileException {
427 completeHistory = new HistoryNode[lastRevisionIndex+1];
428 commitRevisions = new int[completeHistory.length];
429 fileNode.indexWalk(0, lastRevisionIndex, this);
430 if (!followAncestry) {
431 return completeHistory;
432 }
433 /*
434 * Changesets:
435 * o <-- cset from working dir parent (as in dirstate), file not changed (file revision recorded points to that from A)
436 * | x <-- revision with file changed (B')
437 * x / <-- revision with file changed (A)
438 * | x <-- revision with file changed (B)
439 * |/
440 * o <-- another changeset, where file wasn't changed
441 * |
442 * x <-- revision with file changed (C)
443 *
444 * File history: B', A, B, C
445 *
446 * When "follow", SHALL NOT report B and B', but A and C
447 */
448 // strippedHistory: only those HistoryNodes from completeHistory that are on the same
449 // line of descendant, in order from older to newer
450 LinkedList<HistoryNode> strippedHistoryList = new LinkedList<HistoryNode>();
451 LinkedList<HistoryNode> queue = new LinkedList<HistoryNode>();
452 // look for ancestors of the selected history node
453 queue.add(completeHistory[lastRevisionIndex]);
454 do {
455 HistoryNode withFileChange = queue.removeFirst();
456 if (withFileChange.children != null) {
457 withFileChange.children.retainAll(strippedHistoryList);
458 }
459 strippedHistoryList.addFirst(withFileChange);
460 if (withFileChange.parent1 != null) {
461 queue.addLast(withFileChange.parent1);
462 }
463 if (withFileChange.parent2 != null) {
464 queue.addLast(withFileChange.parent2);
465 }
466 } while (!queue.isEmpty());
467 HistoryNode[] strippedHistory = strippedHistoryList.toArray(new HistoryNode[strippedHistoryList.size()]);
468 completeHistory = null;
469 commitRevisions = null;
470 // collected values are no longer valid - shall
471 // strip off elements for missing HistoryNodes, but it's easier just to re-create the array
472 commitRevisions = new int[strippedHistory.length];
473 for (int i = 0; i < commitRevisions.length; i++) {
474 commitRevisions[i] = strippedHistory[i].changeset;
475 }
476 return strippedHistory;
477 }
478
479 /**
480 * handy access to all HistoryNode[i].changeset values
481 */
482 int[] getCommitRevisions() {
483 return commitRevisions;
484 }
485 };
486
391 487
392 // 488 //
393 489
394 public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { 490 public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) {
395 if (limit > 0 && count >= limit) { 491 if (limit > 0 && count >= limit) {