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