# HG changeset patch # User Artem Tikhomirov # Date 1338922206 -7200 # Node ID d0e5dc3cae6e3cf51e8149a373d5dde123fe1090 # Parent 63c5a9d7ca3f91e8345899656e68c15c350586ff Support for phases functionality from Mercurial 2.1 diff -r 63c5a9d7ca3f -r d0e5dc3cae6e cmdline/org/tmatesoft/hg/console/ChangesetDumpHandler.java --- a/cmdline/org/tmatesoft/hg/console/ChangesetDumpHandler.java Wed Mar 21 14:54:02 2012 +0100 +++ b/cmdline/org/tmatesoft/hg/console/ChangesetDumpHandler.java Tue Jun 05 20:50:06 2012 +0200 @@ -106,6 +106,7 @@ sb.append('\n'); } if (complete) { + f.format("phase: %s\n", cset.getPhase().name()); Nodeid p1 = cset.getFirstParentRevision(); Nodeid p2 = cset.getSecondParentRevision(); int p1x = p1.isNull() ? -1 : repo.getChangelog().getRevisionIndex(p1); diff -r 63c5a9d7ca3f -r d0e5dc3cae6e cmdline/org/tmatesoft/hg/console/Main.java --- a/cmdline/org/tmatesoft/hg/console/Main.java Wed Mar 21 14:54:02 2012 +0100 +++ b/cmdline/org/tmatesoft/hg/console/Main.java Tue Jun 05 20:50:06 2012 +0200 @@ -43,6 +43,7 @@ import org.tmatesoft.hg.internal.ByteArrayChannel; import org.tmatesoft.hg.internal.DigestHelper; import org.tmatesoft.hg.internal.PathGlobMatcher; +import org.tmatesoft.hg.internal.PhasesHelper; import org.tmatesoft.hg.internal.RelativePathRewrite; import org.tmatesoft.hg.internal.StreamLogFacility; import org.tmatesoft.hg.repo.HgBranches; @@ -57,6 +58,7 @@ import org.tmatesoft.hg.repo.HgManifest; import org.tmatesoft.hg.repo.HgManifest.Flags; import org.tmatesoft.hg.repo.HgMergeState; +import org.tmatesoft.hg.repo.HgPhase; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.repo.HgStatusCollector; import org.tmatesoft.hg.repo.HgStatusInspector; @@ -93,6 +95,7 @@ public static void main(String[] args) throws Exception { Main m = new Main(args); + m.dumpPhases(); // m.buildFileLog(); // m.testConsoleLog(); // m.testTreeTraversal(); @@ -111,9 +114,28 @@ // m.testStatusInternals(); // m.catCompleteHistory(); // m.dumpCompleteManifestLow(); - m.dumpCompleteManifestHigh(); +// m.dumpCompleteManifestHigh(); // m.bunchOfTests(); } + + // hg/test-phases + // TODO as junit test + private void dumpPhases() throws Exception { + HgChangelog.ParentWalker pw = hgRepo.getChangelog().new ParentWalker(); + pw.init(); + PhasesHelper ph = new PhasesHelper(hgRepo, pw); + System.out.println("With ParentWalker(simulates HgChangeset case)"); + for (int i = 0, l = hgRepo.getChangelog().getLastRevision(); i <= l; i++) { + HgPhase phase = ph.getPhase(i, null); + System.out.printf("rev:%3d, phase:%s\n", i, phase); + } + ph = new PhasesHelper(hgRepo); + System.out.println("Without ParentWalker"); + for (int i = 0, l = hgRepo.getChangelog().getLastRevision(); i <= l; i++) { + HgPhase phase = ph.getPhase(i, null); + System.out.printf("rev:%3d, phase:%s\n", i, phase); + } + } private void buildFileLog() throws Exception { HgLogCommand cmd = new HgLogCommand(hgRepo); diff -r 63c5a9d7ca3f -r d0e5dc3cae6e src/org/tmatesoft/hg/core/HgChangeset.java --- a/src/org/tmatesoft/hg/core/HgChangeset.java Wed Mar 21 14:54:02 2012 +0100 +++ b/src/org/tmatesoft/hg/core/HgChangeset.java Tue Jun 05 20:50:06 2012 +0200 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 TMate Software Ltd + * Copyright (c) 2011-2012 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 @@ -17,12 +17,16 @@ package org.tmatesoft.hg.core; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import org.tmatesoft.hg.internal.PhasesHelper; import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; import org.tmatesoft.hg.repo.HgChangelog; +import org.tmatesoft.hg.repo.HgPhase; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.repo.HgStatusCollector; import org.tmatesoft.hg.util.Path; @@ -51,6 +55,7 @@ private List deletedFiles; private int revNumber; private byte[] parent1, parent2; + private PhasesHelper phaseHelper; // XXX consider CommandContext with StatusCollector, PathPool etc. Commands optionally get CC through a cons or create new // and pass it around @@ -154,7 +159,7 @@ if (parent1 == null) { parent1 = new byte[20]; parent2 = new byte[20]; - statusHelper.getRepo().getChangelog().parents(revNumber, new int[2], parent1, parent2); + getRepo().getChangelog().parents(revNumber, new int[2], parent1, parent2); } return Nodeid.fromBinary(parent1, 0); } @@ -170,10 +175,23 @@ if (parent2 == null) { parent1 = new byte[20]; parent2 = new byte[20]; - statusHelper.getRepo().getChangelog().parents(revNumber, new int[2], parent1, parent2); + getRepo().getChangelog().parents(revNumber, new int[2], parent1, parent2); } return Nodeid.fromBinary(parent2, 0); } + + /** + * Tells the phase this changeset belongs to. + * @return one of {@link HgPhase} values + */ + public HgPhase getPhase() throws HgInvalidControlFileException { + if (phaseHelper == null) { + // XXX would be handy to obtain ProgressSupport (perhaps, from statusHelper?) + // and pass it to #init(), so that there could be indication of file being read and cache being built + phaseHelper = new PhasesHelper(getRepo(), parentHelper); + } + return phaseHelper.getPhase(this); + } @Override public HgChangeset clone() { @@ -185,6 +203,10 @@ throw new InternalError(ex.toString()); } } + + private HgRepository getRepo() { + return statusHelper.getRepo(); + } private /*synchronized*/ void initFileChanges() throws HgInvalidControlFileException { ArrayList deleted = new ArrayList(); @@ -192,7 +214,7 @@ ArrayList added = new ArrayList(); HgStatusCollector.Record r = new HgStatusCollector.Record(); statusHelper.change(revNumber, r); - final HgRepository repo = statusHelper.getRepo(); + final HgRepository repo = getRepo(); for (Path s : r.getModified()) { Nodeid nid = r.nodeidAfterChange(s); if (nid == null) { diff -r 63c5a9d7ca3f -r d0e5dc3cae6e src/org/tmatesoft/hg/internal/PhasesHelper.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/PhasesHelper.java Tue Jun 05 20:50:06 2012 +0200 @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2012 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.internal; + +import static org.tmatesoft.hg.repo.HgPhase.Draft; +import static org.tmatesoft.hg.repo.HgPhase.Secret; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +import org.tmatesoft.hg.core.HgChangeset; +import org.tmatesoft.hg.core.HgInvalidControlFileException; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.repo.HgChangelog; +import org.tmatesoft.hg.repo.HgInternals; +import org.tmatesoft.hg.repo.HgPhase; +import org.tmatesoft.hg.repo.HgRepository; + +/** + * Support to deal with phases feature fo Mercurial (as of Mercutial version 2.1) + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public final class PhasesHelper { + + private final HgRepository hgRepo; + private final HgChangelog.ParentWalker parentHelper; + private Boolean repoSupporsPhases; + private List draftPhaseRoots; + private List secretPhaseRoots; + + public PhasesHelper(HgRepository repo) { + this(repo, null); + } + + public PhasesHelper(HgRepository repo, HgChangelog.ParentWalker pw) { + hgRepo = repo; + parentHelper = pw; + } + + public boolean isCapableOfPhases() throws HgInvalidControlFileException { + if (null == repoSupporsPhases) { + repoSupporsPhases = readRoots(); + } + return repoSupporsPhases.booleanValue(); + } + + + public HgPhase getPhase(HgChangeset cset) throws HgInvalidControlFileException { + final Nodeid csetRev = cset.getNodeid(); + final int csetRevIndex = cset.getRevision(); + return getPhase(csetRevIndex, csetRev); + } + + public HgPhase getPhase(final int csetRevIndex, Nodeid csetRev) throws HgInvalidControlFileException { + if (!isCapableOfPhases()) { + return HgPhase.Undefined; + } + if (csetRev == null || csetRev.isNull()) { + csetRev = hgRepo.getChangelog().getRevision(csetRevIndex); + } + + for (HgPhase phase : new HgPhase[] {HgPhase.Secret, HgPhase.Draft }) { + List roots = getPhaseRoots(phase); + if (roots.isEmpty()) { + continue; + } + if (roots.contains(csetRev)) { + return phase; + } + if (parentHelper != null) { + if (parentHelper.childrenOf(roots).contains(csetRev)) { + return phase; + } + } else { + // no parent helper + // search all descendants + int[] rootIndexes = toIndexes(roots); + Arrays.sort(rootIndexes); + if (rootIndexes[0] > csetRevIndex) { + // this phase started later than our changeset was added, try another phase + continue; + } + /* + * TODO descendants() method to build a BitSet with 1 at index of those that are descendants + * wrap it into a class with root nodeid to + * (a) collect only for a subset of repository, + * (b) be able to answer isDescendant(int csetRevIndex) using absolute indexing (i.e bitAt(csetRevIndex - rootRevIndex)) + */ + final HashSet parents2consider = new HashSet(roots); + final boolean[] result = new boolean[] { false }; + hgRepo.getChangelog().walk(rootIndexes[0], csetRevIndex, new HgChangelog.ParentInspector() { + + public void next(int revisionIndex, Nodeid revision, int parent1, int parent2, Nodeid nidParent1, Nodeid nidParent2) { + boolean descendant = false; + if (!nidParent1.isNull() && parents2consider.contains(nidParent1)) { + parents2consider.add(nidParent1); + descendant = true; + } + if (!nidParent2.isNull() && parents2consider.contains(nidParent2)) { + parents2consider.add(nidParent2); + descendant = true; + } + if (descendant && revisionIndex == csetRevIndex) { + // revision of interest descends from one of the roots + result[0] = true; + } + } + }); + if (result[0]) { + return phase; + } + } + } + return HgPhase.Public; + + } + + + private int[] toIndexes(List roots) throws HgInvalidControlFileException { + int[] rv = new int[roots.size()]; + for (int i = 0; i < rv.length; i++) { + rv[i] = hgRepo.getChangelog().getRevisionIndex(roots.get(i)); + } + return rv; + } + + private Boolean readRoots() throws HgInvalidControlFileException { + // FIXME shall access phaseroots through HgRepository#repoPathHelper + File phaseroots = new File(HgInternals.getRepositoryDir(hgRepo), "store/phaseroots"); + try { + if (!phaseroots.exists()) { + return Boolean.FALSE; + } + HashMap> phase2roots = new HashMap>(); + BufferedReader br = new BufferedReader(new FileReader(phaseroots)); + String line; + while ((line = br.readLine()) != null) { + String[] lc = line.trim().split("\\s+"); + if (lc.length == 0) { + continue; + } + if (lc.length != 2) { + HgInternals.getContext(hgRepo).getLog().warn(getClass(), "Bad line in phaseroots:%s", line); + continue; + } + int phaseIndex = Integer.parseInt(lc[0]); + Nodeid rootRev = Nodeid.fromAscii(lc[1]); + HgPhase phase = HgPhase.parse(phaseIndex); + List roots = phase2roots.get(phase); + if (roots == null) { + phase2roots.put(phase, roots = new LinkedList()); + } + roots.add(rootRev); + } + draftPhaseRoots = phase2roots.containsKey(Draft) ? phase2roots.get(Draft) : Collections.emptyList(); + secretPhaseRoots = phase2roots.containsKey(Secret) ? phase2roots.get(Secret) : Collections.emptyList(); + } catch (IOException ex) { + throw new HgInvalidControlFileException(ex.toString(), ex, phaseroots); + } + return Boolean.TRUE; + } + + private List getPhaseRoots(HgPhase phase) { + switch (phase) { + case Draft : return draftPhaseRoots; + case Secret : return secretPhaseRoots; + } + return Collections.emptyList(); + } +} diff -r 63c5a9d7ca3f -r d0e5dc3cae6e src/org/tmatesoft/hg/repo/HgPhase.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/repo/HgPhase.java Tue Jun 05 20:50:06 2012 +0200 @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2012 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.repo; + +/** + * Phases for a changeset is a new functionality in Mercurial 2.1 + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public enum HgPhase { + + Public("public"), Draft("draft"), Secret("secret"), Undefined(""); + + @SuppressWarnings("unused") + private final String hgString; + + private HgPhase(String stringRepresentation) { + hgString = stringRepresentation; + } + +// public String toMercurialString() { +// return hgString; +// } + + public static HgPhase parse(int value) { + switch (value) { + case 0 : return Public; + case 1 : return Draft; + case 2 : return Secret; + } + throw new IllegalArgumentException(String.format("Bad phase index: %d", value)); + } +} diff -r 63c5a9d7ca3f -r d0e5dc3cae6e src/org/tmatesoft/hg/repo/HgRepository.java --- a/src/org/tmatesoft/hg/repo/HgRepository.java Wed Mar 21 14:54:02 2012 +0100 +++ b/src/org/tmatesoft/hg/repo/HgRepository.java Tue Jun 05 20:50:06 2012 +0200 @@ -70,9 +70,9 @@ private final File workingDir; // .hg/../ private final String repoLocation; private final DataAccessProvider dataAccess; - private final PathRewrite normalizePath; - private final PathRewrite dataPathHelper; - private final PathRewrite repoPathHelper; + private final PathRewrite normalizePath; // normalized slashes but otherwise regular file names + private final PathRewrite dataPathHelper; // access to file storage area (usually under .hg/store/data/), with filenames mangled + private final PathRewrite repoPathHelper; // access to system files private final SessionContext sessionContext; private HgChangelog changelog; diff -r 63c5a9d7ca3f -r d0e5dc3cae6e src/org/tmatesoft/hg/repo/Revlog.java --- a/src/org/tmatesoft/hg/repo/Revlog.java Wed Mar 21 14:54:02 2012 +0100 +++ b/src/org/tmatesoft/hg/repo/Revlog.java Tue Jun 05 20:50:06 2012 +0200 @@ -257,7 +257,7 @@ } @Experimental - public void walk(int start, int end, final Revlog.Inspector inspector) throws HgInvalidRevisionException, HgInvalidControlFileException { + public final void walk(int start, int end, final Revlog.Inspector inspector) throws HgInvalidRevisionException, HgInvalidControlFileException { int lastRev = getLastRevision(); if (start == TIP) { start = lastRev; @@ -268,6 +268,11 @@ final RevisionInspector revisionInsp = Adaptable.Factory.getAdapter(inspector, RevisionInspector.class, null); final ParentInspector parentInsp = Adaptable.Factory.getAdapter(inspector, ParentInspector.class, null); final Nodeid[] allRevisions = parentInsp == null ? null : new Nodeid[end - start + 1]; + if (parentInsp != null && start != 0) { + // e.g. start == 6, end == 7 and parentOf(start) == 5. allRevisions.length == 2, allRevisions[parentOf(start)] => AIOOBE + throw new IllegalStateException("There's a defect in the code that doesn't allow walks other than from the very beginning"); + // TestAuxUtilities#testRevlogInspectors + } content.iterate(start, end, false, new RevlogStream.Inspector() { diff -r 63c5a9d7ca3f -r d0e5dc3cae6e test/org/tmatesoft/hg/test/TestAuxUtilities.java --- a/test/org/tmatesoft/hg/test/TestAuxUtilities.java Wed Mar 21 14:54:02 2012 +0100 +++ b/test/org/tmatesoft/hg/test/TestAuxUtilities.java Tue Jun 05 20:50:06 2012 +0200 @@ -244,9 +244,14 @@ } } }); - fileNode.walk(0, TIP, new HgDataFile.ParentInspector() { - int i = 0; - Nodeid[] all = new Nodeid[fileNode.getRevisionCount()]; + class ParentInspectorCheck implements HgDataFile.ParentInspector { + private int i; + private Nodeid[] all; + + public ParentInspectorCheck(int start, int total) { + i = start; + all = new Nodeid[total]; + } public void next(int localRevision, Nodeid revision, int parent1, int parent2, Nodeid nidParent1, Nodeid nidParent2) { Assert.assertEquals(i++, localRevision); @@ -264,7 +269,11 @@ Assert.assertTrue(nidParent2 == all[parent2]); } } - }); + }; + fileNode.walk(0, TIP, new ParentInspectorCheck(0, fileNode.getRevisionCount())); + assert fileNode.getRevisionCount() > 2 : "prereq"; // need at least few revisions + // there used to be a defect in #walk impl, assumption all parents come prior to a revision + fileNode.walk(1, 3, new ParentInspectorCheck(1, 3)); } @Test