initial import of the hg-git bridging extension for mercurial

This commit is contained in:
Scott Chacon 2009-02-20 18:27:51 -08:00
commit 01bddec68d
2 changed files with 270 additions and 0 deletions

124
__init__.py Normal file
View File

@ -0,0 +1,124 @@
# git.py - git server bridge
#
# Copyright 2008 Scott Chacon <schacon at gmail dot com>
# also some code (and help) borrowed from durin42
#
# This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.
'''push and pull from a Git server
This extension lets you communicate (push and pull) with a Git server.
This way you can use Git hosting for your project or collaborate with a
project that is in Git. A bridger of worlds, this plugin be.
'''
#
# Stage One - use Git commands to do the import / pushes, all in one big uggo file
#
# Stage Two - implement the Git packfile generation and server communication
# in native Python, so we don't need Git locally and don't need
# to keep all the git repo data around. We should just need a SHA
# mapping - since everything is append only in both systems it should
# be pretty simple to do.
#
# just importing every damn thing because i don't know python that well
# and I have no idea what I actually need
from mercurial import util, repair, merge, cmdutil, commands, error, hg, url
from mercurial import extensions, ancestor
from mercurial.commands import templateopts
from mercurial.node import nullrev, nullid, short
from mercurial.i18n import _
import os, errno
import subprocess
def gclone(ui, git_url, hg_repo_path=None):
## TODO : add git_url as the default remote path
if not hg_repo_path:
hg_repo_path = hg.defaultdest(git_url)
if hg_repo_path.endswith('.git'):
hg_repo_path = hg_repo_path[:-4]
hg_repo_path += '-hg'
subprocess.call(['hg', 'init', hg_repo_path])
clone_git(git_url, git_path(hg_repo_path))
import_git_heads(hg_repo_path)
def gpull(ui, repo, source='default', **opts):
"""fetch from a git repo
"""
lock = wlock = None
try:
lock = repo.lock()
wlock = repo.wlock()
ui.write("fetching from the remote\n")
git_fetch()
import_git_heads()
# do the pull
finally:
del lock, wlock
def gpush(ui, repo, dest='default', **opts):
"""push to a git repo
"""
lock = wlock = None
try:
lock = repo.lock()
wlock = repo.wlock()
ui.write("pushing to the remote\n")
# do the push
finally:
del lock, wlock
def git_path(hg_path=None):
return os.path.join(hg_path, '.hg', 'git-remote')
def clone_git(git_url, hg_path=None):
git_initialize(git_path(hg_path), git_url)
git_fetch(git_path(hg_path))
def git_initialize(git_path, git_url):
# TODO: implement this in pure python - should be strait-forward
subprocess.call(['git', '--bare', 'init', git_path])
oldwd = os.getcwd()
os.chdir(git_path)
subprocess.call(['git', 'remote', 'add', 'origin', git_url])
os.chdir(oldwd)
def git_fetch(git_path, remote='origin'):
# TODO: implement this in pure python
# - we'll have to handle ssh and git
oldwd = os.getcwd()
os.chdir(git_path)
subprocess.call(['git', 'fetch', remote])
os.chdir(oldwd)
def git_push():
# find all the local changesets that aren't mapped
# create git commit object shas and map them
# stick those objects in a packfile and push them up (over ssh)
def import_git_heads(hg_path=None):
# go through each branch
# add all commits we don't have locally
# write a SHA<->SHA mapping table
# update the local branches to match
return subprocess.call(['hg', 'convert', git_path(hg_path), hg_path])
commands.norepo += " gclone"
cmdtable = {
"gclone":
(gclone,
[ #('A', 'authors', '', 'username mapping filename'),
],
'Clone a git repository into an hg repository.',
),
"gpush":
(gpush,
[('m', 'merge', None, _('merge automatically'))],
_('hg gpush remote')),
"gpull":
(gpull, [], _('hg gpull [--merge] remote')),
}

