tikhomirov@64: /* tikhomirov@427: s * Copyright (c) 2011-2012 TMate Software Ltd tikhomirov@64: * tikhomirov@64: * This program is free software; you can redistribute it and/or modify tikhomirov@64: * it under the terms of the GNU General Public License as published by tikhomirov@64: * the Free Software Foundation; version 2 of the License. tikhomirov@64: * tikhomirov@64: * This program is distributed in the hope that it will be useful, tikhomirov@64: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@64: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@64: * GNU General Public License for more details. tikhomirov@64: * tikhomirov@64: * For information on how to redistribute this software under tikhomirov@64: * the terms of a license other than GNU General Public License tikhomirov@102: * contact TMate Software at support@hg4j.com tikhomirov@64: */ tikhomirov@64: package org.tmatesoft.hg.core; tikhomirov@64: tikhomirov@74: import static org.tmatesoft.hg.repo.HgRepository.TIP; tikhomirov@456: import static org.tmatesoft.hg.util.LogFacility.Severity.Error; tikhomirov@64: tikhomirov@328: import java.util.ArrayList; tikhomirov@328: import java.util.Arrays; tikhomirov@64: import java.util.Calendar; tikhomirov@328: import java.util.Collection; tikhomirov@64: import java.util.Collections; tikhomirov@511: import java.util.Comparator; tikhomirov@64: import java.util.ConcurrentModificationException; tikhomirov@510: import java.util.Iterator; tikhomirov@64: import java.util.LinkedList; tikhomirov@64: import java.util.List; tikhomirov@510: import java.util.ListIterator; tikhomirov@64: import java.util.Set; tikhomirov@64: import java.util.TreeSet; tikhomirov@64: tikhomirov@328: import org.tmatesoft.hg.internal.IntMap; tikhomirov@328: import org.tmatesoft.hg.internal.IntVector; tikhomirov@215: import org.tmatesoft.hg.repo.HgChangelog; tikhomirov@154: import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; tikhomirov@80: import org.tmatesoft.hg.repo.HgDataFile; tikhomirov@423: import org.tmatesoft.hg.repo.HgInvalidControlFileException; tikhomirov@457: import org.tmatesoft.hg.repo.HgInvalidRevisionException; tikhomirov@423: import org.tmatesoft.hg.repo.HgInvalidStateException; tikhomirov@456: import org.tmatesoft.hg.repo.HgParentChildMap; tikhomirov@74: import org.tmatesoft.hg.repo.HgRepository; tikhomirov@423: import org.tmatesoft.hg.repo.HgRuntimeException; tikhomirov@328: import org.tmatesoft.hg.repo.HgStatusCollector; tikhomirov@328: import org.tmatesoft.hg.util.CancelSupport; tikhomirov@157: import org.tmatesoft.hg.util.CancelledException; tikhomirov@328: import org.tmatesoft.hg.util.Pair; tikhomirov@133: import org.tmatesoft.hg.util.Path; tikhomirov@215: import org.tmatesoft.hg.util.ProgressSupport; tikhomirov@64: tikhomirov@64: tikhomirov@64: /** tikhomirov@131: * Access to changelog, 'hg log' command counterpart. tikhomirov@131: * tikhomirov@64: *
tikhomirov@131:  * Usage:
tikhomirov@70:  *   new LogCommand().limit(20).branch("maintenance-2.1").user("me").execute(new MyHandler());
tikhomirov@64:  * 
tikhomirov@131: * Not thread-safe (each thread has to use own {@link HgLogCommand} instance). tikhomirov@64: * tikhomirov@64: * @author Artem Tikhomirov tikhomirov@64: * @author TMate Software Ltd. tikhomirov@64: */ tikhomirov@215: public class HgLogCommand extends HgAbstractCommand implements HgChangelog.Inspector { tikhomirov@64: tikhomirov@64: private final HgRepository repo; tikhomirov@64: private Set users; tikhomirov@64: private Set branches; tikhomirov@64: private int limit = 0, count = 0; tikhomirov@64: private int startRev = 0, endRev = TIP; tikhomirov@64: private Calendar date; tikhomirov@77: private Path file; tikhomirov@80: private boolean followHistory; // makes sense only when file != null tikhomirov@193: private ChangesetTransformer csetTransform; tikhomirov@432: private HgParentChildMap parentHelper; tikhomirov@80: tikhomirov@131: public HgLogCommand(HgRepository hgRepo) { tikhomirov@107: repo = hgRepo; tikhomirov@64: } tikhomirov@64: tikhomirov@64: /** tikhomirov@148: * Limit search to specified user. Multiple user names may be specified. Once set, user names can't be tikhomirov@148: * cleared, use new command instance in such cases. tikhomirov@64: * @param user - full or partial name of the user, case-insensitive, non-null. tikhomirov@64: * @return this instance for convenience tikhomirov@148: * @throws IllegalArgumentException when argument is null tikhomirov@64: */ tikhomirov@131: public HgLogCommand user(String user) { tikhomirov@64: if (user == null) { tikhomirov@64: throw new IllegalArgumentException(); tikhomirov@64: } tikhomirov@64: if (users == null) { tikhomirov@64: users = new TreeSet(); tikhomirov@64: } tikhomirov@64: users.add(user.toLowerCase()); tikhomirov@64: return this; tikhomirov@64: } tikhomirov@64: tikhomirov@64: /** tikhomirov@64: * Limit search to specified branch. Multiple branch specification possible (changeset from any of these tikhomirov@148: * would be included in result). If unspecified, all branches are considered. There's no way to clean branch selection tikhomirov@148: * once set, create fresh new command instead. tikhomirov@64: * @param branch - branch name, case-sensitive, non-null. tikhomirov@64: * @return this instance for convenience tikhomirov@148: * @throws IllegalArgumentException when branch argument is null tikhomirov@64: */ tikhomirov@131: public HgLogCommand branch(String branch) { tikhomirov@64: if (branch == null) { tikhomirov@64: throw new IllegalArgumentException(); tikhomirov@64: } tikhomirov@64: if (branches == null) { tikhomirov@64: branches = new TreeSet(); tikhomirov@64: } tikhomirov@64: branches.add(branch); tikhomirov@64: return this; tikhomirov@64: } tikhomirov@64: tikhomirov@64: // limit search to specific date tikhomirov@64: // multiple? tikhomirov@131: public HgLogCommand date(Calendar date) { tikhomirov@64: this.date = date; tikhomirov@418: // TODO post-1.0 implement tikhomirov@64: // isSet(field) - false => don't use in detection of 'same date' tikhomirov@64: throw HgRepository.notImplemented(); tikhomirov@64: } tikhomirov@64: tikhomirov@64: /** tikhomirov@64: * tikhomirov@64: * @param num - number of changeset to produce. Pass 0 to clear the limit. tikhomirov@64: * @return this instance for convenience tikhomirov@64: */ tikhomirov@131: public HgLogCommand limit(int num) { tikhomirov@64: limit = num; tikhomirov@64: return this; tikhomirov@64: } tikhomirov@64: tikhomirov@64: /** tikhomirov@64: * Limit to specified subset of Changelog, [min(rev1,rev2), max(rev1,rev2)], inclusive. tikhomirov@64: * Revision may be specified with {@link HgRepository#TIP} tikhomirov@427: * tikhomirov@427: * @param rev1 - local index of start changeset revision tikhomirov@427: * @param rev2 - index of end changeset revision tikhomirov@64: * @return this instance for convenience tikhomirov@64: */ tikhomirov@131: public HgLogCommand range(int rev1, int rev2) { tikhomirov@64: if (rev1 != TIP && rev2 != TIP) { tikhomirov@64: startRev = rev2 < rev1 ? rev2 : rev1; tikhomirov@64: endRev = startRev == rev2 ? rev1 : rev2; tikhomirov@64: } else if (rev1 == TIP && rev2 != TIP) { tikhomirov@64: startRev = rev2; tikhomirov@64: endRev = rev1; tikhomirov@64: } else { tikhomirov@64: startRev = rev1; tikhomirov@64: endRev = rev2; tikhomirov@64: } tikhomirov@64: return this; tikhomirov@64: } tikhomirov@64: tikhomirov@77: /** tikhomirov@253: * Select specific changeset tikhomirov@253: * tikhomirov@253: * @param nid changeset revision tikhomirov@253: * @return this for convenience tikhomirov@427: * @throws HgBadArgumentException if failed to find supplied changeset revision tikhomirov@253: */ tikhomirov@427: public HgLogCommand changeset(Nodeid nid) throws HgBadArgumentException { tikhomirov@253: // XXX perhaps, shall support multiple (...) arguments and extend #execute to handle not only range, but also set of revisions. tikhomirov@427: try { tikhomirov@427: final int csetRevIndex = repo.getChangelog().getRevisionIndex(nid); tikhomirov@427: return range(csetRevIndex, csetRevIndex); tikhomirov@457: } catch (HgInvalidRevisionException ex) { tikhomirov@427: throw new HgBadArgumentException("Can't find revision", ex).setRevision(nid); tikhomirov@427: } tikhomirov@253: } tikhomirov@253: tikhomirov@253: /** tikhomirov@77: * Visit history of a given file only. tikhomirov@77: * @param file path relative to repository root. Pass null to reset. tikhomirov@80: * @param followCopyRename true to report changesets of the original file(-s), if copy/rename ever occured to the file. tikhomirov@77: */ tikhomirov@131: public HgLogCommand file(Path file, boolean followCopyRename) { tikhomirov@77: // multiple? Bad idea, would need to include extra method into Handler to tell start of next file tikhomirov@77: this.file = file; tikhomirov@80: followHistory = followCopyRename; tikhomirov@77: return this; tikhomirov@64: } tikhomirov@142: tikhomirov@142: /** tikhomirov@142: * Handy analog of {@link #file(Path, boolean)} when clients' paths come from filesystem and need conversion to repository's tikhomirov@142: */ tikhomirov@142: public HgLogCommand file(String file, boolean followCopyRename) { tikhomirov@142: return file(Path.create(repo.getToRepoPathHelper().rewrite(file)), followCopyRename); tikhomirov@142: } tikhomirov@64: tikhomirov@64: /** tikhomirov@419: * Similar to {@link #execute(HgChangesetHandler)}, collects and return result as a list. tikhomirov@427: * tikhomirov@427: * @see #execute(HgChangesetHandler) tikhomirov@427: * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state tikhomirov@64: */ tikhomirov@396: public List execute() throws HgException { tikhomirov@64: CollectHandler collector = new CollectHandler(); tikhomirov@215: try { tikhomirov@215: execute(collector); tikhomirov@423: } catch (HgCallbackTargetException ex) { tikhomirov@423: // see below for CanceledException tikhomirov@423: HgInvalidStateException t = new HgInvalidStateException("Internal error"); tikhomirov@423: t.initCause(ex); tikhomirov@423: throw t; tikhomirov@396: } catch (CancelledException ex) { tikhomirov@215: // can't happen as long as our CollectHandler doesn't throw any exception tikhomirov@423: HgInvalidStateException t = new HgInvalidStateException("Internal error"); tikhomirov@423: t.initCause(ex); tikhomirov@423: throw t; tikhomirov@215: } tikhomirov@64: return collector.getChanges(); tikhomirov@64: } tikhomirov@64: tikhomirov@64: /** tikhomirov@402: * Iterate over range of changesets configured in the command. tikhomirov@64: * tikhomirov@205: * @param handler callback to process changesets. tikhomirov@427: * @throws HgCallbackTargetException propagated exception from the handler tikhomirov@427: * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state tikhomirov@380: * @throws CancelledException if execution of the command was cancelled tikhomirov@64: * @throws IllegalArgumentException when inspector argument is null tikhomirov@64: * @throws ConcurrentModificationException if this log command instance is already running tikhomirov@64: */ tikhomirov@370: public void execute(HgChangesetHandler handler) throws HgCallbackTargetException, HgException, CancelledException { tikhomirov@64: if (handler == null) { tikhomirov@64: throw new IllegalArgumentException(); tikhomirov@64: } tikhomirov@193: if (csetTransform != null) { tikhomirov@64: throw new ConcurrentModificationException(); tikhomirov@64: } tikhomirov@215: final ProgressSupport progressHelper = getProgressSupport(handler); tikhomirov@64: try { tikhomirov@64: count = 0; tikhomirov@432: HgParentChildMap pw = getParentHelper(file == null); // leave it uninitialized unless we iterate whole repo tikhomirov@193: // ChangesetTransfrom creates a blank PathPool, and #file(String, boolean) above tikhomirov@193: // may utilize it as well. CommandContext? How about StatusCollector there as well? tikhomirov@322: csetTransform = new ChangesetTransformer(repo, handler, pw, progressHelper, getCancelSupport(handler, true)); tikhomirov@77: if (file == null) { tikhomirov@215: progressHelper.start(endRev - startRev + 1); tikhomirov@77: repo.getChangelog().range(startRev, endRev, this); tikhomirov@215: csetTransform.checkFailure(); tikhomirov@77: } else { tikhomirov@215: progressHelper.start(-1/*XXX enum const, or a dedicated method startUnspecified(). How about startAtLeast(int)?*/); tikhomirov@80: HgDataFile fileNode = repo.getFileNode(file); tikhomirov@427: if (!fileNode.exists()) { tikhomirov@427: throw new HgPathNotFoundException(String.format("File %s not found in the repository", file), file); tikhomirov@427: } tikhomirov@509: // FIXME startRev and endRev ARE CHANGESET REVISIONS, not that of FILE!!! tikhomirov@80: fileNode.history(startRev, endRev, this); tikhomirov@215: csetTransform.checkFailure(); tikhomirov@126: if (fileNode.isCopy()) { tikhomirov@80: // even if we do not follow history, report file rename tikhomirov@80: do { tikhomirov@427: if (handler instanceof HgChangesetHandler.WithCopyHistory) { tikhomirov@415: HgFileRevision src = new HgFileRevision(repo, fileNode.getCopySourceRevision(), null, fileNode.getCopySourceName()); tikhomirov@415: HgFileRevision dst = new HgFileRevision(repo, fileNode.getRevision(0), null, fileNode.getPath(), src.getPath()); tikhomirov@427: ((HgChangesetHandler.WithCopyHistory) handler).copy(src, dst); tikhomirov@126: } tikhomirov@80: if (limit > 0 && count >= limit) { tikhomirov@80: // if limit reach, follow is useless. tikhomirov@80: break; tikhomirov@80: } tikhomirov@80: if (followHistory) { tikhomirov@126: fileNode = repo.getFileNode(fileNode.getCopySourceName()); tikhomirov@80: fileNode.history(this); tikhomirov@215: csetTransform.checkFailure(); tikhomirov@80: } tikhomirov@80: } while (followHistory && fileNode.isCopy()); tikhomirov@80: } tikhomirov@77: } tikhomirov@427: } catch (HgRuntimeException ex) { tikhomirov@427: throw new HgLibraryFailureException(ex); tikhomirov@64: } finally { tikhomirov@193: csetTransform = null; tikhomirov@215: progressHelper.done(); tikhomirov@64: } tikhomirov@64: } tikhomirov@328: tikhomirov@370: /** tikhomirov@402: * Tree-wise iteration of a file history, with handy access to parent-child relations between changesets. tikhomirov@402: * tikhomirov@402: * @param handler callback to process changesets. tikhomirov@427: * @throws HgCallbackTargetException propagated exception from the handler tikhomirov@427: * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state tikhomirov@380: * @throws CancelledException if execution of the command was cancelled tikhomirov@402: * @throws IllegalArgumentException if command is not satisfied with its arguments tikhomirov@402: * @throws ConcurrentModificationException if this log command instance is already running tikhomirov@370: */ tikhomirov@370: public void execute(HgChangesetTreeHandler handler) throws HgCallbackTargetException, HgException, CancelledException { tikhomirov@328: if (handler == null) { tikhomirov@328: throw new IllegalArgumentException(); tikhomirov@328: } tikhomirov@328: if (csetTransform != null) { tikhomirov@328: throw new ConcurrentModificationException(); tikhomirov@328: } tikhomirov@328: if (file == null) { tikhomirov@328: throw new IllegalArgumentException("History tree is supported for files only (at least now), please specify file"); tikhomirov@328: } tikhomirov@328: final ProgressSupport progressHelper = getProgressSupport(handler); tikhomirov@328: final CancelSupport cancelHelper = getCancelSupport(handler, true); tikhomirov@507: tikhomirov@508: // builds tree of nodes according to parents in file's revlog tikhomirov@508: final TreeBuildInspector treeBuildInspector = new TreeBuildInspector(followHistory); tikhomirov@509: // we iterate separate histories of each filename, need to connect tikhomirov@509: // last node of historyA with first node of historyB (A renamed to B case) tikhomirov@509: // to make overall history smooth. tikhomirov@509: HistoryNode lastFromPrevIteration = null; tikhomirov@509: tikhomirov@509: final int CACHE_CSET_IN_ADVANCE_THRESHOLD = 100; /* XXX is it really worth it? */ tikhomirov@509: ElementImpl ei = null; tikhomirov@508: tikhomirov@510: // renamed files in the queue are placed with respect to #iterateDirection tikhomirov@510: // i.e. if we iterate from new to old, recent filenames come first tikhomirov@508: LinkedList> fileRenamesQueue = buildFileRenamesQueue(); tikhomirov@507: progressHelper.start(4 * fileRenamesQueue.size()); tikhomirov@507: do { tikhomirov@510: tikhomirov@510: Pair renameInfo = fileRenamesQueue.removeFirst(); tikhomirov@507: cancelHelper.checkCancelled(); tikhomirov@508: HgDataFile fileNode = renameInfo.first(); tikhomirov@508: Nodeid fileLastRevToVisit = null; tikhomirov@508: if (followHistory) { tikhomirov@508: fileLastRevToVisit = renameInfo.second(); tikhomirov@508: if (fileLastRevToVisit == null) { tikhomirov@510: // it's either first or last item in the queue, depending on iteration order tikhomirov@510: assert fileRenamesQueue.isEmpty() || /*awful way to find out it's first iteration*/ lastFromPrevIteration == null; tikhomirov@508: // TODO subject to dedicated method either in HgRepository (getWorkingCopyParentRevisionIndex) tikhomirov@508: // or in the HgDataFile (getWorkingCopyOriginRevision) tikhomirov@508: Nodeid wdParentChangeset = repo.getWorkingCopyParents().first(); tikhomirov@508: if (!wdParentChangeset.isNull()) { tikhomirov@508: int wdParentRevIndex = repo.getChangelog().getRevisionIndex(wdParentChangeset); tikhomirov@508: fileLastRevToVisit = repo.getManifest().getFileRevision(wdParentRevIndex, fileNode.getPath()); tikhomirov@508: } tikhomirov@508: // else fall-through, assume lastRevision() is ok here tikhomirov@508: } tikhomirov@508: } tikhomirov@508: int fileLastRevIndexToVisit = fileLastRevToVisit == null ? fileNode.getLastRevision() : fileNode.getRevisionIndex(fileLastRevToVisit); tikhomirov@509: final List changeHistory = treeBuildInspector.go(fileNode, fileLastRevIndexToVisit); tikhomirov@509: assert changeHistory.size() > 0; tikhomirov@328: progressHelper.worked(1); tikhomirov@423: cancelHelper.checkCancelled(); tikhomirov@507: final ProgressSupport ph2; tikhomirov@509: if (ei == null) { tikhomirov@509: // when follow is true, changeHistory.size() of the first revision might be quite short tikhomirov@509: // (e.g. bad fname recognized soon), hence ensure at least cache size at once tikhomirov@509: ei = new ElementImpl(Math.max(CACHE_CSET_IN_ADVANCE_THRESHOLD, changeHistory.size())); tikhomirov@509: } tikhomirov@509: if (changeHistory.size() < CACHE_CSET_IN_ADVANCE_THRESHOLD ) { tikhomirov@509: int[] commitRevisions = treeBuildInspector.getCommitRevisions(); tikhomirov@509: assert changeHistory.size() == commitRevisions.length; tikhomirov@508: // read bunch of changesets at once and cache 'em tikhomirov@507: ei.initTransform(); tikhomirov@508: repo.getChangelog().range(ei, commitRevisions); tikhomirov@507: progressHelper.worked(1); tikhomirov@507: ph2 = new ProgressSupport.Sub(progressHelper, 2); tikhomirov@507: } else { tikhomirov@507: ph2 = new ProgressSupport.Sub(progressHelper, 3); tikhomirov@507: } tikhomirov@509: ph2.start(changeHistory.size()); tikhomirov@509: if (lastFromPrevIteration != null) { tikhomirov@510: if (iterateDirection == IterateDirection.FromOldToNew) { tikhomirov@510: // forward, from old to new: tikhomirov@510: // A(0..n) -> B(0..m). First, report A(0)..A(n-1) tikhomirov@510: // then A(n).bind(B(0)) tikhomirov@510: HistoryNode oldestOfTheNextChunk = changeHistory.get(0); tikhomirov@510: lastFromPrevIteration.bindChild(oldestOfTheNextChunk); tikhomirov@510: changeHistory.add(0, lastFromPrevIteration); tikhomirov@510: } else { tikhomirov@510: assert iterateDirection == IterateDirection.FromNewToOld; tikhomirov@510: // A renamed to B. A(0..n) -> B(0..m). tikhomirov@510: // First, report B(m), B(m-1)...B(1), then A(n).bind(B(0)) tikhomirov@510: HistoryNode newestOfNextChunk = changeHistory.get(changeHistory.size() - 1); // A(n) tikhomirov@510: newestOfNextChunk.bindChild(lastFromPrevIteration); tikhomirov@510: changeHistory.add(lastFromPrevIteration); tikhomirov@510: } tikhomirov@509: } tikhomirov@509: if (!fileRenamesQueue.isEmpty()) { tikhomirov@510: if (iterateDirection == IterateDirection.FromOldToNew) { tikhomirov@510: // save newest, and exclude it from this iteration (postpone for next) tikhomirov@510: lastFromPrevIteration = changeHistory.remove(changeHistory.size()-1); tikhomirov@510: } else { tikhomirov@510: assert iterateDirection == IterateDirection.FromNewToOld; tikhomirov@510: // save oldest, and exclude it from thi iteration (postpone for next) tikhomirov@510: lastFromPrevIteration = changeHistory.remove(0); tikhomirov@510: } tikhomirov@509: } else { tikhomirov@509: lastFromPrevIteration = null; // just for the sake of no references to old items tikhomirov@509: } tikhomirov@509: // XXX shall sort changeHistory according to changeset numbers? tikhomirov@510: Iterator it; tikhomirov@510: if (iterateDirection == IterateDirection.FromOldToNew) { tikhomirov@510: it = changeHistory.listIterator(); tikhomirov@510: } else { tikhomirov@510: assert iterateDirection == IterateDirection.FromNewToOld; tikhomirov@510: it = new ReverseIterator(changeHistory); tikhomirov@510: } tikhomirov@510: while(it.hasNext()) { tikhomirov@510: HistoryNode n = it.next(); tikhomirov@507: handler.treeElement(ei.init(n)); tikhomirov@507: ph2.worked(1); tikhomirov@507: cancelHelper.checkCancelled(); tikhomirov@507: } tikhomirov@507: } while (!fileRenamesQueue.isEmpty()); tikhomirov@328: progressHelper.done(); tikhomirov@328: } tikhomirov@328: tikhomirov@510: private IterateDirection iterateDirection = IterateDirection.FromOldToNew; tikhomirov@510: tikhomirov@510: private static class ReverseIterator implements Iterator { tikhomirov@510: private final ListIterator listIterator; tikhomirov@510: tikhomirov@510: public ReverseIterator(List list) { tikhomirov@510: listIterator = list.listIterator(list.size()); tikhomirov@510: } tikhomirov@510: tikhomirov@510: public boolean hasNext() { tikhomirov@510: return listIterator.hasPrevious(); tikhomirov@510: } tikhomirov@510: public E next() { tikhomirov@510: return listIterator.previous(); tikhomirov@510: } tikhomirov@510: public void remove() { tikhomirov@510: listIterator.remove(); tikhomirov@510: } tikhomirov@510: } tikhomirov@510: tikhomirov@507: /** tikhomirov@508: * Follows file renames and build a list of all corresponding file nodes and revisions they were tikhomirov@508: * copied/renamed/branched at (IOW, their latest revision to look at). tikhomirov@508: * tikhomirov@508: * If {@link #followHistory} is false, the list contains one element only, tikhomirov@508: * file node with the name of the file as it was specified by the user. tikhomirov@508: * tikhomirov@508: * For the most recent file revision is null. tikhomirov@508: * tikhomirov@508: * TODO may use HgFileRevision (after some refactoring to accept HgDataFile and Nodeid) instead of Pair tikhomirov@508: * and possibly reuse this functionality tikhomirov@507: * tikhomirov@510: * @return list of file renames, ordered with respect to {@link #iterateDirection} tikhomirov@507: */ tikhomirov@508: private LinkedList> buildFileRenamesQueue() { tikhomirov@508: LinkedList> rv = new LinkedList>(); tikhomirov@507: if (!followHistory) { tikhomirov@508: rv.add(new Pair(repo.getFileNode(file), null)); tikhomirov@507: return rv; tikhomirov@507: } tikhomirov@507: Path fp = file; tikhomirov@508: Nodeid copyRev = null; tikhomirov@507: boolean isCopy; tikhomirov@507: do { tikhomirov@508: HgDataFile fileNode = repo.getFileNode(fp); tikhomirov@510: Pair p = new Pair(fileNode, copyRev); tikhomirov@510: if (iterateDirection == IterateDirection.FromOldToNew) { tikhomirov@510: rv.addFirst(p); tikhomirov@510: } else { tikhomirov@510: assert iterateDirection == IterateDirection.FromNewToOld; tikhomirov@510: rv.addLast(p); tikhomirov@510: } tikhomirov@507: if (isCopy = fileNode.isCopy()) { tikhomirov@507: fp = fileNode.getCopySourceName(); tikhomirov@508: copyRev = fileNode.getCopySourceRevision(); tikhomirov@507: } tikhomirov@507: } while (isCopy); tikhomirov@507: return rv; tikhomirov@507: } tikhomirov@508: tikhomirov@508: private static class TreeBuildInspector implements HgChangelog.ParentInspector, HgChangelog.RevisionInspector { tikhomirov@508: private final boolean followAncestry; tikhomirov@508: tikhomirov@508: private HistoryNode[] completeHistory; tikhomirov@508: private int[] commitRevisions; tikhomirov@509: private List resultHistory; tikhomirov@508: tikhomirov@508: TreeBuildInspector(boolean _followAncestry) { tikhomirov@508: followAncestry = _followAncestry; tikhomirov@508: } tikhomirov@508: tikhomirov@508: public void next(int revisionNumber, Nodeid revision, int linkedRevision) { tikhomirov@508: commitRevisions[revisionNumber] = linkedRevision; tikhomirov@508: } tikhomirov@508: tikhomirov@508: public void next(int revisionNumber, Nodeid revision, int parent1, int parent2, Nodeid nidParent1, Nodeid nidParent2) { tikhomirov@508: HistoryNode p1 = null, p2 = null; tikhomirov@508: if (parent1 != -1) { tikhomirov@508: p1 = completeHistory[parent1]; tikhomirov@508: } tikhomirov@508: if (parent2!= -1) { tikhomirov@508: p2 = completeHistory[parent2]; tikhomirov@508: } tikhomirov@508: completeHistory[revisionNumber] = new HistoryNode(commitRevisions[revisionNumber], revision, p1, p2); tikhomirov@508: } tikhomirov@508: tikhomirov@508: /** tikhomirov@508: * Builds history of file changes (in natural order, from oldest to newest) up to (and including) file revision specified. tikhomirov@508: * If {@link TreeBuildInspector} follows ancestry, only elements that are on the line of ancestry of the revision at tikhomirov@508: * lastRevisionIndex would be included. tikhomirov@509: * tikhomirov@509: * @return list of history elements, from oldest to newest. In case {@link #followAncestry} is true, the list tikhomirov@509: * is modifiable (to further augment with last/first elements of renamed file histories) tikhomirov@508: */ tikhomirov@509: List go(HgDataFile fileNode, int lastRevisionIndex) throws HgInvalidControlFileException { tikhomirov@509: resultHistory = null; tikhomirov@508: completeHistory = new HistoryNode[lastRevisionIndex+1]; tikhomirov@508: commitRevisions = new int[completeHistory.length]; tikhomirov@508: fileNode.indexWalk(0, lastRevisionIndex, this); tikhomirov@508: if (!followAncestry) { tikhomirov@509: // in case when ancestor not followed, it's safe to return unmodifiable list tikhomirov@509: resultHistory = Arrays.asList(completeHistory); tikhomirov@509: completeHistory = null; tikhomirov@509: // keep commitRevisions initialized, no need to recalculate them tikhomirov@509: // as they correspond 1:1 to resultHistory tikhomirov@509: return resultHistory; tikhomirov@508: } tikhomirov@508: /* tikhomirov@509: * Changesets, newest at the top: tikhomirov@508: * o <-- cset from working dir parent (as in dirstate), file not changed (file revision recorded points to that from A) tikhomirov@508: * | x <-- revision with file changed (B') tikhomirov@508: * x / <-- revision with file changed (A) tikhomirov@508: * | x <-- revision with file changed (B) tikhomirov@508: * |/ tikhomirov@508: * o <-- another changeset, where file wasn't changed tikhomirov@508: * | tikhomirov@508: * x <-- revision with file changed (C) tikhomirov@508: * tikhomirov@508: * File history: B', A, B, C tikhomirov@508: * tikhomirov@508: * When "follow", SHALL NOT report B and B', but A and C tikhomirov@508: */ tikhomirov@508: // strippedHistory: only those HistoryNodes from completeHistory that are on the same tikhomirov@508: // line of descendant, in order from older to newer tikhomirov@508: LinkedList strippedHistoryList = new LinkedList(); tikhomirov@508: LinkedList queue = new LinkedList(); tikhomirov@508: // look for ancestors of the selected history node tikhomirov@508: queue.add(completeHistory[lastRevisionIndex]); tikhomirov@508: do { tikhomirov@508: HistoryNode withFileChange = queue.removeFirst(); tikhomirov@511: if (strippedHistoryList.contains(withFileChange)) { tikhomirov@511: // fork point for the change that was later merged (and we traced tikhomirov@511: // both lines of development by now. tikhomirov@511: continue; tikhomirov@511: } tikhomirov@508: if (withFileChange.children != null) { tikhomirov@508: withFileChange.children.retainAll(strippedHistoryList); tikhomirov@508: } tikhomirov@508: strippedHistoryList.addFirst(withFileChange); tikhomirov@508: if (withFileChange.parent1 != null) { tikhomirov@508: queue.addLast(withFileChange.parent1); tikhomirov@508: } tikhomirov@508: if (withFileChange.parent2 != null) { tikhomirov@508: queue.addLast(withFileChange.parent2); tikhomirov@508: } tikhomirov@508: } while (!queue.isEmpty()); tikhomirov@511: Collections.sort(strippedHistoryList, new Comparator() { tikhomirov@511: tikhomirov@511: public int compare(HistoryNode o1, HistoryNode o2) { tikhomirov@511: return o1.changeset - o2.changeset; tikhomirov@511: } tikhomirov@511: }); tikhomirov@508: completeHistory = null; tikhomirov@508: commitRevisions = null; tikhomirov@508: // collected values are no longer valid - shall tikhomirov@508: // strip off elements for missing HistoryNodes, but it's easier just to re-create the array tikhomirov@509: // from resultHistory later, once (and if) needed tikhomirov@509: return resultHistory = strippedHistoryList; tikhomirov@508: } tikhomirov@508: tikhomirov@508: /** tikhomirov@508: * handy access to all HistoryNode[i].changeset values tikhomirov@508: */ tikhomirov@508: int[] getCommitRevisions() { tikhomirov@509: if (commitRevisions == null) { tikhomirov@509: commitRevisions = new int[resultHistory.size()]; tikhomirov@509: int i = 0; tikhomirov@509: for (HistoryNode n : resultHistory) { tikhomirov@509: commitRevisions[i++] = n.changeset; tikhomirov@509: } tikhomirov@509: } tikhomirov@508: return commitRevisions; tikhomirov@508: } tikhomirov@508: }; tikhomirov@508: tikhomirov@507: tikhomirov@64: // tikhomirov@64: tikhomirov@154: public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { tikhomirov@64: if (limit > 0 && count >= limit) { tikhomirov@64: return; tikhomirov@64: } tikhomirov@64: if (branches != null && !branches.contains(cset.branch())) { tikhomirov@64: return; tikhomirov@64: } tikhomirov@64: if (users != null) { tikhomirov@64: String csetUser = cset.user().toLowerCase(); tikhomirov@64: boolean found = false; tikhomirov@64: for (String u : users) { tikhomirov@64: if (csetUser.indexOf(u) != -1) { tikhomirov@64: found = true; tikhomirov@64: break; tikhomirov@64: } tikhomirov@64: } tikhomirov@64: if (!found) { tikhomirov@64: return; tikhomirov@64: } tikhomirov@64: } tikhomirov@64: if (date != null) { tikhomirov@418: // TODO post-1.0 implement date support for log tikhomirov@64: } tikhomirov@64: count++; tikhomirov@193: csetTransform.next(revisionNumber, nodeid, cset); tikhomirov@64: } tikhomirov@195: tikhomirov@432: private HgParentChildMap getParentHelper(boolean create) throws HgInvalidControlFileException { tikhomirov@328: if (parentHelper == null && create) { tikhomirov@432: parentHelper = new HgParentChildMap(repo.getChangelog()); tikhomirov@195: parentHelper.init(); tikhomirov@195: } tikhomirov@195: return parentHelper; tikhomirov@195: } tikhomirov@195: tikhomirov@64: tikhomirov@205: public static class CollectHandler implements HgChangesetHandler { tikhomirov@129: private final List result = new LinkedList(); tikhomirov@64: tikhomirov@129: public List getChanges() { tikhomirov@64: return Collections.unmodifiableList(result); tikhomirov@64: } tikhomirov@64: tikhomirov@427: public void cset(HgChangeset changeset) { tikhomirov@64: result.add(changeset.clone()); tikhomirov@64: } tikhomirov@64: } tikhomirov@328: tikhomirov@328: private static class HistoryNode { tikhomirov@328: final int changeset; tikhomirov@328: final Nodeid fileRevision; tikhomirov@509: HistoryNode parent1; // there's special case when we can alter it, see #bindChild() tikhomirov@509: final HistoryNode parent2; tikhomirov@328: List children; tikhomirov@328: tikhomirov@328: HistoryNode(int cs, Nodeid revision, HistoryNode p1, HistoryNode p2) { tikhomirov@328: changeset = cs; tikhomirov@328: fileRevision = revision; tikhomirov@328: parent1 = p1; tikhomirov@328: parent2 = p2; tikhomirov@328: if (p1 != null) { tikhomirov@328: p1.addChild(this); tikhomirov@328: } tikhomirov@328: if (p2 != null) { tikhomirov@328: p2.addChild(this); tikhomirov@328: } tikhomirov@328: } tikhomirov@328: tikhomirov@509: private void addChild(HistoryNode child) { tikhomirov@328: if (children == null) { tikhomirov@328: children = new ArrayList(2); tikhomirov@328: } tikhomirov@328: children.add(child); tikhomirov@328: } tikhomirov@509: tikhomirov@509: /** tikhomirov@509: * method to merge two history chunks for renamed file so that tikhomirov@509: * this node's history continues with that of child tikhomirov@509: * @param child tikhomirov@509: */ tikhomirov@509: public void bindChild(HistoryNode child) { tikhomirov@509: assert child.parent1 == null && child.parent2 == null; tikhomirov@509: // for the last element in history empty children are by construction: tikhomirov@509: // we don't iterate further than last element of interest in TreeBuildInspector#go tikhomirov@509: assert children == null || children.isEmpty(); tikhomirov@509: child.parent1 = this; tikhomirov@509: addChild(child); tikhomirov@509: } tikhomirov@511: tikhomirov@511: public String toString() { tikhomirov@511: return String.format("", changeset, parent1 == null ? "-" : String.valueOf(parent1.changeset), parent2 == null ? "-" : String.valueOf(parent2.changeset)); tikhomirov@511: } tikhomirov@328: } tikhomirov@328: tikhomirov@328: private class ElementImpl implements HgChangesetTreeHandler.TreeElement, HgChangelog.Inspector { tikhomirov@328: private HistoryNode historyNode; tikhomirov@328: private Pair parents; tikhomirov@328: private List children; tikhomirov@328: private IntMap cachedChangesets; tikhomirov@328: private ChangesetTransformer.Transformation transform; tikhomirov@328: private Nodeid changesetRevision; tikhomirov@328: private Pair parentRevisions; tikhomirov@328: private List childRevisions; tikhomirov@328: tikhomirov@328: public ElementImpl(int total) { tikhomirov@328: cachedChangesets = new IntMap(total); tikhomirov@328: } tikhomirov@328: tikhomirov@328: ElementImpl init(HistoryNode n) { tikhomirov@328: historyNode = n; tikhomirov@328: parents = null; tikhomirov@328: children = null; tikhomirov@328: changesetRevision = null; tikhomirov@328: parentRevisions = null; tikhomirov@328: childRevisions = null; tikhomirov@328: return this; tikhomirov@328: } tikhomirov@328: tikhomirov@328: public Nodeid fileRevision() { tikhomirov@328: return historyNode.fileRevision; tikhomirov@328: } tikhomirov@328: tikhomirov@423: public HgChangeset changeset() { tikhomirov@328: return get(historyNode.changeset)[0]; tikhomirov@328: } tikhomirov@328: tikhomirov@423: public Pair parents() { tikhomirov@328: if (parents != null) { tikhomirov@328: return parents; tikhomirov@328: } tikhomirov@328: HistoryNode p; tikhomirov@328: final int p1, p2; tikhomirov@328: if ((p = historyNode.parent1) != null) { tikhomirov@328: p1 = p.changeset; tikhomirov@328: } else { tikhomirov@328: p1 = -1; tikhomirov@328: } tikhomirov@328: if ((p = historyNode.parent2) != null) { tikhomirov@328: p2 = p.changeset; tikhomirov@328: } else { tikhomirov@328: p2 = -1; tikhomirov@328: } tikhomirov@328: HgChangeset[] r = get(p1, p2); tikhomirov@328: return parents = new Pair(r[0], r[1]); tikhomirov@328: } tikhomirov@328: tikhomirov@423: public Collection children() { tikhomirov@328: if (children != null) { tikhomirov@328: return children; tikhomirov@328: } tikhomirov@328: if (historyNode.children == null) { tikhomirov@328: children = Collections.emptyList(); tikhomirov@328: } else { tikhomirov@328: int[] childrentChangesetNumbers = new int[historyNode.children.size()]; tikhomirov@328: int j = 0; tikhomirov@328: for (HistoryNode hn : historyNode.children) { tikhomirov@328: childrentChangesetNumbers[j++] = hn.changeset; tikhomirov@328: } tikhomirov@328: children = Arrays.asList(get(childrentChangesetNumbers)); tikhomirov@328: } tikhomirov@328: return children; tikhomirov@328: } tikhomirov@328: tikhomirov@328: void populate(HgChangeset cs) { tikhomirov@403: cachedChangesets.put(cs.getRevisionIndex(), cs); tikhomirov@328: } tikhomirov@328: tikhomirov@423: private HgChangeset[] get(int... changelogRevisionIndex) { tikhomirov@403: HgChangeset[] rv = new HgChangeset[changelogRevisionIndex.length]; tikhomirov@403: IntVector misses = new IntVector(changelogRevisionIndex.length, -1); tikhomirov@403: for (int i = 0; i < changelogRevisionIndex.length; i++) { tikhomirov@403: if (changelogRevisionIndex[i] == -1) { tikhomirov@328: rv[i] = null; tikhomirov@328: continue; tikhomirov@328: } tikhomirov@403: HgChangeset cached = cachedChangesets.get(changelogRevisionIndex[i]); tikhomirov@328: if (cached != null) { tikhomirov@328: rv[i] = cached; tikhomirov@328: } else { tikhomirov@403: misses.add(changelogRevisionIndex[i]); tikhomirov@328: } tikhomirov@328: } tikhomirov@328: if (misses.size() > 0) { tikhomirov@328: final int[] changesets2read = misses.toArray(); tikhomirov@328: initTransform(); tikhomirov@328: repo.getChangelog().range(this, changesets2read); tikhomirov@328: for (int changeset2read : changesets2read) { tikhomirov@328: HgChangeset cs = cachedChangesets.get(changeset2read); tikhomirov@403: if (cs == null) { tikhomirov@423: HgInvalidStateException t = new HgInvalidStateException(String.format("Can't get changeset for revision %d", changeset2read)); tikhomirov@423: throw t.setRevisionIndex(changeset2read); tikhomirov@403: } tikhomirov@403: // HgChangelog.range may reorder changesets according to their order in the changelog tikhomirov@403: // thus need to find original index tikhomirov@403: boolean sanity = false; tikhomirov@403: for (int i = 0; i < changelogRevisionIndex.length; i++) { tikhomirov@403: if (changelogRevisionIndex[i] == cs.getRevisionIndex()) { tikhomirov@403: rv[i] = cs; tikhomirov@403: sanity = true; tikhomirov@403: break; tikhomirov@328: } tikhomirov@403: } tikhomirov@403: if (!sanity) { tikhomirov@490: repo.getSessionContext().getLog().dump(getClass(), Error, "Index of revision %d:%s doesn't match any of requested", cs.getRevisionIndex(), cs.getNodeid().shortNotation()); tikhomirov@403: } tikhomirov@403: assert sanity; tikhomirov@328: } tikhomirov@328: } tikhomirov@328: return rv; tikhomirov@328: } tikhomirov@328: tikhomirov@328: // init only when needed tikhomirov@423: void initTransform() throws HgRuntimeException { tikhomirov@328: if (transform == null) { tikhomirov@328: transform = new ChangesetTransformer.Transformation(new HgStatusCollector(repo)/*XXX try to reuse from context?*/, getParentHelper(false)); tikhomirov@328: } tikhomirov@328: } tikhomirov@328: tikhomirov@328: public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { tikhomirov@328: HgChangeset cs = transform.handle(revisionNumber, nodeid, cset); tikhomirov@328: populate(cs.clone()); tikhomirov@328: } tikhomirov@328: tikhomirov@423: public Nodeid changesetRevision() { tikhomirov@328: if (changesetRevision == null) { tikhomirov@328: changesetRevision = getRevision(historyNode.changeset); tikhomirov@328: } tikhomirov@328: return changesetRevision; tikhomirov@328: } tikhomirov@328: tikhomirov@423: public Pair parentRevisions() { tikhomirov@328: if (parentRevisions == null) { tikhomirov@328: HistoryNode p; tikhomirov@328: final Nodeid p1, p2; tikhomirov@328: if ((p = historyNode.parent1) != null) { tikhomirov@328: p1 = getRevision(p.changeset); tikhomirov@328: } else { tikhomirov@328: p1 = Nodeid.NULL;; tikhomirov@328: } tikhomirov@328: if ((p = historyNode.parent2) != null) { tikhomirov@328: p2 = getRevision(p.changeset); tikhomirov@328: } else { tikhomirov@328: p2 = Nodeid.NULL; tikhomirov@328: } tikhomirov@328: parentRevisions = new Pair(p1, p2); tikhomirov@328: } tikhomirov@328: return parentRevisions; tikhomirov@328: } tikhomirov@328: tikhomirov@423: public Collection childRevisions() { tikhomirov@328: if (childRevisions != null) { tikhomirov@328: return childRevisions; tikhomirov@328: } tikhomirov@328: if (historyNode.children == null) { tikhomirov@328: childRevisions = Collections.emptyList(); tikhomirov@328: } else { tikhomirov@328: ArrayList rv = new ArrayList(historyNode.children.size()); tikhomirov@328: for (HistoryNode hn : historyNode.children) { tikhomirov@328: rv.add(getRevision(hn.changeset)); tikhomirov@328: } tikhomirov@328: childRevisions = Collections.unmodifiableList(rv); tikhomirov@328: } tikhomirov@328: return childRevisions; tikhomirov@328: } tikhomirov@328: tikhomirov@328: // reading nodeid involves reading index only, guess, can afford not to optimize multiple reads tikhomirov@423: private Nodeid getRevision(int changelogRevisionNumber) { tikhomirov@423: // TODO post-1.0 pipe through pool tikhomirov@328: HgChangeset cs = cachedChangesets.get(changelogRevisionNumber); tikhomirov@328: if (cs != null) { tikhomirov@328: return cs.getNodeid(); tikhomirov@328: } else { tikhomirov@403: return repo.getChangelog().getRevision(changelogRevisionNumber); tikhomirov@328: } tikhomirov@328: } tikhomirov@328: } tikhomirov@510: tikhomirov@510: private enum IterateDirection { tikhomirov@510: FromOldToNew, FromNewToOld tikhomirov@510: } tikhomirov@64: }