From 5085a4e2e81f295a93df9b4bd98813d771f18a3c Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Sun, 4 Nov 2012 03:13:23 +0100 Subject: [PATCH 01/22] Add new remote-hg transport helper Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/git-remote-hg | 391 +++++++++++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100755 contrib/remote-helpers/git-remote-hg diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg new file mode 100755 index 0000000000..e37e278c26 --- /dev/null +++ b/contrib/remote-helpers/git-remote-hg @@ -0,0 +1,391 @@ +#!/usr/bin/env python +# +# Copyright (c) 2012 Felipe Contreras +# + +# Inspired by Rocco Rutte's hg-fast-export + +# Just copy to your ~/bin, or anywhere in your $PATH. +# Then you can clone with: +# git clone hg::/path/to/mercurial/repo/ + +from mercurial import hg, ui, bookmarks + +import re +import sys +import os +import json + +NAME_RE = re.compile('^([^<>]+)') +AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]+)>$') + +def die(msg, *args): + sys.stderr.write('ERROR: %s\n' % (msg % args)) + sys.exit(1) + +def warn(msg, *args): + sys.stderr.write('WARNING: %s\n' % (msg % args)) + +def gitmode(flags): + return 'l' in flags and '120000' or 'x' in flags and '100755' or '100644' + +def gittz(tz): + return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60) + +class Marks: + + def __init__(self, path): + self.path = path + self.tips = {} + self.marks = {} + self.last_mark = 0 + + self.load() + + def load(self): + if not os.path.exists(self.path): + return + + tmp = json.load(open(self.path)) + + self.tips = tmp['tips'] + self.marks = tmp['marks'] + self.last_mark = tmp['last-mark'] + + def dict(self): + return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark } + + def store(self): + json.dump(self.dict(), open(self.path, 'w')) + + def __str__(self): + return str(self.dict()) + + def from_rev(self, rev): + return self.marks[str(rev)] + + def get_mark(self, rev): + self.last_mark += 1 + self.marks[str(rev)] = self.last_mark + return self.last_mark + + def is_marked(self, rev): + return self.marks.has_key(str(rev)) + + def get_tip(self, branch): + return self.tips.get(branch, 0) + + def set_tip(self, branch, tip): + self.tips[branch] = tip + +class Parser: + + def __init__(self, repo): + self.repo = repo + self.line = self.get_line() + + def get_line(self): + return sys.stdin.readline().strip() + + def __getitem__(self, i): + return self.line.split()[i] + + def check(self, word): + return self.line.startswith(word) + + def each_block(self, separator): + while self.line != separator: + yield self.line + self.line = self.get_line() + + def __iter__(self): + return self.each_block('') + + def next(self): + self.line = self.get_line() + if self.line == 'done': + self.line = None + +def export_file(fc): + d = fc.data() + print "M %s inline %s" % (gitmode(fc.flags()), fc.path()) + print "data %d" % len(d) + print d + +def get_filechanges(repo, ctx, parent): + modified = set() + added = set() + removed = set() + + cur = ctx.manifest() + prev = repo[parent].manifest().copy() + + for fn in cur: + if fn in prev: + if (cur.flags(fn) != prev.flags(fn) or cur[fn] != prev[fn]): + modified.add(fn) + del prev[fn] + else: + added.add(fn) + removed |= set(prev.keys()) + + return added | modified, removed + +def fixup_user(user): + user = user.replace('"', '') + name = mail = None + m = AUTHOR_RE.match(user) + if m: + name = m.group(1) + mail = m.group(2).strip() + else: + m = NAME_RE.match(user) + if m: + name = m.group(1).strip() + + if not name: + name = 'Unknown' + if not mail: + mail = 'unknown' + + return '%s <%s>' % (name, mail) + +def get_repo(url, alias): + global dirname + + myui = ui.ui() + myui.setconfig('ui', 'interactive', 'off') + + if hg.islocal(url): + repo = hg.repository(myui, url) + else: + local_path = os.path.join(dirname, 'clone') + if not os.path.exists(local_path): + peer, dstpeer = hg.clone(myui, {}, url, local_path, update=False, pull=True) + repo = dstpeer.local() + else: + repo = hg.repository(myui, local_path) + peer = hg.peer(myui, {}, url) + repo.pull(peer, heads=None, force=True) + + return repo + +def rev_to_mark(rev): + global marks + return marks.from_rev(rev) + +def export_ref(repo, name, kind, head): + global prefix, marks + + ename = '%s/%s' % (kind, name) + tip = marks.get_tip(ename) + + # mercurial takes too much time checking this + if tip and tip == head.rev(): + # nothing to do + return + revs = repo.revs('%u:%u' % (tip, head)) + count = 0 + + revs = [rev for rev in revs if not marks.is_marked(rev)] + + for rev in revs: + + c = repo[rev] + (manifest, user, (time, tz), files, desc, extra) = repo.changelog.read(c.node()) + rev_branch = extra['branch'] + + author = "%s %d %s" % (fixup_user(user), time, gittz(tz)) + if 'committer' in extra: + user, time, tz = extra['committer'].rsplit(' ', 2) + committer = "%s %s %s" % (user, time, gittz(int(tz))) + else: + committer = author + + parents = [p for p in repo.changelog.parentrevs(rev) if p >= 0] + + if len(parents) == 0: + modified = c.manifest().keys() + removed = [] + else: + modified, removed = get_filechanges(repo, c, parents[0]) + + if len(parents) == 0 and rev: + print 'reset %s/%s' % (prefix, ename) + + print "commit %s/%s" % (prefix, ename) + print "mark :%d" % (marks.get_mark(rev)) + print "author %s" % (author) + print "committer %s" % (committer) + print "data %d" % (len(desc)) + print desc + + if len(parents) > 0: + print "from :%s" % (rev_to_mark(parents[0])) + if len(parents) > 1: + print "merge :%s" % (rev_to_mark(parents[1])) + + for f in modified: + export_file(c.filectx(f)) + for f in removed: + print "D %s" % (f) + print + + count += 1 + if (count % 100 == 0): + print "progress revision %d '%s' (%d/%d)" % (rev, name, count, len(revs)) + print "#############################################################" + + # make sure the ref is updated + print "reset %s/%s" % (prefix, ename) + print "from :%u" % rev_to_mark(rev) + print + + marks.set_tip(ename, rev) + +def export_tag(repo, tag): + export_ref(repo, tag, 'tags', repo[tag]) + +def export_bookmark(repo, bmark): + head = bmarks[bmark] + export_ref(repo, bmark, 'bookmarks', head) + +def export_branch(repo, branch): + tip = get_branch_tip(repo, branch) + head = repo[tip] + export_ref(repo, branch, 'branches', head) + +def export_head(repo): + global g_head + export_ref(repo, g_head[0], 'bookmarks', g_head[1]) + +def do_capabilities(parser): + global prefix, dirname + + print "import" + print "refspec refs/heads/branches/*:%s/branches/*" % prefix + print "refspec refs/heads/*:%s/bookmarks/*" % prefix + print "refspec refs/tags/*:%s/tags/*" % prefix + print + +def get_branch_tip(repo, branch): + global branches + + heads = branches.get(branch, None) + if not heads: + return None + + # verify there's only one head + if (len(heads) > 1): + warn("Branch '%s' has more than one head, consider merging" % branch) + # older versions of mercurial don't have this + if hasattr(repo, "branchtip"): + return repo.branchtip(branch) + + return heads[0] + +def list_head(repo, cur): + global g_head + + head = bookmarks.readcurrent(repo) + if not head: + return + node = repo[head] + print "@refs/heads/%s HEAD" % head + g_head = (head, node) + +def do_list(parser): + global branches, bmarks + + repo = parser.repo + for branch in repo.branchmap(): + heads = repo.branchheads(branch) + if len(heads): + branches[branch] = heads + + for bmark, node in bookmarks.listbookmarks(repo).iteritems(): + bmarks[bmark] = repo[node] + + cur = repo.dirstate.branch() + + list_head(repo, cur) + for branch in branches: + print "? refs/heads/branches/%s" % branch + for bmark in bmarks: + print "? refs/heads/%s" % bmark + + for tag, node in repo.tagslist(): + if tag == 'tip': + continue + print "? refs/tags/%s" % tag + + print + +def do_import(parser): + repo = parser.repo + + path = os.path.join(dirname, 'marks-git') + + print "feature done" + if os.path.exists(path): + print "feature import-marks=%s" % path + print "feature export-marks=%s" % path + sys.stdout.flush() + + # lets get all the import lines + while parser.check('import'): + ref = parser[1] + + if (ref == 'HEAD'): + export_head(repo) + elif ref.startswith('refs/heads/branches/'): + branch = ref[len('refs/heads/branches/'):] + export_branch(repo, branch) + elif ref.startswith('refs/heads/'): + bmark = ref[len('refs/heads/'):] + export_bookmark(repo, bmark) + elif ref.startswith('refs/tags/'): + tag = ref[len('refs/tags/'):] + export_tag(repo, tag) + + parser.next() + + print 'done' + +def main(args): + global prefix, dirname, marks, branches, bmarks + + alias = args[1] + url = args[2] + + gitdir = os.environ['GIT_DIR'] + dirname = os.path.join(gitdir, 'hg', alias) + branches = {} + bmarks = {} + + repo = get_repo(url, alias) + prefix = 'refs/hg/%s' % alias + + if not os.path.exists(dirname): + os.makedirs(dirname) + + marks_path = os.path.join(dirname, 'marks-hg') + marks = Marks(marks_path) + + parser = Parser(repo) + for line in parser: + if parser.check('capabilities'): + do_capabilities(parser) + elif parser.check('list'): + do_list(parser) + elif parser.check('import'): + do_import(parser) + elif parser.check('export'): + do_export(parser) + else: + die('unhandled command: %s' % line) + sys.stdout.flush() + + marks.store() + +sys.exit(main(sys.argv)) From 23b4a11fa47161a5e5f221c58f47400207403bc4 Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Sun, 4 Nov 2012 03:13:24 +0100 Subject: [PATCH 02/22] remote-hg: add support for pushing Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/git-remote-hg | 217 ++++++++++++++++++++++++++- 1 file changed, 215 insertions(+), 2 deletions(-) diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg index e37e278c26..fcceede1bb 100755 --- a/contrib/remote-helpers/git-remote-hg +++ b/contrib/remote-helpers/git-remote-hg @@ -9,7 +9,7 @@ # Then you can clone with: # git clone hg::/path/to/mercurial/repo/ -from mercurial import hg, ui, bookmarks +from mercurial import hg, ui, bookmarks, context import re import sys @@ -18,6 +18,7 @@ import json NAME_RE = re.compile('^([^<>]+)') AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]+)>$') +RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.+)> (\d+) ([+-]\d+)') def die(msg, *args): sys.stderr.write('ERROR: %s\n' % (msg % args)) @@ -32,12 +33,17 @@ def gitmode(flags): def gittz(tz): return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60) +def hgmode(mode): + m = { '0100755': 'x', '0120000': 'l' } + return m.get(mode, '') + class Marks: def __init__(self, path): self.path = path self.tips = {} self.marks = {} + self.rev_marks = {} self.last_mark = 0 self.load() @@ -52,6 +58,9 @@ class Marks: self.marks = tmp['marks'] self.last_mark = tmp['last-mark'] + for rev, mark in self.marks.iteritems(): + self.rev_marks[mark] = int(rev) + def dict(self): return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark } @@ -64,11 +73,19 @@ class Marks: def from_rev(self, rev): return self.marks[str(rev)] + def to_rev(self, mark): + return self.rev_marks[mark] + def get_mark(self, rev): self.last_mark += 1 self.marks[str(rev)] = self.last_mark return self.last_mark + def new_mark(self, rev, mark): + self.marks[str(rev)] = mark + self.rev_marks[mark] = rev + self.last_mark = mark + def is_marked(self, rev): return self.marks.has_key(str(rev)) @@ -106,6 +123,35 @@ class Parser: if self.line == 'done': self.line = None + def get_mark(self): + i = self.line.index(':') + 1 + return int(self.line[i:]) + + def get_data(self): + if not self.check('data'): + return None + i = self.line.index(' ') + 1 + size = int(self.line[i:]) + return sys.stdin.read(size) + + def get_author(self): + m = RAW_AUTHOR_RE.match(self.line) + if not m: + return None + _, name, email, date, tz = m.groups() + + if email != 'unknown': + if name: + user = '%s <%s>' % (name, email) + else: + user = '<%s>' % (email) + else: + user = name + + tz = int(tz) + tz = ((tz / 100) * 3600) + ((tz % 100) * 60) + return (user, int(date), -tz) + def export_file(fc): d = fc.data() print "M %s inline %s" % (gitmode(fc.flags()), fc.path()) @@ -174,6 +220,10 @@ def rev_to_mark(rev): global marks return marks.from_rev(rev) +def mark_to_rev(mark): + global marks + return marks.to_rev(mark) + def export_ref(repo, name, kind, head): global prefix, marks @@ -263,9 +313,17 @@ def do_capabilities(parser): global prefix, dirname print "import" + print "export" print "refspec refs/heads/branches/*:%s/branches/*" % prefix print "refspec refs/heads/*:%s/bookmarks/*" % prefix print "refspec refs/tags/*:%s/tags/*" % prefix + + path = os.path.join(dirname, 'marks-git') + + if os.path.exists(path): + print "*import-marks %s" % path + print "*export-marks %s" % path + print def get_branch_tip(repo, branch): @@ -352,8 +410,161 @@ def do_import(parser): print 'done' +def parse_blob(parser): + global blob_marks + + parser.next() + mark = parser.get_mark() + parser.next() + data = parser.get_data() + blob_marks[mark] = data + parser.next() + return + +def parse_commit(parser): + global marks, blob_marks, bmarks, parsed_refs + + from_mark = merge_mark = None + + ref = parser[1] + parser.next() + + commit_mark = parser.get_mark() + parser.next() + author = parser.get_author() + parser.next() + committer = parser.get_author() + parser.next() + data = parser.get_data() + parser.next() + if parser.check('from'): + from_mark = parser.get_mark() + parser.next() + if parser.check('merge'): + merge_mark = parser.get_mark() + parser.next() + if parser.check('merge'): + die('octopus merges are not supported yet') + + files = {} + + for line in parser: + if parser.check('M'): + t, m, mark_ref, path = line.split(' ') + mark = int(mark_ref[1:]) + f = { 'mode' : hgmode(m), 'data' : blob_marks[mark] } + elif parser.check('D'): + t, path = line.split(' ') + f = { 'deleted' : True } + else: + die('Unknown file command: %s' % line) + files[path] = f + + def getfilectx(repo, memctx, f): + of = files[f] + if 'deleted' in of: + raise IOError + is_exec = of['mode'] == 'x' + is_link = of['mode'] == 'l' + return context.memfilectx(f, of['data'], is_link, is_exec, None) + + repo = parser.repo + + user, date, tz = author + extra = {} + + if committer != author: + extra['committer'] = "%s %u %u" % committer + + if from_mark: + p1 = repo.changelog.node(mark_to_rev(from_mark)) + else: + p1 = '\0' * 20 + + if merge_mark: + p2 = repo.changelog.node(mark_to_rev(merge_mark)) + else: + p2 = '\0' * 20 + + ctx = context.memctx(repo, (p1, p2), data, + files.keys(), getfilectx, + user, (date, tz), extra) + + node = repo.commitctx(ctx) + + rev = repo[node].rev() + + parsed_refs[ref] = node + + marks.new_mark(rev, commit_mark) + +def parse_reset(parser): + ref = parser[1] + parser.next() + # ugh + if parser.check('commit'): + parse_commit(parser) + return + if not parser.check('from'): + return + from_mark = parser.get_mark() + parser.next() + + node = parser.repo.changelog.node(mark_to_rev(from_mark)) + parsed_refs[ref] = node + +def parse_tag(parser): + name = parser[1] + parser.next() + from_mark = parser.get_mark() + parser.next() + tagger = parser.get_author() + parser.next() + data = parser.get_data() + parser.next() + + # nothing to do + +def do_export(parser): + global parsed_refs, bmarks + + parser.next() + + for line in parser.each_block('done'): + if parser.check('blob'): + parse_blob(parser) + elif parser.check('commit'): + parse_commit(parser) + elif parser.check('reset'): + parse_reset(parser) + elif parser.check('tag'): + parse_tag(parser) + elif parser.check('feature'): + pass + else: + die('unhandled export command: %s' % line) + + for ref, node in parsed_refs.iteritems(): + if ref.startswith('refs/heads/branches'): + pass + elif ref.startswith('refs/heads/'): + bmark = ref[len('refs/heads/'):] + if bmark in bmarks: + old = bmarks[bmark].hex() + else: + old = '' + if not bookmarks.pushbookmark(parser.repo, bmark, old, node): + continue + elif ref.startswith('refs/tags/'): + tag = ref[len('refs/tags/'):] + parser.repo.tag([tag], node, None, True, None, {}) + print "ok %s" % ref + + print + def main(args): - global prefix, dirname, marks, branches, bmarks + global prefix, dirname, branches, bmarks + global marks, blob_marks, parsed_refs alias = args[1] url = args[2] @@ -362,6 +573,8 @@ def main(args): dirname = os.path.join(gitdir, 'hg', alias) branches = {} bmarks = {} + blob_marks = {} + parsed_refs = {} repo = get_repo(url, alias) prefix = 'refs/hg/%s' % alias From b4e956f7ef8c8d8e30577277b0ebd0840327d42f Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Sun, 4 Nov 2012 03:13:25 +0100 Subject: [PATCH 03/22] remote-hg: add support for remote pushing Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/git-remote-hg | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg index fcceede1bb..45629e022f 100755 --- a/contrib/remote-helpers/git-remote-hg +++ b/contrib/remote-helpers/git-remote-hg @@ -197,7 +197,7 @@ def fixup_user(user): return '%s <%s>' % (name, mail) def get_repo(url, alias): - global dirname + global dirname, peer myui = ui.ui() myui.setconfig('ui', 'interactive', 'off') @@ -526,7 +526,7 @@ def parse_tag(parser): # nothing to do def do_export(parser): - global parsed_refs, bmarks + global parsed_refs, bmarks, peer parser.next() @@ -562,12 +562,17 @@ def do_export(parser): print + if peer: + parser.repo.push(peer, force=False) + def main(args): global prefix, dirname, branches, bmarks global marks, blob_marks, parsed_refs + global peer alias = args[1] url = args[2] + peer = None gitdir = os.environ['GIT_DIR'] dirname = os.path.join(gitdir, 'hg', alias) From ffaf84c6631e1ee381ca0d04b363ee5c82fe20ac Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Sun, 4 Nov 2012 03:13:26 +0100 Subject: [PATCH 04/22] remote-hg: add support to push URLs Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/git-remote-hg | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg index 45629e022f..a5023c92fa 100755 --- a/contrib/remote-helpers/git-remote-hg +++ b/contrib/remote-helpers/git-remote-hg @@ -9,12 +9,13 @@ # Then you can clone with: # git clone hg::/path/to/mercurial/repo/ -from mercurial import hg, ui, bookmarks, context +from mercurial import hg, ui, bookmarks, context, util import re import sys import os import json +import shutil NAME_RE = re.compile('^([^<>]+)') AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]+)>$') @@ -574,6 +575,12 @@ def main(args): url = args[2] peer = None + if alias[4:] == url: + is_tmp = True + alias = util.sha1(alias).hexdigest() + else: + is_tmp = False + gitdir = os.environ['GIT_DIR'] dirname = os.path.join(gitdir, 'hg', alias) branches = {} @@ -604,6 +611,9 @@ def main(args): die('unhandled command: %s' % line) sys.stdout.flush() - marks.store() + if not is_tmp: + marks.store() + else: + shutil.rmtree(dirname) sys.exit(main(sys.argv)) From ff247d9e56366a3b03d8ca926fd88b51d3c2df52 Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Sun, 4 Nov 2012 03:13:27 +0100 Subject: [PATCH 05/22] remote-hg: make sure the encoding is correct Independently of the environment. Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/git-remote-hg | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg index a5023c92fa..503a9fc324 100755 --- a/contrib/remote-helpers/git-remote-hg +++ b/contrib/remote-helpers/git-remote-hg @@ -9,7 +9,7 @@ # Then you can clone with: # git clone hg::/path/to/mercurial/repo/ -from mercurial import hg, ui, bookmarks, context, util +from mercurial import hg, ui, bookmarks, context, util, encoding import re import sys @@ -391,6 +391,9 @@ def do_import(parser): print "feature export-marks=%s" % path sys.stdout.flush() + tmp = encoding.encoding + encoding.encoding = 'utf-8' + # lets get all the import lines while parser.check('import'): ref = parser[1] @@ -409,6 +412,8 @@ def do_import(parser): parser.next() + encoding.encoding = tmp + print 'done' def parse_blob(parser): @@ -491,8 +496,13 @@ def parse_commit(parser): files.keys(), getfilectx, user, (date, tz), extra) + tmp = encoding.encoding + encoding.encoding = 'utf-8' + node = repo.commitctx(ctx) + encoding.encoding = tmp + rev = repo[node].rev() parsed_refs[ref] = node From 6497a2bab5880dc353b83bb5d4615074d6552959 Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Sun, 4 Nov 2012 03:13:28 +0100 Subject: [PATCH 06/22] remote-hg: match hg merge behavior Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/git-remote-hg | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg index 503a9fc324..247b7cbfc9 100755 --- a/contrib/remote-helpers/git-remote-hg +++ b/contrib/remote-helpers/git-remote-hg @@ -427,6 +427,14 @@ def parse_blob(parser): parser.next() return +def get_merge_files(repo, p1, p2, files): + for e in repo[p1].files(): + if e not in files: + if e not in repo[p1].manifest(): + continue + f = { 'ctx' : repo[p1][e] } + files[e] = f + def parse_commit(parser): global marks, blob_marks, bmarks, parsed_refs @@ -470,6 +478,8 @@ def parse_commit(parser): of = files[f] if 'deleted' in of: raise IOError + if 'ctx' in of: + return of['ctx'] is_exec = of['mode'] == 'x' is_link = of['mode'] == 'l' return context.memfilectx(f, of['data'], is_link, is_exec, None) @@ -492,6 +502,13 @@ def parse_commit(parser): else: p2 = '\0' * 20 + # + # If files changed from any of the parents, hg wants to know, but in git if + # nothing changed from the first parent, nothing changed. + # + if merge_mark: + get_merge_files(repo, p1, p2, files) + ctx = context.memctx(repo, (p1, p2), data, files.keys(), getfilectx, user, (date, tz), extra) From 422ab5beb23d461634d202d2446d943513cf329b Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Sun, 4 Nov 2012 03:13:29 +0100 Subject: [PATCH 07/22] remote-hg: add support for hg-git compat mode Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/git-remote-hg | 89 ++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 6 deletions(-) diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg index 247b7cbfc9..d5857560ed 100755 --- a/contrib/remote-helpers/git-remote-hg +++ b/contrib/remote-helpers/git-remote-hg @@ -16,6 +16,22 @@ import sys import os import json import shutil +import subprocess + +# +# If you want to switch to hg-git compatibility mode: +# git config --global remote-hg.hg-git-compat true +# +# git: +# Sensible defaults for git. +# hg bookmarks are exported as git branches, hg branches are prefixed +# with 'branches/'. +# +# hg: +# Emulate hg-git. +# Only hg bookmarks are exported as git branches. +# Commits are modified to preserve hg information and allow biridectionality. +# NAME_RE = re.compile('^([^<>]+)') AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]+)>$') @@ -226,7 +242,7 @@ def mark_to_rev(mark): return marks.to_rev(mark) def export_ref(repo, name, kind, head): - global prefix, marks + global prefix, marks, mode ename = '%s/%s' % (kind, name) tip = marks.get_tip(ename) @@ -261,6 +277,33 @@ def export_ref(repo, name, kind, head): else: modified, removed = get_filechanges(repo, c, parents[0]) + if mode == 'hg': + extra_msg = '' + + if rev_branch != 'default': + extra_msg += 'branch : %s\n' % rev_branch + + renames = [] + for f in c.files(): + if f not in c.manifest(): + continue + rename = c.filectx(f).renamed() + if rename: + renames.append((rename[0], f)) + + for e in renames: + extra_msg += "rename : %s => %s\n" % e + + for key, value in extra.iteritems(): + if key in ('author', 'committer', 'encoding', 'message', 'branch', 'hg-git'): + continue + else: + extra_msg += "extra : %s : %s\n" % (key, urllib.quote(value)) + + desc += '\n' + if extra_msg: + desc += '\n--HG--\n' + extra_msg + if len(parents) == 0 and rev: print 'reset %s/%s' % (prefix, ename) @@ -354,7 +397,7 @@ def list_head(repo, cur): g_head = (head, node) def do_list(parser): - global branches, bmarks + global branches, bmarks, mode repo = parser.repo for branch in repo.branchmap(): @@ -368,8 +411,11 @@ def do_list(parser): cur = repo.dirstate.branch() list_head(repo, cur) - for branch in branches: - print "? refs/heads/branches/%s" % branch + + if mode != 'hg': + for branch in branches: + print "? refs/heads/branches/%s" % branch + for bmark in bmarks: print "? refs/heads/%s" % bmark @@ -437,6 +483,7 @@ def get_merge_files(repo, p1, p2, files): def parse_commit(parser): global marks, blob_marks, bmarks, parsed_refs + global mode from_mark = merge_mark = None @@ -482,7 +529,9 @@ def parse_commit(parser): return of['ctx'] is_exec = of['mode'] == 'x' is_link = of['mode'] == 'l' - return context.memfilectx(f, of['data'], is_link, is_exec, None) + rename = of.get('rename', None) + return context.memfilectx(f, of['data'], + is_link, is_exec, rename) repo = parser.repo @@ -509,6 +558,21 @@ def parse_commit(parser): if merge_mark: get_merge_files(repo, p1, p2, files) + if mode == 'hg': + i = data.find('\n--HG--\n') + if i >= 0: + tmp = data[i + len('\n--HG--\n'):].strip() + for k, v in [e.split(' : ') for e in tmp.split('\n')]: + if k == 'rename': + old, new = v.split(' => ', 1) + files[new]['rename'] = old + elif k == 'branch': + extra[k] = v + elif k == 'extra': + ek, ev = v.split(' : ', 1) + extra[ek] = urllib.unquote(ev) + data = data[:i] + ctx = context.memctx(repo, (p1, p2), data, files.keys(), getfilectx, user, (date, tz), extra) @@ -596,12 +660,25 @@ def do_export(parser): def main(args): global prefix, dirname, branches, bmarks global marks, blob_marks, parsed_refs - global peer + global peer, mode alias = args[1] url = args[2] peer = None + cmd = ['git', 'config', '--get', 'remote-hg.hg-git-compat'] + hg_git_compat = False + try: + if subprocess.check_output(cmd) == 'true\n': + hg_git_compat = True + except subprocess.CalledProcessError: + pass + + if hg_git_compat: + mode = 'hg' + else: + mode = 'git' + if alias[4:] == url: is_tmp = True alias = util.sha1(alias).hexdigest() From 9490bd04fef0e0f04efa4679668f0a05328768e4 Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Sun, 4 Nov 2012 03:13:30 +0100 Subject: [PATCH 08/22] remote-hg: add compat for hg-git author fixes Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/git-remote-hg | 59 +++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg index d5857560ed..9db4b7e59c 100755 --- a/contrib/remote-helpers/git-remote-hg +++ b/contrib/remote-helpers/git-remote-hg @@ -17,6 +17,7 @@ import os import json import shutil import subprocess +import urllib # # If you want to switch to hg-git compatibility mode: @@ -35,6 +36,7 @@ import subprocess NAME_RE = re.compile('^([^<>]+)') AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]+)>$') +AUTHOR_HG_RE = re.compile('^(.*?) ?<(.+?)(?:>(.+)?)?$') RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.+)> (\d+) ([+-]\d+)') def die(msg, *args): @@ -152,12 +154,20 @@ class Parser: return sys.stdin.read(size) def get_author(self): + global bad_mail + + ex = None m = RAW_AUTHOR_RE.match(self.line) if not m: return None _, name, email, date, tz = m.groups() + if name and 'ext:' in name: + m = re.match('^(.+?) ext:\((.+)\)$', name) + if m: + name = m.group(1) + ex = urllib.unquote(m.group(2)) - if email != 'unknown': + if email != bad_mail: if name: user = '%s <%s>' % (name, email) else: @@ -165,6 +175,9 @@ class Parser: else: user = name + if ex: + user += ex + tz = int(tz) tz = ((tz / 100) * 3600) + ((tz % 100) * 60) return (user, int(date), -tz) @@ -194,9 +207,9 @@ def get_filechanges(repo, ctx, parent): return added | modified, removed -def fixup_user(user): - user = user.replace('"', '') +def fixup_user_git(user): name = mail = None + user = user.replace('"', '') m = AUTHOR_RE.match(user) if m: name = m.group(1) @@ -205,11 +218,41 @@ def fixup_user(user): m = NAME_RE.match(user) if m: name = m.group(1).strip() + return (name, mail) + +def fixup_user_hg(user): + def sanitize(name): + # stole this from hg-git + return re.sub('[<>\n]', '?', name.lstrip('< ').rstrip('> ')) + + m = AUTHOR_HG_RE.match(user) + if m: + name = sanitize(m.group(1)) + mail = sanitize(m.group(2)) + ex = m.group(3) + if ex: + name += ' ext:(' + urllib.quote(ex) + ')' + else: + name = sanitize(user) + if '@' in user: + mail = name + else: + mail = None + + return (name, mail) + +def fixup_user(user): + global mode, bad_mail + + if mode == 'git': + name, mail = fixup_user_git(user) + else: + name, mail = fixup_user_hg(user) if not name: - name = 'Unknown' + name = bad_name if not mail: - mail = 'unknown' + mail = bad_mail return '%s <%s>' % (name, mail) @@ -660,7 +703,7 @@ def do_export(parser): def main(args): global prefix, dirname, branches, bmarks global marks, blob_marks, parsed_refs - global peer, mode + global peer, mode, bad_mail, bad_name alias = args[1] url = args[2] @@ -676,8 +719,12 @@ def main(args): if hg_git_compat: mode = 'hg' + bad_mail = 'none@none' + bad_name = '' else: mode = 'git' + bad_mail = 'unknown' + bad_name = 'Unknown' if alias[4:] == url: is_tmp = True From 46cc3adb60f45273dcb0f9179d20bffe1f77f4ff Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Sun, 4 Nov 2012 03:13:31 +0100 Subject: [PATCH 09/22] remote-hg: fake bookmark when there's none Or at least no current bookmark. Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/git-remote-hg | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg index 9db4b7e59c..dbe309acfe 100755 --- a/contrib/remote-helpers/git-remote-hg +++ b/contrib/remote-helpers/git-remote-hg @@ -26,7 +26,7 @@ import urllib # git: # Sensible defaults for git. # hg bookmarks are exported as git branches, hg branches are prefixed -# with 'branches/'. +# with 'branches/', HEAD is a special case. # # hg: # Emulate hg-git. @@ -430,12 +430,21 @@ def get_branch_tip(repo, branch): return heads[0] def list_head(repo, cur): - global g_head + global g_head, bmarks head = bookmarks.readcurrent(repo) - if not head: - return - node = repo[head] + if head: + node = repo[head] + else: + # fake bookmark from current branch + head = cur + node = repo['.'] + if not node: + return + if head == 'default': + head = 'master' + bmarks[head] = node + print "@refs/heads/%s HEAD" % head g_head = (head, node) From 7ee719e18074a47e41dd315fcf8ced777a71adbd Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Sun, 4 Nov 2012 03:13:32 +0100 Subject: [PATCH 10/22] remote-hg: add basic tests Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/Makefile | 13 ++++ contrib/remote-helpers/test-hg.sh | 112 ++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 contrib/remote-helpers/Makefile create mode 100755 contrib/remote-helpers/test-hg.sh diff --git a/contrib/remote-helpers/Makefile b/contrib/remote-helpers/Makefile new file mode 100644 index 0000000000..9a76575f78 --- /dev/null +++ b/contrib/remote-helpers/Makefile @@ -0,0 +1,13 @@ +TESTS := $(wildcard test*.sh) + +export T := $(addprefix $(CURDIR)/,$(TESTS)) +export MAKE := $(MAKE) -e +export PATH := $(CURDIR):$(PATH) + +test: + $(MAKE) -C ../../t $@ + +$(TESTS): + $(MAKE) -C ../../t $(CURDIR)/$@ + +.PHONY: $(TESTS) diff --git a/contrib/remote-helpers/test-hg.sh b/contrib/remote-helpers/test-hg.sh new file mode 100755 index 0000000000..40e6e3c063 --- /dev/null +++ b/contrib/remote-helpers/test-hg.sh @@ -0,0 +1,112 @@ +#!/bin/sh +# +# Copyright (c) 2012 Felipe Contreras +# +# Base commands from hg-git tests: +# https://bitbucket.org/durin42/hg-git/src +# + +test_description='Test remote-hg' + +. ./test-lib.sh + +if ! test_have_prereq PYTHON; then + skip_all='skipping remote-hg tests; python not available' + test_done +fi + +if ! "$PYTHON_PATH" -c 'import mercurial'; then + skip_all='skipping remote-hg tests; mercurial not available' + test_done +fi + +check () { + (cd $1 && + git log --format='%s' -1 && + git symbolic-ref HEAD) > actual && + (echo $2 && + echo "refs/heads/$3") > expected && + test_cmp expected actual +} + +test_expect_success 'cloning' ' + test_when_finished "rm -rf gitrepo*" && + + ( + hg init hgrepo && + cd hgrepo && + echo zero > content && + hg add content && + hg commit -m zero + ) && + + git clone "hg::$PWD/hgrepo" gitrepo && + check gitrepo zero master +' + +test_expect_success 'cloning with branches' ' + test_when_finished "rm -rf gitrepo*" && + + ( + cd hgrepo && + hg branch next && + echo next > content && + hg commit -m next + ) && + + git clone "hg::$PWD/hgrepo" gitrepo && + check gitrepo next next && + + (cd hgrepo && hg checkout default) && + + git clone "hg::$PWD/hgrepo" gitrepo2 && + check gitrepo2 zero master +' + +test_expect_success 'cloning with bookmarks' ' + test_when_finished "rm -rf gitrepo*" && + + ( + cd hgrepo && + hg bookmark feature-a && + echo feature-a > content && + hg commit -m feature-a + ) && + + git clone "hg::$PWD/hgrepo" gitrepo && + check gitrepo feature-a feature-a +' + +test_expect_success 'cloning with detached head' ' + test_when_finished "rm -rf gitrepo*" && + + ( + cd hgrepo && + hg update -r 0 + ) && + + git clone "hg::$PWD/hgrepo" gitrepo && + check gitrepo zero master +' + +test_expect_success 'update bookmark' ' + test_when_finished "rm -rf gitrepo*" && + + ( + cd hgrepo && + hg bookmark devel + ) && + + ( + git clone "hg::$PWD/hgrepo" gitrepo && + cd gitrepo && + git checkout devel && + echo devel > content && + git commit -a -m devel && + git push + ) && + + hg -R hgrepo bookmarks | grep "devel\s\+3:" +' + +test_done From dd78478fe115072df2cd0756eb1c59dd70d554da Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Sun, 4 Nov 2012 03:13:33 +0100 Subject: [PATCH 11/22] test-lib: avoid full path to store test results No reason to use the full path in case this is used externally. Otherwise we might get errors such as: ./test-lib.sh: line 394: /home/bob/dev/git/t/test-results//home/bob/dev/git/contrib/remote-hg/test-2894.counts: No such file or directory Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- t/test-lib.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/t/test-lib.sh b/t/test-lib.sh index 489bc80fc1..0f45481809 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -389,7 +389,8 @@ test_done () { then test_results_dir="$TEST_OUTPUT_DIRECTORY/test-results" mkdir -p "$test_results_dir" - test_results_path="$test_results_dir/${0%.sh}-$$.counts" + base=${0##*/} + test_results_path="$test_results_dir/${base%.sh}-$$.counts" cat >>"$test_results_path" <<-EOF total $test_count From 74954ee8aa0530a4c1ccaeaa5f8def71ef6b68bd Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Sun, 4 Nov 2012 03:13:34 +0100 Subject: [PATCH 12/22] remote-hg: add bidirectional tests Base commands from hg-git tests: https://bitbucket.org/durin42/hg-git/src Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/test-hg-bidi.sh | 243 +++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100755 contrib/remote-helpers/test-hg-bidi.sh diff --git a/contrib/remote-helpers/test-hg-bidi.sh b/contrib/remote-helpers/test-hg-bidi.sh new file mode 100755 index 0000000000..a94eb28092 --- /dev/null +++ b/contrib/remote-helpers/test-hg-bidi.sh @@ -0,0 +1,243 @@ +#!/bin/sh +# +# Copyright (c) 2012 Felipe Contreras +# +# Base commands from hg-git tests: +# https://bitbucket.org/durin42/hg-git/src +# + +test_description='Test biridectionality of remote-hg' + +. ./test-lib.sh + +if ! test_have_prereq PYTHON; then + skip_all='skipping remote-hg tests; python not available' + test_done +fi + +if ! "$PYTHON_PATH" -c 'import mercurial'; then + skip_all='skipping remote-hg tests; mercurial not available' + test_done +fi + +# clone to a git repo +git_clone () { + hg -R $1 bookmark -f -r tip master && + git clone -q "hg::$PWD/$1" $2 +} + +# clone to an hg repo +hg_clone () { + ( + hg init $2 && + cd $1 && + git push -q "hg::$PWD/../$2" 'refs/tags/*:refs/tags/*' 'refs/heads/*:refs/heads/*' + ) && + + (cd $2 && hg -q update) +} + +# push an hg repo +hg_push () { + ( + cd $2 + old=$(git symbolic-ref --short HEAD) + git checkout -q -b tmp && + git fetch -q "hg::$PWD/../$1" 'refs/tags/*:refs/tags/*' 'refs/heads/*:refs/heads/*' && + git checkout -q $old && + git branch -q -D tmp 2> /dev/null || true + ) +} + +hg_log () { + hg -R $1 log --graph --debug | grep -v 'tag: *default/' +} + +setup () { + ( + echo "[ui]" + echo "username = A U Thor " + echo "[defaults]" + echo "backout = -d \"0 0\"" + echo "commit = -d \"0 0\"" + echo "debugrawcommit = -d \"0 0\"" + echo "tag = -d \"0 0\"" + ) >> "$HOME"/.hgrc && + git config --global remote-hg.hg-git-compat true + + export HGEDITOR=/usr/bin/true + + export GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0230" + export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" +} + +setup + +test_expect_success 'encoding' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + + echo alpha > alpha && + git add alpha && + git commit -m "add älphà" && + + export GIT_AUTHOR_NAME="tést èncödîng" && + echo beta > beta && + git add beta && + git commit -m "add beta" && + + echo gamma > gamma && + git add gamma && + git commit -m "add gämmâ" && + + : TODO git config i18n.commitencoding latin-1 && + echo delta > delta && + git add delta && + git commit -m "add déltà" + ) && + + hg_clone gitrepo hgrepo && + git_clone hgrepo gitrepo2 && + hg_clone gitrepo2 hgrepo2 && + + HGENCODING=utf-8 hg_log hgrepo > expected && + HGENCODING=utf-8 hg_log hgrepo2 > actual && + + test_cmp expected actual +' + +test_expect_success 'file removal' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + echo alpha > alpha && + git add alpha && + git commit -m "add alpha" && + echo beta > beta && + git add beta && + git commit -m "add beta" + mkdir foo && + echo blah > foo/bar && + git add foo && + git commit -m "add foo" && + git rm alpha && + git commit -m "remove alpha" && + git rm foo/bar && + git commit -m "remove foo/bar" + ) && + + hg_clone gitrepo hgrepo && + git_clone hgrepo gitrepo2 && + hg_clone gitrepo2 hgrepo2 && + + hg_log hgrepo > expected && + hg_log hgrepo2 > actual && + + test_cmp expected actual +' + +test_expect_success 'git tags' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + git config receive.denyCurrentBranch ignore && + echo alpha > alpha && + git add alpha && + git commit -m "add alpha" && + git tag alpha && + + echo beta > beta && + git add beta && + git commit -m "add beta" && + git tag -a -m "added tag beta" beta + ) && + + hg_clone gitrepo hgrepo && + git_clone hgrepo gitrepo2 && + hg_clone gitrepo2 hgrepo2 && + + hg_log hgrepo > expected && + hg_log hgrepo2 > actual && + + test_cmp expected actual +' + +test_expect_success 'hg branch' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + + echo alpha > alpha && + git add alpha && + git commit -q -m "add alpha" && + git checkout -q -b not-master + ) && + + ( + hg_clone gitrepo hgrepo && + + cd hgrepo && + hg -q co master && + hg mv alpha beta && + hg -q commit -m "rename alpha to beta" && + hg branch gamma | grep -v "permanent and global" && + hg -q commit -m "started branch gamma" + ) && + + hg_push hgrepo gitrepo && + hg_clone gitrepo hgrepo2 && + + : TODO, avoid "master" bookmark && + (cd hgrepo2 && hg checkout gamma) && + + hg_log hgrepo > expected && + hg_log hgrepo2 > actual && + + test_cmp expected actual +' + +test_expect_success 'hg tags' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + + echo alpha > alpha && + git add alpha && + git commit -m "add alpha" && + git checkout -q -b not-master + ) && + + ( + hg_clone gitrepo hgrepo && + + cd hgrepo && + hg co master && + hg tag alpha + ) && + + hg_push hgrepo gitrepo && + hg_clone gitrepo hgrepo2 && + + hg_log hgrepo > expected && + hg_log hgrepo2 > actual && + + test_cmp expected actual +' + +test_done From bb8a956409bb67e46dead1cc32d11871d45d6fdb Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Sun, 4 Nov 2012 03:13:35 +0100 Subject: [PATCH 13/22] remote-hg: add tests to compare with hg-git The base commands come from the tests of the hg-git project. Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/test-hg-hg-git.sh | 462 +++++++++++++++++++++++ 1 file changed, 462 insertions(+) create mode 100755 contrib/remote-helpers/test-hg-hg-git.sh diff --git a/contrib/remote-helpers/test-hg-hg-git.sh b/contrib/remote-helpers/test-hg-hg-git.sh new file mode 100755 index 0000000000..e07bba5581 --- /dev/null +++ b/contrib/remote-helpers/test-hg-hg-git.sh @@ -0,0 +1,462 @@ +#!/bin/sh +# +# Copyright (c) 2012 Felipe Contreras +# +# Base commands from hg-git tests: +# https://bitbucket.org/durin42/hg-git/src +# + +test_description='Test remote-hg output compared to hg-git' + +. ./test-lib.sh + +if ! test_have_prereq PYTHON; then + skip_all='skipping remote-hg tests; python not available' + test_done +fi + +if ! "$PYTHON_PATH" -c 'import mercurial'; then + skip_all='skipping remote-hg tests; mercurial not available' + test_done +fi + +if ! "$PYTHON_PATH" -c 'import hggit'; then + skip_all='skipping remote-hg tests; hg-git not available' + test_done +fi + +# clone to a git repo with git +git_clone_git () { + hg -R $1 bookmark -f -r tip master && + git clone -q "hg::$PWD/$1" $2 +} + +# clone to an hg repo with git +hg_clone_git () { + ( + hg init $2 && + cd $1 && + git push -q "hg::$PWD/../$2" 'refs/tags/*:refs/tags/*' 'refs/heads/*:refs/heads/*' + ) && + + (cd $2 && hg -q update) +} + +# clone to a git repo with hg +git_clone_hg () { + ( + git init -q $2 && + cd $1 && + hg bookmark -f -r tip master && + hg -q push -r master ../$2 || true + ) +} + +# clone to an hg repo with hg +hg_clone_hg () { + hg -q clone $1 $2 +} + +# push an hg repo with git +hg_push_git () { + ( + cd $2 + old=$(git symbolic-ref --short HEAD) + git checkout -q -b tmp && + git fetch -q "hg::$PWD/../$1" 'refs/tags/*:refs/tags/*' 'refs/heads/*:refs/heads/*' && + git checkout -q $old && + git branch -q -D tmp 2> /dev/null || true + ) +} + +# push an hg git repo with hg +hg_push_hg () { + ( + cd $1 && + hg -q push ../$2 || true + ) +} + +hg_log () { + hg -R $1 log --graph --debug | grep -v 'tag: *default/' +} + +git_log () { + git --git-dir=$1/.git fast-export --branches +} + +setup () { + ( + echo "[ui]" + echo "username = A U Thor " + echo "[defaults]" + echo "backout = -d \"0 0\"" + echo "commit = -d \"0 0\"" + echo "debugrawcommit = -d \"0 0\"" + echo "tag = -d \"0 0\"" + echo "[extensions]" + echo "hgext.bookmarks =" + echo "hggit =" + ) >> "$HOME"/.hgrc && + git config --global receive.denycurrentbranch warn + git config --global remote-hg.hg-git-compat true + + export HGEDITOR=/usr/bin/true + + export GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0230" + export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" +} + +setup + +test_expect_success 'merge conflict 1' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + hg init hgrepo1 && + cd hgrepo1 && + echo A > afile && + hg add afile && + hg ci -m "origin" && + + echo B > afile && + hg ci -m "A->B" && + + hg up -r0 && + echo C > afile && + hg ci -m "A->C" && + + hg merge -r1 || true && + echo C > afile && + hg resolve -m afile && + hg ci -m "merge to C" + ) && + + for x in hg git; do + git_clone_$x hgrepo1 gitrepo-$x && + hg_clone_$x gitrepo-$x hgrepo2-$x && + hg_log hgrepo2-$x > hg-log-$x && + git_log gitrepo-$x > git-log-$x + done && + + test_cmp hg-log-hg hg-log-git && + test_cmp git-log-hg git-log-git +' + +test_expect_success 'merge conflict 2' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + hg init hgrepo1 && + cd hgrepo1 && + echo A > afile && + hg add afile && + hg ci -m "origin" && + + echo B > afile && + hg ci -m "A->B" && + + hg up -r0 && + echo C > afile && + hg ci -m "A->C" && + + hg merge -r1 || true && + echo B > afile && + hg resolve -m afile && + hg ci -m "merge to B" + ) && + + for x in hg git; do + git_clone_$x hgrepo1 gitrepo-$x && + hg_clone_$x gitrepo-$x hgrepo2-$x && + hg_log hgrepo2-$x > hg-log-$x && + git_log gitrepo-$x > git-log-$x + done && + + test_cmp hg-log-hg hg-log-git && + test_cmp git-log-hg git-log-git +' + +test_expect_success 'converged merge' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + hg init hgrepo1 && + cd hgrepo1 && + echo A > afile && + hg add afile && + hg ci -m "origin" && + + echo B > afile && + hg ci -m "A->B" && + + echo C > afile && + hg ci -m "B->C" && + + hg up -r0 && + echo C > afile && + hg ci -m "A->C" && + + hg merge -r2 || true && + hg ci -m "merge" + ) && + + for x in hg git; do + git_clone_$x hgrepo1 gitrepo-$x && + hg_clone_$x gitrepo-$x hgrepo2-$x && + hg_log hgrepo2-$x > hg-log-$x && + git_log gitrepo-$x > git-log-$x + done && + + test_cmp hg-log-hg hg-log-git && + test_cmp git-log-hg git-log-git +' + +test_expect_success 'encoding' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + + echo alpha > alpha && + git add alpha && + git commit -m "add älphà" && + + export GIT_AUTHOR_NAME="tést èncödîng" && + echo beta > beta && + git add beta && + git commit -m "add beta" && + + echo gamma > gamma && + git add gamma && + git commit -m "add gämmâ" && + + : TODO git config i18n.commitencoding latin-1 && + echo delta > delta && + git add delta && + git commit -m "add déltà" + ) && + + for x in hg git; do + hg_clone_$x gitrepo hgrepo-$x && + git_clone_$x hgrepo-$x gitrepo2-$x && + + HGENCODING=utf-8 hg_log hgrepo-$x > hg-log-$x && + git_log gitrepo2-$x > git-log-$x + done && + + test_cmp hg-log-hg hg-log-git && + test_cmp git-log-hg git-log-git +' + +test_expect_success 'file removal' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + echo alpha > alpha && + git add alpha && + git commit -m "add alpha" && + echo beta > beta && + git add beta && + git commit -m "add beta" + mkdir foo && + echo blah > foo/bar && + git add foo && + git commit -m "add foo" && + git rm alpha && + git commit -m "remove alpha" && + git rm foo/bar && + git commit -m "remove foo/bar" + ) && + + for x in hg git; do + ( + hg_clone_$x gitrepo hgrepo-$x && + cd hgrepo-$x && + hg_log . && + hg manifest -r 3 && + hg manifest + ) > output-$x && + + git_clone_$x hgrepo-$x gitrepo2-$x && + git_log gitrepo2-$x > log-$x + done && + + test_cmp output-hg output-git && + test_cmp log-hg log-git +' + +test_expect_success 'git tags' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + ( + git init -q gitrepo && + cd gitrepo && + git config receive.denyCurrentBranch ignore && + echo alpha > alpha && + git add alpha && + git commit -m "add alpha" && + git tag alpha && + + echo beta > beta && + git add beta && + git commit -m "add beta" && + git tag -a -m "added tag beta" beta + ) && + + for x in hg git; do + hg_clone_$x gitrepo hgrepo-$x && + hg_log hgrepo-$x > log-$x + done && + + test_cmp log-hg log-git +' + +test_expect_success 'hg author' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + for x in hg git; do + ( + git init -q gitrepo-$x && + cd gitrepo-$x && + + echo alpha > alpha && + git add alpha && + git commit -m "add alpha" && + git checkout -q -b not-master + ) && + + ( + hg_clone_$x gitrepo-$x hgrepo-$x && + cd hgrepo-$x && + + hg co master && + echo beta > beta && + hg add beta && + hg commit -u "test" -m "add beta" && + + echo gamma >> beta && + hg commit -u "test (comment)" -m "modify beta" && + + echo gamma > gamma && + hg add gamma && + hg commit -u "" -m "add gamma" && + + echo delta > delta && + hg add delta && + hg commit -u "name" -m "add delta" && + + echo epsilon > epsilon && + hg add epsilon && + hg commit -u "name zeta && + hg add zeta && + hg commit -u " test " -m "add zeta" && + + echo eta > eta && + hg add eta && + hg commit -u "test < test@example.com >" -m "add eta" && + + echo theta > theta && + hg add theta && + hg commit -u "test >test@example.com>" -m "add theta" + ) && + + hg_push_$x hgrepo-$x gitrepo-$x && + hg_clone_$x gitrepo-$x hgrepo2-$x && + + hg_log hgrepo2-$x > hg-log-$x && + git_log gitrepo-$x > git-log-$x + done && + + test_cmp git-log-hg git-log-git && + + test_cmp hg-log-hg hg-log-git && + test_cmp git-log-hg git-log-git +' + +test_expect_success 'hg branch' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + for x in hg git; do + ( + git init -q gitrepo-$x && + cd gitrepo-$x && + + echo alpha > alpha && + git add alpha && + git commit -q -m "add alpha" && + git checkout -q -b not-master + ) && + + ( + hg_clone_$x gitrepo-$x hgrepo-$x && + + cd hgrepo-$x && + hg -q co master && + hg mv alpha beta && + hg -q commit -m "rename alpha to beta" && + hg branch gamma | grep -v "permanent and global" && + hg -q commit -m "started branch gamma" + ) && + + hg_push_$x hgrepo-$x gitrepo-$x && + hg_clone_$x gitrepo-$x hgrepo2-$x && + + hg_log hgrepo2-$x > hg-log-$x && + git_log gitrepo-$x > git-log-$x + done && + + test_cmp hg-log-hg hg-log-git && + test_cmp git-log-hg git-log-git +' + +test_expect_success 'hg tags' ' + mkdir -p tmp && cd tmp && + test_when_finished "cd .. && rm -rf tmp" && + + for x in hg git; do + ( + git init -q gitrepo-$x && + cd gitrepo-$x && + + echo alpha > alpha && + git add alpha && + git commit -m "add alpha" && + git checkout -q -b not-master + ) && + + ( + hg_clone_$x gitrepo-$x hgrepo-$x && + + cd hgrepo-$x && + hg co master && + hg tag alpha + ) && + + hg_push_$x hgrepo-$x gitrepo-$x && + hg_clone_$x gitrepo-$x hgrepo2-$x && + + ( + git --git-dir=gitrepo-$x/.git tag -l && + hg_log hgrepo2-$x && + cat hgrepo2-$x/.hgtags + ) > output-$x + done && + + test_cmp output-hg output-git +' + +test_done From aefc605ada7fe00e1c6b8d4bf3aa17dc07855769 Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Sun, 4 Nov 2012 03:13:36 +0100 Subject: [PATCH 14/22] remote-hg: add extra author test For hg.hg. Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/test-hg-hg-git.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contrib/remote-helpers/test-hg-hg-git.sh b/contrib/remote-helpers/test-hg-hg-git.sh index e07bba5581..3e76d9fb60 100755 --- a/contrib/remote-helpers/test-hg-hg-git.sh +++ b/contrib/remote-helpers/test-hg-hg-git.sh @@ -370,7 +370,11 @@ test_expect_success 'hg author' ' echo theta > theta && hg add theta && - hg commit -u "test >test@example.com>" -m "add theta" + hg commit -u "test >test@example.com>" -m "add theta" && + + echo iota > iota && + hg add iota && + hg commit -u "test example com>" -m "add iota" ) && hg_push_$x hgrepo-$x gitrepo-$x && From e30473c185442a9ac137f3841fe2d05e0e5ec81a Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Sun, 4 Nov 2012 03:13:37 +0100 Subject: [PATCH 15/22] remote-hg: add option to not track branches Some people prefer it this way. % git config --global remote-hg.track-branches false Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/git-remote-hg | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg index dbe309acfe..a9ae8446fb 100755 --- a/contrib/remote-helpers/git-remote-hg +++ b/contrib/remote-helpers/git-remote-hg @@ -449,14 +449,9 @@ def list_head(repo, cur): g_head = (head, node) def do_list(parser): - global branches, bmarks, mode + global branches, bmarks, mode, track_branches repo = parser.repo - for branch in repo.branchmap(): - heads = repo.branchheads(branch) - if len(heads): - branches[branch] = heads - for bmark, node in bookmarks.listbookmarks(repo).iteritems(): bmarks[bmark] = repo[node] @@ -464,7 +459,12 @@ def do_list(parser): list_head(repo, cur) - if mode != 'hg': + if track_branches: + for branch in repo.branchmap(): + heads = repo.branchheads(branch) + if len(heads): + branches[branch] = heads + for branch in branches: print "? refs/heads/branches/%s" % branch @@ -713,16 +713,22 @@ def main(args): global prefix, dirname, branches, bmarks global marks, blob_marks, parsed_refs global peer, mode, bad_mail, bad_name + global track_branches alias = args[1] url = args[2] peer = None - cmd = ['git', 'config', '--get', 'remote-hg.hg-git-compat'] hg_git_compat = False + track_branches = True try: + cmd = ['git', 'config', '--get', 'remote-hg.hg-git-compat'] if subprocess.check_output(cmd) == 'true\n': hg_git_compat = True + track_branches = False + cmd = ['git', 'config', '--get', 'remote-hg.track-branches'] + if subprocess.check_output(cmd) == 'false\n': + track_branches = False except subprocess.CalledProcessError: pass From 12ba4bd4ece3b53c810ac72124ff9ed818cffcd6 Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Sun, 4 Nov 2012 03:13:38 +0100 Subject: [PATCH 16/22] remote-hg: the author email can be null Like 'Foo <>'. Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/git-remote-hg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg index a9ae8446fb..7929eec30b 100755 --- a/contrib/remote-helpers/git-remote-hg +++ b/contrib/remote-helpers/git-remote-hg @@ -35,9 +35,9 @@ import urllib # NAME_RE = re.compile('^([^<>]+)') -AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]+)>$') -AUTHOR_HG_RE = re.compile('^(.*?) ?<(.+?)(?:>(.+)?)?$') -RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.+)> (\d+) ([+-]\d+)') +AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$') +AUTHOR_HG_RE = re.compile('^(.*?) ?<(.*?)(?:>(.+)?)?$') +RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.*)> (\d+) ([+-]\d+)') def die(msg, *args): sys.stderr.write('ERROR: %s\n' % (msg % args)) From 7241a9ffab73fe8f0f7eaa50149eaa24109350e5 Mon Sep 17 00:00:00 2001 From: Ramkumar Ramachandra Date: Mon, 12 Nov 2012 18:41:05 +0100 Subject: [PATCH 17/22] remote-hg: add missing config for basic tests 'hg commit' fails otherwise in some versions of mercurial because of the missing user information. Other versions simply throw a warning and guess though. Signed-off-by: Ramkumar Ramachandra Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/test-hg.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/contrib/remote-helpers/test-hg.sh b/contrib/remote-helpers/test-hg.sh index 40e6e3c063..5f81dfae6c 100755 --- a/contrib/remote-helpers/test-hg.sh +++ b/contrib/remote-helpers/test-hg.sh @@ -29,6 +29,15 @@ check () { test_cmp expected actual } +setup () { + ( + echo "[ui]" + echo "username = H G Wells " + ) >> "$HOME"/.hgrc +} + +setup + test_expect_success 'cloning' ' test_when_finished "rm -rf gitrepo*" && From cc8433fad1f64ffaa9c1b8274c79f14419c42f78 Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Mon, 12 Nov 2012 18:41:06 +0100 Subject: [PATCH 18/22] remote-hg: fix compatibility with older versions of hg Turns out repo.revs was introduced quite late, and it doesn't do anything fancy for our refspec; only list all the numbers in that range. Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/git-remote-hg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg index 7929eec30b..bf5e5b473d 100755 --- a/contrib/remote-helpers/git-remote-hg +++ b/contrib/remote-helpers/git-remote-hg @@ -294,7 +294,7 @@ def export_ref(repo, name, kind, head): if tip and tip == head.rev(): # nothing to do return - revs = repo.revs('%u:%u' % (tip, head)) + revs = xrange(tip, head.rev() + 1) count = 0 revs = [rev for rev in revs if not marks.is_marked(rev)] From 55dd56e042f96832327e07ee500a60db7fd244b9 Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Mon, 12 Nov 2012 18:41:07 +0100 Subject: [PATCH 19/22] remote-hg: try the 'tip' if no checkout present There's no concept of HEAD in mercurial, but let's try our best to do something sensible. Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/git-remote-hg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg index bf5e5b473d..a9a1e8fa58 100755 --- a/contrib/remote-helpers/git-remote-hg +++ b/contrib/remote-helpers/git-remote-hg @@ -439,6 +439,8 @@ def list_head(repo, cur): # fake bookmark from current branch head = cur node = repo['.'] + if not node: + node = repo['tip'] if not node: return if head == 'default': From 08c2599c3274c1f473ccae2aae67c04d4bbb57e4 Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Mon, 12 Nov 2012 18:41:08 +0100 Subject: [PATCH 20/22] remote-hg: avoid bad refs Turns out fast-export throws bad 'reset' commands because of a behavior in transport-helper that is not even needed. Either way, better to ignore them, otherwise the user will get warnings when we OK them. Signed-off-by: Felipe Contreras Signed-off-by: Jeff King --- contrib/remote-helpers/git-remote-hg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg index a9a1e8fa58..07754bdeb2 100755 --- a/contrib/remote-helpers/git-remote-hg +++ b/contrib/remote-helpers/git-remote-hg @@ -704,6 +704,9 @@ def do_export(parser): elif ref.startswith('refs/tags/'): tag = ref[len('refs/tags/'):] parser.repo.tag([tag], node, None, True, None, {}) + else: + # transport-helper/fast-export bugs + continue print "ok %s" % ref print From 418673c4bc48c0b54856449739023b4f978ea235 Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Wed, 28 Nov 2012 02:01:32 +0100 Subject: [PATCH 21/22] remote-hg: fix for files with spaces Signed-off-by: Felipe Contreras Signed-off-by: Junio C Hamano --- contrib/remote-helpers/git-remote-hg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg index 07754bdeb2..62c39db5b4 100755 --- a/contrib/remote-helpers/git-remote-hg +++ b/contrib/remote-helpers/git-remote-hg @@ -565,7 +565,7 @@ def parse_commit(parser): for line in parser: if parser.check('M'): - t, m, mark_ref, path = line.split(' ') + t, m, mark_ref, path = line.split(' ', 3) mark = int(mark_ref[1:]) f = { 'mode' : hgmode(m), 'data' : blob_marks[mark] } elif parser.check('D'): From 1e310551e7e3a2efe41cbe1bc0a6b919cd6f7d34 Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Wed, 28 Nov 2012 02:01:33 +0100 Subject: [PATCH 22/22] remote-hg: fix for older versions of python As Amit Bakshi reported, older versions of python (< 2.7) don't have subprocess.check_output, so let's use subprocess.Popen directly as suggested. Suggested-by: Amit Bakshi Signed-off-by: Felipe Contreras Signed-off-by: Junio C Hamano --- contrib/remote-helpers/git-remote-hg | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/contrib/remote-helpers/git-remote-hg b/contrib/remote-helpers/git-remote-hg index 62c39db5b4..016cdadb4d 100755 --- a/contrib/remote-helpers/git-remote-hg +++ b/contrib/remote-helpers/git-remote-hg @@ -56,6 +56,12 @@ def hgmode(mode): m = { '0100755': 'x', '0120000': 'l' } return m.get(mode, '') +def get_config(config): + cmd = ['git', 'config', '--get', config] + process = subprocess.Popen(cmd, stdout=subprocess.PIPE) + output, _ = process.communicate() + return output + class Marks: def __init__(self, path): @@ -727,12 +733,10 @@ def main(args): hg_git_compat = False track_branches = True try: - cmd = ['git', 'config', '--get', 'remote-hg.hg-git-compat'] - if subprocess.check_output(cmd) == 'true\n': + if get_config('remote-hg.hg-git-compat') == 'true\n': hg_git_compat = True track_branches = False - cmd = ['git', 'config', '--get', 'remote-hg.track-branches'] - if subprocess.check_output(cmd) == 'false\n': + if get_config('remote-hg.track-branches') == 'false\n': track_branches = False except subprocess.CalledProcessError: pass