tikhomirov@55: /* tikhomirov@74: * Copyright (c) 2011 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@74: * contact TMate Software at support@svnkit.com tikhomirov@55: */ tikhomirov@74: package org.tmatesoft.hg.repo; tikhomirov@55: tikhomirov@74: import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; tikhomirov@74: import static org.tmatesoft.hg.repo.HgRepository.TIP; tikhomirov@68: tikhomirov@55: import java.util.Collection; tikhomirov@55: import java.util.Collections; tikhomirov@55: import java.util.HashMap; 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@88: import org.tmatesoft.hg.core.Path; tikhomirov@74: tikhomirov@74: tikhomirov@55: /** tikhomirov@55: * RevisionWalker? tikhomirov@74: * tikhomirov@74: * @author Artem Tikhomirov tikhomirov@74: * @author TMate Software Ltd. tikhomirov@55: */ tikhomirov@55: public class StatusCollector { tikhomirov@55: tikhomirov@55: private final HgRepository repo; tikhomirov@55: private final Map cache; // sparse array, in fact tikhomirov@55: tikhomirov@55: public StatusCollector(HgRepository hgRepo) { tikhomirov@55: this.repo = hgRepo; tikhomirov@55: cache = new HashMap(); tikhomirov@56: ManifestRevisionInspector emptyFakeState = new ManifestRevisionInspector(-1, -1); tikhomirov@56: emptyFakeState.begin(-1, null); tikhomirov@68: emptyFakeState.end(-1); // FIXME HgRepo.TIP == -1 as well, need to distinguish fake "prior to first" revision from "the very last" tikhomirov@56: cache.put(-1, emptyFakeState); tikhomirov@55: } tikhomirov@55: tikhomirov@64: public HgRepository getRepo() { tikhomirov@64: return repo; tikhomirov@64: } tikhomirov@64: tikhomirov@55: private ManifestRevisionInspector get(int rev) { tikhomirov@55: ManifestRevisionInspector i = cache.get(rev); tikhomirov@55: if (i == null) { tikhomirov@55: i = new ManifestRevisionInspector(rev, rev); tikhomirov@55: cache.put(rev, i); tikhomirov@55: repo.getManifest().walk(rev, rev, i); tikhomirov@55: } tikhomirov@55: return i; tikhomirov@55: } tikhomirov@56: tikhomirov@59: /*package-local*/ ManifestRevisionInspector raw(int rev) { tikhomirov@59: return get(rev); tikhomirov@59: } tikhomirov@59: tikhomirov@56: // hg status --change tikhomirov@56: public void change(int rev, Inspector inspector) { tikhomirov@56: int[] parents = new int[2]; tikhomirov@56: repo.getChangelog().parents(rev, parents, null, null); tikhomirov@56: walk(parents[0], rev, inspector); tikhomirov@56: } tikhomirov@55: tikhomirov@56: // I assume revision numbers are the same for changelog and manifest - here tikhomirov@56: // user would like to pass changelog revision numbers, and I use them directly to walk manifest. tikhomirov@56: // if this assumption is wrong, fix this (lookup manifest revisions from changeset). tikhomirov@55: public void walk(int rev1, int rev2, Inspector inspector) { 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@64: if (inspector instanceof Record) { tikhomirov@64: ((Record) inspector).init(rev1, rev2, this); tikhomirov@64: } tikhomirov@68: if (rev1 == TIP) { tikhomirov@68: rev1 = repo.getManifest().getRevisionCount() - 1; tikhomirov@68: } tikhomirov@68: if (rev2 == TIP) { tikhomirov@68: rev2 = repo.getManifest().getRevisionCount() - 1; // XXX add Revlog.tip() func ? tikhomirov@68: } 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@55: ManifestRevisionInspector r1, r2; tikhomirov@55: if (!cache.containsKey(rev1) && !cache.containsKey(rev2) && Math.abs(rev1 - rev2) < 5 /*subjective equivalent of 'close enough'*/) { tikhomirov@55: int minRev = rev1 < rev2 ? rev1 : rev2; tikhomirov@55: int maxRev = minRev == rev1 ? rev2 : rev1; tikhomirov@55: r1 = r2 = new ManifestRevisionInspector(minRev, maxRev); tikhomirov@55: for (int i = minRev; i <= maxRev; i++) { tikhomirov@55: cache.put(i, r1); tikhomirov@55: } tikhomirov@55: repo.getManifest().walk(minRev, maxRev, r1); tikhomirov@55: } else { tikhomirov@55: r1 = get(rev1); tikhomirov@55: r2 = get(rev2); tikhomirov@55: } tikhomirov@55: tikhomirov@55: TreeSet r1Files = new TreeSet(r1.files(rev1)); tikhomirov@55: for (String fname : r2.files(rev2)) { tikhomirov@55: if (r1Files.remove(fname)) { tikhomirov@55: Nodeid nidR1 = r1.nodeid(rev1, fname); tikhomirov@55: Nodeid nidR2 = r2.nodeid(rev2, fname); tikhomirov@55: String flagsR1 = r1.flags(rev1, fname); tikhomirov@55: String flagsR2 = r2.flags(rev2, fname); tikhomirov@55: if (nidR1.equals(nidR2) && ((flagsR2 == null && flagsR1 == null) || flagsR2.equals(flagsR1))) { tikhomirov@55: inspector.clean(fname); tikhomirov@55: } else { tikhomirov@55: inspector.modified(fname); tikhomirov@55: } tikhomirov@55: } else { tikhomirov@88: HgDataFile df = repo.getFileNode(fname); tikhomirov@88: boolean isCopy = false; tikhomirov@88: while (df.isCopy()) { tikhomirov@88: Path original = df.getCopySourceName(); tikhomirov@88: if (r1Files.contains(original.toString())) { tikhomirov@88: df = repo.getFileNode(original); tikhomirov@88: int changelogRevision = df.getChangesetLocalRevision(0); tikhomirov@88: if (changelogRevision <= rev1) { tikhomirov@88: // copy/rename source was known prior to rev1 tikhomirov@88: // (both r1Files.contains is true and original was created earlier than rev1) tikhomirov@88: // without r1Files.contains changelogRevision <= rev1 won't suffice as the file tikhomirov@88: // might get removed somewhere in between (changelogRevision < R < rev1) tikhomirov@88: inspector.copied(original.toString(), fname); tikhomirov@88: isCopy = true; tikhomirov@88: } tikhomirov@88: break; tikhomirov@88: } tikhomirov@88: df = repo.getFileNode(original); // try more steps away tikhomirov@88: } tikhomirov@88: if (!isCopy) { tikhomirov@88: inspector.added(fname); tikhomirov@88: } tikhomirov@55: } tikhomirov@55: } tikhomirov@55: for (String left : r1Files) { tikhomirov@55: inspector.removed(left); tikhomirov@55: } tikhomirov@55: // inspector.done() if useful e.g. in UI client tikhomirov@55: } tikhomirov@55: tikhomirov@55: public Record status(int rev1, int rev2) { tikhomirov@55: Record rv = new Record(); tikhomirov@55: walk(rev1, rev2, rv); tikhomirov@55: return rv; tikhomirov@55: } tikhomirov@55: tikhomirov@55: public interface Inspector { tikhomirov@55: void modified(String fname); tikhomirov@55: void added(String fname); tikhomirov@74: // XXX need to specify whether StatusCollector invokes added() along with copied or not! tikhomirov@55: void copied(String fnameOrigin, String fnameAdded); // if copied files of no interest, should delegate to self.added(fnameAdded); tikhomirov@55: void removed(String fname); tikhomirov@55: void clean(String fname); tikhomirov@55: void missing(String fname); // aka deleted (tracked by Hg, but not available in FS any more tikhomirov@55: void unknown(String fname); // not tracked tikhomirov@55: void ignored(String fname); tikhomirov@55: } 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@55: public static class Record implements Inspector { tikhomirov@55: private List modified, added, removed, clean, missing, unknown, ignored; tikhomirov@55: private Map copied; tikhomirov@55: tikhomirov@64: private int startRev, endRev; tikhomirov@64: private StatusCollector 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@64: /*package-local*/void init(int startRevision, int endRevision, StatusCollector self) { tikhomirov@64: startRev = startRevision; tikhomirov@64: endRev = endRevision; tikhomirov@64: statusHelper = self; tikhomirov@64: } tikhomirov@64: tikhomirov@64: public Nodeid nodeidBeforeChange(String fname) { 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@64: return statusHelper.raw(startRev).nodeid(startRev, fname); tikhomirov@64: } tikhomirov@64: public Nodeid nodeidAfterChange(String fname) { 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@64: return statusHelper.raw(endRev).nodeid(endRev, fname); tikhomirov@64: } tikhomirov@64: tikhomirov@55: public List getModified() { tikhomirov@55: return proper(modified); tikhomirov@55: } tikhomirov@55: tikhomirov@55: public List getAdded() { tikhomirov@55: return proper(added); tikhomirov@55: } tikhomirov@55: tikhomirov@55: public List getRemoved() { tikhomirov@55: return proper(removed); tikhomirov@55: } tikhomirov@55: tikhomirov@55: 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@55: public List getClean() { tikhomirov@55: return proper(clean); tikhomirov@55: } tikhomirov@55: tikhomirov@55: public List getMissing() { tikhomirov@55: return proper(missing); tikhomirov@55: } tikhomirov@55: tikhomirov@55: public List getUnknown() { tikhomirov@55: return proper(unknown); tikhomirov@55: } tikhomirov@55: tikhomirov@55: public List getIgnored() { tikhomirov@55: return proper(ignored); tikhomirov@55: } tikhomirov@55: tikhomirov@55: private 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@55: public void modified(String fname) { tikhomirov@55: modified = doAdd(modified, fname); tikhomirov@55: } tikhomirov@55: tikhomirov@55: public void added(String fname) { tikhomirov@55: added = doAdd(added, fname); tikhomirov@55: } tikhomirov@55: tikhomirov@55: public void copied(String fnameOrigin, String fnameAdded) { tikhomirov@55: if (copied == null) { tikhomirov@55: copied = new LinkedHashMap(); tikhomirov@55: } tikhomirov@74: added(fnameAdded); tikhomirov@74: copied.put(fnameAdded, fnameOrigin); tikhomirov@55: } tikhomirov@55: tikhomirov@55: public void removed(String fname) { tikhomirov@55: removed = doAdd(removed, fname); tikhomirov@55: } tikhomirov@55: tikhomirov@55: public void clean(String fname) { tikhomirov@55: clean = doAdd(clean, fname); tikhomirov@55: } tikhomirov@55: tikhomirov@55: public void missing(String fname) { tikhomirov@55: missing = doAdd(missing, fname); tikhomirov@55: } tikhomirov@55: tikhomirov@55: public void unknown(String fname) { tikhomirov@55: unknown = doAdd(unknown, fname); tikhomirov@55: } tikhomirov@55: tikhomirov@55: public void ignored(String fname) { tikhomirov@55: ignored = doAdd(ignored, fname); tikhomirov@55: } tikhomirov@55: tikhomirov@55: private static List doAdd(List l, String s) { tikhomirov@55: if (l == null) { tikhomirov@55: l = new LinkedList(); tikhomirov@55: } tikhomirov@55: l.add(s); tikhomirov@55: return l; tikhomirov@55: } tikhomirov@55: } tikhomirov@55: tikhomirov@64: // XXX in fact, indexed access brings more trouble than benefits, get rid of it? Distinct instance per revision is good enough tikhomirov@55: public /*XXX private, actually. Made public unless repo.statusLocal finds better place*/ static final class ManifestRevisionInspector implements HgManifest.Inspector { tikhomirov@55: private final HashMap[] idsMap; tikhomirov@55: private final HashMap[] flagsMap; tikhomirov@55: private final int baseRevision; tikhomirov@55: private int r = -1; // cursor tikhomirov@55: tikhomirov@55: /** tikhomirov@55: * [minRev, maxRev] tikhomirov@56: * [-1,-1] also accepted (for fake empty instance) tikhomirov@55: * @param minRev - inclusive tikhomirov@55: * @param maxRev - inclusive tikhomirov@55: */ tikhomirov@55: @SuppressWarnings("unchecked") tikhomirov@55: public ManifestRevisionInspector(int minRev, int maxRev) { tikhomirov@55: baseRevision = minRev; tikhomirov@55: int range = maxRev - minRev + 1; tikhomirov@55: idsMap = new HashMap[range]; tikhomirov@55: flagsMap = new HashMap[range]; tikhomirov@55: } tikhomirov@55: tikhomirov@55: public Collection files(int rev) { tikhomirov@55: if (rev < baseRevision || rev >= baseRevision + idsMap.length) { tikhomirov@55: throw new IllegalArgumentException(); tikhomirov@55: } tikhomirov@55: return idsMap[rev - baseRevision].keySet(); tikhomirov@55: } tikhomirov@55: tikhomirov@55: public Nodeid nodeid(int rev, String fname) { tikhomirov@55: if (rev < baseRevision || rev >= baseRevision + idsMap.length) { tikhomirov@55: throw new IllegalArgumentException(); tikhomirov@55: } tikhomirov@55: return idsMap[rev - baseRevision].get(fname); tikhomirov@55: } tikhomirov@55: tikhomirov@55: public String flags(int rev, String fname) { tikhomirov@55: if (rev < baseRevision || rev >= baseRevision + idsMap.length) { tikhomirov@55: throw new IllegalArgumentException(); tikhomirov@55: } tikhomirov@55: return flagsMap[rev - baseRevision].get(fname); tikhomirov@55: } tikhomirov@55: tikhomirov@55: // tikhomirov@55: tikhomirov@55: public boolean next(Nodeid nid, String fname, String flags) { tikhomirov@55: idsMap[r].put(fname, nid); tikhomirov@55: flagsMap[r].put(fname, flags); tikhomirov@55: return true; tikhomirov@55: } tikhomirov@55: tikhomirov@55: public boolean end(int revision) { tikhomirov@55: assert revision == r + baseRevision; tikhomirov@55: r = -1; tikhomirov@55: return revision+1 < baseRevision + idsMap.length; tikhomirov@55: } tikhomirov@55: tikhomirov@55: public boolean begin(int revision, Nodeid nid) { tikhomirov@55: if (revision < baseRevision || revision >= baseRevision + idsMap.length) { tikhomirov@55: throw new IllegalArgumentException(); tikhomirov@55: } tikhomirov@55: r = revision - baseRevision; tikhomirov@55: idsMap[r] = new HashMap(); tikhomirov@55: flagsMap[r] = new HashMap(); tikhomirov@55: return true; tikhomirov@55: } tikhomirov@55: } tikhomirov@55: tikhomirov@55: }