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