commit 01bddec68dab48693af40968567ce4cff535f269 Author: Scott Chacon Date: Fri Feb 20 18:27:51 2009 -0800 initial import of the hg-git bridging extension for mercurial diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000..770a5c8f99 --- /dev/null +++ b/__init__.py @@ -0,0 +1,124 @@ +# git.py - git server bridge +# +# Copyright 2008 Scott Chacon +# 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')), +} \ No newline at end of file diff --git a/git.py b/git.py new file mode 100644 index 0000000000..3de4b668e2 --- /dev/null +++ b/git.py @@ -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