From 9ea4436262b41caab67f664c541e044d0c554984 Mon Sep 17 00:00:00 2001 From: Vadim Gelfer Date: Fri, 14 Jul 2006 11:17:22 -0700 Subject: [PATCH 1/3] add support for streaming clone. existing clone code uses pull to get changes from remote repo. is very slow, uses lots of memory and cpu. new clone code has server write file data straight to client, client writes file data straight to disk. memory and cpu used are very low, clone is much faster over lan. new client can still clone with pull, can still clone from older servers. new server can still serve older clients. --- mercurial/hg.py | 2 +- mercurial/hgweb/hgweb_mod.py | 9 +++- mercurial/httprepo.py | 3 ++ mercurial/localrepo.py | 51 ++++++++++++++++++++-- mercurial/remoterepo.py | 4 +- mercurial/repo.py | 17 +++++++- mercurial/sshrepo.py | 3 ++ mercurial/sshserver.py | 6 ++- mercurial/streamclone.py | 82 ++++++++++++++++++++++++++++++++++++ mercurial/util.py | 21 +++++++++ tests/test-http | 25 +++++++++++ tests/test-http-proxy | 20 ++++++--- tests/test-http-proxy.out | 17 +++++++- tests/test-http.out | 29 +++++++++++++ tests/test-pull | 2 +- tests/test-ssh | 11 ++++- tests/test-ssh.out | 13 +++++- 17 files changed, 294 insertions(+), 21 deletions(-) create mode 100644 mercurial/streamclone.py create mode 100755 tests/test-http create mode 100644 tests/test-http.out diff --git a/mercurial/hg.py b/mercurial/hg.py index fea5b87f50..ca96bcd206 100644 --- a/mercurial/hg.py +++ b/mercurial/hg.py @@ -179,7 +179,7 @@ def clone(ui, source, dest=None, pull=False, rev=None, update=True): revs = [src_repo.lookup(r) for r in rev] if dest_repo.local(): - dest_repo.pull(src_repo, heads=revs) + dest_repo.clone(src_repo, heads=revs, pull=pull) elif src_repo.local(): src_repo.push(dest_repo, revs=revs) else: diff --git a/mercurial/hgweb/hgweb_mod.py b/mercurial/hgweb/hgweb_mod.py index 6da9c1c987..08d36529af 100644 --- a/mercurial/hgweb/hgweb_mod.py +++ b/mercurial/hgweb/hgweb_mod.py @@ -11,7 +11,8 @@ import os.path import mimetypes from mercurial.demandload import demandload demandload(globals(), "re zlib ConfigParser mimetools cStringIO sys tempfile") -demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,templater") +demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,streamclone") +demandload(globals(), "mercurial:templater") demandload(globals(), "mercurial.hgweb.common:get_mtime,staticfile") from mercurial.node import * from mercurial.i18n import gettext as _ @@ -859,7 +860,7 @@ class hgweb(object): or self.t("error", error="%r not found" % fname)) def do_capabilities(self, req): - resp = 'unbundle' + resp = 'unbundle stream=%d' % (self.repo.revlogversion,) req.httphdr("application/mercurial-0.1", length=len(resp)) req.write(resp) @@ -950,3 +951,7 @@ class hgweb(object): finally: fp.close() os.unlink(tempname) + + def do_stream_out(self, req): + req.httphdr("application/mercurial-0.1") + streamclone.stream_out(self.repo, req) diff --git a/mercurial/httprepo.py b/mercurial/httprepo.py index 444ad314e3..6a95b13399 100644 --- a/mercurial/httprepo.py +++ b/mercurial/httprepo.py @@ -326,6 +326,9 @@ class httprepository(remoterepository): fp.close() os.unlink(tempname) + def stream_out(self): + return self.do_cmd('stream_out') + class httpsrepository(httprepository): def __init__(self, ui, path): if not has_https: diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py index eb2e8490f0..0211378e98 100644 --- a/mercurial/localrepo.py +++ b/mercurial/localrepo.py @@ -8,17 +8,19 @@ from node import * from i18n import gettext as _ from demandload import * +import repo demandload(globals(), "appendfile changegroup") -demandload(globals(), "changelog dirstate filelog manifest repo context") +demandload(globals(), "changelog dirstate filelog manifest context") demandload(globals(), "re lock transaction tempfile stat mdiff errno ui") -demandload(globals(), "os revlog util") +demandload(globals(), "os revlog time util") -class localrepository(object): +class localrepository(repo.repository): capabilities = () def __del__(self): self.transhandle = None def __init__(self, parentui, path=None, create=0): + repo.repository.__init__(self) if not path: p = os.getcwd() while not os.path.isdir(os.path.join(p, ".hg")): @@ -1183,7 +1185,7 @@ class localrepository(object): # unbundle assumes local user cannot lock remote repo (new ssh # servers, http servers). - if 'unbundle' in remote.capabilities: + if remote.capable('unbundle'): return self.push_unbundle(remote, force, revs) return self.push_addchangegroup(remote, force, revs) @@ -2201,6 +2203,47 @@ class localrepository(object): self.ui.warn(_("%d integrity errors encountered!\n") % errors[0]) return 1 + def stream_in(self, remote): + self.ui.status(_('streaming all changes\n')) + fp = remote.stream_out() + total_files, total_bytes = map(int, fp.readline().split(' ', 1)) + self.ui.status(_('%d files to transfer, %s of data\n') % + (total_files, util.bytecount(total_bytes))) + start = time.time() + for i in xrange(total_files): + name, size = fp.readline().split('\0', 1) + size = int(size) + self.ui.debug('adding %s (%s)\n' % (name, util.bytecount(size))) + ofp = self.opener(name, 'w') + for chunk in util.filechunkiter(fp, limit=size): + ofp.write(chunk) + ofp.close() + elapsed = time.time() - start + self.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') % + (util.bytecount(total_bytes), elapsed, + util.bytecount(total_bytes / elapsed))) + self.reload() + return len(self.heads()) + 1 + + def clone(self, remote, heads=[], pull=False): + '''clone remote repository. + if possible, changes are streamed from remote server. + + keyword arguments: + heads: list of revs to clone (forces use of pull) + pull: force use of pull, even if remote can stream''' + + # now, all clients that can stream can read repo formats + # supported by all servers that can stream. + + # if revlog format changes, client will have to check version + # and format flags on "stream" capability, and stream only if + # compatible. + + if not pull and not heads and remote.capable('stream'): + return self.stream_in(remote) + return self.pull(remote, heads) + # used to avoid circular references so destructors work def aftertrans(base): p = base diff --git a/mercurial/remoterepo.py b/mercurial/remoterepo.py index aa5ccec523..01b74e4b15 100644 --- a/mercurial/remoterepo.py +++ b/mercurial/remoterepo.py @@ -5,7 +5,9 @@ # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. -class remoterepository(object): +import repo + +class remoterepository(repo.repository): def dev(self): return -1 diff --git a/mercurial/repo.py b/mercurial/repo.py index d87b138cee..90296a987e 100644 --- a/mercurial/repo.py +++ b/mercurial/repo.py @@ -5,4 +5,19 @@ # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. -class RepoError(Exception): pass +class RepoError(Exception): + pass + +class repository(object): + def capable(self, name): + '''tell whether repo supports named capability. + return False if not supported. + if boolean capability, return True. + if string capability, return string.''' + name_eq = name + '=' + for cap in self.capabilities: + if name == cap: + return True + if cap.startswith(name_eq): + return cap[len(name_eq):] + return False diff --git a/mercurial/sshrepo.py b/mercurial/sshrepo.py index 835676b37b..96ae207b30 100644 --- a/mercurial/sshrepo.py +++ b/mercurial/sshrepo.py @@ -198,3 +198,6 @@ class sshrepository(remoterepository): if not r: return 1 return int(r) + + def stream_out(self): + return self.do_cmd('stream_out') diff --git a/mercurial/sshserver.py b/mercurial/sshserver.py index be37fed3e6..cc0e87f453 100644 --- a/mercurial/sshserver.py +++ b/mercurial/sshserver.py @@ -8,7 +8,7 @@ from demandload import demandload from i18n import gettext as _ from node import * -demandload(globals(), "os sys tempfile util") +demandload(globals(), "os streamclone sys tempfile util") class sshserver(object): def __init__(self, ui, repo): @@ -60,7 +60,7 @@ class sshserver(object): capabilities: space separated list of tokens ''' - r = "capabilities: unbundle\n" + r = "capabilities: unbundle stream=%d\n" % (self.repo.revlogversion,) self.respond(r) def do_lock(self): @@ -167,3 +167,5 @@ class sshserver(object): fp.close() os.unlink(tempname) + def do_stream_out(self): + streamclone.stream_out(self.repo, self.fout) diff --git a/mercurial/streamclone.py b/mercurial/streamclone.py new file mode 100644 index 0000000000..86e2844bfa --- /dev/null +++ b/mercurial/streamclone.py @@ -0,0 +1,82 @@ +# streamclone.py - streaming clone server support for mercurial +# +# Copyright 2006 Vadim Gelfer +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +from demandload import demandload +from i18n import gettext as _ +demandload(globals(), "os stat util") + +# if server supports streaming clone, it advertises "stream" +# capability with value that is version+flags of repo it is serving. +# client only streams if it can read that repo format. + +def walkrepo(root): + '''iterate over metadata files in repository. + walk in natural (sorted) order. + yields 2-tuples: name of .d or .i file, size of file.''' + + strip_count = len(root) + len(os.sep) + def walk(path, recurse): + ents = os.listdir(path) + ents.sort() + for e in ents: + pe = os.path.join(path, e) + st = os.lstat(pe) + if stat.S_ISDIR(st.st_mode): + if recurse: + for x in walk(pe, True): + yield x + else: + if not stat.S_ISREG(st.st_mode) or len(e) < 2: + continue + sfx = e[-2:] + if sfx in ('.d', '.i'): + yield pe[strip_count:], st.st_size + # write file data first + for x in walk(os.path.join(root, 'data'), True): + yield x + # write manifest before changelog + meta = list(walk(root, False)) + meta.sort(reverse=True) + for x in meta: + yield x + +# stream file format is simple. +# +# server writes out line that says how many files, how many total +# bytes. separator is ascii space, byte counts are strings. +# +# then for each file: +# +# server writes out line that says file name, how many bytes in +# file. separator is ascii nul, byte count is string. +# +# server writes out raw file data. + +def stream_out(repo, fileobj): + '''stream out all metadata files in repository. + writes to file-like object, must support write() and optional flush().''' + # get consistent snapshot of repo. lock during scan so lock not + # needed while we stream, and commits can happen. + lock = repo.lock() + repo.ui.debug('scanning\n') + entries = [] + total_bytes = 0 + for name, size in walkrepo(repo.path): + entries.append((name, size)) + total_bytes += size + lock.release() + + repo.ui.debug('%d files, %d bytes to transfer\n' % + (len(entries), total_bytes)) + fileobj.write('%d %d\n' % (len(entries), total_bytes)) + for name, size in entries: + repo.ui.debug('sending %s (%d bytes)\n' % (name, size)) + fileobj.write('%s\0%d\n' % (name, size)) + for chunk in util.filechunkiter(repo.opener(name), limit=size): + fileobj.write(chunk) + flush = getattr(fileobj, 'flush', None) + if flush: flush() diff --git a/mercurial/util.py b/mercurial/util.py index dd19284f43..e5f463426b 100644 --- a/mercurial/util.py +++ b/mercurial/util.py @@ -961,3 +961,24 @@ def rcpath(): else: _rcpath = os_rcpath() return _rcpath + +def bytecount(nbytes): + '''return byte count formatted as readable string, with units''' + + units = ( + (100, 1<<30, _('%.0f GB')), + (10, 1<<30, _('%.1f GB')), + (1, 1<<30, _('%.2f GB')), + (100, 1<<20, _('%.0f MB')), + (10, 1<<20, _('%.1f MB')), + (1, 1<<20, _('%.2f MB')), + (100, 1<<10, _('%.0f KB')), + (10, 1<<10, _('%.1f KB')), + (1, 1<<10, _('%.2f KB')), + (1, 1, _('%.0f bytes')), + ) + + for multiplier, divisor, format in units: + if nbytes >= divisor * multiplier: + return format % (nbytes / float(divisor)) + return units[-1][2] % nbytes diff --git a/tests/test-http b/tests/test-http new file mode 100755 index 0000000000..d0ab12eb21 --- /dev/null +++ b/tests/test-http @@ -0,0 +1,25 @@ +#!/bin/sh + +mkdir test +cd test +echo foo>foo +hg init +hg addremove +hg commit -m 1 +hg verify +hg serve -p 20059 -d --pid-file=hg.pid +cat hg.pid >> $DAEMON_PIDS +cd .. + +echo % clone via stream +http_proxy= hg clone http://localhost:20059/ copy 2>&1 | \ + sed -e 's/[0-9][0-9.]*/XXX/g' +cd copy +hg verify + +cd .. + +echo % clone via pull +http_proxy= hg clone --pull http://localhost:20059/ copy-pull +cd copy-pull +hg verify diff --git a/tests/test-http-proxy b/tests/test-http-proxy index ab4dbcb163..9583325fd6 100755 --- a/tests/test-http-proxy +++ b/tests/test-http-proxy @@ -13,17 +13,27 @@ echo $! > proxy.pid) cat proxy.pid >> $DAEMON_PIDS sleep 2 -echo %% url for proxy -http_proxy=http://localhost:20060/ hg --config http_proxy.always=True clone http://localhost:20059/ b +echo %% url for proxy, stream +http_proxy=http://localhost:20060/ hg --config http_proxy.always=True clone http://localhost:20059/ b | \ + sed -e 's/[0-9][0-9.]*/XXX/g' +cd b +hg verify +cd .. + +echo %% url for proxy, pull +http_proxy=http://localhost:20060/ hg --config http_proxy.always=True clone --pull http://localhost:20059/ b-pull +cd b-pull +hg verify +cd .. echo %% host:port for proxy -http_proxy=localhost:20060 hg clone --config http_proxy.always=True http://localhost:20059/ c +http_proxy=localhost:20060 hg clone --pull --config http_proxy.always=True http://localhost:20059/ c echo %% proxy url with user name and password -http_proxy=http://user:passwd@localhost:20060 hg clone --config http_proxy.always=True http://localhost:20059/ d +http_proxy=http://user:passwd@localhost:20060 hg clone --pull --config http_proxy.always=True http://localhost:20059/ d echo %% url with user name and password -http_proxy=http://user:passwd@localhost:20060 hg clone --config http_proxy.always=True http://user:passwd@localhost:20059/ e +http_proxy=http://user:passwd@localhost:20060 hg clone --pull --config http_proxy.always=True http://user:passwd@localhost:20059/ e echo %% bad host:port for proxy http_proxy=localhost:20061 hg clone --config http_proxy.always=True http://localhost:20059/ f diff --git a/tests/test-http-proxy.out b/tests/test-http-proxy.out index 4ce3269ff7..b08e59fe46 100644 --- a/tests/test-http-proxy.out +++ b/tests/test-http-proxy.out @@ -1,11 +1,26 @@ adding a -%% url for proxy +%% url for proxy, stream +streaming all changes +XXX files to transfer, XXX bytes of data +transferred XXX bytes in XXX seconds (XXX KB/sec) +XXX files updated, XXX files merged, XXX files removed, XXX files unresolved +checking changesets +checking manifests +crosschecking files in changesets and manifests +checking files +1 files, 1 changesets, 1 total revisions +%% url for proxy, pull requesting all changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files 1 files updated, 0 files merged, 0 files removed, 0 files unresolved +checking changesets +checking manifests +crosschecking files in changesets and manifests +checking files +1 files, 1 changesets, 1 total revisions %% host:port for proxy requesting all changes adding changesets diff --git a/tests/test-http.out b/tests/test-http.out new file mode 100644 index 0000000000..4a55fdb6c4 --- /dev/null +++ b/tests/test-http.out @@ -0,0 +1,29 @@ +(the addremove command is deprecated; use add and remove --after instead) +adding foo +checking changesets +checking manifests +crosschecking files in changesets and manifests +checking files +1 files, 1 changesets, 1 total revisions +% clone via stream +streaming all changes +XXX files to transfer, XXX bytes of data +transferred XXX bytes in XXX seconds (XXX KB/sec) +XXX files updated, XXX files merged, XXX files removed, XXX files unresolved +checking changesets +checking manifests +crosschecking files in changesets and manifests +checking files +1 files, 1 changesets, 1 total revisions +% clone via pull +requesting all changes +adding changesets +adding manifests +adding file changes +added 1 changesets with 1 changes to 1 files +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +checking changesets +checking manifests +crosschecking files in changesets and manifests +checking files +1 files, 1 changesets, 1 total revisions diff --git a/tests/test-pull b/tests/test-pull index ae1807792a..95d374b133 100755 --- a/tests/test-pull +++ b/tests/test-pull @@ -11,7 +11,7 @@ hg serve -p 20059 -d --pid-file=hg.pid cat hg.pid >> $DAEMON_PIDS cd .. -http_proxy= hg clone http://localhost:20059/ copy +http_proxy= hg clone --pull http://localhost:20059/ copy cd copy hg verify hg co diff --git a/tests/test-ssh b/tests/test-ssh index eceff6a7f5..c8c0b5bb7a 100755 --- a/tests/test-ssh +++ b/tests/test-ssh @@ -30,8 +30,15 @@ hg ci -A -m "init" -d "1000000 0" foo cd .. -echo "# clone remote" -hg clone -e ./dummyssh ssh://user@dummy/remote local +echo "# clone remote via stream" +hg clone -e ./dummyssh ssh://user@dummy/remote local-stream 2>&1 | \ + sed -e 's/[0-9][0-9.]*/XXX/g' +cd local-stream +hg verify +cd .. + +echo "# clone remote via pull" +hg clone -e ./dummyssh --pull ssh://user@dummy/remote local echo "# verify" cd local diff --git a/tests/test-ssh.out b/tests/test-ssh.out index b87458f002..3ad004b48b 100644 --- a/tests/test-ssh.out +++ b/tests/test-ssh.out @@ -1,5 +1,15 @@ # creating 'remote' -# clone remote +# clone remote via stream +streaming all changes +XXX files to transfer, XXX bytes of data +transferred XXX bytes in XXX seconds (XXX KB/sec) +XXX files updated, XXX files merged, XXX files removed, XXX files unresolved +checking changesets +checking manifests +crosschecking files in changesets and manifests +checking files +1 files, 1 changesets, 1 total revisions +# clone remote via pull requesting all changes adding changesets adding manifests @@ -70,6 +80,7 @@ remote: added 1 changesets with 1 changes to 1 files Got arguments 1:user@dummy 2:hg -R remote serve --stdio 3: 4: 5: Got arguments 1:user@dummy 2:hg -R remote serve --stdio 3: 4: 5: Got arguments 1:user@dummy 2:hg -R remote serve --stdio 3: 4: 5: +Got arguments 1:user@dummy 2:hg -R remote serve --stdio 3: 4: 5: Got arguments 1:user@dummy 2:hg -R local serve --stdio 3: 4: 5: Got arguments 1:user@dummy 2:hg -R remote serve --stdio 3: 4: 5: Got arguments 1:user@dummy 2:hg -R remote serve --stdio 3: 4: 5: From 4bc0558c57a4c309007a40aa0039fad7ba7e9c1e Mon Sep 17 00:00:00 2001 From: Vadim Gelfer Date: Fri, 14 Jul 2006 14:51:36 -0700 Subject: [PATCH 2/3] clone: do not make streaming default. add --stream option instead. --- mercurial/commands.py | 2 ++ mercurial/hg.py | 7 +++++-- mercurial/localrepo.py | 5 ++--- tests/test-http | 4 ++-- tests/test-http-proxy | 10 +++++----- tests/test-ssh | 4 ++-- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/mercurial/commands.py b/mercurial/commands.py index d422837a24..7654e85a38 100644 --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -959,6 +959,7 @@ def clone(ui, source, dest=None, **opts): ui.setconfig_remoteopts(**opts) hg.clone(ui, ui.expandpath(source), dest, pull=opts['pull'], + stream=opts['stream'], rev=opts['rev'], update=not opts['noupdate']) @@ -2850,6 +2851,7 @@ table = { ('r', 'rev', [], _('a changeset you would like to have after cloning')), ('', 'pull', None, _('use pull protocol to copy metadata')), + ('', 'stream', None, _('use streaming protocol (fast over LAN)')), ('e', 'ssh', '', _('specify ssh command to use')), ('', 'remotecmd', '', _('specify hg command to run on the remote side'))], diff --git a/mercurial/hg.py b/mercurial/hg.py index ca96bcd206..b9d5818b91 100644 --- a/mercurial/hg.py +++ b/mercurial/hg.py @@ -74,7 +74,8 @@ def repository(ui, path=None, create=0): scheme) return ctor(ui, path) -def clone(ui, source, dest=None, pull=False, rev=None, update=True): +def clone(ui, source, dest=None, pull=False, rev=None, update=True, + stream=False): """Make a copy of an existing repository. Create a copy of an existing repository in a new directory. The @@ -96,6 +97,8 @@ def clone(ui, source, dest=None, pull=False, rev=None, update=True): pull: always pull from source repository, even in local case + stream: stream from repository (fast over LAN, slow over WAN) + rev: revision to clone up to (implies pull=True) update: update working directory after clone completes, if @@ -179,7 +182,7 @@ def clone(ui, source, dest=None, pull=False, rev=None, update=True): revs = [src_repo.lookup(r) for r in rev] if dest_repo.local(): - dest_repo.clone(src_repo, heads=revs, pull=pull) + dest_repo.clone(src_repo, heads=revs, stream=stream) elif src_repo.local(): src_repo.push(dest_repo, revs=revs) else: diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py index 0211378e98..a098f01a67 100644 --- a/mercurial/localrepo.py +++ b/mercurial/localrepo.py @@ -2225,9 +2225,8 @@ class localrepository(repo.repository): self.reload() return len(self.heads()) + 1 - def clone(self, remote, heads=[], pull=False): + def clone(self, remote, heads=[], stream=False): '''clone remote repository. - if possible, changes are streamed from remote server. keyword arguments: heads: list of revs to clone (forces use of pull) @@ -2240,7 +2239,7 @@ class localrepository(repo.repository): # and format flags on "stream" capability, and stream only if # compatible. - if not pull and not heads and remote.capable('stream'): + if stream and not heads and remote.capable('stream'): return self.stream_in(remote) return self.pull(remote, heads) diff --git a/tests/test-http b/tests/test-http index d0ab12eb21..e7430d0f7d 100755 --- a/tests/test-http +++ b/tests/test-http @@ -12,7 +12,7 @@ cat hg.pid >> $DAEMON_PIDS cd .. echo % clone via stream -http_proxy= hg clone http://localhost:20059/ copy 2>&1 | \ +http_proxy= hg clone --stream http://localhost:20059/ copy 2>&1 | \ sed -e 's/[0-9][0-9.]*/XXX/g' cd copy hg verify @@ -20,6 +20,6 @@ hg verify cd .. echo % clone via pull -http_proxy= hg clone --pull http://localhost:20059/ copy-pull +http_proxy= hg clone http://localhost:20059/ copy-pull cd copy-pull hg verify diff --git a/tests/test-http-proxy b/tests/test-http-proxy index 9583325fd6..85385ed824 100755 --- a/tests/test-http-proxy +++ b/tests/test-http-proxy @@ -14,26 +14,26 @@ cat proxy.pid >> $DAEMON_PIDS sleep 2 echo %% url for proxy, stream -http_proxy=http://localhost:20060/ hg --config http_proxy.always=True clone http://localhost:20059/ b | \ +http_proxy=http://localhost:20060/ hg --config http_proxy.always=True clone --stream http://localhost:20059/ b | \ sed -e 's/[0-9][0-9.]*/XXX/g' cd b hg verify cd .. echo %% url for proxy, pull -http_proxy=http://localhost:20060/ hg --config http_proxy.always=True clone --pull http://localhost:20059/ b-pull +http_proxy=http://localhost:20060/ hg --config http_proxy.always=True clone http://localhost:20059/ b-pull cd b-pull hg verify cd .. echo %% host:port for proxy -http_proxy=localhost:20060 hg clone --pull --config http_proxy.always=True http://localhost:20059/ c +http_proxy=localhost:20060 hg clone --config http_proxy.always=True http://localhost:20059/ c echo %% proxy url with user name and password -http_proxy=http://user:passwd@localhost:20060 hg clone --pull --config http_proxy.always=True http://localhost:20059/ d +http_proxy=http://user:passwd@localhost:20060 hg clone --config http_proxy.always=True http://localhost:20059/ d echo %% url with user name and password -http_proxy=http://user:passwd@localhost:20060 hg clone --pull --config http_proxy.always=True http://user:passwd@localhost:20059/ e +http_proxy=http://user:passwd@localhost:20060 hg clone --config http_proxy.always=True http://user:passwd@localhost:20059/ e echo %% bad host:port for proxy http_proxy=localhost:20061 hg clone --config http_proxy.always=True http://localhost:20059/ f diff --git a/tests/test-ssh b/tests/test-ssh index c8c0b5bb7a..4d48dbb6da 100755 --- a/tests/test-ssh +++ b/tests/test-ssh @@ -31,14 +31,14 @@ hg ci -A -m "init" -d "1000000 0" foo cd .. echo "# clone remote via stream" -hg clone -e ./dummyssh ssh://user@dummy/remote local-stream 2>&1 | \ +hg clone -e ./dummyssh --stream ssh://user@dummy/remote local-stream 2>&1 | \ sed -e 's/[0-9][0-9.]*/XXX/g' cd local-stream hg verify cd .. echo "# clone remote via pull" -hg clone -e ./dummyssh --pull ssh://user@dummy/remote local +hg clone -e ./dummyssh ssh://user@dummy/remote local echo "# verify" cd local From 895f59ec1c8b03dcee2797f06b37de2762fca369 Mon Sep 17 00:00:00 2001 From: Vadim Gelfer Date: Fri, 14 Jul 2006 23:19:15 -0700 Subject: [PATCH 3/3] backout: allow backout of merge changeset with --parent option. --parent allows to choose which parent of merge to revert to. --- mercurial/commands.py | 18 +++++++++++++++--- tests/test-backout | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/mercurial/commands.py b/mercurial/commands.py index 7654e85a38..61235baaa6 100644 --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -865,11 +865,22 @@ def backout(ui, repo, rev, **opts): if op2 != nullid: raise util.Abort(_('outstanding uncommitted merge')) node = repo.lookup(rev) - parent, p2 = repo.changelog.parents(node) - if parent == nullid: + p1, p2 = repo.changelog.parents(node) + if p1 == nullid: raise util.Abort(_('cannot back out a change with no parents')) if p2 != nullid: - raise util.Abort(_('cannot back out a merge')) + if not opts['parent']: + raise util.Abort(_('cannot back out a merge changeset without ' + '--parent')) + p = repo.lookup(opts['parent']) + if p not in (p1, p2): + raise util.Abort(_('%s is not a parent of %s' % + (short(p), short(node)))) + parent = p + else: + if opts['parent']: + raise util.Abort(_('cannot use --parent on non-merge changeset')) + parent = p1 repo.update(node, force=True, show_stats=False) revert_opts = opts.copy() revert_opts['rev'] = hex(parent) @@ -2829,6 +2840,7 @@ table = { ('m', 'message', '', _('use as commit message')), ('l', 'logfile', '', _('read commit message from ')), ('d', 'date', '', _('record datecode as commit date')), + ('', 'parent', '', _('parent to choose when backing out merge')), ('u', 'user', '', _('record user as committer')), ('I', 'include', [], _('include names matching the given patterns')), ('X', 'exclude', [], _('exclude names matching the given patterns'))], diff --git a/tests/test-backout b/tests/test-backout index 8c5f471c45..44f74f0ae4 100755 --- a/tests/test-backout +++ b/tests/test-backout @@ -60,4 +60,40 @@ hg commit -d '2 0' -A -m c hg backout -d '3 0' 1 hg locate b +cd .. +hg init m +cd m +echo a > a +hg commit -d '0 0' -A -m a +echo b > b +hg commit -d '1 0' -A -m b +echo c > c +hg commit -d '2 0' -A -m b +hg update 1 +echo d > d +hg commit -d '3 0' -A -m c +hg merge 2 +hg commit -d '4 0' -A -m d + +echo '# backout of merge should fail' + +hg backout 4 + +echo '# backout of merge with bad parent should fail' + +hg backout --parent 0 4 + +echo '# backout of non-merge with parent should fail' + +hg backout --parent 0 3 + +echo '# backout with valid parent should be ok' + +hg backout -d '5 0' --parent 2 4 + +hg rollback +hg update -C + +hg backout -d '6 0' --parent 3 4 + exit 0