# HG changeset patch # User Artem Tikhomirov # Date 1372272758 -7200 # Node ID e79cf9a8130b989a51e363bfb9a4507676d284b1 # Parent 690e71d29bf6dbe915939eaa70cf60e79dea4890 Push: phase4 - update local and remote phase information diff -r 690e71d29bf6 -r e79cf9a8130b src/org/tmatesoft/hg/core/HgPushCommand.java --- a/src/org/tmatesoft/hg/core/HgPushCommand.java Tue Jun 25 20:48:37 2013 +0200 +++ b/src/org/tmatesoft/hg/core/HgPushCommand.java Wed Jun 26 20:52:38 2013 +0200 @@ -19,10 +19,14 @@ import java.io.File; import java.io.IOException; import java.net.URL; +import java.util.ArrayList; import java.util.List; import org.tmatesoft.hg.internal.BundleGenerator; +import org.tmatesoft.hg.internal.Internals; +import org.tmatesoft.hg.internal.PhasesHelper; import org.tmatesoft.hg.internal.RepositoryComparator; +import org.tmatesoft.hg.internal.RevisionSet; import org.tmatesoft.hg.repo.HgBookmarks; import org.tmatesoft.hg.repo.HgBundle; import org.tmatesoft.hg.repo.HgChangelog; @@ -30,12 +34,14 @@ import org.tmatesoft.hg.repo.HgInvalidStateException; import org.tmatesoft.hg.repo.HgLookup; import org.tmatesoft.hg.repo.HgParentChildMap; +import org.tmatesoft.hg.repo.HgPhase; import org.tmatesoft.hg.repo.HgRemoteRepository; 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; +import org.tmatesoft.hg.util.LogFacility.Severity; /** * @@ -63,14 +69,16 @@ // // find out missing // TODO refactor same code in HgOutgoingCommand #getComparator and #getParentHelper - final HgParentChildMap parentHelper = new HgParentChildMap(repo.getChangelog()); + final HgChangelog clog = repo.getChangelog(); + final HgParentChildMap parentHelper = new HgParentChildMap(clog); parentHelper.init(); final RepositoryComparator comparator = new RepositoryComparator(parentHelper, remoteRepo); comparator.compare(new ProgressSupport.Sub(progress, 50), getCancelSupport(null, true)); List l = comparator.getLocalOnlyRevisions(); // // prepare bundle - BundleGenerator bg = new BundleGenerator(HgInternals.getImplementationRepo(repo)); + final Internals implRepo = HgInternals.getImplementationRepo(repo); + BundleGenerator bg = new BundleGenerator(implRepo); File bundleFile = bg.create(l); progress.worked(20); HgBundle b = new HgLookup(repo.getSessionContext()).loadBundle(bundleFile); @@ -79,14 +87,65 @@ remoteRepo.unbundle(b, comparator.getRemoteHeads()); progress.worked(20); // - // FIXME update phase information -// remote.listkeys("phases"); + // 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(); + if (remotePhases.isPublishingServer()) { + // although it's unlikely outgoing would affect secret changesets, + // it doesn't hurt to check secret roots along with draft ones + secretLeft = presentSecret.subtract(outgoing); + draftLeft = presentDraft.subtract(outgoing); + } else { + // shall merge local and remote phase states + ArrayList knownRemoteDraftRoots = new ArrayList(); + 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 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); + } + final RevisionSet newDraftRoots = draftLeft.roots(parentHelper); + final RevisionSet newSecretRoots = secretLeft.roots(parentHelper); + phaseHelper.updateRoots(newDraftRoots.asList(), newSecretRoots.asList()); + } progress.worked(5); // // update bookmark information HgBookmarks localBookmarks = repo.getBookmarks(); if (!localBookmarks.getAllBookmarks().isEmpty()) { - for (Pair bm : remoteRepo.bookmarks()) { + for (Pair bm : remoteRepo.getBookmarks()) { Nodeid localRevision = localBookmarks.getRevision(bm.first()); if (localRevision == null || !parentHelper.knownNode(bm.second())) { continue; @@ -98,7 +157,6 @@ } } } -// remote.listkeys("bookmarks"); // XXX WTF is obsolete in namespaces key?? progress.worked(5); } catch (IOException ex) { @@ -120,7 +178,7 @@ */ public static void main(String[] args) throws Exception { final HgLookup hgLookup = new HgLookup(); - HgRepository r = hgLookup.detect("/home/artem/hg/junit-test-repos/log-1/"); + HgRepository r = hgLookup.detect("/home/artem/hg/test-phases/"); HgRemoteRepository rr = hgLookup.detect(new URL("http://localhost:8000/")); new HgPushCommand(r).destination(rr).execute(); } diff -r 690e71d29bf6 -r e79cf9a8130b src/org/tmatesoft/hg/internal/LineReader.java --- a/src/org/tmatesoft/hg/internal/LineReader.java Tue Jun 25 20:48:37 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/LineReader.java Wed Jun 26 20:52:38 2013 +0200 @@ -95,7 +95,14 @@ return this; } - public void read(LineConsumer consumer, T paramObj) throws HgIOException { + /** + * + * @param consumer where to pipe read lines to + * @param paramObj parameterizes consumer + * @return paramObj value for convenience + * @throws HgIOException if there's {@link IOException} while reading file + */ + public T read(LineConsumer consumer, T paramObj) throws HgIOException { BufferedReader statusFileReader = null; try { // consumer.begin(file, paramObj); @@ -119,6 +126,7 @@ ok = consumer.consume(line, paramObj); } } + return paramObj; } catch (IOException ex) { throw new HgIOException(ex.getMessage(), ex, file); } finally { diff -r 690e71d29bf6 -r e79cf9a8130b src/org/tmatesoft/hg/internal/PhasesHelper.java --- a/src/org/tmatesoft/hg/internal/PhasesHelper.java Tue Jun 25 20:48:37 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/PhasesHelper.java Wed Jun 26 20:52:38 2013 +0200 @@ -19,19 +19,20 @@ import static org.tmatesoft.hg.repo.HgPhase.Draft; import static org.tmatesoft.hg.repo.HgPhase.Secret; import static org.tmatesoft.hg.repo.HgRepositoryFiles.Phaseroots; -import static org.tmatesoft.hg.util.LogFacility.Severity.Info; import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; -import java.io.BufferedReader; import java.io.File; -import java.io.FileReader; +import java.io.FileWriter; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import org.tmatesoft.hg.core.HgChangeset; +import org.tmatesoft.hg.core.HgIOException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.repo.HgChangelog; import org.tmatesoft.hg.repo.HgInvalidControlFileException; @@ -141,9 +142,34 @@ return allOf(HgPhase.Secret); } + /** + * @return all revisions with draft phase + */ public RevisionSet allDraft() { return allOf(HgPhase.Draft).subtract(allOf(HgPhase.Secret)); } + + public void updateRoots(Collection draftRoots, Collection secretRoots) throws HgInvalidControlFileException { + draftPhaseRoots = draftRoots.isEmpty() ? Collections.emptyList() : new ArrayList(draftRoots); + secretPhaseRoots = secretRoots.isEmpty() ? Collections.emptyList() : new ArrayList(secretRoots); + String fmt = "%d %s\n"; + File phaseroots = repo.getRepositoryFile(Phaseroots); + FileWriter fw = null; + try { + fw = new FileWriter(phaseroots); + for (Nodeid n : secretPhaseRoots) { + fw.write(String.format(fmt, HgPhase.Secret.mercurialOrdinal(), n.toString())); + } + for (Nodeid n : draftPhaseRoots) { + fw.write(String.format(fmt, HgPhase.Draft.mercurialOrdinal(), n.toString())); + } + fw.flush(); + } catch (IOException ex) { + throw new HgInvalidControlFileException(ex.getMessage(), ex, phaseroots); + } finally { + new FileUtils(repo.getLog()).closeQuietly(fw); + } + } /** * For a given phase, collect all revisions with phase that is the same or more private (i.e. for Draft, returns Draft+Secret) @@ -168,16 +194,15 @@ private Boolean readRoots() throws HgRuntimeException { File phaseroots = repo.getRepositoryFile(Phaseroots); - BufferedReader br = null; try { if (!phaseroots.exists()) { return Boolean.FALSE; } + LineReader lr = new LineReader(phaseroots, repo.getLog()); + final Collection lines = lr.read(new LineReader.SimpleLineCollector(), new LinkedList()); HashMap> phase2roots = new HashMap>(); - br = new BufferedReader(new FileReader(phaseroots)); - String line; - while ((line = br.readLine()) != null) { - String[] lc = line.trim().split("\\s+"); + for (String line : lines) { + String[] lc = line.split("\\s+"); if (lc.length == 0) { continue; } @@ -200,17 +225,8 @@ } 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); - } finally { - if (br != null) { - try { - br.close(); - } catch (IOException ex) { - repo.getSessionContext().getLog().dump(getClass(), Info, ex, null); - // ignore the exception otherwise - } - } + } catch (HgIOException ex) { + throw new HgInvalidControlFileException(ex, true); } return Boolean.TRUE; } diff -r 690e71d29bf6 -r e79cf9a8130b src/org/tmatesoft/hg/internal/RevisionSet.java --- a/src/org/tmatesoft/hg/internal/RevisionSet.java Tue Jun 25 20:48:37 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/RevisionSet.java Wed Jun 26 20:52:38 2013 +0200 @@ -16,12 +16,17 @@ */ package org.tmatesoft.hg.internal; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; +import java.util.List; import java.util.Set; import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.repo.HgChangelog; +import org.tmatesoft.hg.repo.HgParentChildMap; /** * Unmodifiable collection of revisions with handy set operations @@ -29,7 +34,7 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public final class RevisionSet { +public final class RevisionSet implements Iterable { private final Set elements; @@ -45,12 +50,70 @@ } } - public RevisionSet roots() { - throw Internals.notImplemented(); + /** + * elements of the set with no parents or parents not from the same set + */ + public RevisionSet roots(HgParentChildMap ph) { + HashSet copy = new HashSet(elements); + for (Nodeid n : elements) { + assert ph.knownNode(n); + Nodeid p1 = ph.firstParent(n); + if (p1 != null && elements.contains(p1)) { + copy.remove(n); + continue; + } + Nodeid p2 = ph.secondParent(n); + if (p2 != null && elements.contains(p2)) { + copy.remove(n); + continue; + } + } + return copy.size() == elements.size() ? this : new RevisionSet(copy); } - public RevisionSet heads() { - throw Internals.notImplemented(); + /** + * elements of the set that has no children in this set + */ + public RevisionSet heads(HgParentChildMap ph) { + HashSet copy = new HashSet(elements); + // can't do copy.removeAll(ph.childrenOf(asList())); as actual heads are indeed children of some other node + for (Nodeid n : elements) { + assert ph.knownNode(n); + Nodeid p1 = ph.firstParent(n); + Nodeid p2 = ph.secondParent(n); + if (p1 != null && elements.contains(p1)) { + copy.remove(p1); + } + if (p2 != null && elements.contains(p2)) { + copy.remove(p2); + } + } + return copy.size() == elements.size() ? this : new RevisionSet(copy); + } + + /** + * Immediate parents of the supplied children set found in this one. + */ + public RevisionSet parentsOf(RevisionSet children, HgParentChildMap parentHelper) { + if (isEmpty()) { + return this; + } + if (children.isEmpty()) { + return children; + } + RevisionSet chRoots = children.roots(parentHelper); + HashSet parents = new HashSet(); + for (Nodeid n : chRoots.elements) { + Nodeid p1 = parentHelper.firstParent(n); + Nodeid p2 = parentHelper.secondParent(n); + if (p1 != null && elements.contains(p1)) { + parents.add(p1); + } + if (p2 != null && elements.contains(p2)) { + parents.add(p2); + } + } + return new RevisionSet(parents); } public RevisionSet intersect(RevisionSet other) { @@ -109,6 +172,15 @@ return elements.isEmpty(); } + + public List asList() { + return new ArrayList(elements); + } + + public Iterator iterator() { + return elements.iterator(); + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); diff -r 690e71d29bf6 -r e79cf9a8130b src/org/tmatesoft/hg/repo/HgPhase.java --- a/src/org/tmatesoft/hg/repo/HgPhase.java Tue Jun 25 20:48:37 2013 +0200 +++ b/src/org/tmatesoft/hg/repo/HgPhase.java Wed Jun 26 20:52:38 2013 +0200 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012 TMate Software Ltd + * Copyright (c) 2012-2013 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 @@ -58,4 +58,14 @@ } throw new IllegalArgumentException(String.format("Bad phase name: %d", value)); } + + /** + * @return integer value Mercurial uses to identify the phase + */ + public int mercurialOrdinal() { + if (this == Undefined) { + throw new IllegalStateException("Undefined phase is an artifical value, which doesn't possess a valid native mercurial ordinal"); + } + return ordinal(); // what a coincidence + } } diff -r 690e71d29bf6 -r e79cf9a8130b src/org/tmatesoft/hg/repo/HgRemoteRepository.java --- a/src/org/tmatesoft/hg/repo/HgRemoteRepository.java Tue Jun 25 20:48:37 2013 +0200 +++ b/src/org/tmatesoft/hg/repo/HgRemoteRepository.java Wed Jun 26 20:52:38 2013 +0200 @@ -66,8 +66,10 @@ import org.tmatesoft.hg.internal.Internals; import org.tmatesoft.hg.internal.DataSerializer.OutputStreamSerializer; import org.tmatesoft.hg.internal.PropertyMarshal; +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 @@ -450,7 +452,7 @@ } } - public List> bookmarks() throws HgRemoteConnectionException, HgRuntimeException { + public Bookmarks getBookmarks() throws HgRemoteConnectionException, HgRuntimeException { final String actionName = "Get remote bookmarks"; final List> values = listkeys("bookmarks", actionName); ArrayList> rv = new ArrayList>(); @@ -463,7 +465,7 @@ String bm = new String(l.first()); rv.add(new Pair(bm, n)); } - return rv; + return new Bookmarks(rv); } public void updateBookmark(String name, Nodeid oldRev, Nodeid newRev) throws HgRemoteConnectionException, HgRuntimeException { @@ -488,21 +490,48 @@ } } - private void phases() throws HgRemoteConnectionException, HgRuntimeException { + public Phases getPhases() throws HgRemoteConnectionException, HgRuntimeException { + initCapabilities(); + if (!remoteCapabilities.contains("pushkey")) { + // old server defaults to publishing + return new Phases(true, Collections.emptyList()); + } final List> values = listkeys("phases", "Get remote phases"); + boolean publishing = true; + ArrayList draftRoots = new ArrayList(); for (Pair l : values) { - System.out.printf("%s : %s\n", l.first(), l.second()); + if ("publishing".equalsIgnoreCase(l.first())) { + publishing = Boolean.parseBoolean(l.second()); + continue; + } + Nodeid root = Nodeid.fromAscii(l.first()); + int ph = Integer.parseInt(l.second()); + if (ph == HgPhase.Draft.mercurialOrdinal()) { + draftRoots.add(root); + } else { + assert false; + sessionContext.getLog().dump(getClass(), Severity.Error, "Unexpected phase value %d for revision %s", ph, root); + } } + return new Phases(publishing, draftRoots); } + public Outcome updatePhase(HgPhase from, HgPhase to, Nodeid n) throws HgRemoteConnectionException, HgRuntimeException { + 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(Kind.Failure, String.format("Phase update (%s: %s -> %s) failed", n.shortNotation(), from.name(), to.name())); + } + + 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(); + r.getPhases(); + final Iterable> bm = r.getBookmarks(); for (Pair pair : bm) { System.out.println(pair); } @@ -600,6 +629,35 @@ } } + 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); + URL u = new URL(url, p); + c = setupConnection(u.openConnection()); + c.connect(); + if (debug) { + dumpResponseHeader(u, c); + } + 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) { + 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 checkResponseOk(HttpURLConnection c, String opName, String remoteCmd) throws HgRemoteConnectionException, IOException { if (c.getResponseCode() != 200) { String m = c.getResponseMessage() == null ? "unknown reason" : c.getResponseMessage(); @@ -712,4 +770,45 @@ return head.equals(o.head) && root.equals(o.root) && (p1 == null && o.p1 == null || p1.equals(o.p1)) && (p2 == null && o.p2 == null || p2.equals(o.p2)); } } + + public static final class Bookmarks implements Iterable> { + private final List> bm; + + private Bookmarks(List> bookmarks) { + bm = bookmarks; + } + + public Iterator> iterator() { + return bm.iterator(); + } + } + + public static final class Phases { + private final boolean pub; + private final List droots; + + private Phases(boolean publishing, List draftRoots) { + pub = publishing; + droots = draftRoots; + } + + /** + * Non-publishing servers may (shall?) respond with a list of draft roots. + * This method doesn't make sense when {@link #isPublishingServer()} is true + * + * @return list of draft roots on remote server + */ + public List draftRoots() { + assert !pub; + return droots; + } + + /** + * @return true if revisions on remote server shall be deemed published (either + * old server w/o explicit setting, or a new one with phases.publish == true) + */ + public boolean isPublishingServer() { + return pub; + } + } }