changeset 445:d0e5dc3cae6e smartgit3

Support for phases functionality from Mercurial 2.1
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Tue, 05 Jun 2012 20:50:06 +0200
parents 63c5a9d7ca3f
children 9f0e6dfd417e
files cmdline/org/tmatesoft/hg/console/ChangesetDumpHandler.java cmdline/org/tmatesoft/hg/console/Main.java src/org/tmatesoft/hg/core/HgChangeset.java src/org/tmatesoft/hg/internal/PhasesHelper.java src/org/tmatesoft/hg/repo/HgPhase.java src/org/tmatesoft/hg/repo/HgRepository.java src/org/tmatesoft/hg/repo/Revlog.java test/org/tmatesoft/hg/test/TestAuxUtilities.java
diffstat 8 files changed, 314 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- 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);
--- 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);
--- 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<Path> 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<Path> deleted = new ArrayList<Path>();
@@ -192,7 +214,7 @@
 		ArrayList<HgFileRevision> added = new ArrayList<HgFileRevision>();
 		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) {
--- /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<Nodeid> draftPhaseRoots;
+	private List<Nodeid> 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<Nodeid> 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<Nodeid> parents2consider = new HashSet<Nodeid>(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<Nodeid> 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<HgPhase, List<Nodeid>> phase2roots = new HashMap<HgPhase, List<Nodeid>>();
+			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<Nodeid> roots = phase2roots.get(phase);
+				if (roots == null) {
+					phase2roots.put(phase, roots = new LinkedList<Nodeid>());
+				}
+				roots.add(rootRev);
+			}
+			draftPhaseRoots = phase2roots.containsKey(Draft) ? phase2roots.get(Draft) : Collections.<Nodeid>emptyList();
+			secretPhaseRoots = phase2roots.containsKey(Secret) ? phase2roots.get(Secret) : Collections.<Nodeid>emptyList();
+		} catch (IOException ex) {
+			throw new HgInvalidControlFileException(ex.toString(), ex, phaseroots);
+		}
+		return Boolean.TRUE;
+	}
+
+	private List<Nodeid> getPhaseRoots(HgPhase phase) {
+		switch (phase) {
+		case Draft : return draftPhaseRoots;
+		case Secret : return secretPhaseRoots;
+		}
+		return Collections.emptyList();
+	}
+}
--- /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));
+	}
+}
--- 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;
--- 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() {
 			
--- 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