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