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 parse_commit(parser):
682 global marks, blob_marks, parsed_refs
683 global mode
684
685 from_mark = merge_mark = None
686
687 ref = parser[1]
688 parser.next()
689
690 commit_mark = parser.get_mark()
691 parser.next()
692 author = parser.get_author()
693 parser.next()
694 committer = parser.get_author()
695 parser.next()
696 data = parser.get_data()
697 parser.next()
698 if parser.check('from'):
699 from_mark = parser.get_mark()
700 parser.next()
701 if parser.check('merge'):
702 merge_mark = parser.get_mark()
703 parser.next()
704 if parser.check('merge'):
705 die('octopus merges are not supported yet')
706
707 # fast-export adds an extra newline
708 if data[-1] == '\n':
709 data = data[:-1]
710
711 files = {}
712
713 for line in parser:
714 if parser.check('M'):
715 t, m, mark_ref, path = line.split(' ', 3)
716 mark = int(mark_ref[1:])
717 f = { 'mode' : hgmode(m), 'data' : blob_marks[mark] }
718 elif parser.check('D'):
719 t, path = line.split(' ', 1)
720 f = { 'deleted' : True }
721 else:
722 die('Unknown file command: %s' % line)
723 files[path] = f
724
725 # only export the commits if we are on an internal proxy repo
726 if dry_run and not peer:
727 parsed_refs[ref] = None
728 return
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 try:
816 rev = mark_to_rev(from_mark)
817 except KeyError:
818 rev = None
819 parsed_refs[ref] = rev
820
821def parse_tag(parser):
822 name = parser[1]
823 parser.next()
824 from_mark = parser.get_mark()
825 parser.next()
826 tagger = parser.get_author()
827 parser.next()
828 data = parser.get_data()
829 parser.next()
830
831 parsed_tags[name] = (tagger, data)
832
833def write_tag(repo, tag, node, msg, author):
834 branch = repo[node].branch()
835 tip = branch_tip(branch)
836 tip = repo[tip]
837
838 def getfilectx(repo, memctx, f):
839 try:
840 fctx = tip.filectx(f)
841 data = fctx.data()
842 except error.ManifestLookupError:
843 data = ""
844 content = data + "%s %s\n" % (node, tag)
845 return context.memfilectx(f, content, False, False, None)
846
847 p1 = tip.hex()
848 p2 = '0' * 40
849 if author:
850 user, date, tz = author
851 date_tz = (date, tz)
852 else:
853 cmd = ['git', 'var', 'GIT_COMMITTER_IDENT']
854 process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
855 output, _ = process.communicate()
856 m = re.match('^.* <.*>', output)
857 if m:
858 user = m.group(0)
859 else:
860 user = repo.ui.username()
861 date_tz = None
862
863 ctx = context.memctx(repo, (p1, p2), msg,
864 ['.hgtags'], getfilectx,
865 user, date_tz, {'branch' : branch})
866
867 tmp = encoding.encoding
868 encoding.encoding = 'utf-8'
869
870 tagnode = repo.commitctx(ctx)
871
872 encoding.encoding = tmp
873
874 return (tagnode, branch)
875
876def checkheads_bmark(repo, ref, ctx):
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 if force_push:
886 print "ok %s forced update" % ref
887 else:
888 print "error %s non-fast forward" % ref
889 return False
890
891 return True
892
893def checkheads(repo, remote, p_revs):
894
895 remotemap = remote.branchmap()
896 if not remotemap:
897 # empty repo
898 return True
899
900 new = {}
901 ret = True
902
903 for node, ref in p_revs.iteritems():
904 ctx = repo[node]
905 branch = ctx.branch()
906 if not branch in remotemap:
907 # new branch
908 continue
909 if not ref.startswith('refs/heads/branches'):
910 if ref.startswith('refs/heads/'):
911 if not checkheads_bmark(repo, ref, ctx):
912 ret = False
913
914 # only check branches
915 continue
916 new.setdefault(branch, []).append(ctx.rev())
917
918 for branch, heads in new.iteritems():
919 old = [repo.changelog.rev(x) for x in remotemap[branch]]
920 for rev in heads:
921 if check_version(2, 3):
922 ancestors = repo.changelog.ancestors([rev], stoprev=min(old))
923 else:
924 ancestors = repo.changelog.ancestors(rev)
925 found = False
926
927 for x in old:
928 if x in ancestors:
929 found = True
930 break
931
932 if found:
933 continue
934
935 node = repo.changelog.node(rev)
936 ref = p_revs[node]
937 if force_push:
938 print "ok %s forced update" % ref
939 else:
940 print "error %s non-fast forward" % ref
941 ret = False
942
943 return ret
944
945def push_unsafe(repo, remote, parsed_refs, p_revs):
946
947 force = force_push
948
949 fci = discovery.findcommonincoming
950 commoninc = fci(repo, remote, force=force)
951 common, _, remoteheads = commoninc
952
953 if not checkheads(repo, remote, p_revs):
954 return None
955
956 cg = repo.getbundle('push', heads=list(p_revs), common=common)
957
958 unbundle = remote.capable('unbundle')
959 if unbundle:
960 if force:
961 remoteheads = ['force']
962 return remote.unbundle(cg, remoteheads, 'push')
963 else:
964 return remote.addchangegroup(cg, 'push', repo.url())
965
966def push(repo, remote, parsed_refs, p_revs):
967 if hasattr(remote, 'canpush') and not remote.canpush():
968 print "error cannot push"
969
970 if not p_revs:
971 # nothing to push
972 return
973
974 lock = None
975 unbundle = remote.capable('unbundle')
976 if not unbundle:
977 lock = remote.lock()
978 try:
979 ret = push_unsafe(repo, remote, parsed_refs, p_revs)
980 finally:
981 if lock is not None:
982 lock.release()
983
984 return ret
985
986def check_tip(ref, kind, name, heads):
987 try:
988 ename = '%s/%s' % (kind, name)
989 tip = marks.get_tip(ename)
990 except KeyError:
991 return True
992 else:
993 return tip in heads
994
995def do_export(parser):
996 global parsed_refs, bmarks, peer
997
998 p_bmarks = []
999 p_revs = {}
1000
1001 parser.next()
1002
1003 for line in parser.each_block('done'):
1004 if parser.check('blob'):
1005 parse_blob(parser)
1006 elif parser.check('commit'):
1007 parse_commit(parser)
1008 elif parser.check('reset'):
1009 parse_reset(parser)
1010 elif parser.check('tag'):
1011 parse_tag(parser)
1012 elif parser.check('feature'):
1013 pass
1014 else:
1015 die('unhandled export command: %s' % line)
1016
1017 need_fetch = False
1018
1019 for ref, node in parsed_refs.iteritems():
1020 bnode = hgbin(node) if node else None
1021 if ref.startswith('refs/heads/branches'):
1022 branch = ref[len('refs/heads/branches/'):]
1023 if branch in branches and bnode in branches[branch]:
1024 # up to date
1025 continue
1026
1027 if peer:
1028 remotemap = peer.branchmap()
1029 if remotemap and branch in remotemap:
1030 heads = [hghex(e) for e in remotemap[branch]]
1031 if not check_tip(ref, 'branches', branch, heads):
1032 print "error %s fetch first" % ref
1033 need_fetch = True
1034 continue
1035
1036 p_revs[bnode] = ref
1037 print "ok %s" % ref
1038 elif ref.startswith('refs/heads/'):
1039 bmark = ref[len('refs/heads/'):]
1040 new = node
1041 old = bmarks[bmark].hex() if bmark in bmarks else ''
1042
1043 if old == new:
1044 continue
1045
1046 print "ok %s" % ref
1047 if bmark != fake_bmark and \
1048 not (bmark == 'master' and bmark not in parser.repo._bookmarks):
1049 p_bmarks.append((ref, bmark, old, new))
1050
1051 if peer:
1052 remote_old = peer.listkeys('bookmarks').get(bmark)
1053 if remote_old:
1054 if not check_tip(ref, 'bookmarks', bmark, remote_old):
1055 print "error %s fetch first" % ref
1056 need_fetch = True
1057 continue
1058
1059 p_revs[bnode] = ref
1060 elif ref.startswith('refs/tags/'):
1061 if dry_run:
1062 print "ok %s" % ref
1063 continue
1064 tag = ref[len('refs/tags/'):]
1065 tag = hgref(tag)
1066 author, msg = parsed_tags.get(tag, (None, None))
1067 if mode == 'git':
1068 if not msg:
1069 msg = 'Added tag %s for changeset %s' % (tag, node[:12]);
1070 tagnode, branch = write_tag(parser.repo, tag, node, msg, author)
1071 p_revs[tagnode] = 'refs/heads/branches/' + gitref(branch)
1072 else:
1073 fp = parser.repo.opener('localtags', 'a')
1074 fp.write('%s %s\n' % (node, tag))
1075 fp.close()
1076 p_revs[bnode] = ref
1077 print "ok %s" % ref
1078 else:
1079 # transport-helper/fast-export bugs
1080 continue
1081
1082 if need_fetch:
1083 print
1084 return
1085
1086 if dry_run:
1087 if peer and not force_push:
1088 checkheads(parser.repo, peer, p_revs)
1089 print
1090 return
1091
1092 if peer:
1093 if not push(parser.repo, peer, parsed_refs, p_revs):
1094 # do not update bookmarks
1095 print
1096 return
1097
1098 # update remote bookmarks
1099 remote_bmarks = peer.listkeys('bookmarks')
1100 for ref, bmark, old, new in p_bmarks:
1101 if force_push:
1102 old = remote_bmarks.get(bmark, '')
1103 if not peer.pushkey('bookmarks', bmark, old, new):
1104 print "error %s" % ref
1105 else:
1106 # update local bookmarks
1107 for ref, bmark, old, new in p_bmarks:
1108 if not bookmarks.pushbookmark(parser.repo, bmark, old, new):
1109 print "error %s" % ref
1110
1111 print
1112
1113def do_option(parser):
1114 global dry_run, force_push
1115 _, key, value = parser.line.split(' ')
1116 if key == 'dry-run':
1117 dry_run = (value == 'true')
1118 print 'ok'
1119 elif key == 'force':
1120 force_push = (value == 'true')
1121 print 'ok'
1122 else:
1123 print 'unsupported'
1124
1125def fix_path(alias, repo, orig_url):
1126 url = urlparse.urlparse(orig_url, 'file')
1127 if url.scheme != 'file' or os.path.isabs(url.path):
1128 return
1129 abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url)
1130 cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % abs_url]
1131 subprocess.call(cmd)
1132
1133def main(args):
1134 global prefix, gitdir, dirname, branches, bmarks
1135 global marks, blob_marks, parsed_refs
1136 global peer, mode, bad_mail, bad_name
1137 global track_branches, force_push, is_tmp
1138 global parsed_tags
1139 global filenodes
1140 global fake_bmark, hg_version
1141 global dry_run
1142
1143 alias = args[1]
1144 url = args[2]
1145 peer = None
1146
1147 hg_git_compat = get_config_bool('remote-hg.hg-git-compat')
1148 track_branches = get_config_bool('remote-hg.track-branches', True)
1149 force_push = False
1150
1151 if hg_git_compat:
1152 mode = 'hg'
1153 bad_mail = 'none@none'
1154 bad_name = ''
1155 else:
1156 mode = 'git'
1157 bad_mail = 'unknown'
1158 bad_name = 'Unknown'
1159
1160 if alias[4:] == url:
1161 is_tmp = True
1162 alias = hashlib.sha1(alias).hexdigest()
1163 else:
1164 is_tmp = False
1165
1166 gitdir = os.environ['GIT_DIR']
1167 dirname = os.path.join(gitdir, 'hg', alias)
1168 branches = {}
1169 bmarks = {}
1170 blob_marks = {}
1171 parsed_refs = {}
1172 marks = None
1173 parsed_tags = {}
1174 filenodes = {}
1175 fake_bmark = None
1176 try:
1177 hg_version = tuple(int(e) for e in util.version().split('.'))
1178 except:
1179 hg_version = None
1180 dry_run = False
1181
1182 repo = get_repo(url, alias)
1183 prefix = 'refs/hg/%s' % alias
1184
1185 if not is_tmp:
1186 fix_path(alias, peer or repo, url)
1187
1188 marks_path = os.path.join(dirname, 'marks-hg')
1189 marks = Marks(marks_path, repo)
1190
1191 if sys.platform == 'win32':
1192 import msvcrt
1193 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
1194
1195 parser = Parser(repo)
1196 for line in parser:
1197 if parser.check('capabilities'):
1198 do_capabilities(parser)
1199 elif parser.check('list'):
1200 do_list(parser)
1201 elif parser.check('import'):
1202 do_import(parser)
1203 elif parser.check('export'):
1204 do_export(parser)
1205 elif parser.check('option'):
1206 do_option(parser)
1207 else:
1208 die('unhandled command: %s' % line)
1209 sys.stdout.flush()
1210
1211def bye():
1212 if not marks:
1213 return
1214 if not is_tmp:
1215 marks.store()
1216 else:
1217 shutil.rmtree(dirname)
1218
1219atexit.register(bye)
1220sys.exit(main(sys.argv))