tikhomirov@55: /* tikhomirov@455: * Copyright (c) 2011-2012 TMate Software Ltd tikhomirov@74: * tikhomirov@74: * This program is free software; you can redistribute it and/or modify tikhomirov@74: * it under the terms of the GNU General Public License as published by tikhomirov@74: * the Free Software Foundation; version 2 of the License. tikhomirov@74: * tikhomirov@74: * This program is distributed in the hope that it will be useful, tikhomirov@74: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@74: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@74: * GNU General Public License for more details. tikhomirov@74: * tikhomirov@74: * For information on how to redistribute this software under tikhomirov@74: * the terms of a license other than GNU General Public License tikhomirov@102: * contact TMate Software at support@hg4j.com tikhomirov@55: */ tikhomirov@74: package org.tmatesoft.hg.repo; tikhomirov@55: tikhomirov@403: import static org.tmatesoft.hg.repo.HgRepository.*; tikhomirov@68: tikhomirov@55: import java.util.Collection; tikhomirov@55: import java.util.Collections; tikhomirov@55: import java.util.LinkedHashMap; tikhomirov@55: import java.util.LinkedList; tikhomirov@55: import java.util.List; tikhomirov@55: import java.util.Map; tikhomirov@55: import java.util.TreeSet; tikhomirov@55: tikhomirov@74: import org.tmatesoft.hg.core.Nodeid; tikhomirov@691: import org.tmatesoft.hg.internal.FileRenameHistory; tikhomirov@691: import org.tmatesoft.hg.internal.FileRenameHistory.Chunk; tikhomirov@281: import org.tmatesoft.hg.internal.IntMap; tikhomirov@248: import org.tmatesoft.hg.internal.ManifestRevision; tikhomirov@195: import org.tmatesoft.hg.internal.Pool; tikhomirov@423: import org.tmatesoft.hg.util.CancelSupport; tikhomirov@423: import org.tmatesoft.hg.util.CancelledException; tikhomirov@431: import org.tmatesoft.hg.util.Convertor; tikhomirov@133: import org.tmatesoft.hg.util.Path; tikhomirov@74: tikhomirov@74: tikhomirov@55: /** tikhomirov@423: * Collect status information for changes between two repository revisions. tikhomirov@74: * tikhomirov@74: * @author Artem Tikhomirov tikhomirov@74: * @author TMate Software Ltd. tikhomirov@55: */ tikhomirov@94: public class HgStatusCollector { tikhomirov@55: tikhomirov@55: private final HgRepository repo; tikhomirov@281: private final IntMap cache; // sparse array, in fact tikhomirov@195: // with cpython repository, ~70 000 changes, complete Log (direct out, no reverse) output tikhomirov@195: // no cache limit, no nodeids and fname caching - OOME on changeset 1035 tikhomirov@195: // no cache limit, but with cached nodeids and filenames - 1730+ tikhomirov@195: // cache limit 100 - 19+ minutes to process 10000, and still working (too long, stopped) tikhomirov@197: private final int cacheMaxSize = 50; // do not keep too much manifest revisions tikhomirov@431: private Convertor pathPool; tikhomirov@195: private final Pool cacheNodes; tikhomirov@285: private final Pool cacheFilenames; tikhomirov@248: private final ManifestRevision emptyFakeState; tikhomirov@229: private Path.Matcher scope = new Path.Matcher.Any(); tikhomirov@455: // @see #detectCopies() tikhomirov@455: private boolean detectCopies = true; tikhomirov@195: tikhomirov@55: tikhomirov@94: public HgStatusCollector(HgRepository hgRepo) { tikhomirov@55: this.repo = hgRepo; tikhomirov@281: cache = new IntMap(cacheMaxSize); tikhomirov@195: cacheNodes = new Pool(); tikhomirov@285: cacheFilenames = new Pool(); tikhomirov@197: tikhomirov@284: emptyFakeState = createEmptyManifestRevision(); tikhomirov@55: } tikhomirov@55: tikhomirov@64: public HgRepository getRepo() { tikhomirov@64: return repo; tikhomirov@64: } tikhomirov@64: tikhomirov@628: private ManifestRevision get(int rev) throws HgRuntimeException { tikhomirov@248: ManifestRevision i = cache.get(rev); tikhomirov@55: if (i == null) { tikhomirov@405: if (rev == NO_REVISION) { tikhomirov@197: return emptyFakeState; tikhomirov@197: } tikhomirov@281: ensureCacheSize(); tikhomirov@248: i = new ManifestRevision(cacheNodes, cacheFilenames); tikhomirov@55: cache.put(rev, i); tikhomirov@55: repo.getManifest().walk(rev, rev, i); tikhomirov@55: } tikhomirov@55: return i; tikhomirov@55: } tikhomirov@197: tikhomirov@197: private boolean cached(int revision) { tikhomirov@405: return cache.containsKey(revision) || revision == NO_REVISION; tikhomirov@197: } tikhomirov@197: tikhomirov@281: private void ensureCacheSize() { tikhomirov@281: if (cache.size() > cacheMaxSize) { tikhomirov@281: // assume usually we go from oldest to newest, hence remove oldest as most likely to be no longer necessary tikhomirov@281: cache.removeFromStart(cache.size() - cacheMaxSize + 1 /* room for new element */); tikhomirov@281: } tikhomirov@281: } tikhomirov@281: tikhomirov@628: private void initCacheRange(int minRev, int maxRev) throws HgRuntimeException { tikhomirov@281: ensureCacheSize(); tikhomirov@302: // In fact, walk(minRev, maxRev) doesn't imply tikhomirov@302: // there would be maxRev-minRev+1 revisions visited. For example, tikhomirov@302: // check cpython repo with 'hg log -r 22418:22420 --debug' and admire tikhomirov@302: // manifest revisions 66650, 21683, 21684. Thus, innocent walk(22418,22420) results in 40k+ revisions and OOME tikhomirov@302: // Instead, be explicit of what revisions are of interest tikhomirov@302: assert minRev <= maxRev; tikhomirov@302: int[] revisionsToCollect = new int[maxRev - minRev + 1]; tikhomirov@302: for (int x = minRev, i = 0; x <= maxRev; i++, x++) { tikhomirov@302: revisionsToCollect[i] = x; tikhomirov@302: } tikhomirov@424: repo.getManifest().walk(new HgManifest.Inspector() { tikhomirov@248: private ManifestRevision delegate; tikhomirov@197: private boolean cacheHit; // range may include revisions we already know about, do not re-create them tikhomirov@197: tikhomirov@222: public boolean begin(int manifestRevision, Nodeid nid, int changelogRevision) { tikhomirov@197: assert delegate == null; tikhomirov@405: if (cache.containsKey(changelogRevision)) { // don't need to check emptyFakeState hit as revision never NO_REVISION here tikhomirov@197: cacheHit = true; tikhomirov@197: } else { tikhomirov@248: cache.put(changelogRevision, delegate = new ManifestRevision(cacheNodes, cacheFilenames)); tikhomirov@197: // cache may grow bigger than max size here, but it's ok as present simplistic cache clearing mechanism may tikhomirov@197: // otherwise remove entries we just added tikhomirov@222: delegate.begin(manifestRevision, nid, changelogRevision); tikhomirov@197: cacheHit = false; tikhomirov@197: } tikhomirov@197: return true; tikhomirov@197: } tikhomirov@197: tikhomirov@285: public boolean next(Nodeid nid, Path fname, HgManifest.Flags flags) { tikhomirov@197: if (!cacheHit) { tikhomirov@197: delegate.next(nid, fname, flags); tikhomirov@197: } tikhomirov@197: return true; tikhomirov@197: } tikhomirov@197: tikhomirov@197: public boolean end(int revision) { tikhomirov@197: if (!cacheHit) { tikhomirov@197: delegate.end(revision); tikhomirov@197: } tikhomirov@197: cacheHit = false; tikhomirov@197: delegate = null; tikhomirov@197: return true; tikhomirov@197: } tikhomirov@302: }, revisionsToCollect); tikhomirov@197: } tikhomirov@56: tikhomirov@284: /*package-local*/ static ManifestRevision createEmptyManifestRevision() { tikhomirov@284: ManifestRevision fakeEmptyRev = new ManifestRevision(null, null); tikhomirov@405: fakeEmptyRev.begin(NO_REVISION, null, NO_REVISION); tikhomirov@405: fakeEmptyRev.end(NO_REVISION); tikhomirov@284: return fakeEmptyRev; tikhomirov@284: } tikhomirov@284: tikhomirov@405: /** tikhomirov@405: * Access specific manifest revision tikhomirov@405: * @param rev tikhomirov@405: * @return tikhomirov@405: * @throws HgInvalidControlFileException tikhomirov@405: */ tikhomirov@628: /*package-local*/ ManifestRevision raw(int rev) throws HgRuntimeException { tikhomirov@59: return get(rev); tikhomirov@59: } tikhomirov@431: /*package-local*/ Convertor getPathPool() { tikhomirov@93: if (pathPool == null) { tikhomirov@431: pathPool = cacheFilenames; tikhomirov@93: } tikhomirov@93: return pathPool; tikhomirov@93: } tikhomirov@93: tikhomirov@142: /** tikhomirov@142: * Allows sharing of a common path cache tikhomirov@142: */ tikhomirov@431: public void setPathPool(Convertor pathConvertor) { tikhomirov@431: pathPool = pathConvertor; tikhomirov@93: } tikhomirov@229: tikhomirov@229: /** tikhomirov@229: * Limit activity of the collector to certain sub-tree of the repository. tikhomirov@229: * @param scopeMatcher tells whether collector shall report specific path, can be null tikhomirov@229: */ tikhomirov@229: public void setScope(Path.Matcher scopeMatcher) { tikhomirov@229: // do not assign null, ever tikhomirov@229: scope = scopeMatcher == null ? new Path.Matcher.Any() : scopeMatcher; tikhomirov@229: } tikhomirov@455: tikhomirov@455: /** tikhomirov@455: * Select whether Collector shall tell "added-new" from "added-by-copy/rename" files. tikhomirov@455: * This is analogous to '-C' switch of 'hg status' command. tikhomirov@455: * tikhomirov@455: *

With copy detection turned off, files continue be reported as plain 'added' files. tikhomirov@455: * tikhomirov@455: *

By default, copy detection is on, as it's reasonably cheap. However, tikhomirov@455: * in certain scenarios it may be reasonable to turn it off, for example when it's a merge tikhomirov@455: * of two very different branches and there are a lot of files added/moved. tikhomirov@455: * tikhomirov@455: * Another legitimate reason to set detection to off if you're lazy to tikhomirov@455: * implement {@link HgStatusInspector#copied(Path, Path)} ;) tikhomirov@455: * tikhomirov@455: * @param detect true if copies detection is desirable tikhomirov@455: */ tikhomirov@455: public void detectCopies(boolean detect) { tikhomirov@455: // cpython, revision:72161, p1:72159, p2:72160 tikhomirov@455: // p2 comes from another branch with 321 file added (looks like copied/moved, however, the isCopy tikhomirov@455: // record present only for couple of them). With 2,5 ms per isCopy() operation, almost a second tikhomirov@455: // is spent detecting origins (according to Marc, of little use in this scenario, as it's second parent tikhomirov@455: // in the merge) - in fact, most of the time of the status operation tikhomirov@455: detectCopies = detect; tikhomirov@455: } tikhomirov@59: tikhomirov@403: /** tikhomirov@403: * 'hg status --change REV' command counterpart. tikhomirov@403: * tikhomirov@423: * @throws CancelledException if operation execution was cancelled tikhomirov@423: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception tikhomirov@403: */ tikhomirov@423: public void change(int revisionIndex, HgStatusInspector inspector) throws CancelledException, HgRuntimeException { tikhomirov@405: int p; tikhomirov@405: if (revisionIndex == 0) { tikhomirov@405: p = NO_REVISION; tikhomirov@405: } else { tikhomirov@405: int[] parents = new int[2]; tikhomirov@405: repo.getChangelog().parents(revisionIndex, parents, null, null); tikhomirov@405: // #parents call above is responsible for NO_REVISION tikhomirov@405: p = parents[0]; // hg --change alsways uses first parent, despite the fact there might be valid (-1, 18) pair of parents tikhomirov@405: } tikhomirov@405: walk(p, revisionIndex, inspector); tikhomirov@56: } tikhomirov@197: tikhomirov@403: /** tikhomirov@403: * Parameters rev1 and rev2 are changelog revision indexes, shall not be the same. Argument order matters. tikhomirov@405: * Either rev1 or rev2 may be {@link HgRepository#NO_REVISION} to indicate comparison to empty repository tikhomirov@405: * tikhomirov@403: * @param rev1 from changeset index, non-negative or {@link HgRepository#TIP} tikhomirov@403: * @param rev2 to changeset index, non-negative or {@link HgRepository#TIP} tikhomirov@403: * @param inspector callback for status information tikhomirov@423: * @throws CancelledException if operation execution was cancelled tikhomirov@423: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception tikhomirov@403: * @throws IllegalArgumentException inspector other incorrect argument values tikhomirov@403: */ tikhomirov@423: public void walk(int rev1, int rev2, HgStatusInspector inspector) throws CancelledException, HgRuntimeException, IllegalArgumentException { tikhomirov@55: if (rev1 == rev2) { tikhomirov@55: throw new IllegalArgumentException(); tikhomirov@55: } tikhomirov@64: if (inspector == null) { tikhomirov@64: throw new IllegalArgumentException(); tikhomirov@64: } tikhomirov@403: final int lastChangelogRevision = repo.getChangelog().getLastRevision(); tikhomirov@68: if (rev1 == TIP) { tikhomirov@403: rev1 = lastChangelogRevision; tikhomirov@68: } tikhomirov@68: if (rev2 == TIP) { tikhomirov@403: rev2 = lastChangelogRevision; tikhomirov@403: } tikhomirov@405: if (rev1 != NO_REVISION && (HgInternals.wrongRevisionIndex(rev1) || rev1 == WORKING_COPY || rev1 == BAD_REVISION || rev1 > lastChangelogRevision)) { tikhomirov@403: throw new HgInvalidRevisionException(rev1); tikhomirov@403: } tikhomirov@405: if (rev2 != NO_REVISION && (HgInternals.wrongRevisionIndex(rev2) || rev2 == WORKING_COPY || rev2 == BAD_REVISION || rev2 > lastChangelogRevision)) { tikhomirov@403: throw new HgInvalidRevisionException(rev2); tikhomirov@68: } tikhomirov@282: if (inspector instanceof Record) { tikhomirov@282: ((Record) inspector).init(rev1, rev2, this); tikhomirov@282: } tikhomirov@55: // in fact, rev1 and rev2 are often next (or close) to each other, tikhomirov@55: // thus, we can optimize Manifest reads here (manifest.walk(rev1, rev2)) tikhomirov@248: ManifestRevision r1, r2 ; tikhomirov@197: boolean need1 = !cached(rev1), need2 = !cached(rev2); tikhomirov@197: if (need1 || need2) { tikhomirov@197: int minRev, maxRev; tikhomirov@197: if (need1 && need2 && Math.abs(rev1 - rev2) < 5 /*subjective equivalent of 'close enough'*/) { tikhomirov@197: minRev = rev1 < rev2 ? rev1 : rev2; tikhomirov@197: maxRev = minRev == rev1 ? rev2 : rev1; tikhomirov@197: if (minRev > 0) { tikhomirov@197: minRev--; // expand range a bit tikhomirov@197: } tikhomirov@197: initCacheRange(minRev, maxRev); tikhomirov@197: need1 = need2 = false; tikhomirov@55: } tikhomirov@197: // either both unknown and far from each other, or just one of them. tikhomirov@197: // read with neighbors to save potential subsequent calls for neighboring elements tikhomirov@197: // XXX perhaps, if revlog.baseRevision is cheap, shall expand minRev up to baseRevision tikhomirov@197: // which going to be read anyway tikhomirov@197: if (need1) { tikhomirov@197: minRev = rev1; tikhomirov@403: maxRev = rev1 < lastChangelogRevision-5 ? rev1+5 : lastChangelogRevision; tikhomirov@197: initCacheRange(minRev, maxRev); tikhomirov@197: } tikhomirov@197: if (need2) { tikhomirov@197: minRev = rev2; tikhomirov@403: maxRev = rev2 < lastChangelogRevision-5 ? rev2+5 : lastChangelogRevision; tikhomirov@197: initCacheRange(minRev, maxRev); tikhomirov@197: } tikhomirov@55: } tikhomirov@89: r1 = get(rev1); tikhomirov@89: r2 = get(rev2); tikhomirov@89: tikhomirov@423: final CancelSupport cs = CancelSupport.Factory.get(inspector); tikhomirov@423: tikhomirov@690: tikhomirov@690: Collection allBaseFiles = r1.files(); tikhomirov@690: TreeSet r1Files = new TreeSet(allBaseFiles); tikhomirov@285: for (Path r2fname : r2.files()) { tikhomirov@285: if (!scope.accept(r2fname)) { tikhomirov@226: continue; tikhomirov@226: } tikhomirov@285: if (r1Files.remove(r2fname)) { tikhomirov@285: Nodeid nidR1 = r1.nodeid(r2fname); tikhomirov@285: Nodeid nidR2 = r2.nodeid(r2fname); tikhomirov@285: HgManifest.Flags flagsR1 = r1.flags(r2fname); tikhomirov@285: HgManifest.Flags flagsR2 = r2.flags(r2fname); tikhomirov@285: if (nidR1.equals(nidR2) && flagsR2 == flagsR1) { tikhomirov@285: inspector.clean(r2fname); tikhomirov@55: } else { tikhomirov@285: inspector.modified(r2fname); tikhomirov@55: } tikhomirov@423: cs.checkCancelled(); tikhomirov@55: } else { tikhomirov@157: try { tikhomirov@285: Path copyTarget = r2fname; tikhomirov@690: Path copyOrigin = detectCopies ? getOriginIfCopy(repo, copyTarget, r2.nodeid(copyTarget), allBaseFiles, rev1) : null; tikhomirov@157: if (copyOrigin != null) { tikhomirov@431: inspector.copied(getPathPool().mangle(copyOrigin) /*pipe through pool, just in case*/, copyTarget); tikhomirov@157: } else { tikhomirov@157: inspector.added(copyTarget); tikhomirov@157: } tikhomirov@425: } catch (HgInvalidFileException ex) { tikhomirov@360: // record exception to a mediator and continue, tikhomirov@157: // for a single file not to be irresolvable obstacle for a status operation tikhomirov@360: inspector.invalid(r2fname, ex); tikhomirov@88: } tikhomirov@423: cs.checkCancelled(); tikhomirov@55: } tikhomirov@55: } tikhomirov@285: for (Path r1fname : r1Files) { tikhomirov@285: if (scope.accept(r1fname)) { tikhomirov@285: inspector.removed(r1fname); tikhomirov@423: cs.checkCancelled(); tikhomirov@226: } tikhomirov@55: } tikhomirov@55: } tikhomirov@55: tikhomirov@403: /** tikhomirov@403: * Collects status between two revisions, changes from rev1 up to rev2. tikhomirov@403: * tikhomirov@403: * @param rev1 from changeset index tikhomirov@403: * @param rev2 to changeset index tikhomirov@403: * @return information object that describes change between the revisions tikhomirov@628: * @throws HgInvalidRevisionException if any supplied revision doesn't identify changeset revision. Runtime exception tikhomirov@628: * @throws HgInvalidControlFileException if failed to access revlog index/data entry. Runtime exception tikhomirov@628: * @throws HgRuntimeException subclass thereof to indicate other issues with the library. Runtime exception tikhomirov@403: */ tikhomirov@628: public Record status(int rev1, int rev2) throws HgRuntimeException { tikhomirov@55: Record rv = new Record(); tikhomirov@423: try { tikhomirov@423: walk(rev1, rev2, rv); tikhomirov@423: } catch (CancelledException ex) { tikhomirov@423: // can't happen as long our Record class doesn't implement CancelSupport tikhomirov@423: HgInvalidStateException t = new HgInvalidStateException("Internal error"); tikhomirov@423: t.initCause(ex); tikhomirov@423: throw t; tikhomirov@423: } tikhomirov@55: return rv; tikhomirov@55: } tikhomirov@90: tikhomirov@690: // originals shall include all names known in base revision, not those not yet consumed tikhomirov@690: // see TestStatus#testDetectRenamesInNonFirstRev and log-renames test repository tikhomirov@690: // where a and d in r5 are compared to a and b in r1, while d is in fact descendant of original a, and a is original b (through c) tikhomirov@689: /*package-local*/static Path getOriginIfCopy(HgRepository hgRepo, Path fname, Nodeid fnameRev, Collection originals, int originalChangesetIndex) throws HgRuntimeException { tikhomirov@90: HgDataFile df = hgRepo.getFileNode(fname); tikhomirov@320: if (!df.exists()) { tikhomirov@320: String msg = String.format("Didn't find file '%s' in the repo. Perhaps, bad storage name conversion?", fname); tikhomirov@689: throw new HgInvalidFileException(msg, null).setFileName(fname).setRevisionIndex(originalChangesetIndex); tikhomirov@320: } tikhomirov@689: assert fnameRev != null; tikhomirov@689: assert !Nodeid.NULL.equals(fnameRev); tikhomirov@689: int fileRevIndex = fnameRev == null ? 0 : df.getRevisionIndex(fnameRev); tikhomirov@691: FileRenameHistory frh = new FileRenameHistory(originalChangesetIndex, df.getChangesetRevisionIndex(fileRevIndex)); tikhomirov@691: if (frh.isOutOfRange(df, fileRevIndex)) { tikhomirov@691: return null; tikhomirov@90: } tikhomirov@691: frh.build(df, fileRevIndex); tikhomirov@691: Chunk c = frh.chunkAt(originalChangesetIndex); tikhomirov@691: if (c == null) { tikhomirov@691: // file rename history doesn't go deep up to changeset of interest tikhomirov@691: return null; tikhomirov@691: } tikhomirov@691: Path nameAtOrigin = c.file().getPath(); tikhomirov@691: if (originals.contains(nameAtOrigin)) { tikhomirov@691: return nameAtOrigin; tikhomirov@691: } tikhomirov@691: return null; tikhomirov@90: } tikhomirov@55: tikhomirov@55: // XXX for r1..r2 status, only modified, added, removed (and perhaps, clean) make sense tikhomirov@74: // XXX Need to specify whether copy targets are in added or not (@see Inspector#copied above) tikhomirov@316: /** tikhomirov@316: * Straightforward {@link HgStatusInspector} implementation that collects all status values. tikhomirov@316: * tikhomirov@316: *

Naturally, {@link Record Records} originating from {@link HgStatusCollector} would report only modified, added, tikhomirov@316: * removed and clean values, other are available only when using {@link Record} with {@link HgWorkingCopyStatusCollector}. tikhomirov@316: * tikhomirov@316: *

Note, this implementation records copied files as added, thus key values in {@link #getCopied()} map are subset of paths tikhomirov@316: * from {@link #getAdded()}. tikhomirov@316: */ tikhomirov@93: public static class Record implements HgStatusInspector { tikhomirov@423: // NOTE, shall not implement CancelSupport, or methods that use it and don't expect this exception shall be changed tikhomirov@93: private List modified, added, removed, clean, missing, unknown, ignored; tikhomirov@93: private Map copied; tikhomirov@360: private Map failures; tikhomirov@55: tikhomirov@64: private int startRev, endRev; tikhomirov@94: private HgStatusCollector statusHelper; tikhomirov@64: tikhomirov@64: // XXX StatusCollector may additionally initialize Record instance to speed lookup of changed file revisions tikhomirov@64: // here I need access to ManifestRevisionInspector via #raw(). Perhaps, non-static class (to get tikhomirov@64: // implicit reference to StatusCollector) may be better? tikhomirov@64: // Since users may want to reuse Record instance we've once created (and initialized), we need to tikhomirov@64: // ensure functionality is correct for each/any call (#walk checks instanceof Record and fixes it up) tikhomirov@64: // Perhaps, distinct helper (sc.getRevisionHelper().nodeid(fname)) would be better, just not clear tikhomirov@64: // how to supply [start..end] values there easily tikhomirov@94: /*package-local*/void init(int startRevision, int endRevision, HgStatusCollector self) { tikhomirov@64: startRev = startRevision; tikhomirov@64: endRev = endRevision; tikhomirov@64: statusHelper = self; tikhomirov@64: } tikhomirov@64: tikhomirov@628: public Nodeid nodeidBeforeChange(Path fname) throws HgRuntimeException { tikhomirov@68: if (statusHelper == null || startRev == BAD_REVISION) { tikhomirov@68: return null; tikhomirov@68: } tikhomirov@64: if ((modified == null || !modified.contains(fname)) && (removed == null || !removed.contains(fname))) { tikhomirov@64: return null; tikhomirov@64: } tikhomirov@285: return statusHelper.raw(startRev).nodeid(fname); tikhomirov@64: } tikhomirov@628: public Nodeid nodeidAfterChange(Path fname) throws HgRuntimeException { tikhomirov@68: if (statusHelper == null || endRev == BAD_REVISION) { tikhomirov@68: return null; tikhomirov@68: } tikhomirov@64: if ((modified == null || !modified.contains(fname)) && (added == null || !added.contains(fname))) { tikhomirov@64: return null; tikhomirov@64: } tikhomirov@285: return statusHelper.raw(endRev).nodeid(fname); tikhomirov@64: } tikhomirov@64: tikhomirov@93: public List getModified() { tikhomirov@55: return proper(modified); tikhomirov@55: } tikhomirov@55: tikhomirov@93: public List getAdded() { tikhomirov@55: return proper(added); tikhomirov@55: } tikhomirov@55: tikhomirov@93: public List getRemoved() { tikhomirov@55: return proper(removed); tikhomirov@55: } tikhomirov@55: tikhomirov@316: /** tikhomirov@316: * Map files from {@link #getAdded()} to their original filenames, if were copied/moved. tikhomirov@316: */ tikhomirov@93: public Map getCopied() { tikhomirov@55: if (copied == null) { tikhomirov@55: return Collections.emptyMap(); tikhomirov@55: } tikhomirov@55: return Collections.unmodifiableMap(copied); tikhomirov@55: } tikhomirov@55: tikhomirov@93: public List getClean() { tikhomirov@55: return proper(clean); tikhomirov@55: } tikhomirov@55: tikhomirov@93: public List getMissing() { tikhomirov@55: return proper(missing); tikhomirov@55: } tikhomirov@55: tikhomirov@93: public List getUnknown() { tikhomirov@55: return proper(unknown); tikhomirov@55: } tikhomirov@55: tikhomirov@93: public List getIgnored() { tikhomirov@55: return proper(ignored); tikhomirov@55: } tikhomirov@360: tikhomirov@360: public Map getInvalid() { tikhomirov@360: if (failures == null) { tikhomirov@360: return Collections.emptyMap(); tikhomirov@360: } tikhomirov@360: return Collections.unmodifiableMap(failures); tikhomirov@360: } tikhomirov@55: tikhomirov@316: private static List proper(List l) { tikhomirov@55: if (l == null) { tikhomirov@55: return Collections.emptyList(); tikhomirov@55: } tikhomirov@55: return Collections.unmodifiableList(l); tikhomirov@55: } tikhomirov@55: tikhomirov@55: // tikhomirov@55: // tikhomirov@55: tikhomirov@93: public void modified(Path fname) { tikhomirov@55: modified = doAdd(modified, fname); tikhomirov@55: } tikhomirov@55: tikhomirov@93: public void added(Path fname) { tikhomirov@55: added = doAdd(added, fname); tikhomirov@55: } tikhomirov@55: tikhomirov@93: public void copied(Path fnameOrigin, Path fnameAdded) { tikhomirov@55: if (copied == null) { tikhomirov@93: copied = new LinkedHashMap(); tikhomirov@55: } tikhomirov@74: added(fnameAdded); tikhomirov@74: copied.put(fnameAdded, fnameOrigin); tikhomirov@55: } tikhomirov@55: tikhomirov@93: public void removed(Path fname) { tikhomirov@55: removed = doAdd(removed, fname); tikhomirov@55: } tikhomirov@55: tikhomirov@93: public void clean(Path fname) { tikhomirov@55: clean = doAdd(clean, fname); tikhomirov@55: } tikhomirov@55: tikhomirov@93: public void missing(Path fname) { tikhomirov@55: missing = doAdd(missing, fname); tikhomirov@55: } tikhomirov@55: tikhomirov@93: public void unknown(Path fname) { tikhomirov@55: unknown = doAdd(unknown, fname); tikhomirov@55: } tikhomirov@55: tikhomirov@93: public void ignored(Path fname) { tikhomirov@55: ignored = doAdd(ignored, fname); tikhomirov@55: } tikhomirov@360: tikhomirov@360: public void invalid(Path fname, Exception ex) { tikhomirov@360: if (failures == null) { tikhomirov@360: failures = new LinkedHashMap(); tikhomirov@360: } tikhomirov@360: failures.put(fname, ex); tikhomirov@360: } tikhomirov@55: tikhomirov@93: private static List doAdd(List l, Path p) { tikhomirov@55: if (l == null) { tikhomirov@93: l = new LinkedList(); tikhomirov@55: } tikhomirov@93: l.add(p); tikhomirov@55: return l; tikhomirov@55: } tikhomirov@55: } tikhomirov@55: tikhomirov@55: }