tikhomirov@170: /* tikhomirov@645: * Copyright (c) 2011-2013 TMate Software Ltd tikhomirov@170: * tikhomirov@170: * This program is free software; you can redistribute it and/or modify tikhomirov@170: * it under the terms of the GNU General Public License as published by tikhomirov@170: * the Free Software Foundation; version 2 of the License. tikhomirov@170: * tikhomirov@170: * This program is distributed in the hope that it will be useful, tikhomirov@170: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@170: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@170: * GNU General Public License for more details. tikhomirov@170: * tikhomirov@170: * For information on how to redistribute this software under tikhomirov@170: * the terms of a license other than GNU General Public License tikhomirov@170: * contact TMate Software at support@hg4j.com tikhomirov@170: */ tikhomirov@170: package org.tmatesoft.hg.repo; tikhomirov@170: tikhomirov@687: import static org.tmatesoft.hg.internal.remote.Connector.*; tikhomirov@650: import static org.tmatesoft.hg.util.Outcome.Kind.Failure; tikhomirov@650: import static org.tmatesoft.hg.util.Outcome.Kind.Success; tikhomirov@456: tikhomirov@428: import java.io.BufferedReader; tikhomirov@170: import java.io.File; tikhomirov@176: import java.io.IOException; tikhomirov@179: import java.io.InputStream; tikhomirov@177: import java.io.InputStreamReader; tikhomirov@177: import java.io.OutputStream; tikhomirov@176: import java.io.StreamTokenizer; tikhomirov@178: import java.util.ArrayList; tikhomirov@428: import java.util.Arrays; tikhomirov@176: import java.util.Collection; tikhomirov@171: import java.util.Collections; tikhomirov@428: import java.util.HashSet; tikhomirov@177: import java.util.Iterator; tikhomirov@176: import java.util.LinkedHashMap; tikhomirov@176: import java.util.LinkedList; tikhomirov@170: import java.util.List; tikhomirov@176: import java.util.Map; tikhomirov@428: import java.util.Set; tikhomirov@176: tikhomirov@181: import org.tmatesoft.hg.core.HgBadArgumentException; tikhomirov@645: import org.tmatesoft.hg.core.HgIOException; tikhomirov@215: import org.tmatesoft.hg.core.HgRemoteConnectionException; tikhomirov@425: import org.tmatesoft.hg.core.HgRepositoryNotFoundException; tikhomirov@170: import org.tmatesoft.hg.core.Nodeid; tikhomirov@295: import org.tmatesoft.hg.core.SessionContext; tikhomirov@687: import org.tmatesoft.hg.internal.BundleSerializer; tikhomirov@645: import org.tmatesoft.hg.internal.DataSerializer; tikhomirov@650: import org.tmatesoft.hg.internal.DataSerializer.OutputStreamSerializer; tikhomirov@646: import org.tmatesoft.hg.internal.EncodingHelper; tikhomirov@687: import org.tmatesoft.hg.internal.FileUtils; tikhomirov@645: import org.tmatesoft.hg.internal.Internals; tikhomirov@456: import org.tmatesoft.hg.internal.PropertyMarshal; tikhomirov@687: import org.tmatesoft.hg.internal.remote.Connector; tikhomirov@698: import org.tmatesoft.hg.internal.remote.RemoteConnectorDescriptor; tikhomirov@698: import org.tmatesoft.hg.repo.HgLookup.RemoteDescriptor; tikhomirov@650: import org.tmatesoft.hg.util.LogFacility.Severity; tikhomirov@649: import org.tmatesoft.hg.util.Outcome; tikhomirov@646: import org.tmatesoft.hg.util.Pair; tikhomirov@170: tikhomirov@170: /** tikhomirov@170: * WORK IN PROGRESS, DO NOT USE tikhomirov@170: * tikhomirov@170: * @see http://mercurial.selenic.com/wiki/WireProtocol tikhomirov@646: * @see http://mercurial.selenic.com/wiki/HttpCommandProtocol tikhomirov@170: * tikhomirov@170: * @author Artem Tikhomirov tikhomirov@170: * @author TMate Software Ltd. tikhomirov@170: */ tikhomirov@490: public class HgRemoteRepository implements SessionContext.Source { tikhomirov@171: tikhomirov@407: private final boolean debug; tikhomirov@186: private HgLookup lookupHelper; tikhomirov@295: private final SessionContext sessionContext; tikhomirov@428: private Set remoteCapabilities; tikhomirov@687: private Connector remote; tikhomirov@428: tikhomirov@698: HgRemoteRepository(SessionContext ctx, RemoteDescriptor rd) throws HgBadArgumentException { tikhomirov@698: if (false == rd instanceof RemoteConnectorDescriptor) { tikhomirov@698: throw new IllegalArgumentException(String.format("Present implementation supports remote connections via %s only", Connector.class.getName())); tikhomirov@176: } tikhomirov@295: sessionContext = ctx; tikhomirov@456: debug = new PropertyMarshal(ctx).getBoolean("hg4j.remote.debug", false); tikhomirov@698: remote = ((RemoteConnectorDescriptor) rd).createConnector(); tikhomirov@698: remote.init(rd.getURI(), ctx, null); tikhomirov@171: } tikhomirov@171: tikhomirov@215: public boolean isInvalid() throws HgRemoteConnectionException { tikhomirov@646: initCapabilities(); tikhomirov@428: return remoteCapabilities.isEmpty(); tikhomirov@181: } tikhomirov@181: tikhomirov@181: /** tikhomirov@181: * @return human-readable address of the server, without user credentials or any other security information tikhomirov@181: */ tikhomirov@181: public String getLocation() { tikhomirov@687: return remote.getServerLocation(); tikhomirov@181: } tikhomirov@490: tikhomirov@490: public SessionContext getSessionContext() { tikhomirov@490: return sessionContext; tikhomirov@490: } tikhomirov@181: tikhomirov@215: public List heads() throws HgRemoteConnectionException { tikhomirov@687: if (isInvalid()) { tikhomirov@687: return Collections.emptyList(); tikhomirov@687: } tikhomirov@178: try { tikhomirov@687: remote.sessionBegin(); tikhomirov@687: InputStreamReader is = new InputStreamReader(remote.heads(), "US-ASCII"); tikhomirov@178: StreamTokenizer st = new StreamTokenizer(is); tikhomirov@428: st.ordinaryChars('0', '9'); // wordChars performs |, hence need to 0 first tikhomirov@178: st.wordChars('0', '9'); tikhomirov@178: st.eolIsSignificant(false); tikhomirov@178: LinkedList parseResult = new LinkedList(); tikhomirov@178: while (st.nextToken() != StreamTokenizer.TT_EOF) { tikhomirov@178: parseResult.add(Nodeid.fromAscii(st.sval)); tikhomirov@178: } tikhomirov@178: return parseResult; tikhomirov@178: } catch (IOException ex) { tikhomirov@687: throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_HEADS).setServerInfo(getLocation()); tikhomirov@645: } finally { tikhomirov@687: remote.sessionEnd(); tikhomirov@178: } tikhomirov@171: } tikhomirov@171: tikhomirov@215: public List between(Nodeid tip, Nodeid base) throws HgRemoteConnectionException { tikhomirov@177: Range r = new Range(base, tip); tikhomirov@177: // XXX shall handle errors like no range key in the returned map, not sure how. tikhomirov@177: return between(Collections.singletonList(r)).get(r); tikhomirov@177: } tikhomirov@177: tikhomirov@177: /** tikhomirov@177: * @param ranges tikhomirov@177: * @return map, where keys are input instances, values are corresponding server reply tikhomirov@215: * @throws HgRemoteConnectionException tikhomirov@177: */ tikhomirov@215: public Map> between(Collection ranges) throws HgRemoteConnectionException { tikhomirov@687: if (ranges.isEmpty() || isInvalid()) { tikhomirov@177: return Collections.emptyMap(); tikhomirov@177: } tikhomirov@177: LinkedHashMap> rv = new LinkedHashMap>(ranges.size() * 4 / 3); tikhomirov@176: try { tikhomirov@687: remote.sessionBegin(); tikhomirov@687: InputStreamReader is = new InputStreamReader(remote.between(ranges), "US-ASCII"); tikhomirov@176: StreamTokenizer st = new StreamTokenizer(is); tikhomirov@176: st.ordinaryChars('0', '9'); tikhomirov@176: st.wordChars('0', '9'); tikhomirov@177: st.eolIsSignificant(true); tikhomirov@177: Iterator rangeItr = ranges.iterator(); tikhomirov@177: LinkedList currRangeList = null; tikhomirov@177: Range currRange = null; tikhomirov@177: boolean possiblyEmptyNextLine = true; tikhomirov@176: while (st.nextToken() != StreamTokenizer.TT_EOF) { tikhomirov@177: if (st.ttype == StreamTokenizer.TT_EOL) { tikhomirov@177: if (possiblyEmptyNextLine) { tikhomirov@177: // newline follows newline; tikhomirov@177: assert currRange == null; tikhomirov@177: assert currRangeList == null; tikhomirov@177: if (!rangeItr.hasNext()) { tikhomirov@423: throw new HgInvalidStateException("Internal error"); // TODO revisit-1.1 tikhomirov@177: } tikhomirov@177: rv.put(rangeItr.next(), Collections.emptyList()); tikhomirov@177: } else { tikhomirov@177: if (currRange == null || currRangeList == null) { tikhomirov@423: throw new HgInvalidStateException("Internal error"); // TODO revisit-1.1 tikhomirov@177: } tikhomirov@177: // indicate next range value is needed tikhomirov@177: currRange = null; tikhomirov@177: currRangeList = null; tikhomirov@177: possiblyEmptyNextLine = true; tikhomirov@177: } tikhomirov@177: } else { tikhomirov@177: possiblyEmptyNextLine = false; tikhomirov@177: if (currRange == null) { tikhomirov@177: if (!rangeItr.hasNext()) { tikhomirov@423: throw new HgInvalidStateException("Internal error"); // TODO revisit-1.1 tikhomirov@177: } tikhomirov@177: currRange = rangeItr.next(); tikhomirov@177: currRangeList = new LinkedList(); tikhomirov@177: rv.put(currRange, currRangeList); tikhomirov@177: } tikhomirov@177: Nodeid nid = Nodeid.fromAscii(st.sval); tikhomirov@177: currRangeList.addLast(nid); tikhomirov@177: } tikhomirov@176: } tikhomirov@176: is.close(); tikhomirov@176: return rv; tikhomirov@176: } catch (IOException ex) { tikhomirov@687: throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_BETWEEN).setServerInfo(getLocation()); tikhomirov@645: } finally { tikhomirov@687: remote.sessionEnd(); tikhomirov@176: } tikhomirov@176: } tikhomirov@176: tikhomirov@215: public List branches(List nodes) throws HgRemoteConnectionException { tikhomirov@687: if (isInvalid()) { tikhomirov@687: return Collections.emptyList(); tikhomirov@687: } tikhomirov@178: try { tikhomirov@687: remote.sessionBegin(); tikhomirov@687: InputStreamReader is = new InputStreamReader(remote.branches(nodes), "US-ASCII"); tikhomirov@178: StreamTokenizer st = new StreamTokenizer(is); tikhomirov@178: st.ordinaryChars('0', '9'); tikhomirov@178: st.wordChars('0', '9'); tikhomirov@178: st.eolIsSignificant(false); tikhomirov@178: ArrayList parseResult = new ArrayList(nodes.size() * 4); tikhomirov@178: while (st.nextToken() != StreamTokenizer.TT_EOF) { tikhomirov@178: parseResult.add(Nodeid.fromAscii(st.sval)); tikhomirov@178: } tikhomirov@178: if (parseResult.size() != nodes.size() * 4) { tikhomirov@215: throw new HgRemoteConnectionException(String.format("Bad number of nodeids in result (shall be factor 4), expected %d, got %d", nodes.size()*4, parseResult.size())); tikhomirov@178: } tikhomirov@178: ArrayList rv = new ArrayList(nodes.size()); tikhomirov@178: for (int i = 0; i < nodes.size(); i++) { tikhomirov@178: RemoteBranch rb = new RemoteBranch(parseResult.get(i*4), parseResult.get(i*4 + 1), parseResult.get(i*4 + 2), parseResult.get(i*4 + 3)); tikhomirov@178: rv.add(rb); tikhomirov@178: } tikhomirov@178: return rv; tikhomirov@178: } catch (IOException ex) { tikhomirov@687: throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_BRANCHES).setServerInfo(getLocation()); tikhomirov@645: } finally { tikhomirov@687: remote.sessionEnd(); tikhomirov@178: } tikhomirov@171: } tikhomirov@170: tikhomirov@186: /* tikhomirov@202: * XXX need to describe behavior when roots arg is empty; our RepositoryComparator code currently returns empty lists when tikhomirov@202: * no common elements found, which in turn means we need to query changes starting with NULL nodeid. tikhomirov@202: * tikhomirov@186: * WireProtocol wiki: roots = a list of the latest nodes on every service side changeset branch that both the client and server know about. tikhomirov@186: * tikhomirov@186: * Perhaps, shall be named 'changegroup' tikhomirov@186: tikhomirov@186: * Changegroup: tikhomirov@186: * http://mercurial.selenic.com/wiki/Merge tikhomirov@186: * http://mercurial.selenic.com/wiki/WireProtocol tikhomirov@186: * tikhomirov@186: * according to latter, bundleformat data is sent through zlib tikhomirov@186: * (there's no header like HG10?? with the server output, though, tikhomirov@186: * as one may expect according to http://mercurial.selenic.com/wiki/BundleFormat) tikhomirov@186: */ tikhomirov@425: public HgBundle getChanges(List roots) throws HgRemoteConnectionException, HgRuntimeException { tikhomirov@687: if (isInvalid()) { tikhomirov@687: return null; // XXX valid retval??? tikhomirov@687: } tikhomirov@202: List _roots = roots.isEmpty() ? Collections.singletonList(Nodeid.NULL) : roots; tikhomirov@179: try { tikhomirov@687: remote.sessionBegin(); tikhomirov@697: File tf = writeBundle(remote.changegroup(_roots)); tikhomirov@179: if (debug) { tikhomirov@687: System.out.printf("Wrote bundle %s for roots %s\n", tf, roots); tikhomirov@190: } tikhomirov@186: return getLookupHelper().loadBundle(tf); tikhomirov@179: } catch (IOException ex) { tikhomirov@687: throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_CHANGEGROUP).setServerInfo(getLocation()); tikhomirov@425: } catch (HgRepositoryNotFoundException ex) { tikhomirov@687: throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_CHANGEGROUP).setServerInfo(getLocation()); tikhomirov@645: } finally { tikhomirov@687: remote.sessionEnd(); tikhomirov@645: } tikhomirov@645: } tikhomirov@645: tikhomirov@646: public void unbundle(HgBundle bundle, List remoteHeads) throws HgRemoteConnectionException, HgRuntimeException { tikhomirov@646: if (remoteHeads == null) { tikhomirov@645: // TODO collect heads from bundle: tikhomirov@645: // bundle.inspectChangelog(new HeadCollector(for each c : if collected has c.p1 or c.p2, remove them. Add c)) tikhomirov@646: // or get from remote server??? tikhomirov@645: throw Internals.notImplemented(); tikhomirov@645: } tikhomirov@687: if (isInvalid()) { tikhomirov@687: return; tikhomirov@687: } tikhomirov@673: DataSerializer.DataSource bundleData = BundleSerializer.newInstance(sessionContext, bundle); tikhomirov@687: OutputStream os = null; tikhomirov@645: try { tikhomirov@687: remote.sessionBegin(); tikhomirov@687: os = remote.unbundle(bundleData.serializeLength(), remoteHeads); tikhomirov@645: bundleData.serialize(new OutputStreamSerializer(os)); tikhomirov@645: os.flush(); tikhomirov@645: os.close(); tikhomirov@687: os = null; tikhomirov@645: } catch (IOException ex) { tikhomirov@645: throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("unbundle").setServerInfo(getLocation()); tikhomirov@645: } catch (HgIOException ex) { tikhomirov@645: throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("unbundle").setServerInfo(getLocation()); tikhomirov@645: } finally { tikhomirov@687: new FileUtils(sessionContext.getLog(), this).closeQuietly(os); tikhomirov@687: remote.sessionEnd(); tikhomirov@179: } tikhomirov@170: } tikhomirov@186: tikhomirov@649: public Bookmarks getBookmarks() throws HgRemoteConnectionException, HgRuntimeException { tikhomirov@687: initCapabilities(); tikhomirov@687: if (!remoteCapabilities.contains(CMD_PUSHKEY)) { // (sic!) listkeys is available when pushkey in caps tikhomirov@687: return new Bookmarks(Collections.>emptyList()); tikhomirov@687: } tikhomirov@646: final String actionName = "Get remote bookmarks"; tikhomirov@646: final List> values = listkeys("bookmarks", actionName); tikhomirov@646: ArrayList> rv = new ArrayList>(); tikhomirov@646: for (Pair l : values) { tikhomirov@646: if (l.second().length() != Nodeid.SIZE_ASCII) { tikhomirov@646: sessionContext.getLog().dump(getClass(), Severity.Warn, "%s: bad nodeid '%s', ignored", actionName, l.second()); tikhomirov@646: continue; tikhomirov@646: } tikhomirov@646: Nodeid n = Nodeid.fromAscii(l.second()); tikhomirov@646: String bm = new String(l.first()); tikhomirov@646: rv.add(new Pair(bm, n)); tikhomirov@646: } tikhomirov@649: return new Bookmarks(rv); tikhomirov@646: } tikhomirov@646: tikhomirov@652: public Outcome updateBookmark(String name, Nodeid oldRev, Nodeid newRev) throws HgRemoteConnectionException, HgRuntimeException { tikhomirov@652: initCapabilities(); tikhomirov@687: if (!remoteCapabilities.contains(CMD_PUSHKEY)) { tikhomirov@652: return new Outcome(Failure, "Server doesn't support pushkey protocol"); tikhomirov@646: } tikhomirov@687: if (pushkey("Update remote bookmark", NS_BOOKMARKS, name, oldRev.toString(), newRev.toString())) { tikhomirov@652: return new Outcome(Success, String.format("Bookmark %s updated to %s", name, newRev.shortNotation())); tikhomirov@652: } tikhomirov@652: return new Outcome(Failure, String.format("Bookmark update (%s: %s -> %s) failed", name, oldRev.shortNotation(), newRev.shortNotation())); tikhomirov@646: } tikhomirov@646: tikhomirov@649: public Phases getPhases() throws HgRemoteConnectionException, HgRuntimeException { tikhomirov@649: initCapabilities(); tikhomirov@687: if (!remoteCapabilities.contains(CMD_PUSHKEY)) { tikhomirov@649: // old server defaults to publishing tikhomirov@649: return new Phases(true, Collections.emptyList()); tikhomirov@649: } tikhomirov@687: final List> values = listkeys(NS_PHASES, "Get remote phases"); tikhomirov@650: boolean publishing = false; tikhomirov@649: ArrayList draftRoots = new ArrayList(); tikhomirov@646: for (Pair l : values) { tikhomirov@649: if ("publishing".equalsIgnoreCase(l.first())) { tikhomirov@649: publishing = Boolean.parseBoolean(l.second()); tikhomirov@649: continue; tikhomirov@649: } tikhomirov@649: Nodeid root = Nodeid.fromAscii(l.first()); tikhomirov@649: int ph = Integer.parseInt(l.second()); tikhomirov@649: if (ph == HgPhase.Draft.mercurialOrdinal()) { tikhomirov@649: draftRoots.add(root); tikhomirov@649: } else { tikhomirov@649: assert false; tikhomirov@649: sessionContext.getLog().dump(getClass(), Severity.Error, "Unexpected phase value %d for revision %s", ph, root); tikhomirov@649: } tikhomirov@646: } tikhomirov@649: return new Phases(publishing, draftRoots); tikhomirov@646: } tikhomirov@646: tikhomirov@649: public Outcome updatePhase(HgPhase from, HgPhase to, Nodeid n) throws HgRemoteConnectionException, HgRuntimeException { tikhomirov@650: initCapabilities(); tikhomirov@687: if (!remoteCapabilities.contains(CMD_PUSHKEY)) { tikhomirov@650: return new Outcome(Failure, "Server doesn't support pushkey protocol"); tikhomirov@650: } tikhomirov@687: if (pushkey("Update remote phases", NS_PHASES, n.toString(), String.valueOf(from.mercurialOrdinal()), String.valueOf(to.mercurialOrdinal()))) { tikhomirov@650: return new Outcome(Success, String.format("Phase of %s updated to %s", n.shortNotation(), to.name())); tikhomirov@649: } tikhomirov@650: return new Outcome(Failure, String.format("Phase update (%s: %s -> %s) failed", n.shortNotation(), from.name(), to.name())); tikhomirov@649: } tikhomirov@649: tikhomirov@203: @Override tikhomirov@203: public String toString() { tikhomirov@203: return getClass().getSimpleName() + '[' + getLocation() + ']'; tikhomirov@203: } tikhomirov@646: tikhomirov@646: tikhomirov@646: private void initCapabilities() throws HgRemoteConnectionException { tikhomirov@687: if (remoteCapabilities != null) { tikhomirov@687: return; tikhomirov@687: } tikhomirov@687: remote.connect(); tikhomirov@687: try { tikhomirov@687: remote.sessionBegin(); tikhomirov@687: String capsLine = remote.getCapabilities(); tikhomirov@687: String[] caps = capsLine.split("\\s"); tikhomirov@687: remoteCapabilities = new HashSet(Arrays.asList(caps)); tikhomirov@687: } finally { tikhomirov@687: remote.sessionEnd(); tikhomirov@646: } tikhomirov@646: } tikhomirov@203: tikhomirov@186: private HgLookup getLookupHelper() { tikhomirov@186: if (lookupHelper == null) { tikhomirov@295: lookupHelper = new HgLookup(sessionContext); tikhomirov@186: } tikhomirov@186: return lookupHelper; tikhomirov@186: } tikhomirov@646: tikhomirov@646: private List> listkeys(String namespace, String actionName) throws HgRemoteConnectionException, HgRuntimeException { tikhomirov@646: try { tikhomirov@687: remote.sessionBegin(); tikhomirov@646: ArrayList> rv = new ArrayList>(); tikhomirov@687: InputStream response = remote.listkeys(namespace, actionName); tikhomirov@685: // output of listkeys is encoded with UTF-8 tikhomirov@687: BufferedReader r = new BufferedReader(new InputStreamReader(response, EncodingHelper.getUTF8())); tikhomirov@646: String l; tikhomirov@646: while ((l = r.readLine()) != null) { tikhomirov@646: int sep = l.indexOf('\t'); tikhomirov@646: if (sep == -1) { tikhomirov@646: sessionContext.getLog().dump(getClass(), Severity.Warn, "%s: bad line '%s', ignored", actionName, l); tikhomirov@646: continue; tikhomirov@646: } tikhomirov@646: rv.add(new Pair(l.substring(0, sep), l.substring(sep+1))); tikhomirov@646: } tikhomirov@646: r.close(); tikhomirov@646: return rv; tikhomirov@646: } catch (IOException ex) { tikhomirov@687: throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_LISTKEYS).setServerInfo(getLocation()); tikhomirov@646: } finally { tikhomirov@687: remote.sessionEnd(); tikhomirov@646: } tikhomirov@646: } tikhomirov@176: tikhomirov@652: private boolean pushkey(String opName, String namespace, String key, String oldValue, String newValue) throws HgRemoteConnectionException, HgRuntimeException { tikhomirov@649: try { tikhomirov@687: remote.sessionBegin(); tikhomirov@687: final InputStream is = remote.pushkey(opName, namespace, key, oldValue, newValue); tikhomirov@649: int rv = is.read(); tikhomirov@649: is.close(); tikhomirov@649: return rv == '1'; tikhomirov@649: } catch (IOException ex) { tikhomirov@687: throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand(CMD_PUSHKEY).setServerInfo(getLocation()); tikhomirov@649: } finally { tikhomirov@687: remote.sessionEnd(); tikhomirov@645: } tikhomirov@645: } tikhomirov@645: tikhomirov@697: private File writeBundle(InputStream is) throws IOException { tikhomirov@646: File tf = File.createTempFile("hg4j-bundle-", null); tikhomirov@697: new FileUtils(sessionContext.getLog(), this).write(is, tf); tikhomirov@697: is.close(); tikhomirov@179: return tf; tikhomirov@179: } tikhomirov@179: tikhomirov@178: tikhomirov@176: public static final class Range { tikhomirov@176: /** tikhomirov@176: * Root of the range, earlier revision tikhomirov@176: */ tikhomirov@176: public final Nodeid start; tikhomirov@176: /** tikhomirov@176: * Head of the range, later revision. tikhomirov@176: */ tikhomirov@176: public final Nodeid end; tikhomirov@176: tikhomirov@176: /** tikhomirov@176: * @param from - root/base revision tikhomirov@176: * @param to - head/tip revision tikhomirov@176: */ tikhomirov@176: public Range(Nodeid from, Nodeid to) { tikhomirov@176: start = from; tikhomirov@176: end = to; tikhomirov@176: } tikhomirov@685: tikhomirov@685: /** tikhomirov@685: * Append this range as pair of values 'end-start' to the supplied buffer and return the buffer. tikhomirov@685: */ tikhomirov@685: public StringBuilder append(StringBuilder sb) { tikhomirov@685: sb.append(end.toString()); tikhomirov@685: sb.append('-'); tikhomirov@685: sb.append(start.toString()); tikhomirov@685: return sb; tikhomirov@685: } tikhomirov@176: } tikhomirov@184: tikhomirov@171: public static final class RemoteBranch { tikhomirov@171: public final Nodeid head, root, p1, p2; tikhomirov@171: tikhomirov@171: public RemoteBranch(Nodeid h, Nodeid r, Nodeid parent1, Nodeid parent2) { tikhomirov@171: head = h; tikhomirov@171: root = r; tikhomirov@171: p1 = parent1; tikhomirov@171: p2 = parent2; tikhomirov@171: } tikhomirov@171: tikhomirov@171: @Override tikhomirov@171: public boolean equals(Object obj) { tikhomirov@171: if (this == obj) { tikhomirov@171: return true; tikhomirov@171: } tikhomirov@171: if (false == obj instanceof RemoteBranch) { tikhomirov@171: return false; tikhomirov@171: } tikhomirov@171: RemoteBranch o = (RemoteBranch) obj; tikhomirov@274: // in fact, p1 and p2 are not supposed to be null, ever (at least for RemoteBranch created from server output) tikhomirov@171: 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)); tikhomirov@171: } tikhomirov@698: tikhomirov@698: @Override tikhomirov@698: public int hashCode() { tikhomirov@698: return head.hashCode() ^ root.hashCode(); tikhomirov@698: } tikhomirov@698: tikhomirov@698: @Override tikhomirov@698: public String toString() { tikhomirov@698: String none = String.valueOf(-1); tikhomirov@698: String s1 = p1 == null || p1.isNull() ? none : p1.shortNotation(); tikhomirov@698: String s2 = p2 == null || p2.isNull() ? none : p2.shortNotation(); tikhomirov@698: return String.format("RemoteBranch[root: %s, head:%s, p1:%s, p2:%s]", root.shortNotation(), head.shortNotation(), s1, s2); tikhomirov@698: } tikhomirov@171: } tikhomirov@649: tikhomirov@649: public static final class Bookmarks implements Iterable> { tikhomirov@649: private final List> bm; tikhomirov@649: tikhomirov@649: private Bookmarks(List> bookmarks) { tikhomirov@649: bm = bookmarks; tikhomirov@649: } tikhomirov@649: tikhomirov@649: public Iterator> iterator() { tikhomirov@649: return bm.iterator(); tikhomirov@649: } tikhomirov@649: } tikhomirov@649: tikhomirov@649: public static final class Phases { tikhomirov@649: private final boolean pub; tikhomirov@649: private final List droots; tikhomirov@649: tikhomirov@649: private Phases(boolean publishing, List draftRoots) { tikhomirov@649: pub = publishing; tikhomirov@649: droots = draftRoots; tikhomirov@649: } tikhomirov@649: tikhomirov@649: /** tikhomirov@649: * Non-publishing servers may (shall?) respond with a list of draft roots. tikhomirov@649: * This method doesn't make sense when {@link #isPublishingServer()} is true tikhomirov@649: * tikhomirov@649: * @return list of draft roots on remote server tikhomirov@649: */ tikhomirov@649: public List draftRoots() { tikhomirov@649: return droots; tikhomirov@649: } tikhomirov@649: tikhomirov@649: /** tikhomirov@649: * @return true if revisions on remote server shall be deemed published (either tikhomirov@649: * old server w/o explicit setting, or a new one with phases.publish == true) tikhomirov@649: */ tikhomirov@649: public boolean isPublishingServer() { tikhomirov@649: return pub; tikhomirov@649: } tikhomirov@649: } tikhomirov@170: }