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