tikhomirov@170: /* tikhomirov@407: * Copyright (c) 2011-2012 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@456: import static org.tmatesoft.hg.util.LogFacility.Severity.Info; tikhomirov@456: tikhomirov@428: import java.io.BufferedReader; tikhomirov@170: import java.io.File; tikhomirov@179: import java.io.FileOutputStream; 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@177: import java.net.HttpURLConnection; tikhomirov@176: import java.net.MalformedURLException; tikhomirov@171: import java.net.URL; tikhomirov@176: import java.net.URLConnection; tikhomirov@176: import java.security.cert.CertificateException; tikhomirov@176: import java.security.cert.X509Certificate; 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: import java.util.prefs.BackingStoreException; tikhomirov@176: import java.util.prefs.Preferences; tikhomirov@179: import java.util.zip.InflaterInputStream; tikhomirov@176: tikhomirov@176: import javax.net.ssl.HttpsURLConnection; tikhomirov@176: import javax.net.ssl.SSLContext; tikhomirov@176: import javax.net.ssl.TrustManager; tikhomirov@176: import javax.net.ssl.X509TrustManager; tikhomirov@170: tikhomirov@181: import org.tmatesoft.hg.core.HgBadArgumentException; 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@456: import org.tmatesoft.hg.internal.PropertyMarshal; tikhomirov@170: tikhomirov@170: /** tikhomirov@170: * WORK IN PROGRESS, DO NOT USE tikhomirov@170: * tikhomirov@170: * @see http://mercurial.selenic.com/wiki/WireProtocol 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@176: private final URL url; tikhomirov@176: private final SSLContext sslContext; tikhomirov@176: private final String authInfo; tikhomirov@407: private final boolean debug; tikhomirov@186: private HgLookup lookupHelper; tikhomirov@295: private final SessionContext sessionContext; tikhomirov@428: private Set remoteCapabilities; tikhomirov@428: tikhomirov@295: HgRemoteRepository(SessionContext ctx, URL url) throws HgBadArgumentException { tikhomirov@295: if (url == null || ctx == null) { tikhomirov@176: throw new IllegalArgumentException(); tikhomirov@176: } tikhomirov@176: this.url = url; tikhomirov@295: sessionContext = ctx; tikhomirov@456: debug = new PropertyMarshal(ctx).getBoolean("hg4j.remote.debug", false); tikhomirov@176: if ("https".equals(url.getProtocol())) { tikhomirov@176: try { tikhomirov@176: sslContext = SSLContext.getInstance("SSL"); tikhomirov@176: class TrustEveryone implements X509TrustManager { tikhomirov@176: public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { tikhomirov@207: if (debug) { tikhomirov@207: System.out.println("checkClientTrusted:" + authType); tikhomirov@207: } tikhomirov@176: } tikhomirov@176: public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { tikhomirov@207: if (debug) { tikhomirov@207: System.out.println("checkServerTrusted:" + authType); tikhomirov@207: } tikhomirov@176: } tikhomirov@176: public X509Certificate[] getAcceptedIssuers() { tikhomirov@176: return new X509Certificate[0]; tikhomirov@176: } tikhomirov@176: }; tikhomirov@176: sslContext.init(null, new TrustManager[] { new TrustEveryone() }, null); tikhomirov@176: } catch (Exception ex) { tikhomirov@181: throw new HgBadArgumentException("Can't initialize secure connection", ex); tikhomirov@176: } tikhomirov@176: } else { tikhomirov@176: sslContext = null; tikhomirov@176: } tikhomirov@176: if (url.getUserInfo() != null) { tikhomirov@176: String ai = null; tikhomirov@176: try { tikhomirov@176: // Hack to get Base64-encoded credentials tikhomirov@176: Preferences tempNode = Preferences.userRoot().node("xxx"); tikhomirov@176: tempNode.putByteArray("xxx", url.getUserInfo().getBytes()); tikhomirov@176: ai = tempNode.get("xxx", null); tikhomirov@176: tempNode.removeNode(); tikhomirov@176: } catch (BackingStoreException ex) { tikhomirov@456: sessionContext.getLog().dump(getClass(), Info, ex, null); tikhomirov@176: // IGNORE tikhomirov@176: } tikhomirov@176: authInfo = ai; tikhomirov@176: } else { tikhomirov@176: authInfo = null; tikhomirov@176: } tikhomirov@171: } tikhomirov@171: tikhomirov@215: public boolean isInvalid() throws HgRemoteConnectionException { tikhomirov@428: if (remoteCapabilities == null) { tikhomirov@428: remoteCapabilities = new HashSet(); tikhomirov@428: // say hello to server, check response tikhomirov@428: try { tikhomirov@428: URL u = new URL(url, url.getPath() + "?cmd=hello"); tikhomirov@428: HttpURLConnection c = setupConnection(u.openConnection()); tikhomirov@428: c.connect(); tikhomirov@428: if (debug) { tikhomirov@428: dumpResponseHeader(u, c); tikhomirov@428: } tikhomirov@428: BufferedReader r = new BufferedReader(new InputStreamReader(c.getInputStream(), "US-ASCII")); tikhomirov@428: String line = r.readLine(); tikhomirov@428: c.disconnect(); tikhomirov@428: final String capsPrefix = "capabilities:"; tikhomirov@428: if (line == null || !line.startsWith(capsPrefix)) { tikhomirov@428: // for whatever reason, some servers do not respond to hello command (e.g. svnkit) tikhomirov@428: // but respond to 'capabilities' instead. Try it. tikhomirov@428: // TODO [post-1.0] tests needed tikhomirov@428: u = new URL(url, url.getPath() + "?cmd=capabilities"); tikhomirov@428: c = setupConnection(u.openConnection()); tikhomirov@428: c.connect(); tikhomirov@428: if (debug) { tikhomirov@428: dumpResponseHeader(u, c); tikhomirov@428: } tikhomirov@428: r = new BufferedReader(new InputStreamReader(c.getInputStream(), "US-ASCII")); tikhomirov@428: line = r.readLine(); tikhomirov@428: c.disconnect(); tikhomirov@441: if (line == null || line.trim().length() == 0) { tikhomirov@428: return true; tikhomirov@428: } tikhomirov@428: } else { tikhomirov@428: line = line.substring(capsPrefix.length()).trim(); tikhomirov@428: } tikhomirov@428: String[] caps = line.split("\\s"); tikhomirov@428: remoteCapabilities.addAll(Arrays.asList(caps)); tikhomirov@428: c.disconnect(); tikhomirov@428: } catch (MalformedURLException ex) { tikhomirov@428: throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("hello").setServerInfo(getLocation()); tikhomirov@428: } catch (IOException ex) { tikhomirov@428: throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("hello").setServerInfo(getLocation()); tikhomirov@428: } tikhomirov@181: } 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@181: if (url.getUserInfo() == null) { tikhomirov@181: return url.toExternalForm(); tikhomirov@181: } tikhomirov@181: if (url.getPort() != -1) { tikhomirov@181: return String.format("%s://%s:%d%s", url.getProtocol(), url.getHost(), url.getPort(), url.getPath()); tikhomirov@181: } else { tikhomirov@181: return String.format("%s://%s%s", url.getProtocol(), url.getHost(), url.getPath()); tikhomirov@181: } 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@178: try { tikhomirov@178: URL u = new URL(url, url.getPath() + "?cmd=heads"); tikhomirov@178: HttpURLConnection c = setupConnection(u.openConnection()); tikhomirov@178: c.connect(); tikhomirov@178: if (debug) { tikhomirov@178: dumpResponseHeader(u, c); tikhomirov@178: } tikhomirov@178: InputStreamReader is = new InputStreamReader(c.getInputStream(), "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 (MalformedURLException ex) { tikhomirov@215: throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("heads").setServerInfo(getLocation()); tikhomirov@178: } catch (IOException ex) { tikhomirov@215: throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("heads").setServerInfo(getLocation()); 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@177: if (ranges.isEmpty()) { tikhomirov@177: return Collections.emptyMap(); tikhomirov@177: } tikhomirov@177: // if fact, shall do other way round, this method shall send tikhomirov@177: LinkedHashMap> rv = new LinkedHashMap>(ranges.size() * 4 / 3); tikhomirov@177: StringBuilder sb = new StringBuilder(20 + ranges.size() * 82); tikhomirov@177: sb.append("pairs="); tikhomirov@177: for (Range r : ranges) { tikhomirov@177: sb.append(r.end.toString()); tikhomirov@177: sb.append('-'); tikhomirov@177: sb.append(r.start.toString()); tikhomirov@177: sb.append('+'); tikhomirov@177: } tikhomirov@177: if (sb.charAt(sb.length() - 1) == '+') { tikhomirov@177: // strip last space tikhomirov@177: sb.setLength(sb.length() - 1); tikhomirov@177: } tikhomirov@176: try { tikhomirov@177: boolean usePOST = ranges.size() > 3; tikhomirov@177: URL u = new URL(url, url.getPath() + "?cmd=between" + (usePOST ? "" : '&' + sb.toString())); tikhomirov@177: HttpURLConnection c = setupConnection(u.openConnection()); tikhomirov@177: if (usePOST) { tikhomirov@177: c.setRequestMethod("POST"); tikhomirov@177: c.setRequestProperty("Content-Length", String.valueOf(sb.length()/*nodeids are ASCII, bytes == characters */)); tikhomirov@177: c.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); tikhomirov@177: c.setDoOutput(true); tikhomirov@177: c.connect(); tikhomirov@177: OutputStream os = c.getOutputStream(); tikhomirov@177: os.write(sb.toString().getBytes()); tikhomirov@177: os.flush(); tikhomirov@177: os.close(); tikhomirov@177: } else { tikhomirov@177: c.connect(); tikhomirov@177: } tikhomirov@178: if (debug) { tikhomirov@178: System.out.printf("%d ranges, method:%s \n", ranges.size(), c.getRequestMethod()); tikhomirov@178: dumpResponseHeader(u, c); tikhomirov@176: } tikhomirov@177: InputStreamReader is = new InputStreamReader(c.getInputStream(), "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 (MalformedURLException ex) { tikhomirov@215: throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("between").setServerInfo(getLocation()); tikhomirov@176: } catch (IOException ex) { tikhomirov@215: throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("between").setServerInfo(getLocation()); tikhomirov@176: } tikhomirov@176: } tikhomirov@176: tikhomirov@215: public List branches(List nodes) throws HgRemoteConnectionException { tikhomirov@178: StringBuilder sb = new StringBuilder(20 + nodes.size() * 41); tikhomirov@178: sb.append("nodes="); tikhomirov@178: for (Nodeid n : nodes) { tikhomirov@178: sb.append(n.toString()); tikhomirov@178: sb.append('+'); tikhomirov@178: } tikhomirov@178: if (sb.charAt(sb.length() - 1) == '+') { tikhomirov@178: // strip last space tikhomirov@178: sb.setLength(sb.length() - 1); tikhomirov@178: } tikhomirov@178: try { tikhomirov@178: URL u = new URL(url, url.getPath() + "?cmd=branches&" + sb.toString()); tikhomirov@178: HttpURLConnection c = setupConnection(u.openConnection()); tikhomirov@178: c.connect(); tikhomirov@178: if (debug) { tikhomirov@178: dumpResponseHeader(u, c); tikhomirov@178: } tikhomirov@178: InputStreamReader is = new InputStreamReader(c.getInputStream(), "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 (MalformedURLException ex) { tikhomirov@215: throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("branches").setServerInfo(getLocation()); tikhomirov@178: } catch (IOException ex) { tikhomirov@215: throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("branches").setServerInfo(getLocation()); 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@202: List _roots = roots.isEmpty() ? Collections.singletonList(Nodeid.NULL) : roots; tikhomirov@202: StringBuilder sb = new StringBuilder(20 + _roots.size() * 41); tikhomirov@179: sb.append("roots="); tikhomirov@202: for (Nodeid n : _roots) { tikhomirov@179: sb.append(n.toString()); tikhomirov@179: sb.append('+'); tikhomirov@179: } tikhomirov@179: if (sb.charAt(sb.length() - 1) == '+') { tikhomirov@179: // strip last space tikhomirov@179: sb.setLength(sb.length() - 1); tikhomirov@179: } tikhomirov@179: try { tikhomirov@179: URL u = new URL(url, url.getPath() + "?cmd=changegroup&" + sb.toString()); tikhomirov@179: HttpURLConnection c = setupConnection(u.openConnection()); tikhomirov@179: c.connect(); tikhomirov@179: if (debug) { tikhomirov@179: dumpResponseHeader(u, c); tikhomirov@179: } tikhomirov@179: File tf = writeBundle(c.getInputStream(), false, "HG10GZ" /*didn't see any other that zip*/); tikhomirov@190: if (debug) { tikhomirov@210: System.out.printf("Wrote bundle %s for roots %s\n", tf, sb); tikhomirov@190: } tikhomirov@186: return getLookupHelper().loadBundle(tf); tikhomirov@215: } catch (MalformedURLException ex) { // XXX in fact, this exception might be better to be re-thrown as RuntimeEx, tikhomirov@215: // as there's little user can do about this issue (URLs are constructed by our code) tikhomirov@215: throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("changegroup").setServerInfo(getLocation()); tikhomirov@179: } catch (IOException ex) { tikhomirov@215: throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("changegroup").setServerInfo(getLocation()); tikhomirov@425: } catch (HgRepositoryNotFoundException ex) { tikhomirov@425: throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("changegroup").setServerInfo(getLocation()); tikhomirov@179: } tikhomirov@170: } tikhomirov@186: tikhomirov@203: @Override tikhomirov@203: public String toString() { tikhomirov@203: return getClass().getSimpleName() + '[' + getLocation() + ']'; tikhomirov@203: } 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@176: tikhomirov@177: private HttpURLConnection setupConnection(URLConnection urlConnection) { tikhomirov@177: urlConnection.setRequestProperty("User-Agent", "hg4j/0.5.0"); tikhomirov@176: urlConnection.addRequestProperty("Accept", "application/mercurial-0.1"); tikhomirov@176: if (authInfo != null) { tikhomirov@176: urlConnection.addRequestProperty("Authorization", "Basic " + authInfo); tikhomirov@176: } tikhomirov@176: if (sslContext != null) { tikhomirov@176: ((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslContext.getSocketFactory()); tikhomirov@176: } tikhomirov@177: return (HttpURLConnection) urlConnection; tikhomirov@176: } tikhomirov@171: tikhomirov@178: private void dumpResponseHeader(URL u, HttpURLConnection c) { tikhomirov@178: System.out.printf("Query (%d bytes):%s\n", u.getQuery().length(), u.getQuery()); tikhomirov@178: System.out.println("Response headers:"); tikhomirov@178: final Map> headerFields = c.getHeaderFields(); tikhomirov@178: for (String s : headerFields.keySet()) { tikhomirov@178: System.out.printf("%s: %s\n", s, c.getHeaderField(s)); tikhomirov@178: } tikhomirov@178: } tikhomirov@179: tikhomirov@179: private static File writeBundle(InputStream is, boolean decompress, String header) throws IOException { tikhomirov@179: InputStream zipStream = decompress ? new InflaterInputStream(is) : is; tikhomirov@179: File tf = File.createTempFile("hg-bundle-", null); tikhomirov@179: FileOutputStream fos = new FileOutputStream(tf); tikhomirov@179: fos.write(header.getBytes()); tikhomirov@179: int r; tikhomirov@179: byte[] buf = new byte[8*1024]; tikhomirov@179: while ((r = zipStream.read(buf)) != -1) { tikhomirov@179: fos.write(buf, 0, r); tikhomirov@179: } tikhomirov@179: fos.close(); tikhomirov@179: zipStream.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@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@171: } tikhomirov@170: }