Mercurial > hg4j
diff 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 |
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/core/HgLogCommand.java Wed Dec 12 14:17:12 2012 +0100 +++ b/src/org/tmatesoft/hg/core/HgLogCommand.java Wed Dec 12 20:52:10 2012 +0100 @@ -300,62 +300,57 @@ if (file == null) { throw new IllegalArgumentException("History tree is supported for files only (at least now), please specify file"); } - if (followHistory) { - throw new UnsupportedOperationException("Can't follow file history when building tree (yet?)"); - } - class TreeBuildInspector implements HgChangelog.ParentInspector, HgChangelog.RevisionInspector { - HistoryNode[] completeHistory; - int[] commitRevisions; - - public void next(int revisionNumber, Nodeid revision, int linkedRevision) { - commitRevisions[revisionNumber] = linkedRevision; - } - - public void next(int revisionNumber, Nodeid revision, int parent1, int parent2, Nodeid nidParent1, Nodeid nidParent2) { - HistoryNode p1 = null, p2 = null; - if (parent1 != -1) { - p1 = completeHistory[parent1]; - } - if (parent2!= -1) { - p2 = completeHistory[parent2]; - } - completeHistory[revisionNumber] = new HistoryNode(commitRevisions[revisionNumber], revision, p1, p2); - } - - HistoryNode[] go(HgDataFile fileNode) throws HgInvalidControlFileException { - completeHistory = new HistoryNode[fileNode.getRevisionCount()]; - commitRevisions = new int[completeHistory.length]; - fileNode.indexWalk(0, TIP, this); - return completeHistory; - } - }; final ProgressSupport progressHelper = getProgressSupport(handler); final CancelSupport cancelHelper = getCancelSupport(handler, true); - LinkedList<HgDataFile> fileRenamesQueue = buildFileRenamesQueue(); + // builds tree of nodes according to parents in file's revlog + final TreeBuildInspector treeBuildInspector = new TreeBuildInspector(followHistory); + + // most recent file is first in the queue + LinkedList<Pair<HgDataFile, Nodeid>> fileRenamesQueue = buildFileRenamesQueue(); progressHelper.start(4 * fileRenamesQueue.size()); do { - HgDataFile fileNode = fileRenamesQueue.removeLast(); + // to maintain iteration order from older to newer, take oldest name (in case of renames) first + Pair<HgDataFile, Nodeid> renameInfo = fileRenamesQueue.removeLast(); cancelHelper.checkCancelled(); - // build tree of nodes according to parents in file's revlog - final TreeBuildInspector treeBuildInspector = new TreeBuildInspector(); - final HistoryNode[] completeHistory = treeBuildInspector.go(fileNode); + HgDataFile fileNode = renameInfo.first(); + Nodeid fileLastRevToVisit = null; + if (followHistory) { + fileLastRevToVisit = renameInfo.second(); + if (fileLastRevToVisit == null) { + // only recent file name should not have a changeset when rename has happened. + assert fileRenamesQueue.isEmpty(); + // TODO subject to dedicated method either in HgRepository (getWorkingCopyParentRevisionIndex) + // or in the HgDataFile (getWorkingCopyOriginRevision) + Nodeid wdParentChangeset = repo.getWorkingCopyParents().first(); + if (!wdParentChangeset.isNull()) { + int wdParentRevIndex = repo.getChangelog().getRevisionIndex(wdParentChangeset); + fileLastRevToVisit = repo.getManifest().getFileRevision(wdParentRevIndex, fileNode.getPath()); + } + // else fall-through, assume lastRevision() is ok here + } + } + int fileLastRevIndexToVisit = fileLastRevToVisit == null ? fileNode.getLastRevision() : fileNode.getRevisionIndex(fileLastRevToVisit); + final HistoryNode[] changeHistory = treeBuildInspector.go(fileNode, fileLastRevIndexToVisit); + int[] commitRevisions = treeBuildInspector.getCommitRevisions(); + assert changeHistory.length == commitRevisions.length; progressHelper.worked(1); cancelHelper.checkCancelled(); - ElementImpl ei = new ElementImpl(treeBuildInspector.commitRevisions.length); + ElementImpl ei = new ElementImpl(commitRevisions.length); final ProgressSupport ph2; - if (treeBuildInspector.commitRevisions.length < 100 /*XXX is it really worth it? */) { + if (commitRevisions.length < 100 /*XXX is it really worth it? */) { + // read bunch of changesets at once and cache 'em ei.initTransform(); - repo.getChangelog().range(ei, treeBuildInspector.commitRevisions); + repo.getChangelog().range(ei, commitRevisions); progressHelper.worked(1); ph2 = new ProgressSupport.Sub(progressHelper, 2); } else { ph2 = new ProgressSupport.Sub(progressHelper, 3); } - ph2.start(completeHistory.length); + ph2.start(changeHistory.length); // XXX shall sort completeHistory according to changeset numbers? - for (int i = 0; i < completeHistory.length; i++ ) { - final HistoryNode n = completeHistory[i]; + for (int i = 0; i < changeHistory.length; i++ ) { + final HistoryNode n = changeHistory[i]; handler.treeElement(ei.init(n)); ph2.worked(1); cancelHelper.checkCancelled(); @@ -365,29 +360,130 @@ } /** - * Follows file renames and build a list of all corresponding file nodes. If {@link #followHistory} is <code>false</code>, - * the list contains one element only, file node with the name of the file as it was specified by the user. + * Follows file renames and build a list of all corresponding file nodes and revisions they were + * copied/renamed/branched at (IOW, their latest revision to look at). + * + * If {@link #followHistory} is <code>false</code>, the list contains one element only, + * file node with the name of the file as it was specified by the user. + * + * For the most recent file revision is null. + * + * TODO may use HgFileRevision (after some refactoring to accept HgDataFile and Nodeid) instead of Pair + * and possibly reuse this functionality * * @return list of file renames, with most recent file first */ - private LinkedList<HgDataFile> buildFileRenamesQueue() { - LinkedList<HgDataFile> rv = new LinkedList<HgDataFile>(); + private LinkedList<Pair<HgDataFile, Nodeid>> buildFileRenamesQueue() { + LinkedList<Pair<HgDataFile, Nodeid>> rv = new LinkedList<Pair<HgDataFile, Nodeid>>(); if (!followHistory) { - rv.add(repo.getFileNode(file)); + rv.add(new Pair<HgDataFile, Nodeid>(repo.getFileNode(file), null)); return rv; } - HgDataFile fileNode; Path fp = file; + Nodeid copyRev = null; boolean isCopy; do { - fileNode = repo.getFileNode(fp); - rv.addLast(fileNode); + HgDataFile fileNode = repo.getFileNode(fp); + rv.addLast(new Pair<HgDataFile, Nodeid>(fileNode, copyRev)); if (isCopy = fileNode.isCopy()) { fp = fileNode.getCopySourceName(); + copyRev = fileNode.getCopySourceRevision(); } } while (isCopy); return rv; } + + private static class TreeBuildInspector implements HgChangelog.ParentInspector, HgChangelog.RevisionInspector { + private final boolean followAncestry; + + private HistoryNode[] completeHistory; + private int[] commitRevisions; + + TreeBuildInspector(boolean _followAncestry) { + followAncestry = _followAncestry; + } + + public void next(int revisionNumber, Nodeid revision, int linkedRevision) { + commitRevisions[revisionNumber] = linkedRevision; + } + + public void next(int revisionNumber, Nodeid revision, int parent1, int parent2, Nodeid nidParent1, Nodeid nidParent2) { + HistoryNode p1 = null, p2 = null; + if (parent1 != -1) { + p1 = completeHistory[parent1]; + } + if (parent2!= -1) { + p2 = completeHistory[parent2]; + } + completeHistory[revisionNumber] = new HistoryNode(commitRevisions[revisionNumber], revision, p1, p2); + } + + /** + * Builds history of file changes (in natural order, from oldest to newest) up to (and including) file revision specified. + * If {@link TreeBuildInspector} follows ancestry, only elements that are on the line of ancestry of the revision at + * lastRevisionIndex would be included. + */ + HistoryNode[] go(HgDataFile fileNode, int lastRevisionIndex) throws HgInvalidControlFileException { + completeHistory = new HistoryNode[lastRevisionIndex+1]; + commitRevisions = new int[completeHistory.length]; + fileNode.indexWalk(0, lastRevisionIndex, this); + if (!followAncestry) { + return completeHistory; + } + /* + * Changesets: + * o <-- cset from working dir parent (as in dirstate), file not changed (file revision recorded points to that from A) + * | x <-- revision with file changed (B') + * x / <-- revision with file changed (A) + * | x <-- revision with file changed (B) + * |/ + * o <-- another changeset, where file wasn't changed + * | + * x <-- revision with file changed (C) + * + * File history: B', A, B, C + * + * When "follow", SHALL NOT report B and B', but A and C + */ + // strippedHistory: only those HistoryNodes from completeHistory that are on the same + // line of descendant, in order from older to newer + LinkedList<HistoryNode> strippedHistoryList = new LinkedList<HistoryNode>(); + LinkedList<HistoryNode> queue = new LinkedList<HistoryNode>(); + // look for ancestors of the selected history node + queue.add(completeHistory[lastRevisionIndex]); + do { + HistoryNode withFileChange = queue.removeFirst(); + if (withFileChange.children != null) { + withFileChange.children.retainAll(strippedHistoryList); + } + strippedHistoryList.addFirst(withFileChange); + if (withFileChange.parent1 != null) { + queue.addLast(withFileChange.parent1); + } + if (withFileChange.parent2 != null) { + queue.addLast(withFileChange.parent2); + } + } while (!queue.isEmpty()); + HistoryNode[] strippedHistory = strippedHistoryList.toArray(new HistoryNode[strippedHistoryList.size()]); + completeHistory = null; + commitRevisions = null; + // collected values are no longer valid - shall + // strip off elements for missing HistoryNodes, but it's easier just to re-create the array + commitRevisions = new int[strippedHistory.length]; + for (int i = 0; i < commitRevisions.length; i++) { + commitRevisions[i] = strippedHistory[i].changeset; + } + return strippedHistory; + } + + /** + * handy access to all HistoryNode[i].changeset values + */ + int[] getCommitRevisions() { + return commitRevisions; + } + }; + //