# HG changeset patch # User Vadim Gelfer # Date 1152944408 25200 # Node ID 114790d3a08177d0ffd470d0417c14175e02458c # Parent b075216da118af037cc536a4802a5694690b1051# Parent 8ba1c31f6864ed3de28cdc26ed193cc12ab314a2 merge. diff -r b075216da118 -r 114790d3a081 mercurial/commands.py --- a/mercurial/commands.py Fri Jul 14 22:38:21 2006 -0700 +++ b/mercurial/commands.py Fri Jul 14 23:20:08 2006 -0700 @@ -865,11 +865,22 @@ 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) @@ -959,6 +970,7 @@ ui.setconfig_remoteopts(**opts) hg.clone(ui, ui.expandpath(source), dest, pull=opts['pull'], + stream=opts['stream'], rev=opts['rev'], update=not opts['noupdate']) @@ -2828,6 +2840,7 @@ ('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'))], @@ -2850,6 +2863,7 @@ ('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 -r b075216da118 -r 114790d3a081 mercurial/hg.py --- a/mercurial/hg.py Fri Jul 14 22:38:21 2006 -0700 +++ b/mercurial/hg.py Fri Jul 14 23:20:08 2006 -0700 @@ -74,7 +74,8 @@ 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 @@ 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 @@ 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, stream=stream) elif src_repo.local(): src_repo.push(dest_repo, revs=revs) else: diff -r b075216da118 -r 114790d3a081 mercurial/hgweb/hgweb_mod.py --- a/mercurial/hgweb/hgweb_mod.py Fri Jul 14 22:38:21 2006 -0700 +++ b/mercurial/hgweb/hgweb_mod.py Fri Jul 14 23:20:08 2006 -0700 @@ -11,7 +11,8 @@ 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 @@ 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 @@ 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 -r b075216da118 -r 114790d3a081 mercurial/httprepo.py --- a/mercurial/httprepo.py Fri Jul 14 22:38:21 2006 -0700 +++ b/mercurial/httprepo.py Fri Jul 14 23:20:08 2006 -0700 @@ -326,6 +326,9 @@ 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 -r b075216da118 -r 114790d3a081 mercurial/localrepo.py --- a/mercurial/localrepo.py Fri Jul 14 22:38:21 2006 -0700 +++ b/mercurial/localrepo.py Fri Jul 14 23:20:08 2006 -0700 @@ -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 @@ # 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,46 @@ 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=[], stream=False): + '''clone remote repository. + + 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 stream 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 -r b075216da118 -r 114790d3a081 mercurial/remoterepo.py --- a/mercurial/remoterepo.py Fri Jul 14 22:38:21 2006 -0700 +++ b/mercurial/remoterepo.py Fri Jul 14 23:20:08 2006 -0700 @@ -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 -r b075216da118 -r 114790d3a081 mercurial/repo.py --- a/mercurial/repo.py Fri Jul 14 22:38:21 2006 -0700 +++ b/mercurial/repo.py Fri Jul 14 23:20:08 2006 -0700 @@ -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 -r b075216da118 -r 114790d3a081 mercurial/sshrepo.py --- a/mercurial/sshrepo.py Fri Jul 14 22:38:21 2006 -0700 +++ b/mercurial/sshrepo.py Fri Jul 14 23:20:08 2006 -0700 @@ -198,3 +198,6 @@ if not r: return 1 return int(r) + + def stream_out(self): + return self.do_cmd('stream_out') diff -r b075216da118 -r 114790d3a081 mercurial/sshserver.py --- a/mercurial/sshserver.py Fri Jul 14 22:38:21 2006 -0700 +++ b/mercurial/sshserver.py Fri Jul 14 23:20:08 2006 -0700 @@ -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 @@ 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 @@ fp.close() os.unlink(tempname) + def do_stream_out(self): + streamclone.stream_out(self.repo, self.fout) diff -r b075216da118 -r 114790d3a081 mercurial/streamclone.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/streamclone.py Fri Jul 14 23:20:08 2006 -0700 @@ -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 -r b075216da118 -r 114790d3a081 mercurial/util.py --- a/mercurial/util.py Fri Jul 14 22:38:21 2006 -0700 +++ b/mercurial/util.py Fri Jul 14 23:20:08 2006 -0700 @@ -961,3 +961,24 @@ 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 -r b075216da118 -r 114790d3a081 tests/test-backout --- a/tests/test-backout Fri Jul 14 22:38:21 2006 -0700 +++ b/tests/test-backout Fri Jul 14 23:20:08 2006 -0700 @@ -60,4 +60,40 @@ 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 diff -r b075216da118 -r 114790d3a081 tests/test-http --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-http Fri Jul 14 23:20:08 2006 -0700 @@ -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 --stream 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 http://localhost:20059/ copy-pull +cd copy-pull +hg verify diff -r b075216da118 -r 114790d3a081 tests/test-http-proxy --- a/tests/test-http-proxy Fri Jul 14 22:38:21 2006 -0700 +++ b/tests/test-http-proxy Fri Jul 14 23:20:08 2006 -0700 @@ -13,8 +13,18 @@ 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 --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 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 diff -r b075216da118 -r 114790d3a081 tests/test-http-proxy.out --- a/tests/test-http-proxy.out Fri Jul 14 22:38:21 2006 -0700 +++ b/tests/test-http-proxy.out Fri Jul 14 23:20:08 2006 -0700 @@ -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 -r b075216da118 -r 114790d3a081 tests/test-http.out --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-http.out Fri Jul 14 23:20:08 2006 -0700 @@ -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 -r b075216da118 -r 114790d3a081 tests/test-pull --- a/tests/test-pull Fri Jul 14 22:38:21 2006 -0700 +++ b/tests/test-pull Fri Jul 14 23:20:08 2006 -0700 @@ -11,7 +11,7 @@ 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 -r b075216da118 -r 114790d3a081 tests/test-ssh --- a/tests/test-ssh Fri Jul 14 22:38:21 2006 -0700 +++ b/tests/test-ssh Fri Jul 14 23:20:08 2006 -0700 @@ -30,7 +30,14 @@ cd .. -echo "# clone remote" +echo "# clone remote via stream" +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 ssh://user@dummy/remote local echo "# verify" diff -r b075216da118 -r 114790d3a081 tests/test-ssh.out --- a/tests/test-ssh.out Fri Jul 14 22:38:21 2006 -0700 +++ b/tests/test-ssh.out Fri Jul 14 23:20:08 2006 -0700 @@ -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 @@ 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: