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