/*
s * Copyright (c) 2011-2013 TMate Software Ltd
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* For information on how to redistribute this software under
* the terms of a license other than GNU General Public License
* contact TMate Software at support@hg4j.com
*/
package org.tmatesoft.hg.core;
import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION;
import static org.tmatesoft.hg.repo.HgRepository.TIP;
import static org.tmatesoft.hg.util.LogFacility.Severity.Error;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.tmatesoft.hg.internal.AdapterPlug;
import org.tmatesoft.hg.internal.BatchRangeHelper;
import org.tmatesoft.hg.internal.CsetParamKeeper;
import org.tmatesoft.hg.internal.FileRenameHistory;
import org.tmatesoft.hg.internal.FileRenameHistory.Chunk;
import org.tmatesoft.hg.internal.IntMap;
import org.tmatesoft.hg.internal.IntVector;
import org.tmatesoft.hg.internal.Internals;
import org.tmatesoft.hg.internal.Lifecycle;
import org.tmatesoft.hg.internal.LifecycleProxy;
import org.tmatesoft.hg.internal.ReverseIterator;
import org.tmatesoft.hg.repo.HgChangelog;
import org.tmatesoft.hg.repo.HgChangelog.RawChangeset;
import org.tmatesoft.hg.repo.HgDataFile;
import org.tmatesoft.hg.repo.HgInvalidStateException;
import org.tmatesoft.hg.repo.HgParentChildMap;
import org.tmatesoft.hg.repo.HgRepository;
import org.tmatesoft.hg.repo.HgRuntimeException;
import org.tmatesoft.hg.repo.HgStatusCollector;
import org.tmatesoft.hg.util.Adaptable;
import org.tmatesoft.hg.util.CancelSupport;
import org.tmatesoft.hg.util.CancelledException;
import org.tmatesoft.hg.util.Pair;
import org.tmatesoft.hg.util.Path;
import org.tmatesoft.hg.util.ProgressSupport;
/**
* Access to changelog, 'hg log' command counterpart.
*
* <pre>
* Usage:
* new LogCommand().limit(20).branch("maintenance-2.1").user("me").execute(new MyHandler());
* </pre>
* Not thread-safe (each thread has to use own {@link HgLogCommand} instance).
*
* @author Artem Tikhomirov
* @author TMate Software Ltd.
*/
public class HgLogCommand extends HgAbstractCommand<HgLogCommand> {
private final HgRepository repo;
private Set<String> users;
private Set<String> branches;
private int limit = 0, count = 0;
private int startRev = 0, endRev = TIP;
private Calendar date;
private Path file;
/*
* Whether to iterate file origins, if any.
* Makes sense only when file != null
*/
private boolean followRenames;
/*
* Whether to track history of the selected file version (based on file revision
* in working dir parent), follow ancestors only.
* Note, 'hg log --follow' combines both #followHistory and #followAncestry
*/
private boolean followAncestry;
private HgIterateDirection iterateDirection = HgIterateDirection.OldToNew;
private ChangesetTransformer csetTransform;
private HgParentChildMap<HgChangelog> parentHelper;
public HgLogCommand(HgRepository hgRepo) {
repo = hgRepo;
}
/**
* Limit search to specified user. Multiple user names may be specified. Once set, user names can't be
* cleared, use new command instance in such cases.
* @param user - full or partial name of the user, case-insensitive, non-null.
* @return <code>this</code> instance for convenience
* @throws IllegalArgumentException when argument is null
*/
public HgLogCommand user(String user) {
if (user == null) {
throw new IllegalArgumentException();
}
if (users == null) {
users = new TreeSet<String>();
}
users.add(user.toLowerCase());
return this;
}
/**
* Limit search to specified branch. Multiple branch specification possible (changeset from any of these
* would be included in result). If unspecified, all branches are considered. There's no way to clean branch selection
* once set, create fresh new command instead.
* @param branch - branch name, case-sensitive, non-null.
* @return <code>this</code> instance for convenience
* @throws IllegalArgumentException when branch argument is null
*/
public HgLogCommand branch(String branch) {
if (branch == null) {
throw new IllegalArgumentException();
}
if (branches == null) {
branches = new TreeSet<String>();
}
branches.add(branch);
return this;
}
// limit search to specific date
// multiple?
public HgLogCommand date(Calendar date) {
this.date = date;
// TODO post-1.0 implement
// isSet(field) - false => don't use in detection of 'same date'
throw Internals.notImplemented();
}
/**
*
* @param num - number of changeset to produce. Pass 0 to clear the limit.
* @return <code>this</code> instance for convenience
*/
public HgLogCommand limit(int num) {
limit = num;
return this;
}
/**
* Limit to specified subset of Changelog, [min(rev1,rev2), max(rev1,rev2)], inclusive.
* Revision may be specified with {@link HgRepository#TIP}
*
* @param rev1 - local index of start changeset revision
* @param rev2 - index of end changeset revision
* @return <code>this</code> instance for convenience
*/
public HgLogCommand range(int rev1, int rev2) {
if (rev1 != TIP && rev2 != TIP) {
startRev = rev2 < rev1 ? rev2 : rev1;
endRev = startRev == rev2 ? rev1 : rev2;
} else if (rev1 == TIP && rev2 != TIP) {
startRev = rev2;
endRev = rev1;
} else {
startRev = rev1;
endRev = rev2;
}
// TODO [2.0 API break] shall throw HgBadArgumentException, like other commands do
return this;
}
/**
* Limit history to specified range.
*
* @see #range(int, int)
* @param cset1 range start revision
* @param cset2 range end revision
* @return <code>this</code> instance for convenience
* @throws HgBadArgumentException if revisions are not valid changeset identifiers
*/
public HgLogCommand range(Nodeid cset1, Nodeid cset2) throws HgBadArgumentException {
CsetParamKeeper pk = new CsetParamKeeper(repo);
int r1 = pk.set(cset1).get();
int r2 = pk.set(cset2).get();
return range(r1, r2);
}
/**
* Select specific changeset by index
* @see #changeset(Nodeid)
* @param revisionIndex index of changelog revision
* @return <code>this</code> for convenience
* @throws HgBadArgumentException if failed to find supplied changeset revision
*/
public HgLogCommand changeset(int revisionIndex) throws HgBadArgumentException {
int ri = new CsetParamKeeper(repo).set(revisionIndex).get();
return range(ri, ri);
}
/**
* Select specific changeset
*
* @param nid changeset revision
* @return <code>this</code> for convenience
* @throws HgBadArgumentException if failed to find supplied changeset revision
*/
public HgLogCommand changeset(Nodeid nid) throws HgBadArgumentException {
// XXX perhaps, shall support multiple (...) arguments and extend #execute to handle not only range, but also set of revisions.
final int csetRevIndex = new CsetParamKeeper(repo).set(nid).get();
return range(csetRevIndex, csetRevIndex);
}
/**
* Visit history of a given file only. Note, unlike native <code>hg log</code> command argument <code>--follow</code>, this method doesn't
* follow file ancestry, but reports complete file history (with <code>followCopyRenames == true</code>, for each
* name of the file known in sequence). To achieve output similar to that of <code>hg log --follow filePath</code>, use
* {@link #file(Path, boolean, boolean) file(filePath, true, true)} alternative.
*
* @param filePath path relative to repository root. Pass <code>null</code> to reset.
* @param followCopyRename true to report changesets of the original file(-s), if copy/rename ever occured to the file.
* @return <code>this</code> for convenience
*/
public HgLogCommand file(Path filePath, boolean followCopyRename) {
return file(filePath, followCopyRename, false);
}
/**
* Full control over file history iteration.
*
* @param filePath path relative to repository root. Pass <code>null</code> to reset.
* @param followCopyRename true to report changesets of the original file(-s), if copy/rename ever occured to the file.
* @param followFileAncestry true to follow file history starting from revision at working copy parent. Note, only revisions
* accessible (i.e. on direct parent line) from the selected one will be reported. This is how <code>hg log --follow filePath</code>
* behaves, with the difference that this method allows separate control whether to follow renames or not.
*
* @return <code>this</code> for convenience
*/
public HgLogCommand file(Path filePath, boolean followCopyRename, boolean followFileAncestry) {
file = filePath;
followRenames = followCopyRename;
followAncestry = followFileAncestry;
return this;
}
/**
* Handy analog to {@link #file(Path, boolean)} when clients' paths come from filesystem and need conversion to repository's
* @return <code>this</code> for convenience
*/
public HgLogCommand file(String file, boolean followCopyRename) {
Path.Source ps = repo.getSessionContext().getPathFactory();
return file(ps.path(repo.getToRepoPathHelper().rewrite(file)), followCopyRename);
}
/**
* Handy analog to {@link #file(Path, boolean, boolean)} when clients' paths come from filesystem and need conversion to repository's
* @return <code>this</code> for convenience
*/
public HgLogCommand file(String file, boolean followCopyRename, boolean followFileAncestry) {
Path.Source ps = repo.getSessionContext().getPathFactory();
return file(ps.path(repo.getToRepoPathHelper().rewrite(file)), followCopyRename, followFileAncestry);
}
/**
* Specifies order for changesets reported through #execute(...) methods.
* By default, command reports changeset in their natural repository order, older first,
* newer last (i.e. {@link HgIterateDirection#OldToNew}
*
* @param order {@link HgIterateDirection#NewToOld} to get newer revisions first
* @return <code>this</code> for convenience
*/
public HgLogCommand order(HgIterateDirection order) {
iterateDirection = order;
return this;
}
/**
* Similar to {@link #execute(HgChangesetHandler)}, collects and return result as a list.
*
* @see #execute(HgChangesetHandler)
* @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state
*/
public List<HgChangeset> execute() throws HgException {
CollectHandler collector = new CollectHandler();
try {
execute(collector);
} catch (HgCallbackTargetException ex) {
// see below for CanceledException
HgInvalidStateException t = new HgInvalidStateException("Internal error");
t.initCause(ex);
throw t;
} catch (CancelledException ex) {
// can't happen as long as our CollectHandler doesn't throw any exception
HgInvalidStateException t = new HgInvalidStateException("Internal error");
t.initCause(ex);
throw t;
}
return collector.getChanges();
}
/**
* Iterate over range of changesets configured in the command.
*
* @param handler callback to process changesets.
* @throws HgCallbackTargetException propagated exception from the handler
* @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state
* @throws CancelledException if execution of the command was cancelled
* @throws IllegalArgumentException when inspector argument is null
* @throws ConcurrentModificationException if this log command instance is already running
*/
public void execute(HgChangesetHandler handler) throws HgCallbackTargetException, HgException, CancelledException {
if (handler == null) {
throw new IllegalArgumentException();
}
if (csetTransform != null) {
throw new ConcurrentModificationException();
}
final ProgressSupport progressHelper = getProgressSupport(handler);
try {
if (repo.getChangelog().getRevisionCount() == 0) {
return;
}
final int firstCset = startRev;
final int lastCset = endRev == TIP ? repo.getChangelog().getLastRevision() : endRev;
// XXX pretty much like HgInternals.checkRevlogRange
if (lastCset < 0 || lastCset > repo.getChangelog().getLastRevision()) {
throw new HgBadArgumentException(String.format("Bad value %d for end revision", lastCset), null);
}
if (firstCset < 0 || firstCset > lastCset) {
throw new HgBadArgumentException(String.format("Bad value %d for start revision for range [%1$d..%d]", firstCset, lastCset), null);
}
final int BATCH_SIZE = 100;
count = 0;
HgParentChildMap<HgChangelog> pw = getParentHelper(file == null); // leave it uninitialized unless we iterate whole repo
// ChangesetTransfrom creates a blank PathPool, and #file(String, boolean) above
// may utilize it as well. CommandContext? How about StatusCollector there as well?
csetTransform = new ChangesetTransformer(repo, handler, pw, progressHelper, getCancelSupport(handler, true));
// FilteringInspector is responsible to check command arguments: users, branches, limit, etc.
// prior to passing cset to next Inspector, which is either (a) collector to reverse cset order, then invokes
// transformer from (b), below, with alternative cset order or (b) transformer to hi-level csets.
FilteringInspector filterInsp = new FilteringInspector();
filterInsp.changesets(firstCset, lastCset);
if (file == null) {
progressHelper.start(lastCset - firstCset + 1);
if (iterateDirection == HgIterateDirection.OldToNew) {
filterInsp.delegateTo(csetTransform);
repo.getChangelog().range(firstCset, lastCset, filterInsp);
csetTransform.checkFailure();
} else {
assert iterateDirection == HgIterateDirection.NewToOld;
BatchRangeHelper brh = new BatchRangeHelper(firstCset, lastCset, BATCH_SIZE, true);
BatchChangesetInspector batchInspector = new BatchChangesetInspector(Math.min(lastCset-firstCset+1, BATCH_SIZE));
filterInsp.delegateTo(batchInspector);
// XXX this batching code is bit verbose, refactor
while (brh.hasNext()) {
brh.next();
repo.getChangelog().range(brh.start(), brh.end(), filterInsp);
for (BatchChangesetInspector.BatchRecord br : batchInspector.iterate(true)) {
csetTransform.next(br.csetIndex, br.csetRevision, br.cset);
csetTransform.checkFailure();
}
batchInspector.reset();
}
}
} else {
filterInsp.delegateTo(csetTransform);
final HgFileRenameHandlerMixin withCopyHandler = Adaptable.Factory.getAdapter(handler, HgFileRenameHandlerMixin.class, null);
FileRenameQueueBuilder frqBuilder = new FileRenameQueueBuilder();
List<QueueElement> fileRenames = frqBuilder.buildFileRenamesQueue(firstCset, lastCset);
progressHelper.start(fileRenames.size());
for (int nameIndex = 0, fileRenamesSize = fileRenames.size(); nameIndex < fileRenamesSize; nameIndex++) {
QueueElement curRename = fileRenames.get(nameIndex);
HgDataFile fileNode = curRename.file();
if (followAncestry) {
TreeBuildInspector treeBuilder = new TreeBuildInspector(followAncestry);
@SuppressWarnings("unused")
List<HistoryNode> fileAncestry = treeBuilder.go(curRename);
int[] commitRevisions = narrowChangesetRange(treeBuilder.getCommitRevisions(), firstCset, lastCset);
if (iterateDirection == HgIterateDirection.OldToNew) {
repo.getChangelog().range(filterInsp, commitRevisions);
csetTransform.checkFailure();
} else {
assert iterateDirection == HgIterateDirection.NewToOld;
// visit one by one in the opposite direction
for (int i = commitRevisions.length-1; i >= 0; i--) {
int csetWithFileChange = commitRevisions[i];
repo.getChangelog().range(csetWithFileChange, csetWithFileChange, filterInsp);
}
}
} else {
// report complete file history (XXX may narrow range with [startRev, endRev], but need to go from file rev to link rev)
int fileStartRev = curRename.fileFrom();
int fileEndRev = curRename.file().getLastRevision(); //curRename.fileTo();
if (iterateDirection == HgIterateDirection.OldToNew) {
fileNode.history(fileStartRev, fileEndRev, filterInsp);
csetTransform.checkFailure();
} else {
assert iterateDirection == HgIterateDirection.NewToOld;
BatchRangeHelper brh = new BatchRangeHelper(fileStartRev, fileEndRev, BATCH_SIZE, true);
BatchChangesetInspector batchInspector = new BatchChangesetInspector(Math.min(fileEndRev-fileStartRev+1, BATCH_SIZE));
filterInsp.delegateTo(batchInspector);
while (brh.hasNext()) {
brh.next();
fileNode.history(brh.start(), brh.end(), filterInsp);
for (BatchChangesetInspector.BatchRecord br : batchInspector.iterate(true /*iterateDirection == IterateDirection.FromNewToOld*/)) {
csetTransform.next(br.csetIndex, br.csetRevision, br.cset);
csetTransform.checkFailure();
}
batchInspector.reset();
}
}
}
if (withCopyHandler != null && nameIndex + 1 < fileRenamesSize) {
QueueElement nextRename = fileRenames.get(nameIndex+1);
HgFileRevision src, dst;
// A -> B
if (iterateDirection == HgIterateDirection.OldToNew) {
// curRename: A, nextRename: B
src = curRename.last();
dst = nextRename.first(src);
} else {
assert iterateDirection == HgIterateDirection.NewToOld;
// curRename: B, nextRename: A
src = nextRename.last();
dst = curRename.first(src);
}
withCopyHandler.copy(src, dst);
}
progressHelper.worked(1);
} // for renames
frqBuilder.reportRenameIfNotInQueue(fileRenames, withCopyHandler);
} // file != null
} catch (HgRuntimeException ex) {
throw new HgLibraryFailureException(ex);
} finally {
csetTransform = null;
progressHelper.done();
}
}
private static class BatchChangesetInspector extends AdapterPlug implements HgChangelog.Inspector {
private static class BatchRecord {
public final int csetIndex;
public final Nodeid csetRevision;
public final RawChangeset cset;
public BatchRecord(int index, Nodeid nodeid, RawChangeset changeset) {
csetIndex = index;
csetRevision = nodeid;
cset = changeset;
}
}
private final ArrayList<BatchRecord> batch;
public BatchChangesetInspector(int batchSizeHint) {
batch = new ArrayList<BatchRecord>(batchSizeHint);
}
public BatchChangesetInspector reset() {
batch.clear();
return this;
}
public void next(int revisionIndex, Nodeid nodeid, RawChangeset cset) {
batch.add(new BatchRecord(revisionIndex, nodeid, cset.clone()));
}
public Iterable<BatchRecord> iterate(final boolean reverse) {
return reverse ? ReverseIterator.reversed(batch) : batch;
}
// alternative would be dispatch(HgChangelog.Inspector) and dispatchReverse()
// methods, but progress and cancellation might get messy then
}
// public static void main(String[] args) {
// int[] r = new int[] {17, 19, 21, 23, 25, 29};
// System.out.println(Arrays.toString(narrowChangesetRange(r, 0, 45)));
// System.out.println(Arrays.toString(narrowChangesetRange(r, 0, 25)));
// System.out.println(Arrays.toString(narrowChangesetRange(r, 5, 26)));
// System.out.println(Arrays.toString(narrowChangesetRange(r, 20, 26)));
// System.out.println(Arrays.toString(narrowChangesetRange(r, 26, 28)));
// }
private static int[] narrowChangesetRange(int[] csetRange, int startCset, int endCset) {
int lastInRange = csetRange[csetRange.length-1];
assert csetRange.length < 2 || csetRange[0] < lastInRange; // sorted
assert startCset >= 0 && startCset <= endCset;
if (csetRange[0] >= startCset && lastInRange <= endCset) {
// completely fits in
return csetRange;
}
if (csetRange[0] > endCset || lastInRange < startCset) {
return new int[0]; // trivial
}
int i = 0;
while (i < csetRange.length && csetRange[i] < startCset) {
i++;
}
int j = csetRange.length - 1;
while (j > i && csetRange[j] > endCset) {
j--;
}
if (i == j) {
// no values in csetRange fit into [startCset, endCset]
return new int[0];
}
int[] rv = new int[j-i+1];
System.arraycopy(csetRange, i, rv, 0, rv.length);
return rv;
}
/**
* Tree-wise iteration of a file history, with handy access to parent-child relations between changesets.
* When file history is being followed, handler may additionally implement {@link HgFileRenameHandlerMixin}
* to get notified about switching between history chunks that belong to different names.
*
* @param handler callback to process changesets.
* @see HgFileRenameHandlerMixin
* @throws HgCallbackTargetException propagated exception from the handler
* @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state
* @throws CancelledException if execution of the command was cancelled
* @throws IllegalArgumentException if command is not satisfied with its arguments
* @throws ConcurrentModificationException if this log command instance is already running
*/
public void execute(final HgChangesetTreeHandler handler) throws HgCallbackTargetException, HgException, CancelledException {
if (handler == null) {
throw new IllegalArgumentException();
}
if (csetTransform != null) {
throw new ConcurrentModificationException();
}
if (file == null) {
throw new IllegalArgumentException("History tree is supported for files only (at least now), please specify file");
}
final int firstCset = startRev;
final int lastCset = endRev == TIP ? repo.getChangelog().getLastRevision() : endRev;
// XXX pretty much like HgInternals.checkRevlogRange
if (lastCset < 0 || lastCset > repo.getChangelog().getLastRevision()) {
throw new HgBadArgumentException(String.format("Bad value %d for end revision", lastCset), null);
}
if (firstCset < 0 || startRev > lastCset) {
throw new HgBadArgumentException(String.format("Bad value %d for start revision for range [%1$d..%d]", startRev, lastCset), null);
}
final ProgressSupport progressHelper = getProgressSupport(handler);
final CancelSupport cancelHelper = getCancelSupport(handler, true);
final HgFileRenameHandlerMixin renameHandler = Adaptable.Factory.getAdapter(handler, HgFileRenameHandlerMixin.class, null);
try {
// XXX rename. dispatcher is not a proper name (most of the job done - managing history chunk interconnection)
final HandlerDispatcher dispatcher = new HandlerDispatcher() {
@Override
protected void once(HistoryNode n) throws HgCallbackTargetException, CancelledException, HgRuntimeException {
handler.treeElement(ei.init(n, currentFileNode));
cancelHelper.checkCancelled();
}
};
// renamed files in the queue are placed with respect to #iterateDirection
// i.e. if we iterate from new to old, recent filenames come first
FileRenameQueueBuilder frqBuilder = new FileRenameQueueBuilder();
List<QueueElement> fileRenamesQueue = frqBuilder.buildFileRenamesQueue(firstCset, lastCset);
// XXX perhaps, makes sense to look at selected file's revision when followAncestry is true
// to ensure file we attempt to trace is in the WC's parent. Native hg aborts if not.
progressHelper.start(4 * fileRenamesQueue.size());
for (int namesIndex = 0, renamesQueueSize = fileRenamesQueue.size(); namesIndex < renamesQueueSize; namesIndex++) {
final QueueElement renameInfo = fileRenamesQueue.get(namesIndex);
dispatcher.prepare(progressHelper, renameInfo);
cancelHelper.checkCancelled();
if (namesIndex > 0) {
dispatcher.connectWithLastJunctionPoint(renameInfo, fileRenamesQueue.get(namesIndex - 1));
}
if (namesIndex + 1 < renamesQueueSize) {
// there's at least one more name we are going to look at
dispatcher.updateJunctionPoint(renameInfo, fileRenamesQueue.get(namesIndex+1), renameHandler != null);
} else {
dispatcher.clearJunctionPoint();
}
dispatcher.dispatchAllChanges();
if (renameHandler != null && namesIndex + 1 < renamesQueueSize) {
dispatcher.reportRenames(renameHandler);
}
} // for fileRenamesQueue;
frqBuilder.reportRenameIfNotInQueue(fileRenamesQueue, renameHandler);
} catch (HgRuntimeException ex) {
throw new HgLibraryFailureException(ex);
}
progressHelper.done();
}
private static class QueueElement {
private final HgDataFile df;
private final Nodeid lastRev;
private final int firstRevIndex, lastRevIndex;
QueueElement(HgDataFile file, Nodeid fileLastRev) {
df = file;
lastRev = fileLastRev;
firstRevIndex = 0;
lastRevIndex = lastRev == null ? df.getLastRevision() : df.getRevisionIndex(lastRev);
}
QueueElement(HgDataFile file, int firstFileRev, int lastFileRev) {
df = file;
firstRevIndex = firstFileRev;
lastRevIndex = lastFileRev;
lastRev = null;
}
HgDataFile file() {
return df;
}
int fileFrom() {
return firstRevIndex;
}
int fileTo() {
return lastRevIndex;
}
// never null
Nodeid lastFileRev() {
return lastRev == null ? df.getRevision(fileTo()) : lastRev;
}
HgFileRevision last() {
return new HgFileRevision(df, lastFileRev(), null);
}
HgFileRevision first(HgFileRevision from) {
return new HgFileRevision(df, df.getRevision(0), from.getPath());
}
}
/**
* Utility to build sequence of file renames
*/
private class FileRenameQueueBuilder {
/**
* 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).
*
* @param followRename when <code>false</code>, the list contains one element only,
* file node with the name of the file as it was specified by the user.
*
* @param followAncestry the most recent file revision reported depends on this parameter,
* and it is file revision from working copy parent in there when it's true.
* <code>null</code> as Pair's second indicates file's TIP revision shall be used.
*
* 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, ordered with respect to {@link #iterateDirection}
* @throws HgRuntimeException
*/
public List<QueueElement> buildFileRenamesQueue(int csetStart, int csetEnd) throws HgPathNotFoundException, HgRuntimeException {
LinkedList<QueueElement> rv = new LinkedList<QueueElement>();
Nodeid startRev = null;
HgDataFile fileNode = repo.getFileNode(file);
if (!fileNode.exists()) {
throw new HgPathNotFoundException(String.format("File %s not found in the repository", file), file);
}
if (followAncestry) {
// 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);
startRev = repo.getManifest().getFileRevision(wdParentRevIndex, fileNode.getPath());
}
// else fall-through, assume null (eventually, lastRevision()) is ok here
}
QueueElement p = new QueueElement(fileNode, startRev);
if (!followRenames) {
rv.add(p);
return rv;
}
FileRenameHistory frh = new FileRenameHistory(csetStart, csetEnd);
frh.build(fileNode, p.fileTo());
for (Chunk c : frh.iterate(iterateDirection)) {
rv.add(new QueueElement(c.file(), c.firstFileRev(), c.lastFileRev()));
}
return rv;
}
/**
* Shall report renames based solely on HgFileRenameHandlerMixin presence,
* even if queue didn't get rename information due to followRenames == false
*
* @param queue value from {@link #buildFileRenamesQueue()}
* @param renameHandler may be <code>null</code>
*/
public void reportRenameIfNotInQueue(List<QueueElement> queue, HgFileRenameHandlerMixin renameHandler) throws HgCallbackTargetException, HgRuntimeException {
if (renameHandler != null && !followRenames) {
// If followRenames is true, all the historical names were in the queue and are processed already.
// Hence, shall process origin explicitly only when renameHandler is present but followRenames is not requested.
assert queue.size() == 1; // see the way queue is constructed above
QueueElement curRename = queue.get(0);
if (curRename.file().isCopy(curRename.fileFrom())) {
final HgFileRevision src = curRename.file().getCopySource(curRename.fileFrom());
HgFileRevision dst = curRename.first(src);
renameHandler.copy(src, dst);
}
}
}
}
/**
* Builds list of {@link HistoryNode HistoryNodes} to visit for a given chunk of file rename history
*/
private static class TreeBuildInspector implements HgChangelog.ParentInspector, HgChangelog.RevisionInspector {
private final boolean followAncestry;
private HistoryNode[] completeHistory;
private int[] commitRevisions;
private List<HistoryNode> resultHistory;
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;
// IMPORTANT: method #one(), below, doesn't expect this code expects reasonable values at parent indexes
if (parent1 != -1) {
p1 = completeHistory[parent1];
}
if (parent2!= -1) {
p2 = completeHistory[parent2];
}
completeHistory[revisionNumber] = new HistoryNode(commitRevisions[revisionNumber], revision, p1, p2);
}
HistoryNode one(HgDataFile fileNode, Nodeid fileRevision) throws HgRuntimeException {
int fileRevIndexToVisit = fileNode.getRevisionIndex(fileRevision);
return one(fileNode, fileRevIndexToVisit);
}
HistoryNode one(HgDataFile fileNode, int fileRevIndexToVisit) throws HgRuntimeException {
resultHistory = null;
if (fileRevIndexToVisit == HgRepository.TIP) {
fileRevIndexToVisit = fileNode.getLastRevision();
}
// still, allocate whole array, for #next to be able to get null parent values
completeHistory = new HistoryNode[fileRevIndexToVisit+1];
commitRevisions = new int[completeHistory.length];
fileNode.indexWalk(fileRevIndexToVisit, fileRevIndexToVisit, this);
// it's only single revision, no need to care about followAncestry
// but won't hurt to keep resultHistory != null and commitRevisions initialized just in case
HistoryNode rv = completeHistory[fileRevIndexToVisit];
commitRevisions = new int[] { commitRevisions[fileRevIndexToVisit] };
completeHistory = null; // no need to keep almost empty array in memory
resultHistory = Collections.singletonList(rv);
return rv;
}
/**
* FIXME pretty much the same as FileRevisionHistoryChunk
*
* 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.
*
* @return list of history elements, from oldest to newest. In case {@link #followAncestry} is <code>true</code>, the list
* is modifiable (to further augment with last/first elements of renamed file histories)
*/
List<HistoryNode> go(QueueElement qe) throws HgRuntimeException {
resultHistory = null;
HgDataFile fileNode = qe.file();
// TODO int fileLastRevIndexToVisit = qe.fileTo
int fileLastRevIndexToVisit = followAncestry ? fileNode.getRevisionIndex(qe.lastFileRev()) : fileNode.getLastRevision();
completeHistory = new HistoryNode[fileLastRevIndexToVisit+1];
commitRevisions = new int[completeHistory.length];
fileNode.indexWalk(qe.fileFrom(), fileLastRevIndexToVisit, this);
if (!followAncestry) {
resultHistory = new ArrayList<HistoryNode>(fileLastRevIndexToVisit - qe.fileFrom() + 1);
// items in completeHistory with index < qe.fileFrom are empty
for (int i = qe.fileFrom(); i <= fileLastRevIndexToVisit; i++) {
resultHistory.add(completeHistory[i]);
}
completeHistory = null;
commitRevisions = null;
return resultHistory;
}
/*
* Changesets, newest at the top:
* 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[fileLastRevIndexToVisit]);
do {
HistoryNode withFileChange = queue.removeFirst();
if (strippedHistoryList.contains(withFileChange)) {
// fork point for the change that was later merged (and we traced
// both lines of development by now.
continue;
}
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());
Collections.sort(strippedHistoryList, new Comparator<HistoryNode>() {
public int compare(HistoryNode o1, HistoryNode o2) {
return o1.changeset - o2.changeset;
}
});
completeHistory = null;
commitRevisions = null;
return resultHistory = strippedHistoryList;
}
/**
* handy access to all HistoryNode[i].changeset values
*/
int[] getCommitRevisions() {
if (commitRevisions == null) {
commitRevisions = new int[resultHistory.size()];
int i = 0;
for (HistoryNode n : resultHistory) {
commitRevisions[i++] = n.changeset;
}
}
return commitRevisions;
}
};
/**
* Sends {@link ElementImpl} for each {@link HistoryNode}, and keeps track of junction points - revisions with renames
*/
private abstract class HandlerDispatcher {
private final int CACHE_CSET_IN_ADVANCE_THRESHOLD = 100; /* XXX is it really worth it? */
// builds tree of nodes according to parents in file's revlog
private final TreeBuildInspector treeBuildInspector = new TreeBuildInspector(followAncestry);
private List<HistoryNode> changeHistory;
protected ElementImpl ei = null;
private ProgressSupport progress;
protected HgDataFile currentFileNode;
// node where current file history chunk intersects with same file under other name history
// either mock of B(0) or A(k), depending on iteration order
private HistoryNode junctionNode;
// initialized when there's HgFileRenameHandlerMixin
private HgFileRevision copiedFrom, copiedTo;
// parentProgress shall be initialized with 4 XXX refactor all this stuff with parentProgress
public void prepare(ProgressSupport parentProgress, QueueElement renameInfo) throws HgRuntimeException {
changeHistory = treeBuildInspector.go(renameInfo);
assert changeHistory.size() > 0;
parentProgress.worked(1);
int historyNodeCount = changeHistory.size();
if (ei == null) {
// when follow is true, changeHistory.size() of the first revision might be quite short
// (e.g. bad fname recognized soon), hence ensure at least cache size at once
ei = new ElementImpl(Math.max(CACHE_CSET_IN_ADVANCE_THRESHOLD, historyNodeCount));
}
if (historyNodeCount < CACHE_CSET_IN_ADVANCE_THRESHOLD ) {
int[] commitRevisions = treeBuildInspector.getCommitRevisions();
assert commitRevisions.length == changeHistory.size();
// read bunch of changesets at once and cache 'em
ei.initTransform();
repo.getChangelog().range(ei, commitRevisions);
parentProgress.worked(1);
progress = new ProgressSupport.Sub(parentProgress, 2);
} else {
progress = new ProgressSupport.Sub(parentProgress, 3);
}
progress.start(historyNodeCount);
// switch to present chunk's file node
switchTo(renameInfo.file());
}
public void updateJunctionPoint(QueueElement curRename, QueueElement nextRename, boolean needCopyFromTo) throws HgRuntimeException {
copiedFrom = copiedTo = null;
//
// A (old) renamed to B(new). A(0..k..n) -> B(0..m). If followAncestry, k == n
// curRename.second() points to A(k)
if (iterateDirection == HgIterateDirection.OldToNew) {
// looking at A chunk (curRename), nextRename points to B
HistoryNode junctionSrc = findJunctionPointInCurrentChunk(curRename.lastFileRev()); // A(k)
HistoryNode junctionDestMock = treeBuildInspector.one(nextRename.file(), 0); // B(0)
// junstionDestMock is mock object, once we iterate next rename, there'd be different HistoryNode
// for B's first revision. This means we read it twice, but this seems to be reasonable
// price for simplicity of the code (and opportunity to follow renames while not following ancestry)
junctionSrc.bindChild(junctionDestMock);
// Save mock A(k) 1) not to keep whole A history in memory 2) Don't need it's parent and children once get to B
// moreover, children of original A(k) (junctionSrc) would list mock B(0) which is undesired once we iterate over real B
junctionNode = new HistoryNode(junctionSrc.changeset, junctionSrc.fileRevision, null, null);
if (needCopyFromTo) {
copiedFrom = new HgFileRevision(curRename.file(), junctionNode.fileRevision, null); // "A", A(k)
copiedTo = new HgFileRevision(nextRename.file(), junctionDestMock.fileRevision, copiedFrom.getPath()); // "B", B(0)
}
} else {
assert iterateDirection == HgIterateDirection.NewToOld;
// looking at B chunk (curRename), nextRename points at A
HistoryNode junctionDest = changeHistory.get(0); // B(0)
// prepare mock A(k)
HistoryNode junctionSrcMock = treeBuildInspector.one(nextRename.file(), nextRename.lastFileRev()); // A(k)
// B(0) to list A(k) as its parent
// NOTE, A(k) would be different when we reach A chunk on the next iteration,
// but we do not care as long as TreeElement needs only parent/child changesets
// and not other TreeElements; so that it's enough to have mock parent node (just
// for the sake of parent cset revisions). We have to, indeed, update real A(k),
// once we get to iteration over A, with B(0) (junctionDest) as one more child.
junctionSrcMock.bindChild(junctionDest);
// Save mock B(0), for reasons see above for opposite direction
junctionNode = new HistoryNode(junctionDest.changeset, junctionDest.fileRevision, null, null);
if (needCopyFromTo) {
copiedFrom = new HgFileRevision(nextRename.file(), junctionSrcMock.fileRevision, null); // "A", A(k)
copiedTo = new HgFileRevision(curRename.file(), junctionNode.fileRevision, copiedFrom.getPath()); // "B", B(0)
}
}
}
public void reportRenames(HgFileRenameHandlerMixin renameHandler) throws HgCallbackTargetException, HgRuntimeException {
if (renameHandler != null) { // shall report renames
assert copiedFrom != null;
assert copiedTo != null;
renameHandler.copy(copiedFrom, copiedTo);
}
}
public void clearJunctionPoint() {
junctionNode = null;
copiedFrom = copiedTo = null;
}
/**
* Replace mock src/dest HistoryNode connected to junctionNode with a real one
*/
public void connectWithLastJunctionPoint(QueueElement curRename, QueueElement prevRename) {
assert junctionNode != null;
// A renamed to B. A(0..k..n) -> B(0..m). If followAncestry: k == n
if (iterateDirection == HgIterateDirection.OldToNew) {
// forward, from old to new:
// changeHistory points to B
// Already reported: A(0)..A(n), A(k) is in junctionNode
// Shall connect histories: A(k).bind(B(0))
HistoryNode junctionDest = changeHistory.get(0); // B(0)
// junctionNode is A(k)
junctionNode.bindChild(junctionDest);
} else {
assert iterateDirection == HgIterateDirection.NewToOld;
// changeHistory points to A
// Already reported B(m), B(m-1)...B(0), B(0) is in junctionNode
// Shall connect histories A(k).bind(B(0))
// if followAncestry: A(k) is latest in changeHistory (k == n)
HistoryNode junctionSrc = findJunctionPointInCurrentChunk(curRename.lastFileRev()); // A(k)
junctionSrc.bindChild(junctionNode);
}
}
private HistoryNode findJunctionPointInCurrentChunk(Nodeid fileRevision) {
if (followAncestry) {
// use the fact we don't go past junction point when followAncestry == true
HistoryNode rv = changeHistory.get(changeHistory.size() - 1);
assert rv.fileRevision.equals(fileRevision);
return rv;
}
for (HistoryNode n : changeHistory) {
if (n.fileRevision.equals(fileRevision)) {
return n;
}
}
int csetStart = changeHistory.get(0).changeset;
int csetEnd = changeHistory.get(changeHistory.size() - 1).changeset;
throw new HgInvalidStateException(String.format("For change history (cset[%d..%d]) could not find node for file change %s", csetStart, csetEnd, fileRevision.shortNotation()));
}
protected abstract void once(HistoryNode n) throws HgCallbackTargetException, CancelledException, HgRuntimeException;
public void dispatchAllChanges() throws HgCallbackTargetException, CancelledException, HgRuntimeException {
// XXX shall sort changeHistory according to changeset numbers?
Iterator<HistoryNode> it;
if (iterateDirection == HgIterateDirection.OldToNew) {
it = changeHistory.listIterator();
} else {
assert iterateDirection == HgIterateDirection.NewToOld;
it = new ReverseIterator<HistoryNode>(changeHistory);
}
while(it.hasNext()) {
HistoryNode n = it.next();
once(n);
progress.worked(1);
}
changeHistory = null;
}
public void switchTo(HgDataFile df) {
// from now on, use df in TreeElement
currentFileNode = df;
}
}
//
private class FilteringInspector extends AdapterPlug implements HgChangelog.Inspector, Adaptable {
private int firstCset = BAD_REVISION, lastCset = BAD_REVISION;
private HgChangelog.Inspector delegate;
// we use lifecycle to stop when limit is reached.
// delegate, however, may use lifecycle, too, so give it a chance
private LifecycleProxy lifecycleProxy;
// limit to changesets in this range only
public void changesets(int start, int end) {
firstCset = start;
lastCset = end;
}
public void delegateTo(HgChangelog.Inspector inspector) {
delegate = inspector;
// let delegate control life cycle, too
if (lifecycleProxy == null) {
super.attachAdapter(Lifecycle.class, lifecycleProxy = new LifecycleProxy(inspector));
} else {
lifecycleProxy.init(inspector);
}
}
public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) throws HgRuntimeException {
if (limit > 0 && count >= limit) {
return;
}
// XXX may benefit from optional interface with #isInterested(int csetRev) - to avoid
// RawChangeset instantiation
if (firstCset != BAD_REVISION && revisionNumber < firstCset) {
return;
}
if (lastCset != BAD_REVISION && revisionNumber > lastCset) {
return;
}
if (branches != null && !branches.contains(cset.branch())) {
return;
}
if (users != null) {
String csetUser = cset.user().toLowerCase();
boolean found = false;
for (String u : users) {
if (csetUser.indexOf(u) != -1) {
found = true;
break;
}
}
if (!found) {
return;
}
}
if (date != null) {
// TODO post-1.0 implement date support for log
}
delegate.next(revisionNumber, nodeid, cset);
count++;
if (limit > 0 && count >= limit) {
lifecycleProxy.stop();
}
}
}
private HgParentChildMap<HgChangelog> getParentHelper(boolean create) throws HgRuntimeException {
if (parentHelper == null && create) {
parentHelper = new HgParentChildMap<HgChangelog>(repo.getChangelog());
parentHelper.init();
}
return parentHelper;
}
public static class CollectHandler implements HgChangesetHandler {
private final List<HgChangeset> result = new LinkedList<HgChangeset>();
public List<HgChangeset> getChanges() {
return Collections.unmodifiableList(result);
}
public void cset(HgChangeset changeset) {
result.add(changeset.clone());
}
}
private static class HistoryNode {
final int changeset;
final Nodeid fileRevision;
HistoryNode parent1; // there's special case when we can alter it, see #bindChild()
final HistoryNode parent2;
List<HistoryNode> children;
HistoryNode(int cs, Nodeid revision, HistoryNode p1, HistoryNode p2) {
changeset = cs;
fileRevision = revision;
parent1 = p1;
parent2 = p2;
if (p1 != null) {
p1.addChild(this);
}
if (p2 != null) {
p2.addChild(this);
}
}
private void addChild(HistoryNode child) {
if (children == null) {
children = new ArrayList<HistoryNode>(2);
}
children.add(child);
}
/**
* method to merge two history chunks for renamed file so that
* this node's history continues (or forks, if we don't followAncestry)
* with that of child
* @param child
*/
public void bindChild(HistoryNode child) {
assert child.parent1 == null && child.parent2 == null;
child.parent1 = this;
addChild(child);
}
public String toString() {
return String.format("<cset:%d, parents: %s, %s>", changeset, parent1 == null ? "-" : String.valueOf(parent1.changeset), parent2 == null ? "-" : String.valueOf(parent2.changeset));
}
}
private class ElementImpl implements HgChangesetTreeHandler.TreeElement, HgChangelog.Inspector {
private HistoryNode historyNode;
private HgDataFile fileNode;
private Pair<HgChangeset, HgChangeset> parents;
private List<HgChangeset> children;
private IntMap<HgChangeset> cachedChangesets;
private ChangesetTransformer.Transformation transform;
private Nodeid changesetRevision;
private Pair<Nodeid,Nodeid> parentRevisions;
private List<Nodeid> childRevisions;
public ElementImpl(int total) {
cachedChangesets = new IntMap<HgChangeset>(total);
}
ElementImpl init(HistoryNode n, HgDataFile df) {
historyNode = n;
fileNode = df;
parents = null;
children = null;
changesetRevision = null;
parentRevisions = null;
childRevisions = null;
return this;
}
public Nodeid fileRevision() {
return historyNode.fileRevision;
}
public HgDataFile file() {
return fileNode;
}
public HgChangeset changeset() throws HgRuntimeException {
return get(historyNode.changeset)[0];
}
public Pair<HgChangeset, HgChangeset> parents() throws HgRuntimeException {
if (parents != null) {
return parents;
}
HistoryNode p;
final int p1, p2;
if ((p = historyNode.parent1) != null) {
p1 = p.changeset;
} else {
p1 = -1;
}
if ((p = historyNode.parent2) != null) {
p2 = p.changeset;
} else {
p2 = -1;
}
HgChangeset[] r = get(p1, p2);
return parents = new Pair<HgChangeset, HgChangeset>(r[0], r[1]);
}
public Collection<HgChangeset> children() throws HgRuntimeException {
if (children != null) {
return children;
}
if (historyNode.children == null) {
children = Collections.emptyList();
} else {
int[] childrentChangesetNumbers = new int[historyNode.children.size()];
int j = 0;
for (HistoryNode hn : historyNode.children) {
childrentChangesetNumbers[j++] = hn.changeset;
}
children = Arrays.asList(get(childrentChangesetNumbers));
}
return children;
}
void populate(HgChangeset cs) {
cachedChangesets.put(cs.getRevisionIndex(), cs);
}
private HgChangeset[] get(int... changelogRevisionIndex) throws HgRuntimeException {
HgChangeset[] rv = new HgChangeset[changelogRevisionIndex.length];
IntVector misses = new IntVector(changelogRevisionIndex.length, -1);
for (int i = 0; i < changelogRevisionIndex.length; i++) {
if (changelogRevisionIndex[i] == -1) {
rv[i] = null;
continue;
}
HgChangeset cached = cachedChangesets.get(changelogRevisionIndex[i]);
if (cached != null) {
rv[i] = cached;
} else {
misses.add(changelogRevisionIndex[i]);
}
}
if (misses.size() > 0) {
final int[] changesets2read = misses.toArray();
initTransform();
repo.getChangelog().range(this, changesets2read);
for (int changeset2read : changesets2read) {
HgChangeset cs = cachedChangesets.get(changeset2read);
if (cs == null) {
throw new HgInvalidStateException(String.format("Can't get changeset for revision %d", changeset2read));
}
// HgChangelog.range may reorder changesets according to their order in the changelog
// thus need to find original index
boolean sanity = false;
for (int i = 0; i < changelogRevisionIndex.length; i++) {
if (changelogRevisionIndex[i] == cs.getRevisionIndex()) {
rv[i] = cs;
sanity = true;
break;
}
}
if (!sanity) {
repo.getSessionContext().getLog().dump(getClass(), Error, "Index of revision %d:%s doesn't match any of requested", cs.getRevisionIndex(), cs.getNodeid().shortNotation());
}
assert sanity;
}
}
return rv;
}
// init only when needed
void initTransform() throws HgRuntimeException {
if (transform == null) {
transform = new ChangesetTransformer.Transformation(new HgStatusCollector(repo)/*XXX try to reuse from context?*/, getParentHelper(false));
}
}
public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) {
HgChangeset cs = transform.handle(revisionNumber, nodeid, cset);
populate(cs.clone());
}
public Nodeid changesetRevision() throws HgRuntimeException {
if (changesetRevision == null) {
changesetRevision = getRevision(historyNode.changeset);
}
return changesetRevision;
}
public Pair<Nodeid, Nodeid> parentRevisions() throws HgRuntimeException {
if (parentRevisions == null) {
HistoryNode p;
final Nodeid p1, p2;
if ((p = historyNode.parent1) != null) {
p1 = getRevision(p.changeset);
} else {
p1 = Nodeid.NULL;;
}
if ((p = historyNode.parent2) != null) {
p2 = getRevision(p.changeset);
} else {
p2 = Nodeid.NULL;
}
parentRevisions = new Pair<Nodeid, Nodeid>(p1, p2);
}
return parentRevisions;
}
public Collection<Nodeid> childRevisions() throws HgRuntimeException {
if (childRevisions != null) {
return childRevisions;
}
if (historyNode.children == null) {
childRevisions = Collections.emptyList();
} else {
ArrayList<Nodeid> rv = new ArrayList<Nodeid>(historyNode.children.size());
for (HistoryNode hn : historyNode.children) {
rv.add(getRevision(hn.changeset));
}
childRevisions = Collections.unmodifiableList(rv);
}
return childRevisions;
}
// reading nodeid involves reading index only, guess, can afford not to optimize multiple reads
private Nodeid getRevision(int changelogRevisionNumber) throws HgRuntimeException {
// TODO post-1.0 pipe through pool
HgChangeset cs = cachedChangesets.get(changelogRevisionNumber);
if (cs != null) {
return cs.getNodeid();
} else {
return repo.getChangelog().getRevision(changelogRevisionNumber);
}
}
}
}