# HG changeset patch # User Artem Tikhomirov # Date 1371832235 -7200 # Node ID 3b7d51ed4c65082f9235e3459e282d7ff723aa97 # Parent 14dac192aa262feb8ff6645a102648498483a188 Push: phase3 - update matching remote bookmarks diff -r 14dac192aa26 -r 3b7d51ed4c65 src/org/tmatesoft/hg/core/HgPushCommand.java --- 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 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 diff -r 14dac192aa26 -r 3b7d51ed4c65 src/org/tmatesoft/hg/repo/HgParentChildMap.java --- 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 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 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 true if wannaBeChild is among children of root + */ + 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 parents = new HashSet(); + 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 diff -r 14dac192aa26 -r 3b7d51ed4c65 src/org/tmatesoft/hg/repo/HgRemoteRepository.java --- 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(); - // 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 heads) throws HgRemoteConnectionException, HgRuntimeException { - if (heads == null) { + public void unbundle(HgBundle bundle, List 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> bookmarks() throws HgRemoteConnectionException, HgRuntimeException { + final String actionName = "Get remote bookmarks"; + final List> values = listkeys("bookmarks", actionName); + ArrayList> rv = new ArrayList>(); + for (Pair 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(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> values = listkeys("phases", "Get remote phases"); + for (Pair 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> bm = r.bookmarks(); + for (Pair 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(); + // 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> 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> rv = new ArrayList>(); + 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(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;