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 print "? refs/heads/%s" % gitref(bmark)
648
649 for tag, node in repo.tagslist():
650 if tag == 'tip':
651 continue
652 print "? refs/tags/%s" % gitref(tag)
653
654 print
655
656def do_import(parser):
657 repo = parser.repo
658
659 path = os.path.join(dirname, 'marks-git')
660
661 print "feature done"
662 if os.path.exists(path):
663 print "feature import-marks=%s" % path
664 print "feature export-marks=%s" % path
665 print "feature force"
666 sys.stdout.flush()
667
668 tmp = encoding.encoding
669 encoding.encoding = 'utf-8'
670
671 # lets get all the import lines
672 while parser.check('import'):
673 ref = parser[1]
674
675 if (ref == 'HEAD'):
676 export_head(repo)
677 elif ref.startswith('refs/heads/branches/'):
678 branch = ref[len('refs/heads/branches/'):]
679 export_branch(repo, branch)
680 elif ref.startswith('refs/heads/'):
681 bmark = ref[len('refs/heads/'):]
682 export_bookmark(repo, bmark)
683 elif ref.startswith('refs/tags/'):
684 tag = ref[len('refs/tags/'):]
685 export_tag(repo, tag)
686
687 parser.next()
688
689 encoding.encoding = tmp
690
691 print 'done'
692
693def parse_blob(parser):
694 parser.next()
695 mark = parser.get_mark()
696 parser.next()
697 data = parser.get_data()
698 blob_marks[mark] = data
699 parser.next()
700
701def get_merge_files(repo, p1, p2, files):
702 for e in repo[p1].files():
703 if e not in files:
704 if e not in repo[p1].manifest():
705 continue
706 f = { 'ctx' : repo[p1][e] }
707 files[e] = f
708
709def c_style_unescape(string):
710 if string[0] == string[-1] == '"':
711 return string.decode('string-escape')[1:-1]
712 return string
713
714def parse_commit(parser):
715 from_mark = merge_mark = None
716
717 ref = parser[1]
718 parser.next()
719
720 commit_mark = parser.get_mark()
721 parser.next()
722 author = parser.get_author()
723 parser.next()
724 committer = parser.get_author()
725 parser.next()
726 data = parser.get_data()
727 parser.next()
728 if parser.check('from'):
729 from_mark = parser.get_mark()
730 parser.next()
731 if parser.check('merge'):
732 merge_mark = parser.get_mark()
733 parser.next()
734 if parser.check('merge'):
735 die('octopus merges are not supported yet')
736
737 # fast-export adds an extra newline
738 if data[-1] == '\n':
739 data = data[:-1]
740
741 files = {}
742
743 for line in parser:
744 if parser.check('M'):
745 t, m, mark_ref, path = line.split(' ', 3)
746 mark = int(mark_ref[1:])
747 f = { 'mode' : hgmode(m), 'data' : blob_marks[mark] }
748 elif parser.check('D'):
749 t, path = line.split(' ', 1)
750 f = { 'deleted' : True }
751 else:
752 die('Unknown file command: %s' % line)
753 path = c_style_unescape(path)
754 files[path] = f
755
756 # only export the commits if we are on an internal proxy repo
757 if dry_run and not peer:
758 parsed_refs[ref] = None
759 return
760
761 def getfilectx(repo, memctx, f):
762 of = files[f]
763 if 'deleted' in of:
764 raise IOError
765 if 'ctx' in of:
766 return of['ctx']
767 is_exec = of['mode'] == 'x'
768 is_link = of['mode'] == 'l'
769 rename = of.get('rename', None)
770 return context.memfilectx(f, of['data'],
771 is_link, is_exec, rename)
772
773 repo = parser.repo
774
775 user, date, tz = author
776 extra = {}
777
778 if committer != author:
779 extra['committer'] = "%s %u %u" % committer
780
781 if from_mark:
782 p1 = mark_to_rev(from_mark)
783 else:
784 p1 = '0' * 40
785
786 if merge_mark:
787 p2 = mark_to_rev(merge_mark)
788 else:
789 p2 = '0' * 40
790
791 #
792 # If files changed from any of the parents, hg wants to know, but in git if
793 # nothing changed from the first parent, nothing changed.
794 #
795 if merge_mark:
796 get_merge_files(repo, p1, p2, files)
797
798 # Check if the ref is supposed to be a named branch
799 if ref.startswith('refs/heads/branches/'):
800 branch = ref[len('refs/heads/branches/'):]
801 extra['branch'] = hgref(branch)
802
803 if mode == 'hg':
804 i = data.find('\n--HG--\n')
805 if i >= 0:
806 tmp = data[i + len('\n--HG--\n'):].strip()
807 for k, v in [e.split(' : ', 1) for e in tmp.split('\n')]:
808 if k == 'rename':
809 old, new = v.split(' => ', 1)
810 files[new]['rename'] = old
811 elif k == 'branch':
812 extra[k] = v
813 elif k == 'extra':
814 ek, ev = v.split(' : ', 1)
815 extra[ek] = urllib.unquote(ev)
816 data = data[:i]
817
818 ctx = context.memctx(repo, (p1, p2), data,
819 files.keys(), getfilectx,
820 user, (date, tz), extra)
821
822 tmp = encoding.encoding
823 encoding.encoding = 'utf-8'
824
825 node = hghex(repo.commitctx(ctx))
826
827 encoding.encoding = tmp
828
829 parsed_refs[ref] = node
830 marks.new_mark(node, commit_mark)
831
832def parse_reset(parser):
833 ref = parser[1]
834 parser.next()
835 # ugh
836 if parser.check('commit'):
837 parse_commit(parser)
838 return
839 if not parser.check('from'):
840 return
841 from_mark = parser.get_mark()
842 parser.next()
843
844 try:
845 rev = mark_to_rev(from_mark)
846 except KeyError:
847 rev = None
848 parsed_refs[ref] = rev
849
850def parse_tag(parser):
851 name = parser[1]
852 parser.next()
853 from_mark = parser.get_mark()
854 parser.next()
855 tagger = parser.get_author()
856 parser.next()
857 data = parser.get_data()
858 parser.next()
859
860 parsed_tags[name] = (tagger, data)
861
862def write_tag(repo, tag, node, msg, author):
863 branch = repo[node].branch()
864 tip = branch_tip(branch)
865 tip = repo[tip]
866
867 def getfilectx(repo, memctx, f):
868 try:
869 fctx = tip.filectx(f)
870 data = fctx.data()
871 except error.ManifestLookupError:
872 data = ""
873 content = data + "%s %s\n" % (node, tag)
874 return context.memfilectx(f, content, False, False, None)
875
876 p1 = tip.hex()
877 p2 = '0' * 40
878 if author:
879 user, date, tz = author
880 date_tz = (date, tz)
881 else:
882 cmd = ['git', 'var', 'GIT_COMMITTER_IDENT']
883 process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
884 output, _ = process.communicate()
885 m = re.match('^.* <.*>', output)
886 if m:
887 user = m.group(0)
888 else:
889 user = repo.ui.username()
890 date_tz = None
891
892 ctx = context.memctx(repo, (p1, p2), msg,
893 ['.hgtags'], getfilectx,
894 user, date_tz, {'branch' : branch})
895
896 tmp = encoding.encoding
897 encoding.encoding = 'utf-8'
898
899 tagnode = repo.commitctx(ctx)
900
901 encoding.encoding = tmp
902
903 return (tagnode, branch)
904
905def checkheads_bmark(repo, ref, ctx):
906 bmark = ref[len('refs/heads/'):]
907 if not bmark in bmarks:
908 # new bmark
909 return True
910
911 ctx_old = bmarks[bmark]
912 ctx_new = ctx
913 if not repo.changelog.descendant(ctx_old.rev(), ctx_new.rev()):
914 if force_push:
915 print "ok %s forced update" % ref
916 else:
917 print "error %s non-fast forward" % ref
918 return False
919
920 return True
921
922def checkheads(repo, remote, p_revs):
923
924 remotemap = remote.branchmap()
925 if not remotemap:
926 # empty repo
927 return True
928
929 new = {}
930 ret = True
931
932 for node, ref in p_revs.iteritems():
933 ctx = repo[node]
934 branch = ctx.branch()
935 if not branch in remotemap:
936 # new branch
937 continue
938 if not ref.startswith('refs/heads/branches'):
939 if ref.startswith('refs/heads/'):
940 if not checkheads_bmark(repo, ref, ctx):
941 ret = False
942
943 # only check branches
944 continue
945 new.setdefault(branch, []).append(ctx.rev())
946
947 for branch, heads in new.iteritems():
948 old = [repo.changelog.rev(x) for x in remotemap[branch]]
949 for rev in heads:
950 if check_version(2, 3):
951 ancestors = repo.changelog.ancestors([rev], stoprev=min(old))
952 else:
953 ancestors = repo.changelog.ancestors(rev)
954 found = False
955
956 for x in old:
957 if x in ancestors:
958 found = True
959 break
960
961 if found:
962 continue
963
964 node = repo.changelog.node(rev)
965 ref = p_revs[node]
966 if force_push:
967 print "ok %s forced update" % ref
968 else:
969 print "error %s non-fast forward" % ref
970 ret = False
971
972 return ret
973
974def push_unsafe(repo, remote, parsed_refs, p_revs):
975
976 force = force_push
977
978 fci = discovery.findcommonincoming
979 commoninc = fci(repo, remote, force=force)
980 common, _, remoteheads = commoninc
981
982 if not checkheads(repo, remote, p_revs):
983 return None
984
985 cg = repo.getbundle('push', heads=list(p_revs), common=common)
986
987 unbundle = remote.capable('unbundle')
988 if unbundle:
989 if force:
990 remoteheads = ['force']
991 return remote.unbundle(cg, remoteheads, 'push')
992 else:
993 return remote.addchangegroup(cg, 'push', repo.url())
994
995def push(repo, remote, parsed_refs, p_revs):
996 if hasattr(remote, 'canpush') and not remote.canpush():
997 print "error cannot push"
998
999 if not p_revs:
1000 # nothing to push
1001 return
1002
1003 lock = None
1004 unbundle = remote.capable('unbundle')
1005 if not unbundle:
1006 lock = remote.lock()
1007 try:
1008 ret = push_unsafe(repo, remote, parsed_refs, p_revs)
1009 finally:
1010 if lock is not None:
1011 lock.release()
1012
1013 return ret
1014
1015def check_tip(ref, kind, name, heads):
1016 try:
1017 ename = '%s/%s' % (kind, name)
1018 tip = marks.get_tip(ename)
1019 except KeyError:
1020 return True
1021 else:
1022 return tip in heads
1023
1024def do_export(parser):
1025 p_bmarks = []
1026 p_revs = {}
1027
1028 parser.next()
1029
1030 for line in parser.each_block('done'):
1031 if parser.check('blob'):
1032 parse_blob(parser)
1033 elif parser.check('commit'):
1034 parse_commit(parser)
1035 elif parser.check('reset'):
1036 parse_reset(parser)
1037 elif parser.check('tag'):
1038 parse_tag(parser)
1039 elif parser.check('feature'):
1040 pass
1041 else:
1042 die('unhandled export command: %s' % line)
1043
1044 need_fetch = False
1045
1046 for ref, node in parsed_refs.iteritems():
1047 bnode = hgbin(node) if node else None
1048 if ref.startswith('refs/heads/branches'):
1049 branch = ref[len('refs/heads/branches/'):]
1050 if branch in branches and bnode in branches[branch]:
1051 # up to date
1052 continue
1053
1054 if peer:
1055 remotemap = peer.branchmap()
1056 if remotemap and branch in remotemap:
1057 heads = [hghex(e) for e in remotemap[branch]]
1058 if not check_tip(ref, 'branches', branch, heads):
1059 print "error %s fetch first" % ref
1060 need_fetch = True
1061 continue
1062
1063 p_revs[bnode] = ref
1064 print "ok %s" % ref
1065 elif ref.startswith('refs/heads/'):
1066 bmark = ref[len('refs/heads/'):]
1067 new = node
1068 old = bmarks[bmark].hex() if bmark in bmarks else ''
1069
1070 if old == new:
1071 continue
1072
1073 print "ok %s" % ref
1074 if bmark != fake_bmark and \
1075 not (bmark == 'master' and bmark not in parser.repo._bookmarks):
1076 p_bmarks.append((ref, bmark, old, new))
1077
1078 if peer:
1079 remote_old = peer.listkeys('bookmarks').get(bmark)
1080 if remote_old:
1081 if not check_tip(ref, 'bookmarks', bmark, remote_old):
1082 print "error %s fetch first" % ref
1083 need_fetch = True
1084 continue
1085
1086 p_revs[bnode] = ref
1087 elif ref.startswith('refs/tags/'):
1088 if dry_run:
1089 print "ok %s" % ref
1090 continue
1091 tag = ref[len('refs/tags/'):]
1092 tag = hgref(tag)
1093 author, msg = parsed_tags.get(tag, (None, None))
1094 if mode == 'git':
1095 if not msg:
1096 msg = 'Added tag %s for changeset %s' % (tag, node[:12])
1097 tagnode, branch = write_tag(parser.repo, tag, node, msg, author)
1098 p_revs[tagnode] = 'refs/heads/branches/' + gitref(branch)
1099 else:
1100 fp = parser.repo.opener('localtags', 'a')
1101 fp.write('%s %s\n' % (node, tag))
1102 fp.close()
1103 p_revs[bnode] = ref
1104 print "ok %s" % ref
1105 else:
1106 # transport-helper/fast-export bugs
1107 continue
1108
1109 if need_fetch:
1110 print
1111 return
1112
1113 if dry_run:
1114 if peer and not force_push:
1115 checkheads(parser.repo, peer, p_revs)
1116 print
1117 return
1118
1119 if peer:
1120 if not push(parser.repo, peer, parsed_refs, p_revs):
1121 # do not update bookmarks
1122 print
1123 return
1124
1125 # update remote bookmarks
1126 remote_bmarks = peer.listkeys('bookmarks')
1127 for ref, bmark, old, new in p_bmarks:
1128 if force_push:
1129 old = remote_bmarks.get(bmark, '')
1130 if not peer.pushkey('bookmarks', bmark, old, new):
1131 print "error %s" % ref
1132 else:
1133 # update local bookmarks
1134 for ref, bmark, old, new in p_bmarks:
1135 if not bookmarks.pushbookmark(parser.repo, bmark, old, new):
1136 print "error %s" % ref
1137
1138 print
1139
1140def do_option(parser):
1141 global dry_run, force_push
1142 _, key, value = parser.line.split(' ')
1143 if key == 'dry-run':
1144 dry_run = (value == 'true')
1145 print 'ok'
1146 elif key == 'force':
1147 force_push = (value == 'true')
1148 print 'ok'
1149 else:
1150 print 'unsupported'
1151
1152def fix_path(alias, repo, orig_url):
1153 url = urlparse.urlparse(orig_url, 'file')
1154 if url.scheme != 'file' or os.path.isabs(os.path.expanduser(url.path)):
1155 return
1156 abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url)
1157 cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % abs_url]
1158 subprocess.call(cmd)
1159
1160def main(args):
1161 global prefix, gitdir, dirname, branches, bmarks
1162 global marks, blob_marks, parsed_refs
1163 global peer, mode, bad_mail, bad_name
1164 global track_branches, force_push, is_tmp
1165 global parsed_tags
1166 global filenodes
1167 global fake_bmark, hg_version
1168 global dry_run
1169 global notes, alias
1170
1171 marks = None
1172 is_tmp = False
1173 gitdir = os.environ.get('GIT_DIR', None)
1174
1175 if len(args) < 3:
1176 die('Not enough arguments.')
1177
1178 if not gitdir:
1179 die('GIT_DIR not set')
1180
1181 alias = args[1]
1182 url = args[2]
1183 peer = None
1184
1185 hg_git_compat = get_config_bool('remote-hg.hg-git-compat')
1186 track_branches = get_config_bool('remote-hg.track-branches', True)
1187 force_push = False
1188
1189 if hg_git_compat:
1190 mode = 'hg'
1191 bad_mail = 'none@none'
1192 bad_name = ''
1193 else:
1194 mode = 'git'
1195 bad_mail = 'unknown'
1196 bad_name = 'Unknown'
1197
1198 if alias[4:] == url:
1199 is_tmp = True
1200 alias = hashlib.sha1(alias).hexdigest()
1201
1202 dirname = os.path.join(gitdir, 'hg', alias)
1203 branches = {}
1204 bmarks = {}
1205 blob_marks = {}
1206 parsed_refs = {}
1207 parsed_tags = {}
1208 filenodes = {}
1209 fake_bmark = None
1210 try:
1211 hg_version = tuple(int(e) for e in util.version().split('.'))
1212 except:
1213 hg_version = None
1214 dry_run = False
1215 notes = set()
1216
1217 repo = get_repo(url, alias)
1218 prefix = 'refs/hg/%s' % alias
1219
1220 if not is_tmp:
1221 fix_path(alias, peer or repo, url)
1222
1223 marks_path = os.path.join(dirname, 'marks-hg')
1224 marks = Marks(marks_path, repo)
1225
1226 if sys.platform == 'win32':
1227 import msvcrt
1228 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
1229
1230 parser = Parser(repo)
1231 for line in parser:
1232 if parser.check('capabilities'):
1233 do_capabilities(parser)
1234 elif parser.check('list'):
1235 do_list(parser)
1236 elif parser.check('import'):
1237 do_import(parser)
1238 elif parser.check('export'):
1239 do_export(parser)
1240 elif parser.check('option'):
1241 do_option(parser)
1242 else:
1243 die('unhandled command: %s' % line)
1244 sys.stdout.flush()
1245
1246def bye():
1247 if not marks:
1248 return
1249 if not is_tmp:
1250 marks.store()
1251 else:
1252 shutil.rmtree(dirname)
1253
1254atexit.register(bye)
1255sys.exit(main(sys.argv))