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