Merge branch 'fc/contrib-bzr-hg-fixes'
[alt-git.git] / contrib / remote-helpers / git-remote-hg
blob92d994e470f05db8536ba443a6afb192be8c2452
1 #!/usr/bin/env python
3 # Copyright (c) 2012 Felipe Contreras
6 # Inspired by Rocco Rutte's hg-fast-export
8 # Just copy to your ~/bin, or anywhere in your $PATH.
9 # Then you can clone with:
10 # git clone hg::/path/to/mercurial/repo/
12 # For remote repositories a local clone is stored in
13 # "$GIT_DIR/hg/origin/clone/.hg/".
15 from mercurial import hg, ui, bookmarks, context, encoding, node, error, extensions, discovery, util
17 import re
18 import sys
19 import os
20 import json
21 import shutil
22 import subprocess
23 import urllib
24 import atexit
25 import urlparse, hashlib
26 import time as ptime
29 # If you want to see Mercurial revisions as Git commit notes:
30 # git config core.notesRef refs/notes/hg
32 # If you are not in hg-git-compat mode and want to disable the tracking of
33 # named branches:
34 # git config --global remote-hg.track-branches false
36 # If you want the equivalent of hg's clone/pull--insecure option:
37 # git config --global remote-hg.insecure true
39 # If you want to switch to hg-git compatibility mode:
40 # git config --global remote-hg.hg-git-compat true
42 # git:
43 # Sensible defaults for git.
44 # hg bookmarks are exported as git branches, hg branches are prefixed
45 # with 'branches/', HEAD is a special case.
47 # hg:
48 # Emulate hg-git.
49 # Only hg bookmarks are exported as git branches.
50 # Commits are modified to preserve hg information and allow bidirectionality.
53 NAME_RE = re.compile('^([^<>]+)')
54 AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$')
55 EMAIL_RE = re.compile('^([^<>]+[^ \\\t<>])?\\b(?:[ \\t<>]*?)\\b([^ \\t<>]+@[^ \\t<>]+)')
56 AUTHOR_HG_RE = re.compile('^(.*?) ?<(.*?)(?:>(.+)?)?$')
57 RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.*)> (\d+) ([+-]\d+)')
59 VERSION = 2
61 def die(msg, *args):
62 sys.stderr.write('ERROR: %s\n' % (msg % args))
63 sys.exit(1)
65 def warn(msg, *args):
66 sys.stderr.write('WARNING: %s\n' % (msg % args))
68 def gitmode(flags):
69 return 'l' in flags and '120000' or 'x' in flags and '100755' or '100644'
71 def gittz(tz):
72 return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60)
74 def hgmode(mode):
75 m = { '100755': 'x', '120000': 'l' }
76 return m.get(mode, '')
78 def hghex(n):
79 return node.hex(n)
81 def hgbin(n):
82 return node.bin(n)
84 def hgref(ref):
85 return ref.replace('___', ' ')
87 def gitref(ref):
88 return ref.replace(' ', '___')
90 def check_version(*check):
91 if not hg_version:
92 return True
93 return hg_version >= check
95 def get_config(config):
96 cmd = ['git', 'config', '--get', config]
97 process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
98 output, _ = process.communicate()
99 return output
101 def get_config_bool(config, default=False):
102 value = get_config(config).rstrip('\n')
103 if value == "true":
104 return True
105 elif value == "false":
106 return False
107 else:
108 return default
110 class Marks:
112 def __init__(self, path, repo):
113 self.path = path
114 self.repo = repo
115 self.clear()
116 self.load()
118 if self.version < VERSION:
119 if self.version == 1:
120 self.upgrade_one()
122 # upgraded?
123 if self.version < VERSION:
124 self.clear()
125 self.version = VERSION
127 def clear(self):
128 self.tips = {}
129 self.marks = {}
130 self.rev_marks = {}
131 self.last_mark = 0
132 self.version = 0
133 self.last_note = 0
135 def load(self):
136 if not os.path.exists(self.path):
137 return
139 tmp = json.load(open(self.path))
141 self.tips = tmp['tips']
142 self.marks = tmp['marks']
143 self.last_mark = tmp['last-mark']
144 self.version = tmp.get('version', 1)
145 self.last_note = tmp.get('last-note', 0)
147 for rev, mark in self.marks.iteritems():
148 self.rev_marks[mark] = rev
150 def upgrade_one(self):
151 def get_id(rev):
152 return hghex(self.repo.changelog.node(int(rev)))
153 self.tips = dict((name, get_id(rev)) for name, rev in self.tips.iteritems())
154 self.marks = dict((get_id(rev), mark) for rev, mark in self.marks.iteritems())
155 self.rev_marks = dict((mark, get_id(rev)) for mark, rev in self.rev_marks.iteritems())
156 self.version = 2
158 def dict(self):
159 return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark, 'version' : self.version, 'last-note' : self.last_note }
161 def store(self):
162 json.dump(self.dict(), open(self.path, 'w'))
164 def __str__(self):
165 return str(self.dict())
167 def from_rev(self, rev):
168 return self.marks[rev]
170 def to_rev(self, mark):
171 return str(self.rev_marks[mark])
173 def next_mark(self):
174 self.last_mark += 1
175 return self.last_mark
177 def get_mark(self, rev):
178 self.last_mark += 1
179 self.marks[rev] = self.last_mark
180 return self.last_mark
182 def new_mark(self, rev, mark):
183 self.marks[rev] = mark
184 self.rev_marks[mark] = rev
185 self.last_mark = mark
187 def is_marked(self, rev):
188 return rev in self.marks
190 def get_tip(self, branch):
191 return str(self.tips[branch])
193 def set_tip(self, branch, tip):
194 self.tips[branch] = tip
196 class Parser:
198 def __init__(self, repo):
199 self.repo = repo
200 self.line = self.get_line()
202 def get_line(self):
203 return sys.stdin.readline().strip()
205 def __getitem__(self, i):
206 return self.line.split()[i]
208 def check(self, word):
209 return self.line.startswith(word)
211 def each_block(self, separator):
212 while self.line != separator:
213 yield self.line
214 self.line = self.get_line()
216 def __iter__(self):
217 return self.each_block('')
219 def next(self):
220 self.line = self.get_line()
221 if self.line == 'done':
222 self.line = None
224 def get_mark(self):
225 i = self.line.index(':') + 1
226 return int(self.line[i:])
228 def get_data(self):
229 if not self.check('data'):
230 return None
231 i = self.line.index(' ') + 1
232 size = int(self.line[i:])
233 return sys.stdin.read(size)
235 def get_author(self):
236 ex = None
237 m = RAW_AUTHOR_RE.match(self.line)
238 if not m:
239 return None
240 _, name, email, date, tz = m.groups()
241 if name and 'ext:' in name:
242 m = re.match('^(.+?) ext:\((.+)\)$', name)
243 if m:
244 name = m.group(1)
245 ex = urllib.unquote(m.group(2))
247 if email != bad_mail:
248 if name:
249 user = '%s <%s>' % (name, email)
250 else:
251 user = '<%s>' % (email)
252 else:
253 user = name
255 if ex:
256 user += ex
258 tz = int(tz)
259 tz = ((tz / 100) * 3600) + ((tz % 100) * 60)
260 return (user, int(date), -tz)
262 def fix_file_path(path):
263 if not os.path.isabs(path):
264 return path
265 return os.path.relpath(path, '/')
267 def export_files(files):
268 final = []
269 for f in files:
270 fid = node.hex(f.filenode())
272 if fid in filenodes:
273 mark = filenodes[fid]
274 else:
275 mark = marks.next_mark()
276 filenodes[fid] = mark
277 d = f.data()
279 print "blob"
280 print "mark :%u" % mark
281 print "data %d" % len(d)
282 print d
284 path = fix_file_path(f.path())
285 final.append((gitmode(f.flags()), mark, path))
287 return final
289 def get_filechanges(repo, ctx, parent):
290 modified = set()
291 added = set()
292 removed = set()
294 # load earliest manifest first for caching reasons
295 prev = parent.manifest().copy()
296 cur = ctx.manifest()
298 for fn in cur:
299 if fn in prev:
300 if (cur.flags(fn) != prev.flags(fn) or cur[fn] != prev[fn]):
301 modified.add(fn)
302 del prev[fn]
303 else:
304 added.add(fn)
305 removed |= set(prev.keys())
307 return added | modified, removed
309 def fixup_user_git(user):
310 name = mail = None
311 user = user.replace('"', '')
312 m = AUTHOR_RE.match(user)
313 if m:
314 name = m.group(1)
315 mail = m.group(2).strip()
316 else:
317 m = EMAIL_RE.match(user)
318 if m:
319 name = m.group(1)
320 mail = m.group(2)
321 else:
322 m = NAME_RE.match(user)
323 if m:
324 name = m.group(1).strip()
325 return (name, mail)
327 def fixup_user_hg(user):
328 def sanitize(name):
329 # stole this from hg-git
330 return re.sub('[<>\n]', '?', name.lstrip('< ').rstrip('> '))
332 m = AUTHOR_HG_RE.match(user)
333 if m:
334 name = sanitize(m.group(1))
335 mail = sanitize(m.group(2))
336 ex = m.group(3)
337 if ex:
338 name += ' ext:(' + urllib.quote(ex) + ')'
339 else:
340 name = sanitize(user)
341 if '@' in user:
342 mail = name
343 else:
344 mail = None
346 return (name, mail)
348 def fixup_user(user):
349 if mode == 'git':
350 name, mail = fixup_user_git(user)
351 else:
352 name, mail = fixup_user_hg(user)
354 if not name:
355 name = bad_name
356 if not mail:
357 mail = bad_mail
359 return '%s <%s>' % (name, mail)
361 def updatebookmarks(repo, peer):
362 remotemarks = peer.listkeys('bookmarks')
363 localmarks = repo._bookmarks
365 if not remotemarks:
366 return
368 for k, v in remotemarks.iteritems():
369 localmarks[k] = hgbin(v)
371 if hasattr(localmarks, 'write'):
372 localmarks.write()
373 else:
374 bookmarks.write(repo)
376 def get_repo(url, alias):
377 global peer
379 myui = ui.ui()
380 myui.setconfig('ui', 'interactive', 'off')
381 myui.fout = sys.stderr
383 if get_config_bool('remote-hg.insecure'):
384 myui.setconfig('web', 'cacerts', '')
386 extensions.loadall(myui)
388 if hg.islocal(url) and not os.environ.get('GIT_REMOTE_HG_TEST_REMOTE'):
389 repo = hg.repository(myui, url)
390 if not os.path.exists(dirname):
391 os.makedirs(dirname)
392 else:
393 shared_path = os.path.join(gitdir, 'hg')
395 # check and upgrade old organization
396 hg_path = os.path.join(shared_path, '.hg')
397 if os.path.exists(shared_path) and not os.path.exists(hg_path):
398 repos = os.listdir(shared_path)
399 for x in repos:
400 local_hg = os.path.join(shared_path, x, 'clone', '.hg')
401 if not os.path.exists(local_hg):
402 continue
403 if not os.path.exists(hg_path):
404 shutil.move(local_hg, hg_path)
405 shutil.rmtree(os.path.join(shared_path, x, 'clone'))
407 # setup shared repo (if not there)
408 try:
409 hg.peer(myui, {}, shared_path, create=True)
410 except error.RepoError:
411 pass
413 if not os.path.exists(dirname):
414 os.makedirs(dirname)
416 local_path = os.path.join(dirname, 'clone')
417 if not os.path.exists(local_path):
418 hg.share(myui, shared_path, local_path, update=False)
420 repo = hg.repository(myui, local_path)
421 try:
422 peer = hg.peer(myui, {}, url)
423 except:
424 die('Repository error')
425 repo.pull(peer, heads=None, force=True)
427 updatebookmarks(repo, peer)
429 return repo
431 def rev_to_mark(rev):
432 return marks.from_rev(rev.hex())
434 def mark_to_rev(mark):
435 return marks.to_rev(mark)
437 def export_ref(repo, name, kind, head):
438 ename = '%s/%s' % (kind, name)
439 try:
440 tip = marks.get_tip(ename)
441 tip = repo[tip].rev()
442 except:
443 tip = 0
445 revs = xrange(tip, head.rev() + 1)
446 total = len(revs)
448 for rev in revs:
450 c = repo[rev]
451 node = c.node()
453 if marks.is_marked(c.hex()):
454 continue
456 (manifest, user, (time, tz), files, desc, extra) = repo.changelog.read(node)
457 rev_branch = extra['branch']
459 author = "%s %d %s" % (fixup_user(user), time, gittz(tz))
460 if 'committer' in extra:
461 user, time, tz = extra['committer'].rsplit(' ', 2)
462 committer = "%s %s %s" % (user, time, gittz(int(tz)))
463 else:
464 committer = author
466 parents = [repo[p] for p in repo.changelog.parentrevs(rev) if p >= 0]
468 if len(parents) == 0:
469 modified = c.manifest().keys()
470 removed = []
471 else:
472 modified, removed = get_filechanges(repo, c, parents[0])
474 desc += '\n'
476 if mode == 'hg':
477 extra_msg = ''
479 if rev_branch != 'default':
480 extra_msg += 'branch : %s\n' % rev_branch
482 renames = []
483 for f in c.files():
484 if f not in c.manifest():
485 continue
486 rename = c.filectx(f).renamed()
487 if rename:
488 renames.append((rename[0], f))
490 for e in renames:
491 extra_msg += "rename : %s => %s\n" % e
493 for key, value in extra.iteritems():
494 if key in ('author', 'committer', 'encoding', 'message', 'branch', 'hg-git'):
495 continue
496 else:
497 extra_msg += "extra : %s : %s\n" % (key, urllib.quote(value))
499 if extra_msg:
500 desc += '\n--HG--\n' + extra_msg
502 if len(parents) == 0 and rev:
503 print 'reset %s/%s' % (prefix, ename)
505 modified_final = export_files(c.filectx(f) for f in modified)
507 print "commit %s/%s" % (prefix, ename)
508 print "mark :%d" % (marks.get_mark(c.hex()))
509 print "author %s" % (author)
510 print "committer %s" % (committer)
511 print "data %d" % (len(desc))
512 print desc
514 if len(parents) > 0:
515 print "from :%s" % (rev_to_mark(parents[0]))
516 if len(parents) > 1:
517 print "merge :%s" % (rev_to_mark(parents[1]))
519 for f in removed:
520 print "D %s" % (fix_file_path(f))
521 for f in modified_final:
522 print "M %s :%u %s" % f
523 print
525 progress = (rev - tip)
526 if (progress % 100 == 0):
527 print "progress revision %d '%s' (%d/%d)" % (rev, name, progress, total)
529 # make sure the ref is updated
530 print "reset %s/%s" % (prefix, ename)
531 print "from :%u" % rev_to_mark(head)
532 print
534 pending_revs = set(revs) - notes
535 if pending_revs:
536 note_mark = marks.next_mark()
537 ref = "refs/notes/hg"
539 print "commit %s" % ref
540 print "mark :%d" % (note_mark)
541 print "committer remote-hg <> %s" % (ptime.strftime('%s %z'))
542 desc = "Notes for %s\n" % (name)
543 print "data %d" % (len(desc))
544 print desc
545 if marks.last_note:
546 print "from :%u" % marks.last_note
548 for rev in pending_revs:
549 notes.add(rev)
550 c = repo[rev]
551 print "N inline :%u" % rev_to_mark(c)
552 msg = c.hex()
553 print "data %d" % (len(msg))
554 print msg
555 print
557 marks.last_note = note_mark
559 marks.set_tip(ename, head.hex())
561 def export_tag(repo, tag):
562 export_ref(repo, tag, 'tags', repo[hgref(tag)])
564 def export_bookmark(repo, bmark):
565 head = bmarks[hgref(bmark)]
566 export_ref(repo, bmark, 'bookmarks', head)
568 def export_branch(repo, branch):
569 tip = get_branch_tip(repo, branch)
570 head = repo[tip]
571 export_ref(repo, branch, 'branches', head)
573 def export_head(repo):
574 export_ref(repo, g_head[0], 'bookmarks', g_head[1])
576 def do_capabilities(parser):
577 print "import"
578 print "export"
579 print "refspec refs/heads/branches/*:%s/branches/*" % prefix
580 print "refspec refs/heads/*:%s/bookmarks/*" % prefix
581 print "refspec refs/tags/*:%s/tags/*" % prefix
583 path = os.path.join(dirname, 'marks-git')
585 if os.path.exists(path):
586 print "*import-marks %s" % path
587 print "*export-marks %s" % path
588 print "option"
590 print
592 def branch_tip(branch):
593 return branches[branch][-1]
595 def get_branch_tip(repo, branch):
596 heads = branches.get(hgref(branch), None)
597 if not heads:
598 return None
600 # verify there's only one head
601 if (len(heads) > 1):
602 warn("Branch '%s' has more than one head, consider merging" % branch)
603 return branch_tip(hgref(branch))
605 return heads[0]
607 def list_head(repo, cur):
608 global g_head, fake_bmark
610 if 'default' not in branches:
611 # empty repo
612 return
614 node = repo[branch_tip('default')]
615 head = 'master' if not 'master' in bmarks else 'default'
616 fake_bmark = head
617 bmarks[head] = node
619 head = gitref(head)
620 print "@refs/heads/%s HEAD" % head
621 g_head = (head, node)
623 def do_list(parser):
624 repo = parser.repo
625 for bmark, node in bookmarks.listbookmarks(repo).iteritems():
626 bmarks[bmark] = repo[node]
628 cur = repo.dirstate.branch()
629 orig = peer if peer else repo
631 for branch, heads in orig.branchmap().iteritems():
632 # only open heads
633 heads = [h for h in heads if 'close' not in repo.changelog.read(h)[5]]
634 if heads:
635 branches[branch] = heads
637 list_head(repo, cur)
639 if track_branches:
640 for branch in branches:
641 print "? refs/heads/branches/%s" % gitref(branch)
643 for bmark in bmarks:
644 print "? refs/heads/%s" % gitref(bmark)
646 for tag, node in repo.tagslist():
647 if tag == 'tip':
648 continue
649 print "? refs/tags/%s" % gitref(tag)
651 print
653 def do_import(parser):
654 repo = parser.repo
656 path = os.path.join(dirname, 'marks-git')
658 print "feature done"
659 if os.path.exists(path):
660 print "feature import-marks=%s" % path
661 print "feature export-marks=%s" % path
662 print "feature force"
663 sys.stdout.flush()
665 tmp = encoding.encoding
666 encoding.encoding = 'utf-8'
668 # lets get all the import lines
669 while parser.check('import'):
670 ref = parser[1]
672 if (ref == 'HEAD'):
673 export_head(repo)
674 elif ref.startswith('refs/heads/branches/'):
675 branch = ref[len('refs/heads/branches/'):]
676 export_branch(repo, branch)
677 elif ref.startswith('refs/heads/'):
678 bmark = ref[len('refs/heads/'):]
679 export_bookmark(repo, bmark)
680 elif ref.startswith('refs/tags/'):
681 tag = ref[len('refs/tags/'):]
682 export_tag(repo, tag)
684 parser.next()
686 encoding.encoding = tmp
688 print 'done'
690 def parse_blob(parser):
691 parser.next()
692 mark = parser.get_mark()
693 parser.next()
694 data = parser.get_data()
695 blob_marks[mark] = data
696 parser.next()
698 def get_merge_files(repo, p1, p2, files):
699 for e in repo[p1].files():
700 if e not in files:
701 if e not in repo[p1].manifest():
702 continue
703 f = { 'ctx' : repo[p1][e] }
704 files[e] = f
706 def parse_commit(parser):
707 from_mark = merge_mark = None
709 ref = parser[1]
710 parser.next()
712 commit_mark = parser.get_mark()
713 parser.next()
714 author = parser.get_author()
715 parser.next()
716 committer = parser.get_author()
717 parser.next()
718 data = parser.get_data()
719 parser.next()
720 if parser.check('from'):
721 from_mark = parser.get_mark()
722 parser.next()
723 if parser.check('merge'):
724 merge_mark = parser.get_mark()
725 parser.next()
726 if parser.check('merge'):
727 die('octopus merges are not supported yet')
729 # fast-export adds an extra newline
730 if data[-1] == '\n':
731 data = data[:-1]
733 files = {}
735 for line in parser:
736 if parser.check('M'):
737 t, m, mark_ref, path = line.split(' ', 3)
738 mark = int(mark_ref[1:])
739 f = { 'mode' : hgmode(m), 'data' : blob_marks[mark] }
740 elif parser.check('D'):
741 t, path = line.split(' ', 1)
742 f = { 'deleted' : True }
743 else:
744 die('Unknown file command: %s' % line)
745 files[path] = f
747 # only export the commits if we are on an internal proxy repo
748 if dry_run and not peer:
749 parsed_refs[ref] = None
750 return
752 def getfilectx(repo, memctx, f):
753 of = files[f]
754 if 'deleted' in of:
755 raise IOError
756 if 'ctx' in of:
757 return of['ctx']
758 is_exec = of['mode'] == 'x'
759 is_link = of['mode'] == 'l'
760 rename = of.get('rename', None)
761 return context.memfilectx(f, of['data'],
762 is_link, is_exec, rename)
764 repo = parser.repo
766 user, date, tz = author
767 extra = {}
769 if committer != author:
770 extra['committer'] = "%s %u %u" % committer
772 if from_mark:
773 p1 = mark_to_rev(from_mark)
774 else:
775 p1 = '0' * 40
777 if merge_mark:
778 p2 = mark_to_rev(merge_mark)
779 else:
780 p2 = '0' * 40
783 # If files changed from any of the parents, hg wants to know, but in git if
784 # nothing changed from the first parent, nothing changed.
786 if merge_mark:
787 get_merge_files(repo, p1, p2, files)
789 # Check if the ref is supposed to be a named branch
790 if ref.startswith('refs/heads/branches/'):
791 branch = ref[len('refs/heads/branches/'):]
792 extra['branch'] = hgref(branch)
794 if mode == 'hg':
795 i = data.find('\n--HG--\n')
796 if i >= 0:
797 tmp = data[i + len('\n--HG--\n'):].strip()
798 for k, v in [e.split(' : ', 1) for e in tmp.split('\n')]:
799 if k == 'rename':
800 old, new = v.split(' => ', 1)
801 files[new]['rename'] = old
802 elif k == 'branch':
803 extra[k] = v
804 elif k == 'extra':
805 ek, ev = v.split(' : ', 1)
806 extra[ek] = urllib.unquote(ev)
807 data = data[:i]
809 ctx = context.memctx(repo, (p1, p2), data,
810 files.keys(), getfilectx,
811 user, (date, tz), extra)
813 tmp = encoding.encoding
814 encoding.encoding = 'utf-8'
816 node = hghex(repo.commitctx(ctx))
818 encoding.encoding = tmp
820 parsed_refs[ref] = node
821 marks.new_mark(node, commit_mark)
823 def parse_reset(parser):
824 ref = parser[1]
825 parser.next()
826 # ugh
827 if parser.check('commit'):
828 parse_commit(parser)
829 return
830 if not parser.check('from'):
831 return
832 from_mark = parser.get_mark()
833 parser.next()
835 try:
836 rev = mark_to_rev(from_mark)
837 except KeyError:
838 rev = None
839 parsed_refs[ref] = rev
841 def parse_tag(parser):
842 name = parser[1]
843 parser.next()
844 from_mark = parser.get_mark()
845 parser.next()
846 tagger = parser.get_author()
847 parser.next()
848 data = parser.get_data()
849 parser.next()
851 parsed_tags[name] = (tagger, data)
853 def write_tag(repo, tag, node, msg, author):
854 branch = repo[node].branch()
855 tip = branch_tip(branch)
856 tip = repo[tip]
858 def getfilectx(repo, memctx, f):
859 try:
860 fctx = tip.filectx(f)
861 data = fctx.data()
862 except error.ManifestLookupError:
863 data = ""
864 content = data + "%s %s\n" % (node, tag)
865 return context.memfilectx(f, content, False, False, None)
867 p1 = tip.hex()
868 p2 = '0' * 40
869 if author:
870 user, date, tz = author
871 date_tz = (date, tz)
872 else:
873 cmd = ['git', 'var', 'GIT_COMMITTER_IDENT']
874 process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
875 output, _ = process.communicate()
876 m = re.match('^.* <.*>', output)
877 if m:
878 user = m.group(0)
879 else:
880 user = repo.ui.username()
881 date_tz = None
883 ctx = context.memctx(repo, (p1, p2), msg,
884 ['.hgtags'], getfilectx,
885 user, date_tz, {'branch' : branch})
887 tmp = encoding.encoding
888 encoding.encoding = 'utf-8'
890 tagnode = repo.commitctx(ctx)
892 encoding.encoding = tmp
894 return (tagnode, branch)
896 def checkheads_bmark(repo, ref, ctx):
897 bmark = ref[len('refs/heads/'):]
898 if not bmark in bmarks:
899 # new bmark
900 return True
902 ctx_old = bmarks[bmark]
903 ctx_new = ctx
904 if not repo.changelog.descendant(ctx_old.rev(), ctx_new.rev()):
905 if force_push:
906 print "ok %s forced update" % ref
907 else:
908 print "error %s non-fast forward" % ref
909 return False
911 return True
913 def checkheads(repo, remote, p_revs):
915 remotemap = remote.branchmap()
916 if not remotemap:
917 # empty repo
918 return True
920 new = {}
921 ret = True
923 for node, ref in p_revs.iteritems():
924 ctx = repo[node]
925 branch = ctx.branch()
926 if not branch in remotemap:
927 # new branch
928 continue
929 if not ref.startswith('refs/heads/branches'):
930 if ref.startswith('refs/heads/'):
931 if not checkheads_bmark(repo, ref, ctx):
932 ret = False
934 # only check branches
935 continue
936 new.setdefault(branch, []).append(ctx.rev())
938 for branch, heads in new.iteritems():
939 old = [repo.changelog.rev(x) for x in remotemap[branch]]
940 for rev in heads:
941 if check_version(2, 3):
942 ancestors = repo.changelog.ancestors([rev], stoprev=min(old))
943 else:
944 ancestors = repo.changelog.ancestors(rev)
945 found = False
947 for x in old:
948 if x in ancestors:
949 found = True
950 break
952 if found:
953 continue
955 node = repo.changelog.node(rev)
956 ref = p_revs[node]
957 if force_push:
958 print "ok %s forced update" % ref
959 else:
960 print "error %s non-fast forward" % ref
961 ret = False
963 return ret
965 def push_unsafe(repo, remote, parsed_refs, p_revs):
967 force = force_push
969 fci = discovery.findcommonincoming
970 commoninc = fci(repo, remote, force=force)
971 common, _, remoteheads = commoninc
973 if not checkheads(repo, remote, p_revs):
974 return None
976 cg = repo.getbundle('push', heads=list(p_revs), common=common)
978 unbundle = remote.capable('unbundle')
979 if unbundle:
980 if force:
981 remoteheads = ['force']
982 return remote.unbundle(cg, remoteheads, 'push')
983 else:
984 return remote.addchangegroup(cg, 'push', repo.url())
986 def push(repo, remote, parsed_refs, p_revs):
987 if hasattr(remote, 'canpush') and not remote.canpush():
988 print "error cannot push"
990 if not p_revs:
991 # nothing to push
992 return
994 lock = None
995 unbundle = remote.capable('unbundle')
996 if not unbundle:
997 lock = remote.lock()
998 try:
999 ret = push_unsafe(repo, remote, parsed_refs, p_revs)
1000 finally:
1001 if lock is not None:
1002 lock.release()
1004 return ret
1006 def check_tip(ref, kind, name, heads):
1007 try:
1008 ename = '%s/%s' % (kind, name)
1009 tip = marks.get_tip(ename)
1010 except KeyError:
1011 return True
1012 else:
1013 return tip in heads
1015 def do_export(parser):
1016 p_bmarks = []
1017 p_revs = {}
1019 parser.next()
1021 for line in parser.each_block('done'):
1022 if parser.check('blob'):
1023 parse_blob(parser)
1024 elif parser.check('commit'):
1025 parse_commit(parser)
1026 elif parser.check('reset'):
1027 parse_reset(parser)
1028 elif parser.check('tag'):
1029 parse_tag(parser)
1030 elif parser.check('feature'):
1031 pass
1032 else:
1033 die('unhandled export command: %s' % line)
1035 need_fetch = False
1037 for ref, node in parsed_refs.iteritems():
1038 bnode = hgbin(node) if node else None
1039 if ref.startswith('refs/heads/branches'):
1040 branch = ref[len('refs/heads/branches/'):]
1041 if branch in branches and bnode in branches[branch]:
1042 # up to date
1043 continue
1045 if peer:
1046 remotemap = peer.branchmap()
1047 if remotemap and branch in remotemap:
1048 heads = [hghex(e) for e in remotemap[branch]]
1049 if not check_tip(ref, 'branches', branch, heads):
1050 print "error %s fetch first" % ref
1051 need_fetch = True
1052 continue
1054 p_revs[bnode] = ref
1055 print "ok %s" % ref
1056 elif ref.startswith('refs/heads/'):
1057 bmark = ref[len('refs/heads/'):]
1058 new = node
1059 old = bmarks[bmark].hex() if bmark in bmarks else ''
1061 if old == new:
1062 continue
1064 print "ok %s" % ref
1065 if bmark != fake_bmark and \
1066 not (bmark == 'master' and bmark not in parser.repo._bookmarks):
1067 p_bmarks.append((ref, bmark, old, new))
1069 if peer:
1070 remote_old = peer.listkeys('bookmarks').get(bmark)
1071 if remote_old:
1072 if not check_tip(ref, 'bookmarks', bmark, remote_old):
1073 print "error %s fetch first" % ref
1074 need_fetch = True
1075 continue
1077 p_revs[bnode] = ref
1078 elif ref.startswith('refs/tags/'):
1079 if dry_run:
1080 print "ok %s" % ref
1081 continue
1082 tag = ref[len('refs/tags/'):]
1083 tag = hgref(tag)
1084 author, msg = parsed_tags.get(tag, (None, None))
1085 if mode == 'git':
1086 if not msg:
1087 msg = 'Added tag %s for changeset %s' % (tag, node[:12])
1088 tagnode, branch = write_tag(parser.repo, tag, node, msg, author)
1089 p_revs[tagnode] = 'refs/heads/branches/' + gitref(branch)
1090 else:
1091 fp = parser.repo.opener('localtags', 'a')
1092 fp.write('%s %s\n' % (node, tag))
1093 fp.close()
1094 p_revs[bnode] = ref
1095 print "ok %s" % ref
1096 else:
1097 # transport-helper/fast-export bugs
1098 continue
1100 if need_fetch:
1101 print
1102 return
1104 if dry_run:
1105 if peer and not force_push:
1106 checkheads(parser.repo, peer, p_revs)
1107 print
1108 return
1110 if peer:
1111 if not push(parser.repo, peer, parsed_refs, p_revs):
1112 # do not update bookmarks
1113 print
1114 return
1116 # update remote bookmarks
1117 remote_bmarks = peer.listkeys('bookmarks')
1118 for ref, bmark, old, new in p_bmarks:
1119 if force_push:
1120 old = remote_bmarks.get(bmark, '')
1121 if not peer.pushkey('bookmarks', bmark, old, new):
1122 print "error %s" % ref
1123 else:
1124 # update local bookmarks
1125 for ref, bmark, old, new in p_bmarks:
1126 if not bookmarks.pushbookmark(parser.repo, bmark, old, new):
1127 print "error %s" % ref
1129 print
1131 def do_option(parser):
1132 global dry_run, force_push
1133 _, key, value = parser.line.split(' ')
1134 if key == 'dry-run':
1135 dry_run = (value == 'true')
1136 print 'ok'
1137 elif key == 'force':
1138 force_push = (value == 'true')
1139 print 'ok'
1140 else:
1141 print 'unsupported'
1143 def fix_path(alias, repo, orig_url):
1144 url = urlparse.urlparse(orig_url, 'file')
1145 if url.scheme != 'file' or os.path.isabs(os.path.expanduser(url.path)):
1146 return
1147 abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url)
1148 cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % abs_url]
1149 subprocess.call(cmd)
1151 def main(args):
1152 global prefix, gitdir, dirname, branches, bmarks
1153 global marks, blob_marks, parsed_refs
1154 global peer, mode, bad_mail, bad_name
1155 global track_branches, force_push, is_tmp
1156 global parsed_tags
1157 global filenodes
1158 global fake_bmark, hg_version
1159 global dry_run
1160 global notes, alias
1162 alias = args[1]
1163 url = args[2]
1164 peer = None
1166 hg_git_compat = get_config_bool('remote-hg.hg-git-compat')
1167 track_branches = get_config_bool('remote-hg.track-branches', True)
1168 force_push = False
1170 if hg_git_compat:
1171 mode = 'hg'
1172 bad_mail = 'none@none'
1173 bad_name = ''
1174 else:
1175 mode = 'git'
1176 bad_mail = 'unknown'
1177 bad_name = 'Unknown'
1179 if alias[4:] == url:
1180 is_tmp = True
1181 alias = hashlib.sha1(alias).hexdigest()
1182 else:
1183 is_tmp = False
1185 gitdir = os.environ['GIT_DIR']
1186 dirname = os.path.join(gitdir, 'hg', alias)
1187 branches = {}
1188 bmarks = {}
1189 blob_marks = {}
1190 parsed_refs = {}
1191 marks = None
1192 parsed_tags = {}
1193 filenodes = {}
1194 fake_bmark = None
1195 try:
1196 hg_version = tuple(int(e) for e in util.version().split('.'))
1197 except:
1198 hg_version = None
1199 dry_run = False
1200 notes = set()
1202 repo = get_repo(url, alias)
1203 prefix = 'refs/hg/%s' % alias
1205 if not is_tmp:
1206 fix_path(alias, peer or repo, url)
1208 marks_path = os.path.join(dirname, 'marks-hg')
1209 marks = Marks(marks_path, repo)
1211 if sys.platform == 'win32':
1212 import msvcrt
1213 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
1215 parser = Parser(repo)
1216 for line in parser:
1217 if parser.check('capabilities'):
1218 do_capabilities(parser)
1219 elif parser.check('list'):
1220 do_list(parser)
1221 elif parser.check('import'):
1222 do_import(parser)
1223 elif parser.check('export'):
1224 do_export(parser)
1225 elif parser.check('option'):
1226 do_option(parser)
1227 else:
1228 die('unhandled command: %s' % line)
1229 sys.stdout.flush()
1231 def bye():
1232 if not marks:
1233 return
1234 if not is_tmp:
1235 marks.store()
1236 else:
1237 shutil.rmtree(dirname)
1239 atexit.register(bye)
1240 sys.exit(main(sys.argv))