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(branch):
564 return branches[branch][-1]
565
566def get_branch_tip(repo, branch):
567 global branches
568
569 heads = branches.get(hgref(branch), None)
570 if not heads:
571 return None
572
573 # verify there's only one head
574 if (len(heads) > 1):
575 warn("Branch '%s' has more than one head, consider merging" % branch)
576 return branch_tip(hgref(branch))
577
578 return heads[0]
579
580def list_head(repo, cur):
581 global g_head, bmarks, fake_bmark
582
583 if 'default' not in branches:
584 # empty repo
585 return
586
587 node = repo[branch_tip('default')]
588 head = 'master' if not 'master' in bmarks else 'default'
589 fake_bmark = head
590 bmarks[head] = node
591
592 head = gitref(head)
593 print "@refs/heads/%s HEAD" % head
594 g_head = (head, node)
595
596def do_list(parser):
597 global branches, bmarks, track_branches
598
599 repo = parser.repo
600 for bmark, node in bookmarks.listbookmarks(repo).iteritems():
601 bmarks[bmark] = repo[node]
602
603 cur = repo.dirstate.branch()
604 orig = peer if peer else repo
605
606 for branch, heads in orig.branchmap().iteritems():
607 # only open heads
608 heads = [h for h in heads if 'close' not in repo.changelog.read(h)[5]]
609 if heads:
610 branches[branch] = heads
611
612 list_head(repo, cur)
613
614 if track_branches:
615 for branch in branches:
616 print "? refs/heads/branches/%s" % gitref(branch)
617
618 for bmark in bmarks:
619 print "? refs/heads/%s" % gitref(bmark)
620
621 for tag, node in repo.tagslist():
622 if tag == 'tip':
623 continue
624 print "? refs/tags/%s" % gitref(tag)
625
626 print
627
628def do_import(parser):
629 repo = parser.repo
630
631 path = os.path.join(dirname, 'marks-git')
632
633 print "feature done"
634 if os.path.exists(path):
635 print "feature import-marks=%s" % path
636 print "feature export-marks=%s" % path
637 print "feature force"
638 sys.stdout.flush()
639
640 tmp = encoding.encoding
641 encoding.encoding = 'utf-8'
642
643 # lets get all the import lines
644 while parser.check('import'):
645 ref = parser[1]
646
647 if (ref == 'HEAD'):
648 export_head(repo)
649 elif ref.startswith('refs/heads/branches/'):
650 branch = ref[len('refs/heads/branches/'):]
651 export_branch(repo, branch)
652 elif ref.startswith('refs/heads/'):
653 bmark = ref[len('refs/heads/'):]
654 export_bookmark(repo, bmark)
655 elif ref.startswith('refs/tags/'):
656 tag = ref[len('refs/tags/'):]
657 export_tag(repo, tag)
658
659 parser.next()
660
661 encoding.encoding = tmp
662
663 print 'done'
664
665def parse_blob(parser):
666 global blob_marks
667
668 parser.next()
669 mark = parser.get_mark()
670 parser.next()
671 data = parser.get_data()
672 blob_marks[mark] = data
673 parser.next()
674
675def get_merge_files(repo, p1, p2, files):
676 for e in repo[p1].files():
677 if e not in files:
678 if e not in repo[p1].manifest():
679 continue
680 f = { 'ctx' : repo[p1][e] }
681 files[e] = f
682
683def parse_commit(parser):
684 global marks, blob_marks, parsed_refs
685 global mode
686
687 from_mark = merge_mark = None
688
689 ref = parser[1]
690 parser.next()
691
692 commit_mark = parser.get_mark()
693 parser.next()
694 author = parser.get_author()
695 parser.next()
696 committer = parser.get_author()
697 parser.next()
698 data = parser.get_data()
699 parser.next()
700 if parser.check('from'):
701 from_mark = parser.get_mark()
702 parser.next()
703 if parser.check('merge'):
704 merge_mark = parser.get_mark()
705 parser.next()
706 if parser.check('merge'):
707 die('octopus merges are not supported yet')
708
709 # fast-export adds an extra newline
710 if data[-1] == '\n':
711 data = data[:-1]
712
713 files = {}
714
715 for line in parser:
716 if parser.check('M'):
717 t, m, mark_ref, path = line.split(' ', 3)
718 mark = int(mark_ref[1:])
719 f = { 'mode' : hgmode(m), 'data' : blob_marks[mark] }
720 elif parser.check('D'):
721 t, path = line.split(' ', 1)
722 f = { 'deleted' : True }
723 else:
724 die('Unknown file command: %s' % line)
725 files[path] = f
726
727 def getfilectx(repo, memctx, f):
728 of = files[f]
729 if 'deleted' in of:
730 raise IOError
731 if 'ctx' in of:
732 return of['ctx']
733 is_exec = of['mode'] == 'x'
734 is_link = of['mode'] == 'l'
735 rename = of.get('rename', None)
736 return context.memfilectx(f, of['data'],
737 is_link, is_exec, rename)
738
739 repo = parser.repo
740
741 user, date, tz = author
742 extra = {}
743
744 if committer != author:
745 extra['committer'] = "%s %u %u" % committer
746
747 if from_mark:
748 p1 = mark_to_rev(from_mark)
749 else:
750 p1 = '0' * 40
751
752 if merge_mark:
753 p2 = mark_to_rev(merge_mark)
754 else:
755 p2 = '0' * 40
756
757 #
758 # If files changed from any of the parents, hg wants to know, but in git if
759 # nothing changed from the first parent, nothing changed.
760 #
761 if merge_mark:
762 get_merge_files(repo, p1, p2, files)
763
764 # Check if the ref is supposed to be a named branch
765 if ref.startswith('refs/heads/branches/'):
766 branch = ref[len('refs/heads/branches/'):]
767 extra['branch'] = hgref(branch)
768
769 if mode == 'hg':
770 i = data.find('\n--HG--\n')
771 if i >= 0:
772 tmp = data[i + len('\n--HG--\n'):].strip()
773 for k, v in [e.split(' : ', 1) for e in tmp.split('\n')]:
774 if k == 'rename':
775 old, new = v.split(' => ', 1)
776 files[new]['rename'] = old
777 elif k == 'branch':
778 extra[k] = v
779 elif k == 'extra':
780 ek, ev = v.split(' : ', 1)
781 extra[ek] = urllib.unquote(ev)
782 data = data[:i]
783
784 ctx = context.memctx(repo, (p1, p2), data,
785 files.keys(), getfilectx,
786 user, (date, tz), extra)
787
788 tmp = encoding.encoding
789 encoding.encoding = 'utf-8'
790
791 node = hghex(repo.commitctx(ctx))
792
793 encoding.encoding = tmp
794
795 parsed_refs[ref] = node
796 marks.new_mark(node, commit_mark)
797
798def parse_reset(parser):
799 global parsed_refs
800
801 ref = parser[1]
802 parser.next()
803 # ugh
804 if parser.check('commit'):
805 parse_commit(parser)
806 return
807 if not parser.check('from'):
808 return
809 from_mark = parser.get_mark()
810 parser.next()
811
812 rev = mark_to_rev(from_mark)
813 parsed_refs[ref] = rev
814
815def parse_tag(parser):
816 name = parser[1]
817 parser.next()
818 from_mark = parser.get_mark()
819 parser.next()
820 tagger = parser.get_author()
821 parser.next()
822 data = parser.get_data()
823 parser.next()
824
825 parsed_tags[name] = (tagger, data)
826
827def write_tag(repo, tag, node, msg, author):
828 branch = repo[node].branch()
829 tip = branch_tip(branch)
830 tip = repo[tip]
831
832 def getfilectx(repo, memctx, f):
833 try:
834 fctx = tip.filectx(f)
835 data = fctx.data()
836 except error.ManifestLookupError:
837 data = ""
838 content = data + "%s %s\n" % (node, tag)
839 return context.memfilectx(f, content, False, False, None)
840
841 p1 = tip.hex()
842 p2 = '0' * 40
843 if not author:
844 author = (None, 0, 0)
845 user, date, tz = author
846
847 ctx = context.memctx(repo, (p1, p2), msg,
848 ['.hgtags'], getfilectx,
849 user, (date, tz), {'branch' : branch})
850
851 tmp = encoding.encoding
852 encoding.encoding = 'utf-8'
853
854 tagnode = repo.commitctx(ctx)
855
856 encoding.encoding = tmp
857
858 return (tagnode, branch)
859
860def checkheads_bmark(repo, ref, ctx):
861 if force_push:
862 return True
863
864 bmark = ref[len('refs/heads/'):]
865 if not bmark in bmarks:
866 # new bmark
867 return True
868
869 ctx_old = bmarks[bmark]
870 ctx_new = ctx
871 if not repo.changelog.descendant(ctx_old.rev(), ctx_new.rev()):
872 print "error %s non-fast forward" % ref
873 return False
874
875 return True
876
877def checkheads(repo, remote, p_revs):
878
879 remotemap = remote.branchmap()
880 if not remotemap:
881 # empty repo
882 return True
883
884 new = {}
885 ret = True
886
887 for node, ref in p_revs.iteritems():
888 ctx = repo[node]
889 branch = ctx.branch()
890 if not branch in remotemap:
891 # new branch
892 continue
893 if not ref.startswith('refs/heads/branches'):
894 if ref.startswith('refs/heads/'):
895 if not checkheads_bmark(repo, ref, ctx):
896 ret = False
897
898 # only check branches
899 continue
900 new.setdefault(branch, []).append(ctx.rev())
901
902 for branch, heads in new.iteritems():
903 old = [repo.changelog.rev(x) for x in remotemap[branch]]
904 for rev in heads:
905 if check_version(2, 3):
906 ancestors = repo.changelog.ancestors([rev], stoprev=min(old))
907 else:
908 ancestors = repo.changelog.ancestors(rev)
909 found = False
910
911 for x in old:
912 if x in ancestors:
913 found = True
914 break
915
916 if found:
917 continue
918
919 node = repo.changelog.node(rev)
920 print "error %s non-fast forward" % p_revs[node]
921 ret = False
922
923 return ret
924
925def push_unsafe(repo, remote, parsed_refs, p_revs):
926
927 force = force_push
928
929 fci = discovery.findcommonincoming
930 commoninc = fci(repo, remote, force=force)
931 common, _, remoteheads = commoninc
932
933 if not force and not checkheads(repo, remote, p_revs):
934 return None
935
936 cg = repo.getbundle('push', heads=list(p_revs), common=common)
937
938 unbundle = remote.capable('unbundle')
939 if unbundle:
940 if force:
941 remoteheads = ['force']
942 return remote.unbundle(cg, remoteheads, 'push')
943 else:
944 return remote.addchangegroup(cg, 'push', repo.url())
945
946def push(repo, remote, parsed_refs, p_revs):
947 if hasattr(remote, 'canpush') and not remote.canpush():
948 print "error cannot push"
949
950 if not p_revs:
951 # nothing to push
952 return
953
954 lock = None
955 unbundle = remote.capable('unbundle')
956 if not unbundle:
957 lock = remote.lock()
958 try:
959 ret = push_unsafe(repo, remote, parsed_refs, p_revs)
960 finally:
961 if lock is not None:
962 lock.release()
963
964 return ret
965
966def do_export(parser):
967 global parsed_refs, bmarks, peer
968
969 p_bmarks = []
970 p_revs = {}
971
972 parser.next()
973
974 for line in parser.each_block('done'):
975 if parser.check('blob'):
976 parse_blob(parser)
977 elif parser.check('commit'):
978 parse_commit(parser)
979 elif parser.check('reset'):
980 parse_reset(parser)
981 elif parser.check('tag'):
982 parse_tag(parser)
983 elif parser.check('feature'):
984 pass
985 else:
986 die('unhandled export command: %s' % line)
987
988 for ref, node in parsed_refs.iteritems():
989 bnode = hgbin(node)
990 if ref.startswith('refs/heads/branches'):
991 branch = ref[len('refs/heads/branches/'):]
992 if branch in branches and bnode in branches[branch]:
993 # up to date
994 continue
995 p_revs[bnode] = ref
996 print "ok %s" % ref
997 elif ref.startswith('refs/heads/'):
998 bmark = ref[len('refs/heads/'):]
999 new = node
1000 old = bmarks[bmark].hex() if bmark in bmarks else ''
1001
1002 if old == new:
1003 continue
1004
1005 print "ok %s" % ref
1006 if bmark != fake_bmark and \
1007 not (bmark == 'master' and bmark not in parser.repo._bookmarks):
1008 p_bmarks.append((ref, bmark, old, new))
1009
1010 p_revs[bnode] = ref
1011 elif ref.startswith('refs/tags/'):
1012 tag = ref[len('refs/tags/'):]
1013 tag = hgref(tag)
1014 author, msg = parsed_tags.get(tag, (None, None))
1015 if mode == 'git':
1016 if not msg:
1017 msg = 'Added tag %s for changeset %s' % (tag, node[:12]);
1018 tagnode, branch = write_tag(parser.repo, tag, node, msg, author)
1019 p_revs[tagnode] = 'refs/heads/branches/' + gitref(branch)
1020 else:
1021 fp = parser.repo.opener('localtags', 'a')
1022 fp.write('%s %s\n' % (node, tag))
1023 fp.close()
1024 p_revs[bnode] = ref
1025 print "ok %s" % ref
1026 else:
1027 # transport-helper/fast-export bugs
1028 continue
1029
1030 if peer:
1031 if not push(parser.repo, peer, parsed_refs, p_revs):
1032 # do not update bookmarks
1033 print
1034 return
1035
1036 # update remote bookmarks
1037 remote_bmarks = peer.listkeys('bookmarks')
1038 for ref, bmark, old, new in p_bmarks:
1039 if force_push:
1040 old = remote_bmarks.get(bmark, '')
1041 if not peer.pushkey('bookmarks', bmark, old, new):
1042 print "error %s" % ref
1043 else:
1044 # update local bookmarks
1045 for ref, bmark, old, new in p_bmarks:
1046 if not bookmarks.pushbookmark(parser.repo, bmark, old, new):
1047 print "error %s" % ref
1048
1049 print
1050
1051def fix_path(alias, repo, orig_url):
1052 url = urlparse.urlparse(orig_url, 'file')
1053 if url.scheme != 'file' or os.path.isabs(url.path):
1054 return
1055 abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url)
1056 cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % abs_url]
1057 subprocess.call(cmd)
1058
1059def main(args):
1060 global prefix, gitdir, dirname, branches, bmarks
1061 global marks, blob_marks, parsed_refs
1062 global peer, mode, bad_mail, bad_name
1063 global track_branches, force_push, is_tmp
1064 global parsed_tags
1065 global filenodes
1066 global fake_bmark, hg_version
1067
1068 alias = args[1]
1069 url = args[2]
1070 peer = None
1071
1072 hg_git_compat = get_config_bool('remote-hg.hg-git-compat')
1073 track_branches = get_config_bool('remote-hg.track-branches', True)
1074 force_push = get_config_bool('remote-hg.force-push')
1075
1076 if hg_git_compat:
1077 mode = 'hg'
1078 bad_mail = 'none@none'
1079 bad_name = ''
1080 else:
1081 mode = 'git'
1082 bad_mail = 'unknown'
1083 bad_name = 'Unknown'
1084
1085 if alias[4:] == url:
1086 is_tmp = True
1087 alias = hashlib.sha1(alias).hexdigest()
1088 else:
1089 is_tmp = False
1090
1091 gitdir = os.environ['GIT_DIR']
1092 dirname = os.path.join(gitdir, 'hg', alias)
1093 branches = {}
1094 bmarks = {}
1095 blob_marks = {}
1096 parsed_refs = {}
1097 marks = None
1098 parsed_tags = {}
1099 filenodes = {}
1100 fake_bmark = None
1101 try:
1102 hg_version = tuple(int(e) for e in util.version().split('.'))
1103 except:
1104 hg_version = None
1105
1106 repo = get_repo(url, alias)
1107 prefix = 'refs/hg/%s' % alias
1108
1109 if not is_tmp:
1110 fix_path(alias, peer or repo, url)
1111
1112 marks_path = os.path.join(dirname, 'marks-hg')
1113 marks = Marks(marks_path, repo)
1114
1115 if sys.platform == 'win32':
1116 import msvcrt
1117 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
1118
1119 parser = Parser(repo)
1120 for line in parser:
1121 if parser.check('capabilities'):
1122 do_capabilities(parser)
1123 elif parser.check('list'):
1124 do_list(parser)
1125 elif parser.check('import'):
1126 do_import(parser)
1127 elif parser.check('export'):
1128 do_export(parser)
1129 else:
1130 die('unhandled command: %s' % line)
1131 sys.stdout.flush()
1132
1133def bye():
1134 if not marks:
1135 return
1136 if not is_tmp:
1137 marks.store()
1138 else:
1139 shutil.rmtree(dirname)
1140
1141atexit.register(bye)
1142sys.exit(main(sys.argv))