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