Merge branch 'ap/remote-hg-skip-null-bookmarks'
[git.git] / contrib / remote-helpers / git-remote-hg
blob36b526106ba7b3c61b70549770c1ec82f45e1baf
1 #!/usr/bin/env python
3 # Copyright (c) 2012 Felipe Contreras
6 # Inspired by Rocco Rutte's hg-fast-export
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/
12 # For remote repositories a local clone is stored in
13 # "$GIT_DIR/hg/origin/clone/.hg/".
15 from mercurial import hg, ui, bookmarks, context, encoding, node, error, extensions, discovery, util
17 import re
18 import sys
19 import os
20 import json
21 import shutil
22 import subprocess
23 import urllib
24 import atexit
25 import urlparse, hashlib
26 import time as ptime
29 # If you want to see Mercurial revisions as Git commit notes:
30 # git config core.notesRef refs/notes/hg
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
36 # If you want the equivalent of hg's clone/pull--insecure option:
37 # git config --global remote-hg.insecure true
39 # If you want to switch to hg-git compatibility mode:
40 # git config --global remote-hg.hg-git-compat true
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.
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.
53 NAME_RE = re.compile('^([^<>]+)')
54 AUTHOR_RE = re.compile('^([^<>]+?)? ?[<>]([^<>]*)(?:$|>)')
55 EMAIL_RE = re.compile(r'([^ \t<>]+@[^ \t<>]+)')
56 AUTHOR_HG_RE = re.compile('^(.*?) ?<(.*?)(?:>(.+)?)?$')
57 RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.*)> (\d+) ([+-]\d+)')
59 VERSION = 2
61 def die(msg, *args):
62 sys.stderr.write('ERROR: %s\n' % (msg % args))
63 sys.exit(1)
65 def warn(msg, *args):
66 sys.stderr.write('WARNING: %s\n' % (msg % args))
68 def gitmode(flags):
69 return 'l' in flags and '120000' or 'x' in flags and '100755' or '100644'
71 def gittz(tz):
72 return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60)
74 def hgmode(mode):
75 m = { '100755': 'x', '120000': 'l' }
76 return m.get(mode, '')
78 def hghex(n):
79 return node.hex(n)
81 def hgbin(n):
82 return node.bin(n)
84 def hgref(ref):
85 return ref.replace('___', ' ')
87 def gitref(ref):
88 return ref.replace(' ', '___')
90 def check_version(*check):
91 if not hg_version:
92 return True
93 return hg_version >= check
95 def get_config(config):
96 cmd = ['git', 'config', '--get', config]
97 process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
98 output, _ = process.communicate()
99 return output
101 def 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
110 class Marks:
112 def __init__(self, path, repo):
113 self.path = path
114 self.repo = repo
115 self.clear()
116 self.load()
118 if self.version < VERSION:
119 if self.version == 1:
120 self.upgrade_one()
122 # upgraded?
123 if self.version < VERSION:
124 self.clear()
125 self.version = VERSION
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
135 def load(self):
136 if not os.path.exists(self.path):
137 return
139 tmp = json.load(open(self.path))
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)
147 for rev, mark in self.marks.iteritems():
148 self.rev_marks[mark] = rev
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
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 }
161 def store(self):
162 json.dump(self.dict(), open(self.path, 'w'))
164 def __str__(self):
165 return str(self.dict())
167 def from_rev(self, rev):
168 return self.marks[rev]
170 def to_rev(self, mark):
171 return str(self.rev_marks[mark])
173 def next_mark(self):
174 self.last_mark += 1
175 return self.last_mark
177 def get_mark(self, rev):
178 self.last_mark += 1
179 self.marks[rev] = self.last_mark
180 return self.last_mark
182 def new_mark(self, rev, mark):
183 self.marks[rev] = mark
184 self.rev_marks[mark] = rev
185 self.last_mark = mark
187 def is_marked(self, rev):
188 return rev in self.marks
190 def get_tip(self, branch):
191 return str(self.tips[branch])
193 def set_tip(self, branch, tip):
194 self.tips[branch] = tip
196 class Parser:
198 def __init__(self, repo):
199 self.repo = repo
200 self.line = self.get_line()
202 def get_line(self):
203 return sys.stdin.readline().strip()
205 def __getitem__(self, i):
206 return self.line.split()[i]
208 def check(self, word):
209 return self.line.startswith(word)
211 def each_block(self, separator):
212 while self.line != separator:
213 yield self.line
214 self.line = self.get_line()
216 def __iter__(self):
217 return self.each_block('')
219 def next(self):
220 self.line = self.get_line()
221 if self.line == 'done':
222 self.line = None
224 def get_mark(self):
225 i = self.line.index(':') + 1
226 return int(self.line[i:])
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)
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))
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
255 if ex:
256 user += ex
258 tz = int(tz)
259 tz = ((tz / 100) * 3600) + ((tz % 100) * 60)
260 return (user, int(date), -tz)
262 def fix_file_path(path):
263 if not os.path.isabs(path):
264 return path
265 return os.path.relpath(path, '/')
267 def export_files(files):
268 final = []
269 for f in files:
270 fid = node.hex(f.filenode())
272 if fid in filenodes:
273 mark = filenodes[fid]
274 else:
275 mark = marks.next_mark()
276 filenodes[fid] = mark
277 d = f.data()
279 print "blob"
280 print "mark :%u" % mark
281 print "data %d" % len(d)
282 print d
284 path = fix_file_path(f.path())
285 final.append((gitmode(f.flags()), mark, path))
287 return final
289 def get_filechanges(repo, ctx, parent):
290 modified = set()
291 added = set()
292 removed = set()
294 # load earliest manifest first for caching reasons
295 prev = parent.manifest().copy()
296 cur = ctx.manifest()
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())
307 return added | modified, removed
309 def 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)
326 def fixup_user_hg(user):
327 def sanitize(name):
328 # stole this from hg-git
329 return re.sub('[<>\n]', '?', name.lstrip('< ').rstrip('> '))
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
345 return (name, mail)
347 def fixup_user(user):
348 if mode == 'git':
349 name, mail = fixup_user_git(user)
350 else:
351 name, mail = fixup_user_hg(user)
353 if not name:
354 name = bad_name
355 if not mail:
356 mail = bad_mail
358 return '%s <%s>' % (name, mail)
360 def updatebookmarks(repo, peer):
361 remotemarks = peer.listkeys('bookmarks')
362 localmarks = repo._bookmarks
364 if not remotemarks:
365 return
367 for k, v in remotemarks.iteritems():
368 localmarks[k] = hgbin(v)
370 if hasattr(localmarks, 'write'):
371 localmarks.write()
372 else:
373 bookmarks.write(repo)
375 def get_repo(url, alias):
376 global peer
378 myui = ui.ui()
379 myui.setconfig('ui', 'interactive', 'off')
380 myui.fout = sys.stderr
382 if get_config_bool('remote-hg.insecure'):
383 myui.setconfig('web', 'cacerts', '')
385 extensions.loadall(myui)
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')
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'))
406 # setup shared repo (if not there)
407 try:
408 hg.peer(myui, {}, shared_path, create=True)
409 except error.RepoError:
410 pass
412 if not os.path.exists(dirname):
413 os.makedirs(dirname)
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)
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)
429 updatebookmarks(repo, peer)
431 return repo
433 def rev_to_mark(rev):
434 return marks.from_rev(rev.hex())
436 def mark_to_rev(mark):
437 return marks.to_rev(mark)
439 def 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
447 revs = xrange(tip, head.rev() + 1)
448 total = len(revs)
450 for rev in revs:
452 c = repo[rev]
453 node = c.node()
455 if marks.is_marked(c.hex()):
456 continue
458 (manifest, user, (time, tz), files, desc, extra) = repo.changelog.read(node)
459 rev_branch = extra['branch']
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
468 parents = [repo[p] for p in repo.changelog.parentrevs(rev) if p >= 0]
470 if len(parents) == 0:
471 modified = c.manifest().keys()
472 removed = []
473 else:
474 modified, removed = get_filechanges(repo, c, parents[0])
476 desc += '\n'
478 if mode == 'hg':
479 extra_msg = ''
481 if rev_branch != 'default':
482 extra_msg += 'branch : %s\n' % rev_branch
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))
492 for e in renames:
493 extra_msg += "rename : %s => %s\n" % e
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))
501 if extra_msg:
502 desc += '\n--HG--\n' + extra_msg
504 if len(parents) == 0 and rev:
505 print 'reset %s/%s' % (prefix, ename)
507 modified_final = export_files(c.filectx(f) for f in modified)
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
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]))
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
527 progress = (rev - tip)
528 if (progress % 100 == 0):
529 print "progress revision %d '%s' (%d/%d)" % (rev, name, progress, total)
531 # make sure the ref is updated
532 print "reset %s/%s" % (prefix, ename)
533 print "from :%u" % rev_to_mark(head)
534 print
536 pending_revs = set(revs) - notes
537 if pending_revs:
538 note_mark = marks.next_mark()
539 ref = "refs/notes/hg"
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
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
559 marks.last_note = note_mark
561 marks.set_tip(ename, head.hex())
563 def export_tag(repo, tag):
564 export_ref(repo, tag, 'tags', repo[hgref(tag)])
566 def export_bookmark(repo, bmark):
567 head = bmarks[hgref(bmark)]
568 export_ref(repo, bmark, 'bookmarks', head)
570 def export_branch(repo, branch):
571 tip = get_branch_tip(repo, branch)
572 head = repo[tip]
573 export_ref(repo, branch, 'branches', head)
575 def export_head(repo):
576 export_ref(repo, g_head[0], 'bookmarks', g_head[1])
578 def 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
585 path = os.path.join(dirname, 'marks-git')
587 if os.path.exists(path):
588 print "*import-marks %s" % path
589 print "*export-marks %s" % path
590 print "option"
592 print
594 def branch_tip(branch):
595 return branches[branch][-1]
597 def get_branch_tip(repo, branch):
598 heads = branches.get(hgref(branch), None)
599 if not heads:
600 return None
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))
607 return heads[0]
609 def list_head(repo, cur):
610 global g_head, fake_bmark
612 if 'default' not in branches:
613 # empty repo
614 return
616 node = repo[branch_tip('default')]
617 head = 'master' if not 'master' in bmarks else 'default'
618 fake_bmark = head
619 bmarks[head] = node
621 head = gitref(head)
622 print "@refs/heads/%s HEAD" % head
623 g_head = (head, node)
625 def do_list(parser):
626 repo = parser.repo
627 for bmark, node in bookmarks.listbookmarks(repo).iteritems():
628 bmarks[bmark] = repo[node]
630 cur = repo.dirstate.branch()
631 orig = peer if peer else repo
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
639 list_head(repo, cur)
641 if track_branches:
642 for branch in branches:
643 print "? refs/heads/branches/%s" % gitref(branch)
645 for bmark in bmarks:
646 if bmarks[bmark].hex() == '0000000000000000000000000000000000000000':
647 warn("Ignoring invalid bookmark '%s'", bmark)
648 else:
649 print "? refs/heads/%s" % gitref(bmark)
651 for tag, node in repo.tagslist():
652 if tag == 'tip':
653 continue
654 print "? refs/tags/%s" % gitref(tag)
656 print
658 def do_import(parser):
659 repo = parser.repo
661 path = os.path.join(dirname, 'marks-git')
663 print "feature done"
664 if os.path.exists(path):
665 print "feature import-marks=%s" % path
666 print "feature export-marks=%s" % path
667 print "feature force"
668 sys.stdout.flush()
670 tmp = encoding.encoding
671 encoding.encoding = 'utf-8'
673 # lets get all the import lines
674 while parser.check('import'):
675 ref = parser[1]
677 if (ref == 'HEAD'):
678 export_head(repo)
679 elif ref.startswith('refs/heads/branches/'):
680 branch = ref[len('refs/heads/branches/'):]
681 export_branch(repo, branch)
682 elif ref.startswith('refs/heads/'):
683 bmark = ref[len('refs/heads/'):]
684 export_bookmark(repo, bmark)
685 elif ref.startswith('refs/tags/'):
686 tag = ref[len('refs/tags/'):]
687 export_tag(repo, tag)
689 parser.next()
691 encoding.encoding = tmp
693 print 'done'
695 def parse_blob(parser):
696 parser.next()
697 mark = parser.get_mark()
698 parser.next()
699 data = parser.get_data()
700 blob_marks[mark] = data
701 parser.next()
703 def get_merge_files(repo, p1, p2, files):
704 for e in repo[p1].files():
705 if e not in files:
706 if e not in repo[p1].manifest():
707 continue
708 f = { 'ctx' : repo[p1][e] }
709 files[e] = f
711 def c_style_unescape(string):
712 if string[0] == string[-1] == '"':
713 return string.decode('string-escape')[1:-1]
714 return string
716 def parse_commit(parser):
717 from_mark = merge_mark = None
719 ref = parser[1]
720 parser.next()
722 commit_mark = parser.get_mark()
723 parser.next()
724 author = parser.get_author()
725 parser.next()
726 committer = parser.get_author()
727 parser.next()
728 data = parser.get_data()
729 parser.next()
730 if parser.check('from'):
731 from_mark = parser.get_mark()
732 parser.next()
733 if parser.check('merge'):
734 merge_mark = parser.get_mark()
735 parser.next()
736 if parser.check('merge'):
737 die('octopus merges are not supported yet')
739 # fast-export adds an extra newline
740 if data[-1] == '\n':
741 data = data[:-1]
743 files = {}
745 for line in parser:
746 if parser.check('M'):
747 t, m, mark_ref, path = line.split(' ', 3)
748 mark = int(mark_ref[1:])
749 f = { 'mode' : hgmode(m), 'data' : blob_marks[mark] }
750 elif parser.check('D'):
751 t, path = line.split(' ', 1)
752 f = { 'deleted' : True }
753 else:
754 die('Unknown file command: %s' % line)
755 path = c_style_unescape(path)
756 files[path] = f
758 # only export the commits if we are on an internal proxy repo
759 if dry_run and not peer:
760 parsed_refs[ref] = None
761 return
763 def getfilectx(repo, memctx, f):
764 of = files[f]
765 if 'deleted' in of:
766 raise IOError
767 if 'ctx' in of:
768 return of['ctx']
769 is_exec = of['mode'] == 'x'
770 is_link = of['mode'] == 'l'
771 rename = of.get('rename', None)
772 return context.memfilectx(f, of['data'],
773 is_link, is_exec, rename)
775 repo = parser.repo
777 user, date, tz = author
778 extra = {}
780 if committer != author:
781 extra['committer'] = "%s %u %u" % committer
783 if from_mark:
784 p1 = mark_to_rev(from_mark)
785 else:
786 p1 = '0' * 40
788 if merge_mark:
789 p2 = mark_to_rev(merge_mark)
790 else:
791 p2 = '0' * 40
794 # If files changed from any of the parents, hg wants to know, but in git if
795 # nothing changed from the first parent, nothing changed.
797 if merge_mark:
798 get_merge_files(repo, p1, p2, files)
800 # Check if the ref is supposed to be a named branch
801 if ref.startswith('refs/heads/branches/'):
802 branch = ref[len('refs/heads/branches/'):]
803 extra['branch'] = hgref(branch)
805 if mode == 'hg':
806 i = data.find('\n--HG--\n')
807 if i >= 0:
808 tmp = data[i + len('\n--HG--\n'):].strip()
809 for k, v in [e.split(' : ', 1) for e in tmp.split('\n')]:
810 if k == 'rename':
811 old, new = v.split(' => ', 1)
812 files[new]['rename'] = old
813 elif k == 'branch':
814 extra[k] = v
815 elif k == 'extra':
816 ek, ev = v.split(' : ', 1)
817 extra[ek] = urllib.unquote(ev)
818 data = data[:i]
820 ctx = context.memctx(repo, (p1, p2), data,
821 files.keys(), getfilectx,
822 user, (date, tz), extra)
824 tmp = encoding.encoding
825 encoding.encoding = 'utf-8'
827 node = hghex(repo.commitctx(ctx))
829 encoding.encoding = tmp
831 parsed_refs[ref] = node
832 marks.new_mark(node, commit_mark)
834 def parse_reset(parser):
835 ref = parser[1]
836 parser.next()
837 # ugh
838 if parser.check('commit'):
839 parse_commit(parser)
840 return
841 if not parser.check('from'):
842 return
843 from_mark = parser.get_mark()
844 parser.next()
846 try:
847 rev = mark_to_rev(from_mark)
848 except KeyError:
849 rev = None
850 parsed_refs[ref] = rev
852 def parse_tag(parser):
853 name = parser[1]
854 parser.next()
855 from_mark = parser.get_mark()
856 parser.next()
857 tagger = parser.get_author()
858 parser.next()
859 data = parser.get_data()
860 parser.next()
862 parsed_tags[name] = (tagger, data)
864 def write_tag(repo, tag, node, msg, author):
865 branch = repo[node].branch()
866 tip = branch_tip(branch)
867 tip = repo[tip]
869 def getfilectx(repo, memctx, f):
870 try:
871 fctx = tip.filectx(f)
872 data = fctx.data()
873 except error.ManifestLookupError:
874 data = ""
875 content = data + "%s %s\n" % (node, tag)
876 return context.memfilectx(f, content, False, False, None)
878 p1 = tip.hex()
879 p2 = '0' * 40
880 if author:
881 user, date, tz = author
882 date_tz = (date, tz)
883 else:
884 cmd = ['git', 'var', 'GIT_COMMITTER_IDENT']
885 process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
886 output, _ = process.communicate()
887 m = re.match('^.* <.*>', output)
888 if m:
889 user = m.group(0)
890 else:
891 user = repo.ui.username()
892 date_tz = None
894 ctx = context.memctx(repo, (p1, p2), msg,
895 ['.hgtags'], getfilectx,
896 user, date_tz, {'branch' : branch})
898 tmp = encoding.encoding
899 encoding.encoding = 'utf-8'
901 tagnode = repo.commitctx(ctx)
903 encoding.encoding = tmp
905 return (tagnode, branch)
907 def checkheads_bmark(repo, ref, ctx):
908 bmark = ref[len('refs/heads/'):]
909 if not bmark in bmarks:
910 # new bmark
911 return True
913 ctx_old = bmarks[bmark]
914 ctx_new = ctx
915 if not repo.changelog.descendant(ctx_old.rev(), ctx_new.rev()):
916 if force_push:
917 print "ok %s forced update" % ref
918 else:
919 print "error %s non-fast forward" % ref
920 return False
922 return True
924 def checkheads(repo, remote, p_revs):
926 remotemap = remote.branchmap()
927 if not remotemap:
928 # empty repo
929 return True
931 new = {}
932 ret = True
934 for node, ref in p_revs.iteritems():
935 ctx = repo[node]
936 branch = ctx.branch()
937 if not branch in remotemap:
938 # new branch
939 continue
940 if not ref.startswith('refs/heads/branches'):
941 if ref.startswith('refs/heads/'):
942 if not checkheads_bmark(repo, ref, ctx):
943 ret = False
945 # only check branches
946 continue
947 new.setdefault(branch, []).append(ctx.rev())
949 for branch, heads in new.iteritems():
950 old = [repo.changelog.rev(x) for x in remotemap[branch]]
951 for rev in heads:
952 if check_version(2, 3):
953 ancestors = repo.changelog.ancestors([rev], stoprev=min(old))
954 else:
955 ancestors = repo.changelog.ancestors(rev)
956 found = False
958 for x in old:
959 if x in ancestors:
960 found = True
961 break
963 if found:
964 continue
966 node = repo.changelog.node(rev)
967 ref = p_revs[node]
968 if force_push:
969 print "ok %s forced update" % ref
970 else:
971 print "error %s non-fast forward" % ref
972 ret = False
974 return ret
976 def push_unsafe(repo, remote, parsed_refs, p_revs):
978 force = force_push
980 fci = discovery.findcommonincoming
981 commoninc = fci(repo, remote, force=force)
982 common, _, remoteheads = commoninc
984 if not checkheads(repo, remote, p_revs):
985 return None
987 cg = repo.getbundle('push', heads=list(p_revs), common=common)
989 unbundle = remote.capable('unbundle')
990 if unbundle:
991 if force:
992 remoteheads = ['force']
993 return remote.unbundle(cg, remoteheads, 'push')
994 else:
995 return remote.addchangegroup(cg, 'push', repo.url())
997 def push(repo, remote, parsed_refs, p_revs):
998 if hasattr(remote, 'canpush') and not remote.canpush():
999 print "error cannot push"
1001 if not p_revs:
1002 # nothing to push
1003 return
1005 lock = None
1006 unbundle = remote.capable('unbundle')
1007 if not unbundle:
1008 lock = remote.lock()
1009 try:
1010 ret = push_unsafe(repo, remote, parsed_refs, p_revs)
1011 finally:
1012 if lock is not None:
1013 lock.release()
1015 return ret
1017 def check_tip(ref, kind, name, heads):
1018 try:
1019 ename = '%s/%s' % (kind, name)
1020 tip = marks.get_tip(ename)
1021 except KeyError:
1022 return True
1023 else:
1024 return tip in heads
1026 def do_export(parser):
1027 p_bmarks = []
1028 p_revs = {}
1030 parser.next()
1032 for line in parser.each_block('done'):
1033 if parser.check('blob'):
1034 parse_blob(parser)
1035 elif parser.check('commit'):
1036 parse_commit(parser)
1037 elif parser.check('reset'):
1038 parse_reset(parser)
1039 elif parser.check('tag'):
1040 parse_tag(parser)
1041 elif parser.check('feature'):
1042 pass
1043 else:
1044 die('unhandled export command: %s' % line)
1046 need_fetch = False
1048 for ref, node in parsed_refs.iteritems():
1049 bnode = hgbin(node) if node else None
1050 if ref.startswith('refs/heads/branches'):
1051 branch = ref[len('refs/heads/branches/'):]
1052 if branch in branches and bnode in branches[branch]:
1053 # up to date
1054 continue
1056 if peer:
1057 remotemap = peer.branchmap()
1058 if remotemap and branch in remotemap:
1059 heads = [hghex(e) for e in remotemap[branch]]
1060 if not check_tip(ref, 'branches', branch, heads):
1061 print "error %s fetch first" % ref
1062 need_fetch = True
1063 continue
1065 p_revs[bnode] = ref
1066 print "ok %s" % ref
1067 elif ref.startswith('refs/heads/'):
1068 bmark = ref[len('refs/heads/'):]
1069 new = node
1070 old = bmarks[bmark].hex() if bmark in bmarks else ''
1072 if old == new:
1073 continue
1075 print "ok %s" % ref
1076 if bmark != fake_bmark and \
1077 not (bmark == 'master' and bmark not in parser.repo._bookmarks):
1078 p_bmarks.append((ref, bmark, old, new))
1080 if peer:
1081 remote_old = peer.listkeys('bookmarks').get(bmark)
1082 if remote_old:
1083 if not check_tip(ref, 'bookmarks', bmark, remote_old):
1084 print "error %s fetch first" % ref
1085 need_fetch = True
1086 continue
1088 p_revs[bnode] = ref
1089 elif ref.startswith('refs/tags/'):
1090 if dry_run:
1091 print "ok %s" % ref
1092 continue
1093 tag = ref[len('refs/tags/'):]
1094 tag = hgref(tag)
1095 author, msg = parsed_tags.get(tag, (None, None))
1096 if mode == 'git':
1097 if not msg:
1098 msg = 'Added tag %s for changeset %s' % (tag, node[:12])
1099 tagnode, branch = write_tag(parser.repo, tag, node, msg, author)
1100 p_revs[tagnode] = 'refs/heads/branches/' + gitref(branch)
1101 else:
1102 fp = parser.repo.opener('localtags', 'a')
1103 fp.write('%s %s\n' % (node, tag))
1104 fp.close()
1105 p_revs[bnode] = ref
1106 print "ok %s" % ref
1107 else:
1108 # transport-helper/fast-export bugs
1109 continue
1111 if need_fetch:
1112 print
1113 return
1115 if dry_run:
1116 if peer and not force_push:
1117 checkheads(parser.repo, peer, p_revs)
1118 print
1119 return
1121 if peer:
1122 if not push(parser.repo, peer, parsed_refs, p_revs):
1123 # do not update bookmarks
1124 print
1125 return
1127 # update remote bookmarks
1128 remote_bmarks = peer.listkeys('bookmarks')
1129 for ref, bmark, old, new in p_bmarks:
1130 if force_push:
1131 old = remote_bmarks.get(bmark, '')
1132 if not peer.pushkey('bookmarks', bmark, old, new):
1133 print "error %s" % ref
1134 else:
1135 # update local bookmarks
1136 for ref, bmark, old, new in p_bmarks:
1137 if not bookmarks.pushbookmark(parser.repo, bmark, old, new):
1138 print "error %s" % ref
1140 print
1142 def do_option(parser):
1143 global dry_run, force_push
1144 _, key, value = parser.line.split(' ')
1145 if key == 'dry-run':
1146 dry_run = (value == 'true')
1147 print 'ok'
1148 elif key == 'force':
1149 force_push = (value == 'true')
1150 print 'ok'
1151 else:
1152 print 'unsupported'
1154 def fix_path(alias, repo, orig_url):
1155 url = urlparse.urlparse(orig_url, 'file')
1156 if url.scheme != 'file' or os.path.isabs(os.path.expanduser(url.path)):
1157 return
1158 abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url)
1159 cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % abs_url]
1160 subprocess.call(cmd)
1162 def main(args):
1163 global prefix, gitdir, dirname, branches, bmarks
1164 global marks, blob_marks, parsed_refs
1165 global peer, mode, bad_mail, bad_name
1166 global track_branches, force_push, is_tmp
1167 global parsed_tags
1168 global filenodes
1169 global fake_bmark, hg_version
1170 global dry_run
1171 global notes, alias
1173 marks = None
1174 is_tmp = False
1175 gitdir = os.environ.get('GIT_DIR', None)
1177 if len(args) < 3:
1178 die('Not enough arguments.')
1180 if not gitdir:
1181 die('GIT_DIR not set')
1183 alias = args[1]
1184 url = args[2]
1185 peer = None
1187 hg_git_compat = get_config_bool('remote-hg.hg-git-compat')
1188 track_branches = get_config_bool('remote-hg.track-branches', True)
1189 force_push = False
1191 if hg_git_compat:
1192 mode = 'hg'
1193 bad_mail = 'none@none'
1194 bad_name = ''
1195 else:
1196 mode = 'git'
1197 bad_mail = 'unknown'
1198 bad_name = 'Unknown'
1200 if alias[4:] == url:
1201 is_tmp = True
1202 alias = hashlib.sha1(alias).hexdigest()
1204 dirname = os.path.join(gitdir, 'hg', alias)
1205 branches = {}
1206 bmarks = {}
1207 blob_marks = {}
1208 parsed_refs = {}
1209 parsed_tags = {}
1210 filenodes = {}
1211 fake_bmark = None
1212 try:
1213 hg_version = tuple(int(e) for e in util.version().split('.'))
1214 except:
1215 hg_version = None
1216 dry_run = False
1217 notes = set()
1219 repo = get_repo(url, alias)
1220 prefix = 'refs/hg/%s' % alias
1222 if not is_tmp:
1223 fix_path(alias, peer or repo, url)
1225 marks_path = os.path.join(dirname, 'marks-hg')
1226 marks = Marks(marks_path, repo)
1228 if sys.platform == 'win32':
1229 import msvcrt
1230 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
1232 parser = Parser(repo)
1233 for line in parser:
1234 if parser.check('capabilities'):
1235 do_capabilities(parser)
1236 elif parser.check('list'):
1237 do_list(parser)
1238 elif parser.check('import'):
1239 do_import(parser)
1240 elif parser.check('export'):
1241 do_export(parser)
1242 elif parser.check('option'):
1243 do_option(parser)
1244 else:
1245 die('unhandled command: %s' % line)
1246 sys.stdout.flush()
1248 def bye():
1249 if not marks:
1250 return
1251 if not is_tmp:
1252 marks.store()
1253 else:
1254 shutil.rmtree(dirname)
1256 atexit.register(bye)
1257 sys.exit(main(sys.argv))