1#!/usr/bin/env python
2#
3# Copyright (c) 2012 Felipe Contreras
4#
5
6# Inspired by Rocco Rutte's hg-fast-export
7
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/
11#
12# For remote repositories a local clone is stored in
13# "$GIT_DIR/hg/origin/clone/.hg/".
14
15from mercurial import hg, ui, bookmarks, context, encoding, node, error, extensions, discovery, util
16
17import re
18import sys
19import os
20import json
21import shutil
22import subprocess
23import urllib
24import atexit
25import urlparse, hashlib
26
27#
28# If you are not in hg-git-compat mode and want to disable the tracking of
29# named branches:
30# git config --global remote-hg.track-branches false
31#
32# If you don't want to force pushes (and thus risk creating new remote heads):
33# git config --global remote-hg.force-push false
34#
35# If you want the equivalent of hg's clone/pull--insecure option:
36# git config --global remote-hg.insecure true
37#
38# If you want to switch to hg-git compatibility mode:
39# git config --global remote-hg.hg-git-compat true
40#
41# git:
42# Sensible defaults for git.
43# hg bookmarks are exported as git branches, hg branches are prefixed
44# with 'branches/', HEAD is a special case.
45#
46# hg:
47# Emulate hg-git.
48# Only hg bookmarks are exported as git branches.
49# Commits are modified to preserve hg information and allow bidirectionality.
50#
51
52NAME_RE = re.compile('^([^<>]+)')
53AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$')
54EMAIL_RE = re.compile('^([^<>]+[^ \\\t<>])?\\b(?:[ \\t<>]*?)\\b([^ \\t<>]+@[^ \\t<>]+)')
55AUTHOR_HG_RE = re.compile('^(.*?) ?<(.*?)(?:>(.+)?)?$')
56RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.*)> (\d+) ([+-]\d+)')
57
58VERSION = 2
59
60def die(msg, *args):
61 sys.stderr.write('ERROR: %s\n' % (msg % args))
62 sys.exit(1)
63
64def warn(msg, *args):
65 sys.stderr.write('WARNING: %s\n' % (msg % args))
66
67def gitmode(flags):
68 return 'l' in flags and '120000' or 'x' in flags and '100755' or '100644'
69
70def gittz(tz):
71 return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60)
72
73def hgmode(mode):
74 m = { '100755': 'x', '120000': 'l' }
75 return m.get(mode, '')
76
77def hghex(n):
78 return node.hex(n)
79
80def hgbin(n):
81 return node.bin(n)
82
83def hgref(ref):
84 return ref.replace('___', ' ')
85
86def gitref(ref):
87 return ref.replace(' ', '___')
88
89def check_version(*check):
90 if not hg_version:
91 return True
92 return hg_version >= check
93
94def get_config(config):
95 cmd = ['git', 'config', '--get', config]
96 process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
97 output, _ = process.communicate()
98 return output
99
100def get_config_bool(config, default=False):
101 value = get_config(config).rstrip('\n')
102 if value == "true":
103 return True
104 elif value == "false":
105 return False
106 else:
107 return default
108
109class Marks:
110
111 def __init__(self, path, repo):
112 self.path = path
113 self.repo = repo
114 self.clear()
115 self.load()
116
117 if self.version < VERSION:
118 if self.version == 1:
119 self.upgrade_one()
120
121 # upgraded?
122 if self.version < VERSION:
123 self.clear()
124 self.version = VERSION
125
126 def clear(self):
127 self.tips = {}
128 self.marks = {}
129 self.rev_marks = {}
130 self.last_mark = 0
131 self.version = 0
132
133 def load(self):
134 if not os.path.exists(self.path):
135 return
136
137 tmp = json.load(open(self.path))
138
139 self.tips = tmp['tips']
140 self.marks = tmp['marks']
141 self.last_mark = tmp['last-mark']
142 self.version = tmp.get('version', 1)
143
144 for rev, mark in self.marks.iteritems():
145 self.rev_marks[mark] = rev
146
147 def upgrade_one(self):
148 def get_id(rev):
149 return hghex(self.repo.changelog.node(int(rev)))
150 self.tips = dict((name, get_id(rev)) for name, rev in self.tips.iteritems())
151 self.marks = dict((get_id(rev), mark) for rev, mark in self.marks.iteritems())
152 self.rev_marks = dict((mark, get_id(rev)) for mark, rev in self.rev_marks.iteritems())
153 self.version = 2
154
155 def dict(self):
156 return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark, 'version' : self.version }
157
158 def store(self):
159 json.dump(self.dict(), open(self.path, 'w'))
160
161 def __str__(self):
162 return str(self.dict())
163
164 def from_rev(self, rev):
165 return self.marks[rev]
166
167 def to_rev(self, mark):
168 return self.rev_marks[mark]
169
170 def next_mark(self):
171 self.last_mark += 1
172 return self.last_mark
173
174 def get_mark(self, rev):
175 self.last_mark += 1
176 self.marks[rev] = self.last_mark
177 return self.last_mark
178
179 def new_mark(self, rev, mark):
180 self.marks[rev] = mark
181 self.rev_marks[mark] = rev
182 self.last_mark = mark
183
184 def is_marked(self, rev):
185 return rev in self.marks
186
187 def get_tip(self, branch):
188 return self.tips.get(branch, None)
189
190 def set_tip(self, branch, tip):
191 self.tips[branch] = tip
192
193class Parser:
194
195 def __init__(self, repo):
196 self.repo = repo
197 self.line = self.get_line()
198
199 def get_line(self):
200 return sys.stdin.readline().strip()
201
202 def __getitem__(self, i):
203 return self.line.split()[i]
204
205 def check(self, word):
206 return self.line.startswith(word)
207
208 def each_block(self, separator):
209 while self.line != separator:
210 yield self.line
211 self.line = self.get_line()
212
213 def __iter__(self):
214 return self.each_block('')
215
216 def next(self):
217 self.line = self.get_line()
218 if self.line == 'done':
219 self.line = None
220
221 def get_mark(self):
222 i = self.line.index(':') + 1
223 return int(self.line[i:])
224
225 def get_data(self):
226 if not self.check('data'):
227 return None
228 i = self.line.index(' ') + 1
229 size = int(self.line[i:])
230 return sys.stdin.read(size)
231
232 def get_author(self):
233 global bad_mail
234
235 ex = None
236 m = RAW_AUTHOR_RE.match(self.line)
237 if not m:
238 return None
239 _, name, email, date, tz = m.groups()
240 if name and 'ext:' in name:
241 m = re.match('^(.+?) ext:\((.+)\)$', name)
242 if m:
243 name = m.group(1)
244 ex = urllib.unquote(m.group(2))
245
246 if email != bad_mail:
247 if name:
248 user = '%s <%s>' % (name, email)
249 else:
250 user = '<%s>' % (email)
251 else:
252 user = name
253
254 if ex:
255 user += ex
256
257 tz = int(tz)
258 tz = ((tz / 100) * 3600) + ((tz % 100) * 60)
259 return (user, int(date), -tz)
260
261def fix_file_path(path):
262 if not os.path.isabs(path):
263 return path
264 return os.path.relpath(path, '/')
265
266def export_files(files):
267 global marks, filenodes
268
269 final = []
270 for f in files:
271 fid = node.hex(f.filenode())
272
273 if fid in filenodes:
274 mark = filenodes[fid]
275 else:
276 mark = marks.next_mark()
277 filenodes[fid] = mark
278 d = f.data()
279
280 print "blob"
281 print "mark :%u" % mark
282 print "data %d" % len(d)
283 print d
284
285 path = fix_file_path(f.path())
286 final.append((gitmode(f.flags()), mark, path))
287
288 return final
289
290def get_filechanges(repo, ctx, parent):
291 modified = set()
292 added = set()
293 removed = set()
294
295 # load earliest manifest first for caching reasons
296 prev = parent.manifest().copy()
297 cur = ctx.manifest()
298
299 for fn in cur:
300 if fn in prev:
301 if (cur.flags(fn) != prev.flags(fn) or cur[fn] != prev[fn]):
302 modified.add(fn)
303 del prev[fn]
304 else:
305 added.add(fn)
306 removed |= set(prev.keys())
307
308 return added | modified, removed
309
310def fixup_user_git(user):
311 name = mail = None
312 user = user.replace('"', '')
313 m = AUTHOR_RE.match(user)
314 if m:
315 name = m.group(1)
316 mail = m.group(2).strip()
317 else:
318 m = EMAIL_RE.match(user)
319 if m:
320 name = m.group(1)
321 mail = m.group(2)
322 else:
323 m = NAME_RE.match(user)
324 if m:
325 name = m.group(1).strip()
326 return (name, mail)
327
328def fixup_user_hg(user):
329 def sanitize(name):
330 # stole this from hg-git
331 return re.sub('[<>\n]', '?', name.lstrip('< ').rstrip('> '))
332
333 m = AUTHOR_HG_RE.match(user)
334 if m:
335 name = sanitize(m.group(1))
336 mail = sanitize(m.group(2))
337 ex = m.group(3)
338 if ex:
339 name += ' ext:(' + urllib.quote(ex) + ')'
340 else:
341 name = sanitize(user)
342 if '@' in user:
343 mail = name
344 else:
345 mail = None
346
347 return (name, mail)
348
349def fixup_user(user):
350 global mode, bad_mail
351
352 if mode == 'git':
353 name, mail = fixup_user_git(user)
354 else:
355 name, mail = fixup_user_hg(user)
356
357 if not name:
358 name = bad_name
359 if not mail:
360 mail = bad_mail
361
362 return '%s <%s>' % (name, mail)
363
364def updatebookmarks(repo, peer):
365 remotemarks = peer.listkeys('bookmarks')
366 localmarks = repo._bookmarks
367
368 if not remotemarks:
369 return
370
371 for k, v in remotemarks.iteritems():
372 localmarks[k] = hgbin(v)
373
374 if hasattr(localmarks, 'write'):
375 localmarks.write()
376 else:
377 bookmarks.write(repo)
378
379def get_repo(url, alias):
380 global dirname, peer
381
382 myui = ui.ui()
383 myui.setconfig('ui', 'interactive', 'off')
384 myui.fout = sys.stderr
385
386 if get_config_bool('remote-hg.insecure'):
387 myui.setconfig('web', 'cacerts', '')
388
389 extensions.loadall(myui)
390
391 if hg.islocal(url) and not os.environ.get('GIT_REMOTE_HG_TEST_REMOTE'):
392 repo = hg.repository(myui, url)
393 if not os.path.exists(dirname):
394 os.makedirs(dirname)
395 else:
396 shared_path = os.path.join(gitdir, 'hg')
397 if not os.path.exists(shared_path):
398 try:
399 hg.clone(myui, {}, url, shared_path, update=False, pull=True)
400 except:
401 die('Repository error')
402
403 if not os.path.exists(dirname):
404 os.makedirs(dirname)
405
406 local_path = os.path.join(dirname, 'clone')
407 if not os.path.exists(local_path):
408 hg.share(myui, shared_path, local_path, update=False)
409
410 repo = hg.repository(myui, local_path)
411 try:
412 peer = hg.peer(myui, {}, url)
413 except:
414 die('Repository error')
415 repo.pull(peer, heads=None, force=True)
416
417 updatebookmarks(repo, peer)
418
419 return repo
420
421def rev_to_mark(rev):
422 global marks
423 return marks.from_rev(rev.hex())
424
425def mark_to_rev(mark):
426 global marks
427 return marks.to_rev(mark)
428
429def export_ref(repo, name, kind, head):
430 global prefix, marks, mode
431
432 ename = '%s/%s' % (kind, name)
433 tip = marks.get_tip(ename)
434 if tip and tip in repo:
435 tip = repo[tip].rev()
436 else:
437 tip = 0
438
439 revs = xrange(tip, head.rev() + 1)
440 total = len(revs)
441
442 for rev in revs:
443
444 c = repo[rev]
445 node = c.node()
446
447 if marks.is_marked(c.hex()):
448 continue
449
450 (manifest, user, (time, tz), files, desc, extra) = repo.changelog.read(node)
451 rev_branch = extra['branch']
452
453 author = "%s %d %s" % (fixup_user(user), time, gittz(tz))
454 if 'committer' in extra:
455 user, time, tz = extra['committer'].rsplit(' ', 2)
456 committer = "%s %s %s" % (user, time, gittz(int(tz)))
457 else:
458 committer = author
459
460 parents = [repo[p] for p in repo.changelog.parentrevs(rev) if p >= 0]
461
462 if len(parents) == 0:
463 modified = c.manifest().keys()
464 removed = []
465 else:
466 modified, removed = get_filechanges(repo, c, parents[0])
467
468 desc += '\n'
469
470 if mode == 'hg':
471 extra_msg = ''
472
473 if rev_branch != 'default':
474 extra_msg += 'branch : %s\n' % rev_branch
475
476 renames = []
477 for f in c.files():
478 if f not in c.manifest():
479 continue
480 rename = c.filectx(f).renamed()
481 if rename:
482 renames.append((rename[0], f))
483
484 for e in renames:
485 extra_msg += "rename : %s => %s\n" % e
486
487 for key, value in extra.iteritems():
488 if key in ('author', 'committer', 'encoding', 'message', 'branch', 'hg-git'):
489 continue
490 else:
491 extra_msg += "extra : %s : %s\n" % (key, urllib.quote(value))
492
493 if extra_msg:
494 desc += '\n--HG--\n' + extra_msg
495
496 if len(parents) == 0 and rev:
497 print 'reset %s/%s' % (prefix, ename)
498
499 modified_final = export_files(c.filectx(f) for f in modified)
500
501 print "commit %s/%s" % (prefix, ename)
502 print "mark :%d" % (marks.get_mark(c.hex()))
503 print "author %s" % (author)
504 print "committer %s" % (committer)
505 print "data %d" % (len(desc))
506 print desc
507
508 if len(parents) > 0:
509 print "from :%s" % (rev_to_mark(parents[0]))
510 if len(parents) > 1:
511 print "merge :%s" % (rev_to_mark(parents[1]))
512
513 for f in modified_final:
514 print "M %s :%u %s" % f
515 for f in removed:
516 print "D %s" % (fix_file_path(f))
517 print
518
519 progress = (rev - tip)
520 if (progress % 100 == 0):
521 print "progress revision %d '%s' (%d/%d)" % (rev, name, progress, total)
522
523 # make sure the ref is updated
524 print "reset %s/%s" % (prefix, ename)
525 print "from :%u" % rev_to_mark(head)
526 print
527
528 marks.set_tip(ename, head.hex())
529
530def export_tag(repo, tag):
531 export_ref(repo, tag, 'tags', repo[hgref(tag)])
532
533def export_bookmark(repo, bmark):
534 head = bmarks[hgref(bmark)]
535 export_ref(repo, bmark, 'bookmarks', head)
536
537def export_branch(repo, branch):
538 tip = get_branch_tip(repo, branch)
539 head = repo[tip]
540 export_ref(repo, branch, 'branches', head)
541
542def export_head(repo):
543 global g_head
544 export_ref(repo, g_head[0], 'bookmarks', g_head[1])
545
546def do_capabilities(parser):
547 global prefix, dirname
548
549 print "import"
550 print "export"
551 print "refspec refs/heads/branches/*:%s/branches/*" % prefix
552 print "refspec refs/heads/*:%s/bookmarks/*" % prefix
553 print "refspec refs/tags/*:%s/tags/*" % prefix
554
555 path = os.path.join(dirname, 'marks-git')
556
557 if os.path.exists(path):
558 print "*import-marks %s" % path
559 print "*export-marks %s" % path
560
561 print
562
563def branch_tip(repo, branch):
564 # older versions of mercurial don't have this
565 if hasattr(repo, 'branchtip'):
566 return repo.branchtip(branch)
567 else:
568 return repo.branchtags()[branch]
569
570def get_branch_tip(repo, branch):
571 global branches
572
573 heads = branches.get(hgref(branch), None)
574 if not heads:
575 return None
576
577 # verify there's only one head
578 if (len(heads) > 1):
579 warn("Branch '%s' has more than one head, consider merging" % branch)
580 return branch_tip(repo, hgref(branch))
581
582 return heads[0]
583
584def list_head(repo, cur):
585 global g_head, bmarks, fake_bmark
586
587 if 'default' not in repo:
588 # empty repo
589 return
590
591 node = repo['default']
592 head = 'master' if not 'master' in bmarks else 'default'
593 fake_bmark = head
594 bmarks[head] = node
595
596 head = gitref(head)
597 print "@refs/heads/%s HEAD" % head
598 g_head = (head, node)
599
600def do_list(parser):
601 global branches, bmarks, track_branches
602
603 repo = parser.repo
604 for bmark, node in bookmarks.listbookmarks(repo).iteritems():
605 bmarks[bmark] = repo[node]
606
607 cur = repo.dirstate.branch()
608
609 list_head(repo, cur)
610
611 if track_branches:
612 for branch in repo.branchmap():
613 heads = repo.branchheads(branch)
614 if len(heads):
615 branches[branch] = heads
616
617 for branch in branches:
618 print "? refs/heads/branches/%s" % gitref(branch)
619
620 for bmark in bmarks:
621 print "? refs/heads/%s" % gitref(bmark)
622
623 for tag, node in repo.tagslist():
624 if tag == 'tip':
625 continue
626 print "? refs/tags/%s" % gitref(tag)
627
628 print
629
630def do_import(parser):
631 repo = parser.repo
632
633 path = os.path.join(dirname, 'marks-git')
634
635 print "feature done"
636 if os.path.exists(path):
637 print "feature import-marks=%s" % path
638 print "feature export-marks=%s" % path
639 print "feature force"
640 sys.stdout.flush()
641
642 tmp = encoding.encoding
643 encoding.encoding = 'utf-8'
644
645 # lets get all the import lines
646 while parser.check('import'):
647 ref = parser[1]
648
649 if (ref == 'HEAD'):
650 export_head(repo)
651 elif ref.startswith('refs/heads/branches/'):
652 branch = ref[len('refs/heads/branches/'):]
653 export_branch(repo, branch)
654 elif ref.startswith('refs/heads/'):
655 bmark = ref[len('refs/heads/'):]
656 export_bookmark(repo, bmark)
657 elif ref.startswith('refs/tags/'):
658 tag = ref[len('refs/tags/'):]
659 export_tag(repo, tag)
660
661 parser.next()
662
663 encoding.encoding = tmp
664
665 print 'done'
666
667def parse_blob(parser):
668 global blob_marks
669
670 parser.next()
671 mark = parser.get_mark()
672 parser.next()
673 data = parser.get_data()
674 blob_marks[mark] = data
675 parser.next()
676
677def get_merge_files(repo, p1, p2, files):
678 for e in repo[p1].files():
679 if e not in files:
680 if e not in repo[p1].manifest():
681 continue
682 f = { 'ctx' : repo[p1][e] }
683 files[e] = f
684
685def parse_commit(parser):
686 global marks, blob_marks, parsed_refs
687 global mode
688
689 from_mark = merge_mark = None
690
691 ref = parser[1]
692 parser.next()
693
694 commit_mark = parser.get_mark()
695 parser.next()
696 author = parser.get_author()
697 parser.next()
698 committer = parser.get_author()
699 parser.next()
700 data = parser.get_data()
701 parser.next()
702 if parser.check('from'):
703 from_mark = parser.get_mark()
704 parser.next()
705 if parser.check('merge'):
706 merge_mark = parser.get_mark()
707 parser.next()
708 if parser.check('merge'):
709 die('octopus merges are not supported yet')
710
711 # fast-export adds an extra newline
712 if data[-1] == '\n':
713 data = data[:-1]
714
715 files = {}
716
717 for line in parser:
718 if parser.check('M'):
719 t, m, mark_ref, path = line.split(' ', 3)
720 mark = int(mark_ref[1:])
721 f = { 'mode' : hgmode(m), 'data' : blob_marks[mark] }
722 elif parser.check('D'):
723 t, path = line.split(' ', 1)
724 f = { 'deleted' : True }
725 else:
726 die('Unknown file command: %s' % line)
727 files[path] = f
728
729 def getfilectx(repo, memctx, f):
730 of = files[f]
731 if 'deleted' in of:
732 raise IOError
733 if 'ctx' in of:
734 return of['ctx']
735 is_exec = of['mode'] == 'x'
736 is_link = of['mode'] == 'l'
737 rename = of.get('rename', None)
738 return context.memfilectx(f, of['data'],
739 is_link, is_exec, rename)
740
741 repo = parser.repo
742
743 user, date, tz = author
744 extra = {}
745
746 if committer != author:
747 extra['committer'] = "%s %u %u" % committer
748
749 if from_mark:
750 p1 = mark_to_rev(from_mark)
751 else:
752 p1 = '0' * 40
753
754 if merge_mark:
755 p2 = mark_to_rev(merge_mark)
756 else:
757 p2 = '0' * 40
758
759 #
760 # If files changed from any of the parents, hg wants to know, but in git if
761 # nothing changed from the first parent, nothing changed.
762 #
763 if merge_mark:
764 get_merge_files(repo, p1, p2, files)
765
766 # Check if the ref is supposed to be a named branch
767 if ref.startswith('refs/heads/branches/'):
768 branch = ref[len('refs/heads/branches/'):]
769 extra['branch'] = hgref(branch)
770
771 if mode == 'hg':
772 i = data.find('\n--HG--\n')
773 if i >= 0:
774 tmp = data[i + len('\n--HG--\n'):].strip()
775 for k, v in [e.split(' : ', 1) for e in tmp.split('\n')]:
776 if k == 'rename':
777 old, new = v.split(' => ', 1)
778 files[new]['rename'] = old
779 elif k == 'branch':
780 extra[k] = v
781 elif k == 'extra':
782 ek, ev = v.split(' : ', 1)
783 extra[ek] = urllib.unquote(ev)
784 data = data[:i]
785
786 ctx = context.memctx(repo, (p1, p2), data,
787 files.keys(), getfilectx,
788 user, (date, tz), extra)
789
790 tmp = encoding.encoding
791 encoding.encoding = 'utf-8'
792
793 node = hghex(repo.commitctx(ctx))
794
795 encoding.encoding = tmp
796
797 parsed_refs[ref] = node
798 marks.new_mark(node, commit_mark)
799
800def parse_reset(parser):
801 global parsed_refs
802
803 ref = parser[1]
804 parser.next()
805 # ugh
806 if parser.check('commit'):
807 parse_commit(parser)
808 return
809 if not parser.check('from'):
810 return
811 from_mark = parser.get_mark()
812 parser.next()
813
814 rev = mark_to_rev(from_mark)
815 parsed_refs[ref] = rev
816
817def parse_tag(parser):
818 name = parser[1]
819 parser.next()
820 from_mark = parser.get_mark()
821 parser.next()
822 tagger = parser.get_author()
823 parser.next()
824 data = parser.get_data()
825 parser.next()
826
827 parsed_tags[name] = (tagger, data)
828
829def write_tag(repo, tag, node, msg, author):
830 branch = repo[node].branch()
831 tip = branch_tip(repo, branch)
832 tip = repo[tip]
833
834 def getfilectx(repo, memctx, f):
835 try:
836 fctx = tip.filectx(f)
837 data = fctx.data()
838 except error.ManifestLookupError:
839 data = ""
840 content = data + "%s %s\n" % (node, tag)
841 return context.memfilectx(f, content, False, False, None)
842
843 p1 = tip.hex()
844 p2 = '0' * 40
845 if not author:
846 author = (None, 0, 0)
847 user, date, tz = author
848
849 ctx = context.memctx(repo, (p1, p2), msg,
850 ['.hgtags'], getfilectx,
851 user, (date, tz), {'branch' : branch})
852
853 tmp = encoding.encoding
854 encoding.encoding = 'utf-8'
855
856 tagnode = repo.commitctx(ctx)
857
858 encoding.encoding = tmp
859
860 return (tagnode, branch)
861
862def checkheads_bmark(repo, ref, ctx):
863 if force_push:
864 return True
865
866 bmark = ref[len('refs/heads/'):]
867 if not bmark in bmarks:
868 # new bmark
869 return True
870
871 ctx_old = bmarks[bmark]
872 ctx_new = ctx
873 if not repo.changelog.descendant(ctx_old.rev(), ctx_new.rev()):
874 print "error %s non-fast forward" % ref
875 return False
876
877 return True
878
879def checkheads(repo, remote, p_revs):
880
881 remotemap = remote.branchmap()
882 if not remotemap:
883 # empty repo
884 return True
885
886 new = {}
887 ret = True
888
889 for node, ref in p_revs.iteritems():
890 ctx = repo[node]
891 branch = ctx.branch()
892 if not branch in remotemap:
893 # new branch
894 continue
895 if not ref.startswith('refs/heads/branches'):
896 if ref.startswith('refs/heads/'):
897 if not checkheads_bmark(repo, ref, ctx):
898 ret = False
899
900 # only check branches
901 continue
902 new.setdefault(branch, []).append(ctx.rev())
903
904 for branch, heads in new.iteritems():
905 old = [repo.changelog.rev(x) for x in remotemap[branch]]
906 for rev in heads:
907 if check_version(2, 3):
908 ancestors = repo.changelog.ancestors([rev], stoprev=min(old))
909 else:
910 ancestors = repo.changelog.ancestors(rev)
911 found = False
912
913 for x in old:
914 if x in ancestors:
915 found = True
916 break
917
918 if found:
919 continue
920
921 node = repo.changelog.node(rev)
922 print "error %s non-fast forward" % p_revs[node]
923 ret = False
924
925 return ret
926
927def push_unsafe(repo, remote, parsed_refs, p_revs):
928
929 force = force_push
930
931 fci = discovery.findcommonincoming
932 commoninc = fci(repo, remote, force=force)
933 common, _, remoteheads = commoninc
934
935 if not force and not checkheads(repo, remote, p_revs):
936 return None
937
938 cg = repo.getbundle('push', heads=list(p_revs), common=common)
939
940 unbundle = remote.capable('unbundle')
941 if unbundle:
942 if force:
943 remoteheads = ['force']
944 return remote.unbundle(cg, remoteheads, 'push')
945 else:
946 return remote.addchangegroup(cg, 'push', repo.url())
947
948def push(repo, remote, parsed_refs, p_revs):
949 if hasattr(remote, 'canpush') and not remote.canpush():
950 print "error cannot push"
951
952 if not p_revs:
953 # nothing to push
954 return
955
956 lock = None
957 unbundle = remote.capable('unbundle')
958 if not unbundle:
959 lock = remote.lock()
960 try:
961 ret = push_unsafe(repo, remote, parsed_refs, p_revs)
962 finally:
963 if lock is not None:
964 lock.release()
965
966 return ret
967
968def do_export(parser):
969 global parsed_refs, bmarks, peer
970
971 p_bmarks = []
972 p_revs = {}
973
974 parser.next()
975
976 for line in parser.each_block('done'):
977 if parser.check('blob'):
978 parse_blob(parser)
979 elif parser.check('commit'):
980 parse_commit(parser)
981 elif parser.check('reset'):
982 parse_reset(parser)
983 elif parser.check('tag'):
984 parse_tag(parser)
985 elif parser.check('feature'):
986 pass
987 else:
988 die('unhandled export command: %s' % line)
989
990 for ref, node in parsed_refs.iteritems():
991 bnode = hgbin(node)
992 if ref.startswith('refs/heads/branches'):
993 branch = ref[len('refs/heads/branches/'):]
994 if branch in branches and bnode in branches[branch]:
995 # up to date
996 continue
997 p_revs[bnode] = ref
998 print "ok %s" % ref
999 elif ref.startswith('refs/heads/'):
1000 bmark = ref[len('refs/heads/'):]
1001 new = node
1002 old = bmarks[bmark].hex() if bmark in bmarks else ''
1003
1004 if old == new:
1005 continue
1006
1007 print "ok %s" % ref
1008 if bmark != fake_bmark and \
1009 not (bmark == 'master' and bmark not in parser.repo._bookmarks):
1010 p_bmarks.append((ref, bmark, old, new))
1011
1012 p_revs[bnode] = ref
1013 elif ref.startswith('refs/tags/'):
1014 tag = ref[len('refs/tags/'):]
1015 tag = hgref(tag)
1016 author, msg = parsed_tags.get(tag, (None, None))
1017 if mode == 'git':
1018 if not msg:
1019 msg = 'Added tag %s for changeset %s' % (tag, node[:12]);
1020 tagnode, branch = write_tag(parser.repo, tag, node, msg, author)
1021 p_revs[tagnode] = 'refs/heads/branches/' + gitref(branch)
1022 else:
1023 fp = parser.repo.opener('localtags', 'a')
1024 fp.write('%s %s\n' % (node, tag))
1025 fp.close()
1026 p_revs[bnode] = ref
1027 print "ok %s" % ref
1028 else:
1029 # transport-helper/fast-export bugs
1030 continue
1031
1032 if peer:
1033 if not push(parser.repo, peer, parsed_refs, p_revs):
1034 # do not update bookmarks
1035 print
1036 return
1037
1038 # update remote bookmarks
1039 remote_bmarks = peer.listkeys('bookmarks')
1040 for ref, bmark, old, new in p_bmarks:
1041 if force_push:
1042 old = remote_bmarks.get(bmark, '')
1043 if not peer.pushkey('bookmarks', bmark, old, new):
1044 print "error %s" % ref
1045 else:
1046 # update local bookmarks
1047 for ref, bmark, old, new in p_bmarks:
1048 if not bookmarks.pushbookmark(parser.repo, bmark, old, new):
1049 print "error %s" % ref
1050
1051 print
1052
1053def fix_path(alias, repo, orig_url):
1054 url = urlparse.urlparse(orig_url, 'file')
1055 if url.scheme != 'file' or os.path.isabs(url.path):
1056 return
1057 abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url)
1058 cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % abs_url]
1059 subprocess.call(cmd)
1060
1061def main(args):
1062 global prefix, gitdir, dirname, branches, bmarks
1063 global marks, blob_marks, parsed_refs
1064 global peer, mode, bad_mail, bad_name
1065 global track_branches, force_push, is_tmp
1066 global parsed_tags
1067 global filenodes
1068 global fake_bmark, hg_version
1069
1070 alias = args[1]
1071 url = args[2]
1072 peer = None
1073
1074 hg_git_compat = get_config_bool('remote-hg.hg-git-compat')
1075 track_branches = get_config_bool('remote-hg.track-branches', True)
1076 force_push = get_config_bool('remote-hg.force-push')
1077
1078 if hg_git_compat:
1079 mode = 'hg'
1080 bad_mail = 'none@none'
1081 bad_name = ''
1082 else:
1083 mode = 'git'
1084 bad_mail = 'unknown'
1085 bad_name = 'Unknown'
1086
1087 if alias[4:] == url:
1088 is_tmp = True
1089 alias = hashlib.sha1(alias).hexdigest()
1090 else:
1091 is_tmp = False
1092
1093 gitdir = os.environ['GIT_DIR']
1094 dirname = os.path.join(gitdir, 'hg', alias)
1095 branches = {}
1096 bmarks = {}
1097 blob_marks = {}
1098 parsed_refs = {}
1099 marks = None
1100 parsed_tags = {}
1101 filenodes = {}
1102 fake_bmark = None
1103 try:
1104 hg_version = tuple(int(e) for e in util.version().split('.'))
1105 except:
1106 hg_version = None
1107
1108 repo = get_repo(url, alias)
1109 prefix = 'refs/hg/%s' % alias
1110
1111 if not is_tmp:
1112 fix_path(alias, peer or repo, url)
1113
1114 marks_path = os.path.join(dirname, 'marks-hg')
1115 marks = Marks(marks_path, repo)
1116
1117 if sys.platform == 'win32':
1118 import msvcrt
1119 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
1120
1121 parser = Parser(repo)
1122 for line in parser:
1123 if parser.check('capabilities'):
1124 do_capabilities(parser)
1125 elif parser.check('list'):
1126 do_list(parser)
1127 elif parser.check('import'):
1128 do_import(parser)
1129 elif parser.check('export'):
1130 do_export(parser)
1131 else:
1132 die('unhandled command: %s' % line)
1133 sys.stdout.flush()
1134
1135def bye():
1136 if not marks:
1137 return
1138 if not is_tmp:
1139 marks.store()
1140 else:
1141 shutil.rmtree(dirname)
1142
1143atexit.register(bye)
1144sys.exit(main(sys.argv))