This commit is contained in:
Vadim Gelfer 2006-07-14 23:20:08 -07:00
commit 6a496db429
19 changed files with 346 additions and 21 deletions

View File

@ -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)
@ -959,6 +970,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'])
@ -2828,6 +2840,7 @@ table = {
('m', 'message', '', _('use <text> as commit message')),
('l', 'logfile', '', _('read commit message from <file>')),
('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 @@ 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'))],

View File

@ -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.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:

View File

@ -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)

View File

@ -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:

View File

@ -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,46 @@ 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=[], 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

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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)

82
mercurial/streamclone.py Normal file
View File

@ -0,0 +1,82 @@
# streamclone.py - streaming clone server support for mercurial
#
# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
#
# 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()

View File

@ -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

View File

@ -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

25
tests/test-http Executable file
View File

@ -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

View File

@ -13,8 +13,18 @@ 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 --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

View File

@ -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

29
tests/test-http.out Normal file
View File

@ -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

View File

@ -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

View File

@ -30,7 +30,14 @@ hg ci -A -m "init" -d "1000000 0" foo
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"

View File

@ -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: