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