146
git.py Normal file
View File

@ -0,0 +1,146 @@
# git support for the convert extension
import os
from mercurial import util
from common import NoRepo, commit, converter_source, checktool
class git_tool:
# Windows does not support GIT_DIR= construct while other systems
# cannot remove environment variable. Just assume none have
# both issues.
if hasattr(os, 'unsetenv'):
def gitcmd(self, s):
prevgitdir = os.environ.get('GIT_DIR')
os.environ['GIT_DIR'] = self.path
try:
return util.popen(s, 'rb')
finally:
if prevgitdir is None:
del os.environ['GIT_DIR']
else:
os.environ['GIT_DIR'] = prevgitdir
else:
def gitcmd(self, s):
return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb')
def __init__(self, ui, path, rev=None):
super(convert_git, self).__init__(ui, path, rev=rev)
if os.path.isdir(path + "/.git"):
path += "/.git"
if not os.path.exists(path + "/objects"):
raise NoRepo("%s does not look like a Git repo" % path)
checktool('git', 'git')
self.path = path
def getheads(self):
if not self.rev:
return self.gitcmd('git rev-parse --branches --remotes').read().splitlines()
else:
fh = self.gitcmd("git rev-parse --verify %s" % self.rev)
return [fh.read()[:-1]]
def catfile(self, rev, type):
if rev == "0" * 40: raise IOError()
fh = self.gitcmd("git cat-file %s %s" % (type, rev))
return fh.read()
def getfile(self, name, rev):
return self.catfile(rev, "blob")
def getmode(self, name, rev):
return self.modecache[(name, rev)]
def getchanges(self, version):
self.modecache = {}
fh = self.gitcmd("git diff-tree -z --root -m -r %s" % version)
changes = []
seen = {}
entry = None
for l in fh.read().split('\x00'):
if not entry:
if not l.startswith(':'):
continue
entry = l
continue
f = l
if f not in seen:
seen[f] = 1
entry = entry.split()
h = entry[3]
p = (entry[1] == "100755")
s = (entry[1] == "120000")
self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
changes.append((f, h))
entry = None
return (changes, {})
def getcommit(self, version):
c = self.catfile(version, "commit") # read the commit hash
end = c.find("\n\n")
message = c[end+2:]
message = self.recode(message)
l = c[:end].splitlines()
manifest = l[0].split()[1]
parents = []
for e in l[1:]:
n, v = e.split(" ", 1)
if n == "author":
p = v.split()
tm, tz = p[-2:]
author = " ".join(p[:-2])
if author[0] == "<": author = author[1:-1]
author = self.recode(author)
if n == "committer":
p = v.split()
tm, tz = p[-2:]
committer = " ".join(p[:-2])
if committer[0] == "<": committer = committer[1:-1]
committer = self.recode(committer)
message += "\ncommitter: %s\n" % committer
if n == "parent": parents.append(v)
tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
date = tm + " " + str(tz)
c = commit(parents=parents, date=date, author=author, desc=message,
rev=version)
return c
def gettags(self):
tags = {}
fh = self.gitcmd('git ls-remote --tags "%s"' % self.path)
prefix = 'refs/tags/'
for line in fh:
line = line.strip()
if not line.endswith("^{}"):
continue
node, tag = line.split(None, 1)
if not tag.startswith(prefix):
continue
tag = tag[len(prefix):-3]
tags[tag] = node
return tags
def getchangedfiles(self, version, i):
changes = []
if i is None:
fh = self.gitcmd("git diff-tree --root -m -r %s" % version)
for l in fh:
if "\t" not in l:
continue
m, f = l[:-1].split("\t")
changes.append(f)
fh.close()
else:
fh = self.gitcmd('git diff-tree --name-only --root -r %s "%s^%s" --'
% (version, version, i+1))
changes = [f.rstrip('\n') for f in fh]
fh.close()
return changes