changeset 650:3b275cc2d2aa

Push: phase4 - settle local and remote phases, push updated phases regardless of server publishing state, do not push secret changesets
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Fri, 28 Jun 2013 19:27:26 +0200
parents e79cf9a8130b
children 6e98d34eaca8
files src/org/tmatesoft/hg/core/HgPushCommand.java src/org/tmatesoft/hg/internal/PhasesHelper.java src/org/tmatesoft/hg/internal/RevisionSet.java src/org/tmatesoft/hg/repo/HgParentChildMap.java src/org/tmatesoft/hg/repo/HgRemoteRepository.java
diffstat 5 files changed, 122 insertions(+), 59 deletions(-) [+]
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/core/HgPushCommand.java	Wed Jun 26 20:52:38 2013 +0200
+++ b/src/org/tmatesoft/hg/core/HgPushCommand.java	Fri Jun 28 19:27:26 2013 +0200
@@ -39,6 +39,7 @@
 import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.repo.HgRuntimeException;
 import org.tmatesoft.hg.util.CancelledException;
+import org.tmatesoft.hg.util.Outcome;
 import org.tmatesoft.hg.util.Pair;
 import org.tmatesoft.hg.util.ProgressSupport;
 import org.tmatesoft.hg.util.LogFacility.Severity;
@@ -72,14 +73,22 @@
 			final HgChangelog clog = repo.getChangelog();
 			final HgParentChildMap<HgChangelog> parentHelper = new HgParentChildMap<HgChangelog>(clog);
 			parentHelper.init();
+			final Internals implRepo = HgInternals.getImplementationRepo(repo);
+			final PhasesHelper phaseHelper = new PhasesHelper(implRepo, parentHelper);
 			final RepositoryComparator comparator = new RepositoryComparator(parentHelper, remoteRepo);
 			comparator.compare(new ProgressSupport.Sub(progress, 50), getCancelSupport(null, true));
 			List<Nodeid> l = comparator.getLocalOnlyRevisions();
+			final RevisionSet outgoing;
+			if (phaseHelper.isCapableOfPhases() && phaseHelper.withSecretRoots()) {
+				RevisionSet secret = phaseHelper.allSecret();
+				outgoing = new RevisionSet(l).subtract(secret);
+			} else {
+				outgoing = new RevisionSet(l);
+			}
 			//
 			// prepare bundle
-			final Internals implRepo = HgInternals.getImplementationRepo(repo);
 			BundleGenerator bg = new BundleGenerator(implRepo);
-			File bundleFile = bg.create(l);
+			File bundleFile = bg.create(outgoing.asList());
 			progress.worked(20);
 			HgBundle b = new HgLookup(repo.getSessionContext()).loadBundle(bundleFile);
 			//
@@ -88,13 +97,12 @@
 			progress.worked(20);
 			//
 			// update phase information
-			PhasesHelper phaseHelper = new PhasesHelper(implRepo, parentHelper);
 			if (phaseHelper.isCapableOfPhases()) {
-				RevisionSet outgoing = new RevisionSet(l);
 				RevisionSet presentSecret = phaseHelper.allSecret();
 				RevisionSet presentDraft = phaseHelper.allDraft();
 				RevisionSet secretLeft, draftLeft;
 				HgRemoteRepository.Phases remotePhases = remoteRepo.getPhases();
+				RevisionSet remoteDrafts = knownRemoteDrafts(remotePhases, parentHelper, outgoing);
 				if (remotePhases.isPublishingServer()) {
 					// although it's unlikely outgoing would affect secret changesets,
 					// it doesn't hurt to check secret roots along with draft ones
@@ -102,43 +110,55 @@
 					draftLeft = presentDraft.subtract(outgoing);
 				} else {
 					// shall merge local and remote phase states
-					ArrayList<Nodeid> knownRemoteDraftRoots = new ArrayList<Nodeid>();
-					for (Nodeid rdr : remotePhases.draftRoots()) {
-						if (clog.isKnown(rdr)) {
-							knownRemoteDraftRoots.add(rdr);
-						}
-					}
-					// childrenOf(knownRemoteDraftRoots) is everything remote may treat as Draft
-					RevisionSet remoteDrafts = new RevisionSet(parentHelper.childrenOf(knownRemoteDraftRoots));
-					List<Nodeid> localChildrenNotSent = parentHelper.childrenOf(outgoing.heads(parentHelper).asList());
-					// remote shall know only what we've sent, subtract revisions we didn't actually sent
-					remoteDrafts = remoteDrafts.subtract(new RevisionSet(localChildrenNotSent));
-					// if there's a remote draft root that points to revision we know is public
-					RevisionSet remoteDraftsLocallyPublic = remoteDrafts.subtract(presentSecret).subtract(presentDraft);
-					if (!remoteDraftsLocallyPublic.isEmpty()) {
-						// foreach remoteDraftsLocallyPublic.heads() do push Draft->Public
-						for (Nodeid n : remoteDraftsLocallyPublic.heads(parentHelper)) {
-							try {
-								remoteRepo.updatePhase(HgPhase.Draft, HgPhase.Public, n);
-							} catch (HgRemoteConnectionException ex) {
-								implRepo.getLog().dump(getClass(), Severity.Error, ex, String.format("Failed to update phase of %s", n.shortNotation()));
-							}
-						}
-						remoteDrafts = remoteDrafts.subtract(remoteDraftsLocallyPublic);
-					}
 					// revisions that cease to be secret (gonna become Public), e.g. someone else pushed them
 					RevisionSet secretGone = presentSecret.intersect(remoteDrafts);
-					// trace parents of these published secret revisions
-					RevisionSet secretMadePublic = presentSecret.parentsOf(secretGone, parentHelper);
-					secretLeft = presentSecret.subtract(secretGone).subtract(secretMadePublic);
-					// same for drafts
-					RevisionSet draftGone = presentDraft.intersect(remoteDrafts);
-					RevisionSet draftMadePublic = presentDraft.parentsOf(draftGone, parentHelper);
-					draftLeft = presentDraft.subtract(draftGone).subtract(draftMadePublic);
+					// parents of those remote drafts are public, mark them as public locally, too
+					RevisionSet remotePublic = presentSecret.ancestors(secretGone, parentHelper);
+					secretLeft = presentSecret.subtract(secretGone).subtract(remotePublic);
+					/*
+					 * Revisions grow from left to right (parents to the left, children to the right)
+					 * 
+					 * I: Set of local is subset of remote
+					 * 
+					 *               local draft 
+					 * --o---r---o---l---o--
+					 *       remote draft
+					 * 
+					 * Remote draft roots shall be updated
+					 *
+					 *
+					 * II: Set of local is superset of remote
+					 * 
+					 *       local draft 
+					 * --o---l---o---r---o--
+					 *               remote draft 
+					 *               
+					 * Local draft roots shall be updated
+					 */
+					RevisionSet sharedDraft = presentDraft.intersect(remoteDrafts); // (I: ~presentDraft; II: ~remoteDraft
+					RevisionSet localDraftRemotePublic = presentDraft.ancestors(sharedDraft, parentHelper); // I: 0; II: those treated public on remote
+					// forget those deemed public by remote (drafts shared by both remote and local are ok to stay)
+					draftLeft = presentDraft.subtract(localDraftRemotePublic);
 				}
 				final RevisionSet newDraftRoots = draftLeft.roots(parentHelper);
 				final RevisionSet newSecretRoots = secretLeft.roots(parentHelper);
 				phaseHelper.updateRoots(newDraftRoots.asList(), newSecretRoots.asList());
+				//
+				// if there's a remote draft root that points to revision we know is public
+				RevisionSet remoteDraftsLocalPublic = remoteDrafts.subtract(draftLeft).subtract(secretLeft);
+				if (!remoteDraftsLocalPublic.isEmpty()) {
+					// foreach remoteDraftsLocallyPublic.heads() do push Draft->Public
+					for (Nodeid n : remoteDraftsLocalPublic.heads(parentHelper)) {
+						try {
+							Outcome upo = remoteRepo.updatePhase(HgPhase.Draft, HgPhase.Public, n);
+							if (!upo.isOk()) {
+								implRepo.getLog().dump(getClass(), Severity.Info, "Failed to update remote phase, reason: %s", upo.getMessage());
+							}
+						} catch (HgRemoteConnectionException ex) {
+							implRepo.getLog().dump(getClass(), Severity.Error, ex, String.format("Failed to update phase of %s", n.shortNotation()));
+						}
+					}
+				}
 			}
 			progress.worked(5);
 			//
@@ -172,6 +192,25 @@
 		}
 	}
 	
+	private RevisionSet knownRemoteDrafts(HgRemoteRepository.Phases remotePhases, HgParentChildMap<HgChangelog> parentHelper, RevisionSet outgoing) {
+		ArrayList<Nodeid> knownRemoteDraftRoots = new ArrayList<Nodeid>();
+		for (Nodeid rdr : remotePhases.draftRoots()) {
+			if (parentHelper.knownNode(rdr)) {
+				knownRemoteDraftRoots.add(rdr);
+			}
+		}
+		// knownRemoteDraftRoots + childrenOf(knownRemoteDraftRoots) is everything remote may treat as Draft
+		RevisionSet remoteDrafts = new RevisionSet(knownRemoteDraftRoots);
+		remoteDrafts = remoteDrafts.union(remoteDrafts.children(parentHelper));
+		// 1) outgoing.children gives all local revisions accessible from outgoing.
+		// 2) outgoing.roots.children is equivalent with smaller intermediate set, the way we build
+		// childrenOf doesn't really benefits from that.
+		RevisionSet localChildrenNotSent = outgoing.children(parentHelper).subtract(outgoing);
+		// remote shall know only what we've sent, subtract revisions we didn't actually sent
+		remoteDrafts = remoteDrafts.subtract(localChildrenNotSent);
+		return remoteDrafts;
+	}
+	
 	/*
 	 * To test, start a server:
 	 * $ hg --config web.allow_push=* --config web.push_ssl=False --config server.validate=True --debug serve
--- a/src/org/tmatesoft/hg/internal/PhasesHelper.java	Wed Jun 26 20:52:38 2013 +0200
+++ b/src/org/tmatesoft/hg/internal/PhasesHelper.java	Fri Jun 28 19:27:26 2013 +0200
@@ -78,7 +78,10 @@
 		}
 		return repoSupporsPhases.booleanValue();
 	}
-
+	
+	public boolean withSecretRoots() {
+		return !secretPhaseRoots.isEmpty();
+	}
 
 	/**
 	 * @param cset revision to query
--- a/src/org/tmatesoft/hg/internal/RevisionSet.java	Wed Jun 26 20:52:38 2013 +0200
+++ b/src/org/tmatesoft/hg/internal/RevisionSet.java	Fri Jun 28 19:27:26 2013 +0200
@@ -92,9 +92,9 @@
 	}
 
 	/**
-	 * Immediate parents of the supplied children set found in this one.
+	 * Any ancestor of an element from the supplied children set found in this one.
 	 */
-	public RevisionSet parentsOf(RevisionSet children, HgParentChildMap<HgChangelog> parentHelper) {
+	public RevisionSet ancestors(RevisionSet children, HgParentChildMap<HgChangelog> parentHelper) {
 		if (isEmpty()) {
 			return this;
 		}
@@ -102,18 +102,36 @@
 			return children;
 		}
 		RevisionSet chRoots = children.roots(parentHelper);
-		HashSet<Nodeid> parents = new HashSet<Nodeid>();
-		for (Nodeid n : chRoots.elements) {
-			Nodeid p1 = parentHelper.firstParent(n);
-			Nodeid p2 = parentHelper.secondParent(n);
-			if (p1 != null && elements.contains(p1)) {
-				parents.add(p1);
+		HashSet<Nodeid> ancestors = new HashSet<Nodeid>();
+		Set<Nodeid> childrenToCheck = chRoots.elements;
+		while (!childrenToCheck.isEmpty()) {
+			HashSet<Nodeid> nextRound = new HashSet<Nodeid>();
+			for (Nodeid n : childrenToCheck) {
+				Nodeid p1 = parentHelper.firstParent(n);
+				Nodeid p2 = parentHelper.secondParent(n);
+				if (p1 != null && elements.contains(p1)) {
+					nextRound.add(p1);
+				}
+				if (p2 != null && elements.contains(p2)) {
+					nextRound.add(p2);
+				}
 			}
-			if (p2 != null && elements.contains(p2)) {
-				parents.add(p2);
-			}
+			ancestors.addAll(nextRound);
+			childrenToCheck = nextRound;
+		} 
+		return new RevisionSet(ancestors);
+	}
+	
+	/**
+	 * Revisions that are both direct and indirect children of elements of this revision set
+	 * as known in supplied parent-child map
+	 */
+	public RevisionSet children(HgParentChildMap<HgChangelog> parentHelper) {
+		if (isEmpty()) {
+			return this;
 		}
-		return new RevisionSet(parents);
+		List<Nodeid> children = parentHelper.childrenOf(elements);
+		return new RevisionSet(new HashSet<Nodeid>(children));
 	}
 
 	public RevisionSet intersect(RevisionSet other) {
--- a/src/org/tmatesoft/hg/repo/HgParentChildMap.java	Wed Jun 26 20:52:38 2013 +0200
+++ b/src/org/tmatesoft/hg/repo/HgParentChildMap.java	Fri Jun 28 19:27:26 2013 +0200
@@ -180,7 +180,7 @@
 	
 	// @return ordered collection of all children rooted at supplied nodes. Nodes shall not be descendants of each other!
 	// Nodeids shall belong to this revlog
-	public List<Nodeid> childrenOf(List<Nodeid> roots) {
+	public List<Nodeid> childrenOf(Collection<Nodeid> roots) {
 		if (roots.isEmpty()) {
 			return Collections.emptyList();
 		}
--- a/src/org/tmatesoft/hg/repo/HgRemoteRepository.java	Wed Jun 26 20:52:38 2013 +0200
+++ b/src/org/tmatesoft/hg/repo/HgRemoteRepository.java	Fri Jun 28 19:27:26 2013 +0200
@@ -17,6 +17,8 @@
 package org.tmatesoft.hg.repo;
 
 import static org.tmatesoft.hg.util.LogFacility.Severity.Info;
+import static org.tmatesoft.hg.util.Outcome.Kind.Failure;
+import static org.tmatesoft.hg.util.Outcome.Kind.Success;
 
 import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
@@ -62,14 +64,13 @@
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.core.SessionContext;
 import org.tmatesoft.hg.internal.DataSerializer;
+import org.tmatesoft.hg.internal.DataSerializer.OutputStreamSerializer;
 import org.tmatesoft.hg.internal.EncodingHelper;
 import org.tmatesoft.hg.internal.Internals;
-import org.tmatesoft.hg.internal.DataSerializer.OutputStreamSerializer;
 import org.tmatesoft.hg.internal.PropertyMarshal;
+import org.tmatesoft.hg.util.LogFacility.Severity;
 import org.tmatesoft.hg.util.Outcome;
 import org.tmatesoft.hg.util.Pair;
-import org.tmatesoft.hg.util.LogFacility.Severity;
-import org.tmatesoft.hg.util.Outcome.Kind;
 
 /**
  * WORK IN PROGRESS, DO NOT USE
@@ -497,7 +498,7 @@
 			return new Phases(true, Collections.<Nodeid>emptyList());
 		}
 		final List<Pair<String, String>> values = listkeys("phases", "Get remote phases");
-		boolean publishing = true;
+		boolean publishing = false;
 		ArrayList<Nodeid> draftRoots = new ArrayList<Nodeid>();
 		for (Pair<String, String> l : values) {
 			if ("publishing".equalsIgnoreCase(l.first())) {
@@ -517,10 +518,14 @@
 	}
 	
 	public Outcome updatePhase(HgPhase from, HgPhase to, Nodeid n) throws HgRemoteConnectionException, HgRuntimeException {
+		initCapabilities();
+		if (!remoteCapabilities.contains("pushkey")) {
+			return new Outcome(Failure, "Server doesn't support pushkey protocol");
+		}
 		if (pushkey("phases", n.toString(), String.valueOf(from.mercurialOrdinal()), String.valueOf(to.mercurialOrdinal()))) {
-			return new Outcome(Kind.Success, String.format("Phase of %s updated to %s", n.shortNotation(), to.name()));
+			return new Outcome(Success, String.format("Phase of %s updated to %s", n.shortNotation(), to.name()));
 		}
-		return new Outcome(Kind.Failure, String.format("Phase update (%s: %s -> %s) failed", n.shortNotation(), from.name(), to.name()));
+		return new Outcome(Failure, String.format("Phase update (%s: %s -> %s) failed", n.shortNotation(), from.name(), to.name()));
 	}
 
 	
@@ -632,9 +637,10 @@
 	private boolean pushkey(String namespace, String key, String oldValue, String newValue) throws HgRemoteConnectionException, HgRuntimeException {
 		HttpURLConnection c = null;
 		try {
-			final String p = String.format("%s?cmd=pushkey&namespace=%s&key=%s&old=%s&new=&s", url.getPath(), namespace, key, oldValue, newValue);
+			final String p = String.format("%s?cmd=pushkey&namespace=%s&key=%s&old=%s&new=%s", url.getPath(), namespace, key, oldValue, newValue);
 			URL u = new URL(url, p);
 			c = setupConnection(u.openConnection());
+			c.setRequestMethod("POST");
 			c.connect();
 			if (debug) {
 				dumpResponseHeader(u, c);
@@ -642,9 +648,6 @@
 			checkResponseOk(c, key, "pushkey");
 			final InputStream is = c.getInputStream();
 			int rv = is.read();
-			if (is.read() != -1) {
-				sessionContext.getLog().dump(getClass(), Severity.Error, "Unexpected data in response to pushkey");
-			}
 			is.close();
 			return rv == '1';
 		} catch (MalformedURLException ex) {