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