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