changeset 230:51e4d6ebbc40

rework pushing to support --rev and --force options
author Abderrahim Kitouni <a.kitouni@gmail.com>
date Thu, 30 Jul 2009 22:09:53 +0100
parents f2c2061aacd1
children bdaec2a079ce
files git_handler.py hgrepo.py tests/test-push tests/test-push.out
diffstat 4 files changed, 200 insertions(+), 58 deletions(-) [+]
line wrap: on
line diff
--- a/git_handler.py
+++ b/git_handler.py
@@ -1,6 +1,7 @@
 import os, sys, math, urllib, re
 import toposort
 
+from dulwich.errors import HangupException
 from dulwich.index import commit_tree
 from dulwich.objects import Blob, Commit, Tag, Tree, format_timezone
 from dulwich.pack import create_delta, apply_delta
@@ -122,9 +123,9 @@
         self.update_references()
         self.save_map()
 
-    def push(self, remote):
+    def push(self, remote, revs, force):
         self.export_commits()
-        changed_refs = self.upload_pack(remote)
+        changed_refs = self.upload_pack(remote, revs, force)
         remote_name = self.remote_name(remote)
 
         if remote_name and changed_refs:
@@ -479,70 +480,54 @@
 
     ## PACK UPLOADING AND FETCHING
 
-    def upload_pack(self, remote):
+    def upload_pack(self, remote, revs, force):
         client, path = self.get_transport_and_path(remote)
-        changed = self.get_changed_refs
+        def changed(refs):
+            to_push = revs or set(self.local_heads().values() + self.tags.values())
+            if not to_push and refs.keys()[0] == 'capabilities^{}':
+                to_push = [self.repo.lookup('tip')]
+            return self.get_changed_refs(refs, to_push, force)
+
         genpack = self.git.object_store.generate_pack_contents
         try:
             self.ui.status(_("creating and sending data\n"))
             changed_refs = client.send_pack(path, changed, genpack)
             return changed_refs
-        except:
-            # TODO: remove try/except or do something useful here
-            raise
-
-    # TODO: for now, we'll just push all heads that match remote heads
-    #        * we should have specified push, tracking branches and --all
-    # takes a dict of refs:shas from the server and returns what should be
-    # pushed up
-    def get_changed_refs(self, refs):
-        keys = refs.keys()
+        except HangupException:
+            raise hgutil.Abort("the remote end hung up unexpectedly")
 
-        changed = {}
-        if not keys:
-            return None
+    def get_changed_refs(self, refs, revs, force):
+        new_refs = refs.copy()
+        for rev in revs:
+            ctx = self.repo[rev]
+            heads = [t for t in ctx.tags() if t in self.local_heads()]
+            tags = [t for t in ctx.tags() if t in self.tags]
 
-        # TODO: this is a huge hack
-        if keys[0] == 'capabilities^{}':
-            # nothing on the server yet - first push
-            if not 'master' in self.repo.tags():
-                tip = self.repo.lookup('tip')
-                changed['refs/heads/master'] = self.map_git_get(hex(tip))
-
-        for tag, sha in self.tags.iteritems():
-            tag_name = 'refs/tags/' + tag
-            if tag_name not in refs:
-                changed[tag_name] = self.map_git_get(sha)
+            if not (heads or tags):
+                raise hgutil.Abort("revision %s cannot be pushed since"
+                                   " it doesn't have a ref" % ctx)
 
-        for ref_name in keys:
-            parts = ref_name.split('/')
-            if parts[0] == 'refs' and parts[1] == 'heads':
-                # strip off 'refs/heads'
-                head = "/".join([v for v in parts[2:]])
-                try:
-                    local_ref = self.repo.lookup(head)
-                    remote_ref = self.map_hg_get(refs[ref_name])
-                    if remote_ref:
-                        remotectx = self.repo[remote_ref]
-                        localctx = self.repo[local_ref]
-                        if remotectx.ancestor(localctx) == remotectx:
-                            # fast forward push
-                            changed[ref_name] = self.map_git_get(hex(local_ref))
-                        else:
-                            # XXX: maybe abort completely
-                            self.ui.warn('not pushing branch %s, please merge\n'% head)
-                except RepoError: #pragma: no cover
-                    # remote_ref is not here
-                    pass
+            for r in heads + tags:
+                if r in heads:
+                    ref = 'refs/heads/'+r
+                else:
+                    ref = 'refs/tags/'+r
 
-        # Also push any local branches not on the server yet
-        for head in self.local_heads():
-            ref = 'refs/heads/' + head
-            if not ref in refs:
-                node = self.repo.lookup(head)
-                changed[ref] = self.map_git_get(hex(node))
+                if ref not in refs:
+                    new_refs[ref] = self.map_git_get(ctx.hex())
+                elif new_refs[ref] in self._map_git:
+                    rctx = self.repo[self.map_hg_get(new_refs[ref])]
+                    if rctx.ancestor(ctx) == rctx or force:
+                        new_refs[ref] = self.map_git_get(ctx.hex())
+                    else:
+                        raise hgutil.Abort("pushing %s overwrites %s"
+                                           % (ref, ctx))
+                else:
+                    raise hgutil.Abort("%s changed on the server, please pull "
+                                       "and merge before pushing" % ref)
 
-        return changed
+        return new_refs
+
 
     def fetch_pack(self, remote_name):
         client, path = self.get_transport_and_path(remote_name)
@@ -551,6 +536,8 @@
         f, commit = self.git.object_store.add_pack()
         try:
             return client.fetch_pack(path, determine_wants, graphwalker, f.write, self.ui.status)
+        except HangupException:
+            raise hgutil.Abort("the remote end hung up unexpectedly")
         finally:
             commit()
 
@@ -562,7 +549,7 @@
         # Create a local Git branch name for each
         # Mercurial bookmark.
         for key in heads:
-            self.git.refs['refs/heads/' + key] = heads[key]
+            self.git.refs['refs/heads/' + key] = self.map_git_get(heads[key])
 
     def export_hg_tags(self):
         for tag, sha in self.repo.tags().iteritems():
@@ -573,7 +560,7 @@
     def local_heads(self):
         try:
             bms = bookmarks.parse(self.repo)
-            return dict([(bm, self.map_git_get(hex(bms[bm]))) for bm in bms])
+            return dict([(bm, hex(bms[bm])) for bm in bms])
         except AttributeError: #pragma: no cover
             return {}
 
--- a/hgrepo.py
+++ b/hgrepo.py
@@ -20,7 +20,7 @@
         def push(self, remote, force=False, revs=None):
             if isinstance(remote, gitrepo):
                 git = GitHandler(self, self.ui)
-                git.push(remote.path)
+                git.push(remote.path, revs, force)
             else: #pragma: no cover
                 return super(hgrepo, self).push(remote, force, revs)
 
new file mode 100755
--- /dev/null
+++ b/tests/test-push
@@ -0,0 +1,100 @@
+#!/bin/sh
+
+# Fails for some reason, need to investigate
+# "$TESTDIR/hghave" git || exit 80
+
+# bail early if the user is already running git-daemon
+echo hi | nc localhost 9418 2>/dev/null && exit 80
+
+echo "[extensions]" >> $HGRCPATH
+echo "hggit=$(echo $(dirname $(dirname $0)))" >> $HGRCPATH
+echo 'hgext.graphlog =' >> $HGRCPATH
+echo 'hgext.bookmarks =' >> $HGRCPATH
+
+GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME
+GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL
+GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0000"; export GIT_AUTHOR_DATE
+GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME
+GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL
+GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE
+
+count=10
+commit()
+{
+    GIT_AUTHOR_DATE="2007-01-01 00:00:$count +0000"
+    GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"
+    git commit "$@" >/dev/null 2>/dev/null || echo "hg commit error"
+    count=`expr $count + 1`
+}
+hgcommit()
+{
+    HGDATE="2007-01-01 00:00:$count +0000"
+    hg commit -d "$HGDATE" "$@" >/dev/null 2>/dev/null || echo "hg commit error"
+    count=`expr $count + 1`
+}
+
+mkdir gitrepo
+cd gitrepo
+git init | python -c "import sys; print sys.stdin.read().replace('$(dirname $(pwd))/', '')"
+
+echo alpha > alpha
+git add alpha
+commit -m "add alpha"
+
+# dulwich does not presently support local git repos, workaround
+cd ..
+git daemon --base-path="$(pwd)"\
+ --listen=localhost\
+ --export-all\
+ --pid-file=gitdaemon.pid \
+ --detach --reuseaddr \
+ --enable=receive-pack
+
+hg clone git://localhost/gitrepo hgrepo
+
+cd hgrepo
+echo beta > beta
+hg add beta
+hgcommit -m 'add beta'
+
+
+echo gamma > gamma
+hg add gamma
+hgcommit -m 'add gamma'
+
+hg book -r 1 beta
+hg push -r beta
+
+cd ..
+
+echo % should have two different branches
+cd gitrepo
+git branch -v
+
+echo % some more work on master from git
+git checkout master
+echo delta > delta
+git add delta
+commit -m "add delta"
+
+cd ..
+
+cd hgrepo
+echo % this should fail
+hg push -r master
+
+echo % ... even with -f
+hg push -fr master
+
+hg pull
+echo % master and default/master should be diferent
+hg tags
+
+echo % this should also fail
+hg push -r master
+
+echo % ... but succeed with -f
+hg push -fr master
+
+cd ..
+kill `cat gitdaemon.pid`
new file mode 100644
--- /dev/null
+++ b/tests/test-push.out
@@ -0,0 +1,55 @@
+Initialized empty Git repository in gitrepo/.git/
+
+importing Hg objects into Git
+Counting objects: 3, done.
+Total 3 (delta 0), reused 0 (delta 0)
+importing Git objects into Hg
+at: 0/1
+updating working directory
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+pushing to git://localhost/gitrepo
+importing Hg objects into Git
+at: 0/2
+creating and sending data
+    default::refs/heads/beta => GIT:cffa0e8d
+    default::refs/heads/master => GIT:7eeab2ea
+% should have two different branches
+  beta   cffa0e8 add beta
+* master 7eeab2e add alpha
+% some more work on master from git
+Already on "master"
+% this should fail
+pushing to git://localhost/gitrepo
+importing Hg objects into Git
+creating and sending data
+abort: refs/heads/master changed on the server, please pull and merge before pushing
+% ... even with -f
+pushing to git://localhost/gitrepo
+importing Hg objects into Git
+creating and sending data
+abort: refs/heads/master changed on the server, please pull and merge before pushing
+pulling from git://localhost/gitrepo
+importing Hg objects into Git
+Counting objects: 4, done.
+Compressing objects:  50% (1/2)   
Compressing objects: 100% (2/2)   
Compressing objects: 100% (2/2), done.
+Total 3 (delta 0), reused 0 (delta 0)
+importing Git objects into Hg
+at: 0/1
+(run 'hg update' to get a working copy)
+% master and default/master should be diferent
+tip                                3:1436150b86c2
+default/master                     3:1436150b86c2
+master                             2:72f56395749d
+default/beta                       1:0564f526fb0f
+beta                               1:0564f526fb0f
+% this should also fail
+pushing to git://localhost/gitrepo
+importing Hg objects into Git
+creating and sending data
+abort: pushing refs/heads/master overwrites 72f56395749d
+% ... but succeed with -f
+pushing to git://localhost/gitrepo
+importing Hg objects into Git
+creating and sending data
+    default::refs/heads/beta => GIT:cffa0e8d
+    default::refs/heads/master => GIT:cc119202