changeset 646:3b7d51ed4c65

Push: phase3 - update matching remote bookmarks
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Fri, 21 Jun 2013 18:30:35 +0200
parents 14dac192aa26
children c75297c17867
files src/org/tmatesoft/hg/core/HgPushCommand.java src/org/tmatesoft/hg/repo/HgParentChildMap.java src/org/tmatesoft/hg/repo/HgRemoteRepository.java
diffstat 3 files changed, 207 insertions(+), 54 deletions(-) [+]
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/core/HgPushCommand.java	Thu Jun 20 19:15:09 2013 +0200
+++ b/src/org/tmatesoft/hg/core/HgPushCommand.java	Fri Jun 21 18:30:35 2013 +0200
@@ -23,6 +23,7 @@
 
 import org.tmatesoft.hg.internal.BundleGenerator;
 import org.tmatesoft.hg.internal.RepositoryComparator;
+import org.tmatesoft.hg.repo.HgBookmarks;
 import org.tmatesoft.hg.repo.HgBundle;
 import org.tmatesoft.hg.repo.HgChangelog;
 import org.tmatesoft.hg.repo.HgInternals;
@@ -33,6 +34,7 @@
 import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.repo.HgRuntimeException;
 import org.tmatesoft.hg.util.CancelledException;
+import org.tmatesoft.hg.util.Pair;
 import org.tmatesoft.hg.util.ProgressSupport;
 
 /**
@@ -81,8 +83,23 @@
 //			remote.listkeys("phases");
 			progress.worked(5);
 			//
-			// FIXME update bookmark information
+			// update bookmark information
+			HgBookmarks localBookmarks = repo.getBookmarks();
+			if (!localBookmarks.getAllBookmarks().isEmpty()) {
+				for (Pair<String,Nodeid> bm : remoteRepo.bookmarks()) {
+					Nodeid localRevision = localBookmarks.getRevision(bm.first());
+					if (localRevision == null || !parentHelper.knownNode(bm.second())) {
+						continue;
+					}
+					// we know both localRevision and revision of remote bookmark,
+					// need to make sure we don't push  older revision than it's at the server
+					if (parentHelper.isChild(bm.second(), localRevision)) {
+						remoteRepo.updateBookmark(bm.first(), bm.second(), localRevision);
+					}
+				}
+			}
 //			remote.listkeys("bookmarks");
+			// XXX WTF is obsolete in namespaces key??
 			progress.worked(5);
 		} catch (IOException ex) {
 			throw new HgIOException(ex.getMessage(), null); // XXX not a nice idea to throw IOException from BundleGenerator#create
--- a/src/org/tmatesoft/hg/repo/HgParentChildMap.java	Thu Jun 20 19:15:09 2013 +0200
+++ b/src/org/tmatesoft/hg/repo/HgParentChildMap.java	Fri Jun 21 18:30:35 2013 +0200
@@ -57,14 +57,15 @@
  */
 public final class HgParentChildMap<T extends Revlog> implements ParentInspector {
 
+	// IMPORTANT: Nodeid instances shall be shared between all arrays
+
 	private Nodeid[] sequential; // natural repository order, childrenOf rely on ordering
 	private Nodeid[] sorted; // for binary search
-	private int[] sorted2natural;
+	private int[] sorted2natural; // indexes in sorted to indexes in sequential
 	private Nodeid[] firstParent;
 	private Nodeid[] secondParent;
 	private final T revlog;
 
-	// Nodeid instances shall be shared between all arrays
 
 	public HgParentChildMap(T owner) {
 		revlog = owner;
@@ -254,4 +255,33 @@
 	public List<Nodeid> all() {
 		return Arrays.asList(sequential);
 	}
+
+	/**
+	 * Find out whether a given node is among descendants of another.
+	 * 
+	 * @param root revision to check for being (grand-)*parent of a child
+	 * @param wannaBeChild candidate descendant revision
+	 * @return <code>true</code> if <code>wannaBeChild</code> is among children of <code>root</code>
+	 */
+	public boolean isChild(Nodeid root, Nodeid wannaBeChild) {
+		int x = Arrays.binarySearch(sorted, root);
+		assertSortedIndex(x);
+		root = sorted[x]; // canonical instance
+		int y = Arrays.binarySearch(sorted, wannaBeChild);
+		if (y < 0 || y <= x) {
+			// not found or comes earlier than root
+			return false;
+		}
+		wannaBeChild = sorted[y]; // canonicalize
+		final int start = sorted2natural[x];
+		final int end = sorted2natural[y];
+		HashSet<Nodeid> parents = new HashSet<Nodeid>();
+		parents.add(root);
+		for (int i = start + 1; i < end; i++) {
+			if (parents.contains(firstParent[i]) || parents.contains(secondParent[i])) {
+				parents.add(sequential[i]); // collect ancestors line
+			}
+		}
+		return parents.contains(firstParent[end]) || parents.contains(secondParent[end]);
+	}
 }
\ No newline at end of file
--- a/src/org/tmatesoft/hg/repo/HgRemoteRepository.java	Thu Jun 20 19:15:09 2013 +0200
+++ b/src/org/tmatesoft/hg/repo/HgRemoteRepository.java	Fri Jun 21 18:30:35 2013 +0200
@@ -62,14 +62,18 @@
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.core.SessionContext;
 import org.tmatesoft.hg.internal.DataSerializer;
+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.Pair;
+import org.tmatesoft.hg.util.LogFacility.Severity;
 
 /**
  * WORK IN PROGRESS, DO NOT USE
  * 
  * @see http://mercurial.selenic.com/wiki/WireProtocol
+ * @see http://mercurial.selenic.com/wiki/HttpCommandProtocol
  * 
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
@@ -162,48 +166,7 @@
 	}
 	
 	public boolean isInvalid() throws HgRemoteConnectionException {
-		if (remoteCapabilities == null) {
-			remoteCapabilities = new HashSet<String>();
-			// say hello to server, check response
-			try {
-				URL u = new URL(url, url.getPath() + "?cmd=hello");
-				HttpURLConnection c = setupConnection(u.openConnection());
-				c.connect();
-				if (debug) {
-					dumpResponseHeader(u, c);
-				}
-				BufferedReader r = new BufferedReader(new InputStreamReader(c.getInputStream(), "US-ASCII"));
-				String line = r.readLine();
-				c.disconnect();
-				final String capsPrefix = "capabilities:";
-				if (line == null || !line.startsWith(capsPrefix)) {
-					// for whatever reason, some servers do not respond to hello command (e.g. svnkit)
-					// but respond to 'capabilities' instead. Try it.
-					// TODO [post-1.0] tests needed
-					u = new URL(url, url.getPath() + "?cmd=capabilities");
-					c = setupConnection(u.openConnection());
-					c.connect();
-					if (debug) {
-						dumpResponseHeader(u, c);
-					}
-					r = new BufferedReader(new InputStreamReader(c.getInputStream(), "US-ASCII"));
-					line = r.readLine();
-					c.disconnect();
-					if (line == null || line.trim().length() == 0) {
-						return true;
-					}
-				} else {
-					line = line.substring(capsPrefix.length()).trim();
-				}
-				String[] caps = line.split("\\s");
-				remoteCapabilities.addAll(Arrays.asList(caps));
-				c.disconnect();
-			} catch (MalformedURLException ex) {
-				throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("hello").setServerInfo(getLocation());
-			} catch (IOException ex) {
-				throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("hello").setServerInfo(getLocation());
-			}
-		}
+		initCapabilities();
 		return remoteCapabilities.isEmpty();
 	}
 
@@ -446,13 +409,14 @@
 		}
 	}
 	
-	public void unbundle(HgBundle bundle, List<Nodeid> heads) throws HgRemoteConnectionException, HgRuntimeException {
-		if (heads == null) {
+	public void unbundle(HgBundle bundle, List<Nodeid> remoteHeads) throws HgRemoteConnectionException, HgRuntimeException {
+		if (remoteHeads == null) {
 			// TODO collect heads from bundle:
 			// bundle.inspectChangelog(new HeadCollector(for each c : if collected has c.p1 or c.p2, remove them. Add c))
+			// or get from remote server???
 			throw Internals.notImplemented();
 		}
-		StringBuilder sb = appendNodeidListArgument("heads", heads, null);
+		StringBuilder sb = appendNodeidListArgument("heads", remoteHeads, null);
 		
 		HttpURLConnection c = null;
 		DataSerializer.DataSource bundleData = bundle.new BundleSerializer();
@@ -472,11 +436,7 @@
 				dumpResponseHeader(u, c);
 				dumpResponse(c);
 			}
-			if (c.getResponseCode() != 200) {
-				String m = c.getResponseMessage() == null ? "unknown reason" : c.getResponseMessage();
-				String em = String.format("Push failed: %s (HTTP error:%d)", m, c.getResponseCode());
-				throw new HgRemoteConnectionException(em).setRemoteCommand("unbundle").setServerInfo(getLocation());
-			}
+			checkResponseOk(c, "Push", "unbundle");
 		} catch (MalformedURLException ex) {
 			throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("unbundle").setServerInfo(getLocation());
 		} catch (IOException ex) {
@@ -490,10 +450,114 @@
 		}
 	}
 
+	public List<Pair<String,Nodeid>> bookmarks() throws HgRemoteConnectionException, HgRuntimeException {
+		final String actionName = "Get remote bookmarks";
+		final List<Pair<String, String>> values = listkeys("bookmarks", actionName);
+		ArrayList<Pair<String, Nodeid>> rv = new ArrayList<Pair<String, Nodeid>>();
+		for (Pair<String, String> l : values) {
+			if (l.second().length() != Nodeid.SIZE_ASCII) {
+				sessionContext.getLog().dump(getClass(), Severity.Warn, "%s: bad nodeid '%s', ignored", actionName, l.second());
+				continue;
+			}
+			Nodeid n = Nodeid.fromAscii(l.second());
+			String bm = new String(l.first());
+			rv.add(new Pair<String, Nodeid>(bm, n));
+		}
+		return rv;
+	}
+
+	public void updateBookmark(String name, Nodeid oldRev, Nodeid newRev) throws HgRemoteConnectionException, HgRuntimeException {
+		final String namespace = "bookmarks";
+		HttpURLConnection c = null;
+		try {
+			URL u = new URL(url, String.format("%s?cmd=pushkey&namespace=%s&key=%s&old=%s&new=%s",url.getPath(), namespace, name, oldRev.toString(), newRev.toString()));
+			c = setupConnection(u.openConnection());
+			c.connect();
+			if (debug) {
+				dumpResponseHeader(u, c);
+			}
+			checkResponseOk(c, "Update remote bookmark", "pushkey");
+		} catch (MalformedURLException ex) {
+			throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("pushkey").setServerInfo(getLocation());
+		} catch (IOException ex) {
+			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("pushkey").setServerInfo(getLocation());
+		} finally {
+			if (c != null) {
+				c.disconnect();
+			}
+		}
+	}
+	
+	private void phases() throws HgRemoteConnectionException, HgRuntimeException {
+		final List<Pair<String, String>> values = listkeys("phases", "Get remote phases");
+		for (Pair<String, String> l : values) {
+			System.out.printf("%s : %s\n", l.first(), l.second());
+		}
+	}
+	
+	public static void main(String[] args) throws Exception {
+		final HgRemoteRepository r = new HgLookup().detectRemote("http://selenic.com/hg", null);
+		if (r.isInvalid()) {
+			return;
+		}
+		System.out.println(r.remoteCapabilities);
+		r.phases();
+		final List<Pair<String, Nodeid>> bm = r.bookmarks();
+		for (Pair<String, Nodeid> pair : bm) {
+			System.out.println(pair);
+		}
+	}
+
 	@Override
 	public String toString() {
 		return getClass().getSimpleName() + '[' + getLocation() + ']';
 	}
+	
+	
+	private void initCapabilities() throws HgRemoteConnectionException {
+		if (remoteCapabilities == null) {
+			remoteCapabilities = new HashSet<String>();
+			// say hello to server, check response
+			try {
+				URL u = new URL(url, url.getPath() + "?cmd=hello");
+				HttpURLConnection c = setupConnection(u.openConnection());
+				c.connect();
+				if (debug) {
+					dumpResponseHeader(u, c);
+				}
+				BufferedReader r = new BufferedReader(new InputStreamReader(c.getInputStream(), "US-ASCII"));
+				String line = r.readLine();
+				c.disconnect();
+				final String capsPrefix = "capabilities:";
+				if (line == null || !line.startsWith(capsPrefix)) {
+					// for whatever reason, some servers do not respond to hello command (e.g. svnkit)
+					// but respond to 'capabilities' instead. Try it.
+					// TODO [post-1.0] tests needed
+					u = new URL(url, url.getPath() + "?cmd=capabilities");
+					c = setupConnection(u.openConnection());
+					c.connect();
+					if (debug) {
+						dumpResponseHeader(u, c);
+					}
+					r = new BufferedReader(new InputStreamReader(c.getInputStream(), "US-ASCII"));
+					line = r.readLine();
+					c.disconnect();
+					if (line == null || line.trim().length() == 0) {
+						return;
+					}
+				} else {
+					line = line.substring(capsPrefix.length()).trim();
+				}
+				String[] caps = line.split("\\s");
+				remoteCapabilities.addAll(Arrays.asList(caps));
+				c.disconnect();
+			} catch (MalformedURLException ex) {
+				throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("hello").setServerInfo(getLocation());
+			} catch (IOException ex) {
+				throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("hello").setServerInfo(getLocation());
+			}
+		}
+	}
 
 	private HgLookup getLookupHelper() {
 		if (lookupHelper == null) {
@@ -501,7 +565,49 @@
 		}
 		return lookupHelper;
 	}
+
+	private List<Pair<String,String>> listkeys(String namespace, String actionName) throws HgRemoteConnectionException, HgRuntimeException {
+		HttpURLConnection c = null;
+		try {
+			URL u = new URL(url, url.getPath() + "?cmd=listkeys&namespace=" + namespace);
+			c = setupConnection(u.openConnection());
+			c.connect();
+			if (debug) {
+				dumpResponseHeader(u, c);
+			}
+			checkResponseOk(c, actionName, "listkeys");
+			ArrayList<Pair<String, String>> rv = new ArrayList<Pair<String, String>>();
+			BufferedReader r = new BufferedReader(new InputStreamReader(c.getInputStream(), EncodingHelper.getUTF8()));
+			String l;
+			while ((l = r.readLine()) != null) {
+				int sep = l.indexOf('\t');
+				if (sep == -1) {
+					sessionContext.getLog().dump(getClass(), Severity.Warn, "%s: bad line '%s', ignored", actionName, l);
+					continue;
+				}
+				rv.add(new Pair<String,String>(l.substring(0, sep), l.substring(sep+1)));
+			}
+			r.close();
+			return rv;
+		} catch (MalformedURLException ex) {
+			throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("listkeys").setServerInfo(getLocation());
+		} catch (IOException ex) {
+			throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("listkeys").setServerInfo(getLocation());
+		} finally {
+			if (c != null) {
+				c.disconnect();
+			}
+		}
+	}
 	
+	private void checkResponseOk(HttpURLConnection c, String opName, String remoteCmd) throws HgRemoteConnectionException, IOException {
+		if (c.getResponseCode() != 200) {
+			String m = c.getResponseMessage() == null ? "unknown reason" : c.getResponseMessage();
+			String em = String.format("%s failed: %s (HTTP error:%d)", opName, m, c.getResponseCode());
+			throw new HgRemoteConnectionException(em).setRemoteCommand(remoteCmd).setServerInfo(getLocation());
+		}
+	}
+
 	private HttpURLConnection setupConnection(URLConnection urlConnection) {
 		urlConnection.setRequestProperty("User-Agent", "hg4j/1.0.0");
 		urlConnection.addRequestProperty("Accept", "application/mercurial-0.1");
@@ -549,7 +655,7 @@
 	
 	private static File writeBundle(InputStream is, boolean decompress, String header) throws IOException {
 		InputStream zipStream = decompress ? new InflaterInputStream(is) : is;
-		File tf = File.createTempFile("hg-bundle-", null);
+		File tf = File.createTempFile("hg4j-bundle-", null);
 		FileOutputStream fos = new FileOutputStream(tf);
 		fos.write(header.getBytes());
 		int r;