Merge branch 'fc/remote-hg'

* fc/remote-hg:
  remote-hg: strip extra newline
  remote-hg: use marks instead of inlined files
  remote-hg: small performance improvement
  remote-hg: allow refs with spaces
  remote-hg: don't update bookmarks unnecessarily
  remote-hg: add support for schemes extension
  remote-hg: improve email sanitation
  remote-hg: add custom local tag write code
  remote-hg: write tags in the appropriate branch
  remote-hg: custom method to write tags
  remote-hg: add support for tag objects
  remote-hg: add branch_tip() helper
  remote-hg: properly mark branches up-to-date
  remote-hg: use python urlparse
  remote-hg: safer bookmark pushing
  remote-helpers: avoid has_key
This commit is contained in:
Junio C Hamano
2013-04-26 15:19:03 -07:00
3 changed files with 141 additions and 36 deletions

View File

@ -94,7 +94,7 @@ class Marks:
return self.last_mark return self.last_mark
def is_marked(self, rev): def is_marked(self, rev):
return self.marks.has_key(rev) return str(rev) in self.marks
def new_mark(self, rev, mark): def new_mark(self, rev, mark):
self.marks[rev] = mark self.marks[rev] = mark

View File

@ -12,7 +12,7 @@
# For remote repositories a local clone is stored in # For remote repositories a local clone is stored in
# "$GIT_DIR/hg/origin/clone/.hg/". # "$GIT_DIR/hg/origin/clone/.hg/".
from mercurial import hg, ui, bookmarks, context, util, encoding, node, error from mercurial import hg, ui, bookmarks, context, util, encoding, node, error, extensions
import re import re
import sys import sys
@ -22,6 +22,7 @@ import shutil
import subprocess import subprocess
import urllib import urllib
import atexit import atexit
import urlparse
# #
# If you want to switch to hg-git compatibility mode: # If you want to switch to hg-git compatibility mode:
@ -50,6 +51,7 @@ import atexit
NAME_RE = re.compile('^([^<>]+)') NAME_RE = re.compile('^([^<>]+)')
AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$') AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$')
EMAIL_RE = re.compile('^([^<>]+[^ \\\t<>])?\\b(?:[ \\t<>]*?)\\b([^ \\t<>]+@[^ \\t<>]+)')
AUTHOR_HG_RE = re.compile('^(.*?) ?<(.*?)(?:>(.+)?)?$') AUTHOR_HG_RE = re.compile('^(.*?) ?<(.*?)(?:>(.+)?)?$')
RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.*)> (\d+) ([+-]\d+)') RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.*)> (\d+) ([+-]\d+)')
@ -73,6 +75,12 @@ def hgmode(mode):
def hghex(node): def hghex(node):
return hg.node.hex(node) return hg.node.hex(node)
def hgref(ref):
return ref.replace('___', ' ')
def gitref(ref):
return ref.replace(' ', '___')
def get_config(config): def get_config(config):
cmd = ['git', 'config', '--get', config] cmd = ['git', 'config', '--get', config]
process = subprocess.Popen(cmd, stdout=subprocess.PIPE) process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
@ -118,6 +126,10 @@ class Marks:
def to_rev(self, mark): def to_rev(self, mark):
return self.rev_marks[mark] return self.rev_marks[mark]
def next_mark(self):
self.last_mark += 1
return self.last_mark
def get_mark(self, rev): def get_mark(self, rev):
self.last_mark += 1 self.last_mark += 1
self.marks[str(rev)] = self.last_mark self.marks[str(rev)] = self.last_mark
@ -129,7 +141,7 @@ class Marks:
self.last_mark = mark self.last_mark = mark
def is_marked(self, rev): def is_marked(self, rev):
return self.marks.has_key(str(rev)) return str(rev) in self.marks
def get_tip(self, branch): def get_tip(self, branch):
return self.tips.get(branch, 0) return self.tips.get(branch, 0)
@ -210,20 +222,38 @@ def fix_file_path(path):
return path return path
return os.path.relpath(path, '/') return os.path.relpath(path, '/')
def export_file(fc): def export_files(files):
d = fc.data() global marks, filenodes
path = fix_file_path(fc.path())
print "M %s inline %s" % (gitmode(fc.flags()), path) final = []
print "data %d" % len(d) for f in files:
print d fid = node.hex(f.filenode())
if fid in filenodes:
mark = filenodes[fid]
else:
mark = marks.next_mark()
filenodes[fid] = mark
d = f.data()
print "blob"
print "mark :%u" % mark
print "data %d" % len(d)
print d
path = fix_file_path(f.path())
final.append((gitmode(f.flags()), mark, path))
return final
def get_filechanges(repo, ctx, parent): def get_filechanges(repo, ctx, parent):
modified = set() modified = set()
added = set() added = set()
removed = set() removed = set()
cur = ctx.manifest() # load earliest manifest first for caching reasons
prev = repo[parent].manifest().copy() prev = repo[parent].manifest().copy()
cur = ctx.manifest()
for fn in cur: for fn in cur:
if fn in prev: if fn in prev:
@ -244,9 +274,14 @@ def fixup_user_git(user):
name = m.group(1) name = m.group(1)
mail = m.group(2).strip() mail = m.group(2).strip()
else: else:
m = NAME_RE.match(user) m = EMAIL_RE.match(user)
if m: if m:
name = m.group(1).strip() name = m.group(1)
mail = m.group(2)
else:
m = NAME_RE.match(user)
if m:
name = m.group(1).strip()
return (name, mail) return (name, mail)
def fixup_user_hg(user): def fixup_user_hg(user):
@ -298,6 +333,12 @@ def get_repo(url, alias):
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
pass pass
try:
mod = extensions.load(myui, 'hgext.schemes', None)
mod.extsetup(myui)
except ImportError:
pass
if hg.islocal(url): if hg.islocal(url):
repo = hg.repository(myui, url) repo = hg.repository(myui, url)
else: else:
@ -393,6 +434,8 @@ def export_ref(repo, name, kind, head):
if len(parents) == 0 and rev: if len(parents) == 0 and rev:
print 'reset %s/%s' % (prefix, ename) print 'reset %s/%s' % (prefix, ename)
modified_final = export_files(c.filectx(f) for f in modified)
print "commit %s/%s" % (prefix, ename) print "commit %s/%s" % (prefix, ename)
print "mark :%d" % (marks.get_mark(rev)) print "mark :%d" % (marks.get_mark(rev))
print "author %s" % (author) print "author %s" % (author)
@ -405,8 +448,8 @@ def export_ref(repo, name, kind, head):
if len(parents) > 1: if len(parents) > 1:
print "merge :%s" % (rev_to_mark(parents[1])) print "merge :%s" % (rev_to_mark(parents[1]))
for f in modified: for f in modified_final:
export_file(c.filectx(f)) print "M %s :%u %s" % f
for f in removed: for f in removed:
print "D %s" % (fix_file_path(f)) print "D %s" % (fix_file_path(f))
print print
@ -424,10 +467,10 @@ def export_ref(repo, name, kind, head):
marks.set_tip(ename, rev) marks.set_tip(ename, rev)
def export_tag(repo, tag): def export_tag(repo, tag):
export_ref(repo, tag, 'tags', repo[tag]) export_ref(repo, tag, 'tags', repo[hgref(tag)])
def export_bookmark(repo, bmark): def export_bookmark(repo, bmark):
head = bmarks[bmark] head = bmarks[hgref(bmark)]
export_ref(repo, bmark, 'bookmarks', head) export_ref(repo, bmark, 'bookmarks', head)
def export_branch(repo, branch): def export_branch(repo, branch):
@ -456,19 +499,24 @@ def do_capabilities(parser):
print print
def branch_tip(repo, branch):
# older versions of mercurial don't have this
if hasattr(repo, 'branchtip'):
return repo.branchtip(branch)
else:
return repo.branchtags()[branch]
def get_branch_tip(repo, branch): def get_branch_tip(repo, branch):
global branches global branches
heads = branches.get(branch, None) heads = branches.get(hgref(branch), None)
if not heads: if not heads:
return None return None
# verify there's only one head # verify there's only one head
if (len(heads) > 1): if (len(heads) > 1):
warn("Branch '%s' has more than one head, consider merging" % branch) warn("Branch '%s' has more than one head, consider merging" % branch)
# older versions of mercurial don't have this return branch_tip(repo, hgref(branch))
if hasattr(repo, "branchtip"):
return repo.branchtip(branch)
return heads[0] return heads[0]
@ -490,6 +538,7 @@ def list_head(repo, cur):
head = 'master' head = 'master'
bmarks[head] = node bmarks[head] = node
head = gitref(head)
print "@refs/heads/%s HEAD" % head print "@refs/heads/%s HEAD" % head
g_head = (head, node) g_head = (head, node)
@ -511,15 +560,15 @@ def do_list(parser):
branches[branch] = heads branches[branch] = heads
for branch in branches: for branch in branches:
print "? refs/heads/branches/%s" % branch print "? refs/heads/branches/%s" % gitref(branch)
for bmark in bmarks: for bmark in bmarks:
print "? refs/heads/%s" % bmark print "? refs/heads/%s" % gitref(bmark)
for tag, node in repo.tagslist(): for tag, node in repo.tagslist():
if tag == 'tip': if tag == 'tip':
continue continue
print "? refs/tags/%s" % tag print "? refs/tags/%s" % gitref(tag)
print print
@ -603,6 +652,10 @@ def parse_commit(parser):
if parser.check('merge'): if parser.check('merge'):
die('octopus merges are not supported yet') die('octopus merges are not supported yet')
# fast-export adds an extra newline
if data[-1] == '\n':
data = data[:-1]
files = {} files = {}
for line in parser: for line in parser:
@ -656,7 +709,8 @@ def parse_commit(parser):
# Check if the ref is supposed to be a named branch # Check if the ref is supposed to be a named branch
if ref.startswith('refs/heads/branches/'): if ref.startswith('refs/heads/branches/'):
extra['branch'] = ref[len('refs/heads/branches/'):] branch = ref[len('refs/heads/branches/'):]
extra['branch'] = hgref(branch)
if mode == 'hg': if mode == 'hg':
i = data.find('\n--HG--\n') i = data.find('\n--HG--\n')
@ -716,7 +770,40 @@ def parse_tag(parser):
data = parser.get_data() data = parser.get_data()
parser.next() parser.next()
# nothing to do parsed_tags[name] = (tagger, data)
def write_tag(repo, tag, node, msg, author):
branch = repo[node].branch()
tip = branch_tip(repo, branch)
tip = repo[tip]
def getfilectx(repo, memctx, f):
try:
fctx = tip.filectx(f)
data = fctx.data()
except error.ManifestLookupError:
data = ""
content = data + "%s %s\n" % (hghex(node), tag)
return context.memfilectx(f, content, False, False, None)
p1 = tip.hex()
p2 = '\0' * 20
if not author:
author = (None, 0, 0)
user, date, tz = author
ctx = context.memctx(repo, (p1, p2), msg,
['.hgtags'], getfilectx,
user, (date, tz), {'branch' : branch})
tmp = encoding.encoding
encoding.encoding = 'utf-8'
tagnode = repo.commitctx(ctx)
encoding.encoding = tmp
return tagnode
def do_export(parser): def do_export(parser):
global parsed_refs, bmarks, peer global parsed_refs, bmarks, peer
@ -741,6 +828,10 @@ def do_export(parser):
for ref, node in parsed_refs.iteritems(): for ref, node in parsed_refs.iteritems():
if ref.startswith('refs/heads/branches'): if ref.startswith('refs/heads/branches'):
branch = ref[len('refs/heads/branches/'):]
if branch in branches and node in branches[branch]:
# up to date
continue
print "ok %s" % ref print "ok %s" % ref
elif ref.startswith('refs/heads/'): elif ref.startswith('refs/heads/'):
bmark = ref[len('refs/heads/'):] bmark = ref[len('refs/heads/'):]
@ -748,11 +839,16 @@ def do_export(parser):
continue continue
elif ref.startswith('refs/tags/'): elif ref.startswith('refs/tags/'):
tag = ref[len('refs/tags/'):] tag = ref[len('refs/tags/'):]
tag = hgref(tag)
author, msg = parsed_tags.get(tag, (None, None))
if mode == 'git': if mode == 'git':
msg = 'Added tag %s for changeset %s' % (tag, hghex(node[:6])); if not msg:
parser.repo.tag([tag], node, msg, False, None, {}) msg = 'Added tag %s for changeset %s' % (tag, hghex(node[:6]));
write_tag(parser.repo, tag, node, msg, author)
else: else:
parser.repo.tag([tag], node, None, True, None, {}) fp = parser.repo.opener('localtags', 'a')
fp.write('%s %s\n' % (hghex(node), tag))
fp.close()
print "ok %s" % ref print "ok %s" % ref
else: else:
# transport-helper/fast-export bugs # transport-helper/fast-export bugs
@ -771,6 +867,9 @@ def do_export(parser):
else: else:
old = '' old = ''
if old == new:
continue
if bmark == 'master' and 'master' not in parser.repo._bookmarks: if bmark == 'master' and 'master' not in parser.repo._bookmarks:
# fake bookmark # fake bookmark
pass pass
@ -782,6 +881,8 @@ def do_export(parser):
continue continue
if peer: if peer:
rb = peer.listkeys('bookmarks')
old = rb.get(bmark, '')
if not peer.pushkey('bookmarks', bmark, old, new): if not peer.pushkey('bookmarks', bmark, old, new):
print "error %s" % ref print "error %s" % ref
continue continue
@ -791,11 +892,11 @@ def do_export(parser):
print print
def fix_path(alias, repo, orig_url): def fix_path(alias, repo, orig_url):
repo_url = util.url(repo.url()) url = urlparse.urlparse(orig_url, 'file')
url = util.url(orig_url) if url.scheme != 'file' or os.path.isabs(url.path):
if str(url) == str(repo_url):
return return
cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % repo_url] abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url)
cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % abs_url]
subprocess.call(cmd) subprocess.call(cmd)
def main(args): def main(args):
@ -803,6 +904,8 @@ def main(args):
global marks, blob_marks, parsed_refs global marks, blob_marks, parsed_refs
global peer, mode, bad_mail, bad_name global peer, mode, bad_mail, bad_name
global track_branches, force_push, is_tmp global track_branches, force_push, is_tmp
global parsed_tags
global filenodes
alias = args[1] alias = args[1]
url = args[2] url = args[2]
@ -845,6 +948,8 @@ def main(args):
blob_marks = {} blob_marks = {}
parsed_refs = {} parsed_refs = {}
marks = None marks = None
parsed_tags = {}
filenodes = {}
repo = get_repo(url, alias) repo = get_repo(url, alias)
prefix = 'refs/hg/%s' % alias prefix = 'refs/hg/%s' % alias

