# HG changeset patch # User Vadim Gelfer # Date 1150414703 25200 # Node ID cc1011a7ed09e30ba62a863fbd2e02dede03beba # Parent a765f853439d44ff765e75adcef2d7b492b08d3c# Parent e8c4f3d3df8c461d4868268c3faeda9639a9a902 merge with self. diff --git a/mercurial/httprepo.py b/mercurial/httprepo.py --- a/mercurial/httprepo.py +++ b/mercurial/httprepo.py @@ -71,6 +71,7 @@ def netlocunsplit(host, port, user=None, class httprepository(remoterepository): def __init__(self, ui, path): + self.capabilities = () scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path) if query or frag: raise util.Abort(_('unsupported URL component: "%s"') % @@ -234,5 +235,8 @@ class httprepository(remoterepository): return util.chunkbuffer(zgenerator(util.filechunkiter(f))) + def unbundle(self, cg, heads, source): + raise util.Abort(_('operation not supported over http')) + class httpsrepository(httprepository): pass diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py --- a/mercurial/localrepo.py +++ b/mercurial/localrepo.py @@ -15,6 +15,8 @@ demandload(globals(), "re lock transacti demandload(globals(), "revlog") class localrepository(object): + capabilities = () + def __del__(self): self.transhandle = None def __init__(self, parentui, path=None, create=0): @@ -1105,8 +1107,20 @@ class localrepository(object): return self.addchangegroup(cg, 'pull') def push(self, remote, force=False, revs=None): - lock = remote.lock() + # there are two ways to push to remote repo: + # + # addchangegroup assumes local user can lock remote + # repo (local filesystem, old ssh servers). + # + # unbundle assumes local user cannot lock remote repo (new ssh + # servers, http servers). + if 'unbundle' in remote.capabilities: + self.push_unbundle(remote, force, revs) + else: + self.push_addchangegroup(remote, force, revs) + + def prepush(self, remote, force, revs): base = {} remote_heads = remote.heads() inc = self.findincoming(remote, base, remote_heads, force=force) @@ -1114,7 +1128,7 @@ class localrepository(object): self.ui.warn(_("abort: unsynced remote changes!\n")) self.ui.status(_("(did you forget to sync?" " use push -f to force)\n")) - return 1 + return None, 1 update, updated_heads = self.findoutgoing(remote, base, remote_heads) if revs is not None: @@ -1124,7 +1138,7 @@ class localrepository(object): if not bases: self.ui.status(_("no changes found\n")) - return 1 + return None, 1 elif not force: # FIXME we don't properly detect creation of new heads # in the push -r case, assume the user knows what he's doing @@ -1133,13 +1147,35 @@ class localrepository(object): self.ui.warn(_("abort: push creates new remote branches!\n")) self.ui.status(_("(did you forget to merge?" " use push -f to force)\n")) - return 1 + return None, 1 if revs is None: cg = self.changegroup(update, 'push') else: cg = self.changegroupsubset(update, revs, 'push') - return remote.addchangegroup(cg, 'push') + return cg, remote_heads + + def push_addchangegroup(self, remote, force, revs): + lock = remote.lock() + + ret = self.prepush(remote, force, revs) + if ret[0] is not None: + cg, remote_heads = ret + return remote.addchangegroup(cg, 'push') + return ret[1] + + def push_unbundle(self, remote, force, revs): + # local repo finds heads on server, finds out what revs it + # must push. once revs transferred, if server finds it has + # different heads (someone else won commit/push race), server + # aborts. + + ret = self.prepush(remote, force, revs) + if ret[0] is not None: + cg, remote_heads = ret + if force: remote_heads = ['force'] + return remote.unbundle(cg, remote_heads, 'push') + return ret[1] def changegroupsubset(self, bases, heads, source): """This function generates a changegroup consisting of all the nodes diff --git a/mercurial/sshrepo.py b/mercurial/sshrepo.py --- a/mercurial/sshrepo.py +++ b/mercurial/sshrepo.py @@ -141,11 +141,36 @@ class sshrepository(remoterepository): f = self.do_cmd("changegroup", roots=n) return self.pipei + def unbundle(self, cg, heads, source): + d = self.call("unbundle", heads=' '.join(map(hex, heads))) + if d: + raise hg.RepoError(_("push refused: %s") % d) + + while 1: + d = cg.read(4096) + if not d: break + self.pipeo.write(str(len(d)) + '\n') + self.pipeo.write(d) + self.readerr() + + self.pipeo.write('0\n') + self.pipeo.flush() + + self.readerr() + d = self.pipei.readline() + if d != '\n': + return 1 + + l = int(self.pipei.readline()) + r = self.pipei.read(l) + if not r: + return 1 + return int(r) + def addchangegroup(self, cg, source): d = self.call("addchangegroup") if d: raise hg.RepoError(_("push refused: %s") % d) - while 1: d = cg.read(4096) if not d: break diff --git a/mercurial/sshserver.py b/mercurial/sshserver.py --- 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(), "sys util") +demandload(globals(), "os sys tempfile util") class sshserver(object): def __init__(self, ui, repo): @@ -60,14 +60,18 @@ class sshserver(object): capabilities: space separated list of tokens ''' - r = "capabilities:\n" + r = "capabilities: unbundle\n" self.respond(r) def do_lock(self): + '''DEPRECATED - allowing remote client to lock repo is not safe''' + self.lock = self.repo.lock() self.respond("") def do_unlock(self): + '''DEPRECATED''' + if self.lock: self.lock.release() self.lock = None @@ -104,6 +108,8 @@ class sshserver(object): self.fout.flush() def do_addchangegroup(self): + '''DEPRECATED''' + if not self.lock: self.respond("not locked") return @@ -111,3 +117,53 @@ class sshserver(object): self.respond("") r = self.repo.addchangegroup(self.fin, 'serve') self.respond(str(r)) + + def do_unbundle(self): + their_heads = self.getarg()[1].split() + + def check_heads(): + heads = map(hex, self.repo.heads()) + return their_heads == [hex('force')] or their_heads == heads + + # fail early if possible + if not check_heads(): + self.respond(_('unsynced changes')) + return + + self.respond('') + + # write bundle data to temporary file because it can be big + + try: + fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-') + fp = os.fdopen(fd, 'wb+') + + count = int(self.fin.readline()) + while count: + fp.write(self.fin.read(count)) + count = int(self.fin.readline()) + + was_locked = self.lock is not None + if not was_locked: + self.lock = self.repo.lock() + try: + if not check_heads(): + # someone else committed/pushed/unbundled while we + # were transferring data + self.respond(_('unsynced changes')) + return + self.respond('') + + # push can proceed + + fp.seek(0) + r = self.repo.addchangegroup(fp, 'serve') + self.respond(str(r)) + finally: + if not was_locked: + self.lock.release() + self.lock = None + finally: + fp.close() + os.unlink(tempname) + diff --git a/tests/test-ssh b/tests/test-ssh --- a/tests/test-ssh +++ b/tests/test-ssh @@ -66,5 +66,18 @@ hg tip hg verify hg cat foo +echo z > z +hg ci -A -m z -d '1000001 0' z + +cd ../local +echo r > r +hg ci -A -m z -d '1000002 0' r + +echo "# push should fail" +hg push + +echo "# push should succeed" +hg push -f + cd .. cat dummylog diff --git a/tests/test-ssh.out b/tests/test-ssh.out --- a/tests/test-ssh.out +++ b/tests/test-ssh.out @@ -55,8 +55,22 @@ crosschecking files in changesets and ma checking files 1 files, 2 changesets, 2 total revisions bleah +# push should fail +pushing to ssh://user@dummy/remote +searching for changes +abort: unsynced remote changes! +(did you forget to sync? use push -f to force) +# push should succeed +pushing to ssh://user@dummy/remote +searching for changes +remote: adding changesets +remote: adding manifests +remote: adding file changes +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 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: +Got arguments 1:user@dummy 2:hg -R remote serve --stdio 3: 4: 5: