changeset 408:2dcfd4bbfc1a

Support for hg incoming
author Brendan Cully <brendan@kublai.com>
date Tue, 24 May 2011 11:16:45 -0700
parents 3a58fe455b0b
children 2e773ed95066
files hggit/__init__.py hggit/git_handler.py hggit/overlay.py tests/test-incoming tests/test-incoming.out
diffstat 5 files changed, 489 insertions(+), 5 deletions(-) [+]
line wrap: on
line diff
--- a/hggit/__init__.py
+++ b/hggit/__init__.py
@@ -18,6 +18,7 @@
 import inspect
 import os
 
+from mercurial import bundlerepo
 from mercurial import commands
 from mercurial import demandimport
 from mercurial import extensions
@@ -154,6 +155,21 @@
 except ImportError:
     pass
 
+def getremotechanges(orig, ui, repo, other, revs, *args, **opts):
+    if isinstance(other, gitrepo.gitrepo):
+        git = GitHandler(repo, ui)
+        r, c, cleanup = git.getremotechanges(other, revs)
+        # ugh. This is ugly even by mercurial API compatibility standards
+        if 'onlyheads' not in orig.func_code.co_varnames:
+            cleanup = None
+        return r, c, cleanup
+    return orig(ui, repo, other, revs, *args, **opts)
+try:
+    extensions.wrapfunction(bundlerepo, 'getremotechanges', getremotechanges)
+except AttributeError:
+    # 1.7+
+    pass
+
 cmdtable = {
   "gimport":
         (gimport, [], _('hg gimport')),
--- a/hggit/git_handler.py
+++ b/hggit/git_handler.py
@@ -20,6 +20,7 @@
 
 import _ssh
 import util
+from overlay import overlayrepo
 
 class GitHandler(object):
     mapfile = 'git-mapfile'
@@ -122,9 +123,9 @@
             rn = remote_name or 'default'
             return 'refs/remotes/' + rn + ref[10:]
 
-        modheads = set([refs[k] for k in refs if k.startswith('refs/heads/')
-                        and not k.endswith('^{}')
-                        and refs[k] != oldrefs.get(remoteref(k))])
+        modheads = [refs[k] for k in refs if k.startswith('refs/heads/')
+                    and not k.endswith('^{}')
+                    and refs[k] != oldrefs.get(remoteref(k))]
 
         if not modheads:
             self.ui.status(_("no changes found\n"))
@@ -190,6 +191,27 @@
         if os.path.exists(mapfile):
             os.remove(mapfile)
 
+    # incoming support
+    def getremotechanges(self, remote, revs):
+        self.export_commits()
+        refs = self.fetch_pack(remote.path, revs)
+
+        # refs contains all remote refs. Prune to only those requested.
+        if revs:
+            reqrefs = {}
+            for rev in revs:
+                for n in ('refs/heads/' + rev, 'refs/tags/' + rev):
+                    if n in refs:
+                        reqrefs[n] = refs[n]
+        else:
+            reqrefs = refs
+
+        commits = [bin(c) for c in self.getnewgitcommits(reqrefs)[1]]
+
+        b = overlayrepo(self, commits, refs)
+
+        return (b, commits, lambda: None)
+
     ## CHANGESET CONVERSION METHODS
 
     def export_git_objects(self):
@@ -374,7 +396,7 @@
 
             yield f, blobid, mode
 
-    def import_git_objects(self, remote_name=None, refs=None):
+    def getnewgitcommits(self, refs=None):
         self.init_if_missing()
 
         # import heads and fetched tags as remote references
@@ -427,7 +449,10 @@
                 done.add(sha)
                 todo.pop()
 
-        commits = [commit for commit in commits if not commit in self._map_git]
+        return convert_list, [commit for commit in commits if not commit in self._map_git]
+
+    def import_git_objects(self, remote_name=None, refs=None):
+        convert_list, commits = self.getnewgitcommits(refs)
         # import each of the commits, oldest first
         total = len(commits)
         if total:
new file mode 100644
--- /dev/null
+++ b/hggit/overlay.py
@@ -0,0 +1,254 @@
+# overlay classes for repositories
+# unifies access to unimported git objects and committed hg objects
+# designed to support incoming
+#
+# incomplete, implemented on demand
+
+from mercurial import context
+from mercurial.node import bin, hex, nullid
+
+class overlaymanifest(object):
+    def __init__(self, repo, sha):
+        self.repo = repo
+        self.tree = repo.handler.git.get_object(sha)
+        self._map = None
+        self._flagmap = None
+
+    def copy(self):
+        return overlaymanifest(self.repo, self.tree.id)
+
+    def keys(self):
+        self.load()
+        return self._map.keys()
+
+    def flags(self, path):
+        self.load()
+
+        def hgflag(gitflag):
+            if gitflag & 0100:
+                return 'x'
+            elif gitflag & 020000:
+                return 'l'
+            else:
+                return ''
+
+        return hgflag(self._flagmap[path])
+
+    def load(self):
+        if self._map is not None:
+            return
+
+        self._map = {}
+        self._flagmap = {}
+
+        def addtree(tree, dirname):
+            for entry in tree.entries():
+                if entry[0] & 040000:
+                    # expand directory
+                    subtree = self.repo.handler.git.get_object(entry[2])
+                    addtree(subtree, dirname + entry[1] + '/')
+                else:
+                    path = dirname + entry[1]
+                    self._map[path] = bin(entry[2])
+                    self._flagmap[path] = entry[0]
+
+        addtree(self.tree, '')
+
+    def __iter__(self):
+        self.load()
+        return self._map.__iter__()
+
+    def __getitem__(self, path):
+        self.load()
+        return self._map[path]
+
+    def __delitem__(self, path):
+        del self._map[path]
+
+class overlayfilectx(object):
+    def __init__(self, repo, path, fileid=None):
+        self.repo = repo
+        self._path = path
+        self.fileid = fileid
+
+    # this is a hack to skip copy detection
+    def ancestors(self):
+        return [self, self]
+
+    def rev(self):
+        return -1
+
+    def path(self):
+        return self._path
+
+    def filelog(self):
+        return self.fileid
+
+    def data(self):
+        blob = self.repo.handler.git.get_object(self.fileid)
+        return blob.data
+
+class overlaychangectx(context.changectx):
+    def __init__(self, repo, sha):
+        self.repo = repo
+        self.commit = repo.handler.git.get_object(sha)
+
+    def node(self):
+        return bin(self.commit.id)
+
+    def rev(self):
+        return self.repo.rev(bin(self.commit.id))
+
+    def date(self):
+        return self.commit.author_time, self.commit.author_timezone
+
+    def branch(self):
+        return 'default'
+
+    def user(self):
+        return self.commit.author
+
+    def files(self):
+        return []
+
+    def extra(self):
+        return {}
+
+    def description(self):
+        return self.commit.message
+
+    def parents(self):
+        return [overlaychangectx(self.repo, sha) for sha in self.commit.parents]
+
+    def manifestnode(self):
+        return bin(self.commit.tree)
+
+    def hex(self):
+        return self.commit.id
+
+    def tags(self):
+        return []
+
+    def bookmarks(self):
+        return []
+
+    def manifest(self):
+        return overlaymanifest(self.repo, self.commit.tree)
+
+    def filectx(self, path, filelog=None):
+        mf = self.manifest()
+        return overlayfilectx(self.repo, path, mf[path])
+
+    def flags(self, path):
+        mf = self.manifest()
+        return mf.flags(path)
+
+    def __nonzero__(self):
+        return True
+
+class overlayrevlog(object):
+    def __init__(self, repo, base):
+        self.repo = repo
+        self.base = base
+
+    def parents(self, n):
+        gitrev = self.repo.revmap.get(n)
+        if not gitrev:
+            # we've reached a revision we have
+            return self.base.parents(n)
+        commit = self.repo.handler.git.get_object(n)
+
+        def gitorhg(n):
+            hn = self.repo.handler.map_hg_get(hex(n))
+            if hn is not None:
+                return bin(hn)
+            return n
+
+        # currently ignores the octopus
+        p1 = gitorhg(bin(commit.parents[0]))
+        if len(commit.parents) > 1:
+            p2 = gitorhg(bin(commit.parents[1]))
+        else:
+            p2 = nullid
+
+        return [p1, p2]
+
+    def parentrevs(self, rev):
+        return [self.rev(p) for p in self.parents(self.node(rev))]
+
+    def node(self, rev):
+        gitnode = self.repo.nodemap.get(rev)
+        if gitnode is None:
+            return self.base.node(rev)
+        return gitnode
+
+    def rev(self, n):
+        gitrev = self.repo.revmap.get(n)
+        if gitrev is None:
+             return self.base.rev(n)
+        return gitrev
+
+    def nodesbetween(self, nodelist, revs):
+        # this is called by pre-1.9 incoming with the nodelist we returned from
+        # getremotechanges. Just return it back.
+        return [nodelist]
+
+    def __len__(self):
+        return len(self.repo.handler.repo) + len(self.repo.revmap)
+
+
+class overlayrepo(object):
+    def __init__(self, handler, commits, refs):
+        self.handler = handler
+
+        self.changelog = overlayrevlog(self, handler.repo.changelog)
+        self.manifest = overlayrevlog(self, handler.repo.manifest)
+
+        # for incoming -p
+        self.root = handler.repo.root
+        self.getcwd = handler.repo.getcwd
+        self.status = handler.repo.status
+        self.ui = handler.repo.ui
+
+        self.revmap = None
+        self.nodemap = None
+        self.refmap = None
+        self.tagmap = None
+
+        self._makemaps(commits, refs)
+
+    def __getitem__(self, n):
+        if n not in self.revmap:
+            return self.handler.repo[n]
+        return overlaychangectx(self, n)
+
+    def nodebookmarks(self, n):
+        return self.refmap.get(n, [])
+
+    def nodetags(self, n):
+        return self.tagmap.get(n, [])
+
+    def rev(self, n):
+        return self.revmap[n]
+
+    def filectx(self, path, fileid=None):
+        return overlayfilectx(self, path, fileid=fileid)
+
+    def _makemaps(self, commits, refs):
+        baserev = self.handler.repo['tip'].rev()
+        self.revmap = {}
+        self.nodemap = {}
+        for i, n in enumerate(commits):
+            rev = baserev + i + 1
+            self.revmap[n] = rev
+            self.nodemap[rev] = n
+
+        self.refmap = {}
+        self.tagmap = {}
+        for ref in refs:
+            if ref.startswith('refs/heads/'):
+                refname = ref[11:]
+                self.refmap.setdefault(bin(refs[ref]), []).append(refname)
+            elif ref.startswith('refs/tags/'):
+                tagname = ref[10:]
+                self.tagmap.setdefault(bin(refs[ref]), []).append(tagname)
new file mode 100755
--- /dev/null
+++ b/tests/test-incoming
@@ -0,0 +1,93 @@
+#!/bin/sh
+
+# Fails for some reason, need to investigate
+# "$TESTDIR/hghave" git || exit 80
+
+# bail if the user does not have dulwich
+python -c 'import dulwich, dulwich.repo' || 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)))/hggit" >> $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 "git 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="$DAEMON_PIDS" \
+ --detach --reuseaddr \
+ --enable=receive-pack
+
+hg clone git://localhost/gitrepo hgrepo | grep -v '^updating'
+
+cd hgrepo
+hg incoming
+
+cd ../gitrepo
+echo beta > beta
+git add beta
+commit -m 'add beta'
+
+cd ../hgrepo
+hg incoming
+
+cd ../gitrepo
+git checkout -b b1 HEAD^
+mkdir d
+echo gamma > d/gamma
+git add d/gamma
+commit -m'add d/gamma'
+git tag t1
+
+echo gamma 2 >> d/gamma
+git add d/gamma
+commit -m'add d/gamma line 2'
+
+cd ../hgrepo
+hg incoming -p
+
+echo % incoming -r
+hg incoming -r master
+hg incoming -r b1
+hg incoming -r t1
+
+echo % nothing incoming after pull
+hg pull
+hg incoming
+
+echo 'done'
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/tests/test-incoming.out
@@ -0,0 +1,96 @@
+Initialized empty Git repository in gitrepo/.git/
+
+Counting objects: 3, done.
+Total 3 (delta 0), reused 0 (delta 0)
+importing git objects into hg
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+comparing with git://localhost/gitrepo
+no changes found
+comparing with git://localhost/gitrepo
+Counting objects: 3, 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)
+changeset:   1:9497a4ee62e1
+bookmark:    master
+user:        test <test@example.org>
+date:        Mon Jan 01 00:00:11 2007 +0000
+summary:     add beta
+
+Switched to a new branch 'b1'
+comparing with git://localhost/gitrepo
+Counting objects: 8, done.
+Compressing objects:  25% (1/4)   
Compressing objects:  50% (2/4)   
Compressing objects:  75% (3/4)   
Compressing objects: 100% (4/4)   
Compressing objects: 100% (4/4), done.
+Total 8 (delta 0), reused 0 (delta 0)
+changeset:   1:9497a4ee62e1
+bookmark:    master
+user:        test <test@example.org>
+date:        Mon Jan 01 00:00:11 2007 +0000
+summary:     add beta
+
+diff -r 3442585be8a6 -r 9497a4ee62e1 beta
+--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
++++ b/beta	Mon Jan 01 00:00:11 2007 +0000
+@@ -0,0 +1,1 @@
++beta
+
+changeset:   2:9865e289be73
+tag:         t1
+parent:      0:3442585be8a6
+user:        test <test@example.org>
+date:        Mon Jan 01 00:00:12 2007 +0000
+summary:     add d/gamma
+
+diff -r 3442585be8a6 -r 9865e289be73 d/gamma
+--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
++++ b/d/gamma	Mon Jan 01 00:00:12 2007 +0000
+@@ -0,0 +1,1 @@
++gamma
+
+changeset:   3:5202f48c20c9
+bookmark:    b1
+user:        test <test@example.org>
+date:        Mon Jan 01 00:00:13 2007 +0000
+summary:     add d/gamma line 2
+
+diff -r 9865e289be73 -r 5202f48c20c9 d/gamma
+--- a/d/gamma	Mon Jan 01 00:00:12 2007 +0000
++++ b/d/gamma	Mon Jan 01 00:00:13 2007 +0000
+@@ -1,1 +1,2 @@
+ gamma
++gamma 2
+
+% incoming -r
+comparing with git://localhost/gitrepo
+changeset:   1:9497a4ee62e1
+bookmark:    master
+user:        test <test@example.org>
+date:        Mon Jan 01 00:00:11 2007 +0000
+summary:     add beta
+
+comparing with git://localhost/gitrepo
+changeset:   1:9865e289be73
+tag:         t1
+user:        test <test@example.org>
+date:        Mon Jan 01 00:00:12 2007 +0000
+summary:     add d/gamma
+
+changeset:   2:5202f48c20c9
+bookmark:    b1
+user:        test <test@example.org>
+date:        Mon Jan 01 00:00:13 2007 +0000
+summary:     add d/gamma line 2
+
+comparing with git://localhost/gitrepo
+changeset:   1:9865e289be73
+tag:         t1
+user:        test <test@example.org>
+date:        Mon Jan 01 00:00:12 2007 +0000
+summary:     add d/gamma
+
+% nothing incoming after pull
+pulling from git://localhost/gitrepo
+importing git objects into hg
+(run 'hg heads' to see heads, 'hg merge' to merge)
+comparing with git://localhost/gitrepo
+no changes found
+done