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