View File

@ -137,15 +137,15 @@ test_expect_success 'authors' '
author_test alpha "" "H G Wells <wells@example.com>" && author_test alpha "" "H G Wells <wells@example.com>" &&
author_test beta "test" "test <unknown>" && author_test beta "test" "test <unknown>" &&
author_test beta "test <test@example.com> (comment)" "test <unknown>" && author_test beta "test <test@example.com> (comment)" "test <test@example.com>" &&
author_test gamma "<test@example.com>" "Unknown <test@example.com>" && author_test gamma "<test@example.com>" "Unknown <test@example.com>" &&
author_test delta "name<test@example.com>" "name <test@example.com>" && author_test delta "name<test@example.com>" "name <test@example.com>" &&
author_test epsilon "name <test@example.com" "name <unknown>" && author_test epsilon "name <test@example.com" "name <test@example.com>" &&
author_test zeta " test " "test <unknown>" && author_test zeta " test " "test <unknown>" &&
author_test eta "test < test@example.com >" "test <test@example.com>" && author_test eta "test < test@example.com >" "test <test@example.com>" &&
author_test theta "test >test@example.com>" "test <unknown>" && author_test theta "test >test@example.com>" "test <test@example.com>" &&
author_test iota "test < test <at> example <dot> com>" "test <unknown>" && author_test iota "test < test <at> example <dot> com>" "test <unknown>" &&
author_test kappa "test@example.com" "test@example.com <unknown>" author_test kappa "test@example.com" "Unknown <test@example.com>"
) && ) &&
git clone "hg::$PWD/hgrepo" gitrepo && git clone "hg::$PWD/hgrepo" gitrepo &&