remote-hg: remove files before modifications
[git.git] / contrib / remote-helpers / git-remote-hg
blobbd545aa380d3751fdfbc416cb20947035bb4ec8b
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
28 # If you are not in hg-git-compat mode and want to disable the tracking of
29 # named branches:
30 # git config --global remote-hg.track-branches false
32 # If you don't want to force pushes (and thus risk creating new remote heads):
33 # git config --global remote-hg.force-push false
35 # If you want the equivalent of hg's clone/pull--insecure option:
36 # git config --global remote-hg.insecure true
38 # If you want to switch to hg-git compatibility mode:
39 # git config --global remote-hg.hg-git-compat true
41 # git:
42 # Sensible defaults for git.
43 # hg bookmarks are exported as git branches, hg branches are prefixed
44 # with 'branches/', HEAD is a special case.
46 # hg:
47 # Emulate hg-git.
48 # Only hg bookmarks are exported as git branches.
49 # Commits are modified to preserve hg information and allow bidirectionality.
52 NAME_RE = re.compile('^([^<>]+)')
53 AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$')
54 EMAIL_RE = re.compile('^([^<>]+[^ \\\t<>])?\\b(?:[ \\t<>]*?)\\b([^ \\t<>]+@[^ \\t<>]+)')
55 AUTHOR_HG_RE = re.compile('^(.*?) ?<(.*?)(?:>(.+)?)?$')
56 RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.*)> (\d+) ([+-]\d+)')
58 VERSION = 2
60 def die(msg, *args):
61 sys.stderr.write('ERROR: %s\n' % (msg % args))
62 sys.exit(1)
64 def warn(msg, *args):
65 sys.stderr.write('WARNING: %s\n' % (msg % args))
67 def gitmode(flags):
68 return 'l' in flags and '120000' or 'x' in flags and '100755' or '100644'
70 def gittz(tz):
71 return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60)
73 def hgmode(mode):
74 m = { '100755': 'x', '120000': 'l' }
75 return m.get(mode, '')
77 def hghex(n):
78 return node.hex(n)
80 def hgbin(n):
81 return node.bin(n)
83 def hgref(ref):
84 return ref.replace('___', ' ')
86 def gitref(ref):
87 return ref.replace(' ', '___')
89 def check_version(*check):
90 if not hg_version:
91 return True
92 return hg_version >= check
94 def get_config(config):
95 cmd = ['git', 'config', '--get', config]
96 process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
97 output, _ = process.communicate()
98 return output
100 def get_config_bool(config, default=False):
101 value = get_config(config).rstrip('\n')
102 if value == "true":
103 return True
104 elif value == "false":
105 return False
106 else:
107 return default
109 class Marks:
111 def __init__(self, path, repo):
112 self.path = path
113 self.repo = repo
114 self.clear()
115 self.load()
117 if self.version < VERSION:
118 if self.version == 1:
119 self.upgrade_one()
121 # upgraded?
122 if self.version < VERSION:
123 self.clear()
124 self.version = VERSION
126 def clear(self):
127 self.tips = {}
128 self.marks = {}
129 self.rev_marks = {}
130 self.last_mark = 0
131 self.version = 0
133 def load(self):
134 if not os.path.exists(self.path):
135 return
137 tmp = json.load(open(self.path))
139 self.tips = tmp['tips']
140 self.marks = tmp['marks']
141 self.last_mark = tmp['last-mark']
142 self.version = tmp.get('version', 1)
144 for rev, mark in self.marks.iteritems():
145 self.rev_marks[mark] = rev
147 def upgrade_one(self):
148 def get_id(rev):
149 return hghex(self.repo.changelog.node(int(rev)))
150 self.tips = dict((name, get_id(rev)) for name, rev in self.tips.iteritems())
151 self.marks = dict((get_id(rev), mark) for rev, mark in self.marks.iteritems())
152 self.rev_marks = dict((mark, get_id(rev)) for mark, rev in self.rev_marks.iteritems())
153 self.version = 2
155 def dict(self):
156 return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark, 'version' : self.version }
158 def store(self):
159 json.dump(self.dict(), open(self.path, 'w'))
161 def __str__(self):
162 return str(self.dict())
164 def from_rev(self, rev):
165 return self.marks[rev]
167 def to_rev(self, mark):
168 return self.rev_marks[mark]
170 def next_mark(self):
171 self.last_mark += 1
172 return self.last_mark
174 def get_mark(self, rev):
175 self.last_mark += 1
176 self.marks[rev] = self.last_mark
177 return self.last_mark
179 def new_mark(self, rev, mark):
180 self.marks[rev] = mark
181 self.rev_marks[mark] = rev
182 self.last_mark = mark
184 def is_marked(self, rev):
185 return rev in self.marks
187 def get_tip(self, branch):
188 return self.tips.get(branch, None)
190 def set_tip(self, branch, tip):
191 self.tips[branch] = tip
193 class Parser:
195 def __init__(self, repo):
196 self.repo = repo
197 self.line = self.get_line()
199 def get_line(self):
200 return sys.stdin.readline().strip()
202 def __getitem__(self, i):
203 return self.line.split()[i]
205 def check(self, word):
206 return self.line.startswith(word)
208 def each_block(self, separator):
209 while self.line != separator:
210 yield self.line
211 self.line = self.get_line()
213 def __iter__(self):
214 return self.each_block('')
216 def next(self):
217 self.line = self.get_line()
218 if self.line == 'done':
219 self.line = None
221 def get_mark(self):
222 i = self.line.index(':') + 1
223 return int(self.line[i:])
225 def get_data(self):
226 if not self.check('data'):
227 return None
228 i = self.line.index(' ') + 1
229 size = int(self.line[i:])
230 return sys.stdin.read(size)
232 def get_author(self):
233 global bad_mail
235 ex = None
236 m = RAW_AUTHOR_RE.match(self.line)
237 if not m:
238 return None
239 _, name, email, date, tz = m.groups()
240 if name and 'ext:' in name:
241 m = re.match('^(.+?) ext:\((.+)\)$', name)
242 if m:
243 name = m.group(1)
244 ex = urllib.unquote(m.group(2))
246 if email != bad_mail:
247 if name:
248 user = '%s <%s>' % (name, email)
249 else:
250 user = '<%s>' % (email)
251 else:
252 user = name
254 if ex:
255 user += ex
257 tz = int(tz)
258 tz = ((tz / 100) * 3600) + ((tz % 100) * 60)
259 return (user, int(date), -tz)
261 def fix_file_path(path):
262 if not os.path.isabs(path):
263 return path
264 return os.path.relpath(path, '/')
266 def export_files(files):
267 global marks, filenodes
269 final = []
270 for f in files:
271 fid = node.hex(f.filenode())
273 if fid in filenodes:
274 mark = filenodes[fid]
275 else:
276 mark = marks.next_mark()
277 filenodes[fid] = mark
278 d = f.data()
280 print "blob"
281 print "mark :%u" % mark
282 print "data %d" % len(d)
283 print d
285 path = fix_file_path(f.path())
286 final.append((gitmode(f.flags()), mark, path))
288 return final
290 def get_filechanges(repo, ctx, parent):
291 modified = set()
292 added = set()
293 removed = set()
295 # load earliest manifest first for caching reasons
296 prev = parent.manifest().copy()
297 cur = ctx.manifest()
299 for fn in cur:
300 if fn in prev:
301 if (cur.flags(fn) != prev.flags(fn) or cur[fn] != prev[fn]):
302 modified.add(fn)
303 del prev[fn]
304 else:
305 added.add(fn)
306 removed |= set(prev.keys())
308 return added | modified, removed
310 def fixup_user_git(user):
311 name = mail = None
312 user = user.replace('"', '')
313 m = AUTHOR_RE.match(user)
314 if m:
315 name = m.group(1)
316 mail = m.group(2).strip()
317 else:
318 m = EMAIL_RE.match(user)
319 if m:
320 name = m.group(1)
321 mail = m.group(2)
322 else:
323 m = NAME_RE.match(user)
324 if m:
325 name = m.group(1).strip()
326 return (name, mail)
328 def fixup_user_hg(user):
329 def sanitize(name):
330 # stole this from hg-git
331 return re.sub('[<>\n]', '?', name.lstrip('< ').rstrip('> '))
333 m = AUTHOR_HG_RE.match(user)
334 if m:
335 name = sanitize(m.group(1))
336 mail = sanitize(m.group(2))
337 ex = m.group(3)
338 if ex:
339 name += ' ext:(' + urllib.quote(ex) + ')'
340 else:
341 name = sanitize(user)
342 if '@' in user:
343 mail = name
344 else:
345 mail = None
347 return (name, mail)
349 def fixup_user(user):
350 global mode, bad_mail
352 if mode == 'git':
353 name, mail = fixup_user_git(user)
354 else:
355 name, mail = fixup_user_hg(user)
357 if not name:
358 name = bad_name
359 if not mail:
360 mail = bad_mail
362 return '%s <%s>' % (name, mail)
364 def updatebookmarks(repo, peer):
365 remotemarks = peer.listkeys('bookmarks')
366 localmarks = repo._bookmarks
368 if not remotemarks:
369 return
371 for k, v in remotemarks.iteritems():
372 localmarks[k] = hgbin(v)
374 if hasattr(localmarks, 'write'):
375 localmarks.write()
376 else:
377 bookmarks.write(repo)
379 def get_repo(url, alias):
380 global dirname, peer
382 myui = ui.ui()
383 myui.setconfig('ui', 'interactive', 'off')
384 myui.fout = sys.stderr
386 if get_config_bool('remote-hg.insecure'):
387 myui.setconfig('web', 'cacerts', '')
389 extensions.loadall(myui)
391 if hg.islocal(url) and not os.environ.get('GIT_REMOTE_HG_TEST_REMOTE'):
392 repo = hg.repository(myui, url)
393 if not os.path.exists(dirname):
394 os.makedirs(dirname)
395 else:
396 shared_path = os.path.join(gitdir, 'hg')
397 if not os.path.exists(shared_path):
398 try:
399 hg.clone(myui, {}, url, shared_path, update=False, pull=True)
400 except:
401 die('Repository error')
403 if not os.path.exists(dirname):
404 os.makedirs(dirname)
406 local_path = os.path.join(dirname, 'clone')
407 if not os.path.exists(local_path):
408 hg.share(myui, shared_path, local_path, update=False)
410 repo = hg.repository(myui, local_path)
411 try:
412 peer = hg.peer(myui, {}, url)
413 except:
414 die('Repository error')
415 repo.pull(peer, heads=None, force=True)
417 updatebookmarks(repo, peer)
419 return repo
421 def rev_to_mark(rev):
422 global marks
423 return marks.from_rev(rev.hex())
425 def mark_to_rev(mark):
426 global marks
427 return marks.to_rev(mark)
429 def export_ref(repo, name, kind, head):
430 global prefix, marks, mode
432 ename = '%s/%s' % (kind, name)
433 tip = marks.get_tip(ename)
434 if tip and tip in repo:
435 tip = repo[tip].rev()
436 else:
437 tip = 0
439 revs = xrange(tip, head.rev() + 1)
440 total = len(revs)
442 for rev in revs:
444 c = repo[rev]
445 node = c.node()
447 if marks.is_marked(c.hex()):
448 continue
450 (manifest, user, (time, tz), files, desc, extra) = repo.changelog.read(node)
451 rev_branch = extra['branch']
453 author = "%s %d %s" % (fixup_user(user), time, gittz(tz))
454 if 'committer' in extra:
455 user, time, tz = extra['committer'].rsplit(' ', 2)
456 committer = "%s %s %s" % (user, time, gittz(int(tz)))
457 else:
458 committer = author
460 parents = [repo[p] for p in repo.changelog.parentrevs(rev) if p >= 0]
462 if len(parents) == 0:
463 modified = c.manifest().keys()
464 removed = []
465 else:
466 modified, removed = get_filechanges(repo, c, parents[0])
468 desc += '\n'
470 if mode == 'hg':
471 extra_msg = ''
473 if rev_branch != 'default':
474 extra_msg += 'branch : %s\n' % rev_branch
476 renames = []
477 for f in c.files():
478 if f not in c.manifest():
479 continue
480 rename = c.filectx(f).renamed()
481 if rename:
482 renames.append((rename[0], f))
484 for e in renames:
485 extra_msg += "rename : %s => %s\n" % e
487 for key, value in extra.iteritems():
488 if key in ('author', 'committer', 'encoding', 'message', 'branch', 'hg-git'):
489 continue
490 else:
491 extra_msg += "extra : %s : %s\n" % (key, urllib.quote(value))
493 if extra_msg:
494 desc += '\n--HG--\n' + extra_msg
496 if len(parents) == 0 and rev:
497 print 'reset %s/%s' % (prefix, ename)
499 modified_final = export_files(c.filectx(f) for f in modified)
501 print "commit %s/%s" % (prefix, ename)
502 print "mark :%d" % (marks.get_mark(c.hex()))
503 print "author %s" % (author)
504 print "committer %s" % (committer)
505 print "data %d" % (len(desc))
506 print desc
508 if len(parents) > 0:
509 print "from :%s" % (rev_to_mark(parents[0]))
510 if len(parents) > 1:
511 print "merge :%s" % (rev_to_mark(parents[1]))
513 for f in removed:
514 print "D %s" % (fix_file_path(f))
515 for f in modified_final:
516 print "M %s :%u %s" % f
517 print
519 progress = (rev - tip)
520 if (progress % 100 == 0):
521 print "progress revision %d '%s' (%d/%d)" % (rev, name, progress, total)
523 # make sure the ref is updated
524 print "reset %s/%s" % (prefix, ename)
525 print "from :%u" % rev_to_mark(head)
526 print
528 marks.set_tip(ename, head.hex())
530 def export_tag(repo, tag):
531 export_ref(repo, tag, 'tags', repo[hgref(tag)])
533 def export_bookmark(repo, bmark):
534 head = bmarks[hgref(bmark)]
535 export_ref(repo, bmark, 'bookmarks', head)
537 def export_branch(repo, branch):
538 tip = get_branch_tip(repo, branch)
539 head = repo[tip]
540 export_ref(repo, branch, 'branches', head)
542 def export_head(repo):
543 global g_head
544 export_ref(repo, g_head[0], 'bookmarks', g_head[1])
546 def do_capabilities(parser):
547 global prefix, dirname
549 print "import"
550 print "export"
551 print "refspec refs/heads/branches/*:%s/branches/*" % prefix
552 print "refspec refs/heads/*:%s/bookmarks/*" % prefix
553 print "refspec refs/tags/*:%s/tags/*" % prefix
555 path = os.path.join(dirname, 'marks-git')
557 if os.path.exists(path):
558 print "*import-marks %s" % path
559 print "*export-marks %s" % path
561 print
563 def branch_tip(branch):
564 return branches[branch][-1]
566 def get_branch_tip(repo, branch):
567 global branches
569 heads = branches.get(hgref(branch), None)
570 if not heads:
571 return None
573 # verify there's only one head
574 if (len(heads) > 1):
575 warn("Branch '%s' has more than one head, consider merging" % branch)
576 return branch_tip(hgref(branch))
578 return heads[0]
580 def list_head(repo, cur):
581 global g_head, bmarks, fake_bmark
583 if 'default' not in branches:
584 # empty repo
585 return
587 node = repo[branch_tip('default')]
588 head = 'master' if not 'master' in bmarks else 'default'
589 fake_bmark = head
590 bmarks[head] = node
592 head = gitref(head)
593 print "@refs/heads/%s HEAD" % head
594 g_head = (head, node)
596 def do_list(parser):
597 global branches, bmarks, track_branches
599 repo = parser.repo
600 for bmark, node in bookmarks.listbookmarks(repo).iteritems():
601 bmarks[bmark] = repo[node]
603 cur = repo.dirstate.branch()
604 orig = peer if peer else repo
606 for branch, heads in orig.branchmap().iteritems():
607 # only open heads
608 heads = [h for h in heads if 'close' not in repo.changelog.read(h)[5]]
609 if heads:
610 branches[branch] = heads
612 list_head(repo, cur)
614 if track_branches:
615 for branch in branches:
616 print "? refs/heads/branches/%s" % gitref(branch)
618 for bmark in bmarks:
619 print "? refs/heads/%s" % gitref(bmark)
621 for tag, node in repo.tagslist():
622 if tag == 'tip':
623 continue
624 print "? refs/tags/%s" % gitref(tag)
626 print
628 def do_import(parser):
629 repo = parser.repo
631 path = os.path.join(dirname, 'marks-git')
633 print "feature done"
634 if os.path.exists(path):
635 print "feature import-marks=%s" % path
636 print "feature export-marks=%s" % path
637 print "feature force"
638 sys.stdout.flush()
640 tmp = encoding.encoding
641 encoding.encoding = 'utf-8'
643 # lets get all the import lines
644 while parser.check('import'):
645 ref = parser[1]
647 if (ref == 'HEAD'):
648 export_head(repo)
649 elif ref.startswith('refs/heads/branches/'):
650 branch = ref[len('refs/heads/branches/'):]
651 export_branch(repo, branch)
652 elif ref.startswith('refs/heads/'):
653 bmark = ref[len('refs/heads/'):]
654 export_bookmark(repo, bmark)
655 elif ref.startswith('refs/tags/'):
656 tag = ref[len('refs/tags/'):]
657 export_tag(repo, tag)
659 parser.next()
661 encoding.encoding = tmp
663 print 'done'
665 def parse_blob(parser):
666 global blob_marks
668 parser.next()
669 mark = parser.get_mark()
670 parser.next()
671 data = parser.get_data()
672 blob_marks[mark] = data
673 parser.next()
675 def get_merge_files(repo, p1, p2, files):
676 for e in repo[p1].files():
677 if e not in files:
678 if e not in repo[p1].manifest():
679 continue
680 f = { 'ctx' : repo[p1][e] }
681 files[e] = f
683 def parse_commit(parser):
684 global marks, blob_marks, parsed_refs
685 global mode
687 from_mark = merge_mark = None
689 ref = parser[1]
690 parser.next()
692 commit_mark = parser.get_mark()
693 parser.next()
694 author = parser.get_author()
695 parser.next()
696 committer = parser.get_author()
697 parser.next()
698 data = parser.get_data()
699 parser.next()
700 if parser.check('from'):
701 from_mark = parser.get_mark()
702 parser.next()
703 if parser.check('merge'):
704 merge_mark = parser.get_mark()
705 parser.next()
706 if parser.check('merge'):
707 die('octopus merges are not supported yet')
709 # fast-export adds an extra newline
710 if data[-1] == '\n':
711 data = data[:-1]
713 files = {}
715 for line in parser:
716 if parser.check('M'):
717 t, m, mark_ref, path = line.split(' ', 3)
718 mark = int(mark_ref[1:])
719 f = { 'mode' : hgmode(m), 'data' : blob_marks[mark] }
720 elif parser.check('D'):
721 t, path = line.split(' ', 1)
722 f = { 'deleted' : True }
723 else:
724 die('Unknown file command: %s' % line)
725 files[path] = f
727 def getfilectx(repo, memctx, f):
728 of = files[f]
729 if 'deleted' in of:
730 raise IOError
731 if 'ctx' in of:
732 return of['ctx']
733 is_exec = of['mode'] == 'x'
734 is_link = of['mode'] == 'l'
735 rename = of.get('rename', None)
736 return context.memfilectx(f, of['data'],
737 is_link, is_exec, rename)
739 repo = parser.repo
741 user, date, tz = author
742 extra = {}
744 if committer != author:
745 extra['committer'] = "%s %u %u" % committer
747 if from_mark:
748 p1 = mark_to_rev(from_mark)
749 else:
750 p1 = '0' * 40
752 if merge_mark:
753 p2 = mark_to_rev(merge_mark)
754 else:
755 p2 = '0' * 40
758 # If files changed from any of the parents, hg wants to know, but in git if
759 # nothing changed from the first parent, nothing changed.
761 if merge_mark:
762 get_merge_files(repo, p1, p2, files)
764 # Check if the ref is supposed to be a named branch
765 if ref.startswith('refs/heads/branches/'):
766 branch = ref[len('refs/heads/branches/'):]
767 extra['branch'] = hgref(branch)
769 if mode == 'hg':
770 i = data.find('\n--HG--\n')
771 if i >= 0:
772 tmp = data[i + len('\n--HG--\n'):].strip()
773 for k, v in [e.split(' : ', 1) for e in tmp.split('\n')]:
774 if k == 'rename':
775 old, new = v.split(' => ', 1)
776 files[new]['rename'] = old
777 elif k == 'branch':
778 extra[k] = v
779 elif k == 'extra':
780 ek, ev = v.split(' : ', 1)
781 extra[ek] = urllib.unquote(ev)
782 data = data[:i]
784 ctx = context.memctx(repo, (p1, p2), data,
785 files.keys(), getfilectx,
786 user, (date, tz), extra)
788 tmp = encoding.encoding
789 encoding.encoding = 'utf-8'
791 node = hghex(repo.commitctx(ctx))
793 encoding.encoding = tmp
795 parsed_refs[ref] = node
796 marks.new_mark(node, commit_mark)
798 def parse_reset(parser):
799 global parsed_refs
801 ref = parser[1]
802 parser.next()
803 # ugh
804 if parser.check('commit'):
805 parse_commit(parser)
806 return
807 if not parser.check('from'):
808 return
809 from_mark = parser.get_mark()
810 parser.next()
812 rev = mark_to_rev(from_mark)
813 parsed_refs[ref] = rev
815 def parse_tag(parser):
816 name = parser[1]
817 parser.next()
818 from_mark = parser.get_mark()
819 parser.next()
820 tagger = parser.get_author()
821 parser.next()
822 data = parser.get_data()
823 parser.next()
825 parsed_tags[name] = (tagger, data)
827 def write_tag(repo, tag, node, msg, author):
828 branch = repo[node].branch()
829 tip = branch_tip(branch)
830 tip = repo[tip]
832 def getfilectx(repo, memctx, f):
833 try:
834 fctx = tip.filectx(f)
835 data = fctx.data()
836 except error.ManifestLookupError:
837 data = ""
838 content = data + "%s %s\n" % (node, tag)
839 return context.memfilectx(f, content, False, False, None)
841 p1 = tip.hex()
842 p2 = '0' * 40
843 if author:
844 user, date, tz = author
845 date_tz = (date, tz)
846 else:
847 cmd = ['git', 'var', 'GIT_COMMITTER_IDENT']
848 process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
849 output, _ = process.communicate()
850 m = re.match('^.* <.*>', output)
851 if m:
852 user = m.group(0)
853 else:
854 user = repo.ui.username()
855 date_tz = None
857 ctx = context.memctx(repo, (p1, p2), msg,
858 ['.hgtags'], getfilectx,
859 user, date_tz, {'branch' : branch})
861 tmp = encoding.encoding
862 encoding.encoding = 'utf-8'
864 tagnode = repo.commitctx(ctx)
866 encoding.encoding = tmp
868 return (tagnode, branch)
870 def checkheads_bmark(repo, ref, ctx):
871 if force_push:
872 return True
874 bmark = ref[len('refs/heads/'):]
875 if not bmark in bmarks:
876 # new bmark
877 return True
879 ctx_old = bmarks[bmark]
880 ctx_new = ctx
881 if not repo.changelog.descendant(ctx_old.rev(), ctx_new.rev()):
882 print "error %s non-fast forward" % ref
883 return False
885 return True
887 def checkheads(repo, remote, p_revs):
889 remotemap = remote.branchmap()
890 if not remotemap:
891 # empty repo
892 return True
894 new = {}
895 ret = True
897 for node, ref in p_revs.iteritems():
898 ctx = repo[node]
899 branch = ctx.branch()
900 if not branch in remotemap:
901 # new branch
902 continue
903 if not ref.startswith('refs/heads/branches'):
904 if ref.startswith('refs/heads/'):
905 if not checkheads_bmark(repo, ref, ctx):
906 ret = False
908 # only check branches
909 continue
910 new.setdefault(branch, []).append(ctx.rev())
912 for branch, heads in new.iteritems():
913 old = [repo.changelog.rev(x) for x in remotemap[branch]]
914 for rev in heads:
915 if check_version(2, 3):
916 ancestors = repo.changelog.ancestors([rev], stoprev=min(old))
917 else:
918 ancestors = repo.changelog.ancestors(rev)
919 found = False
921 for x in old:
922 if x in ancestors:
923 found = True
924 break
926 if found:
927 continue
929 node = repo.changelog.node(rev)
930 print "error %s non-fast forward" % p_revs[node]
931 ret = False
933 return ret
935 def push_unsafe(repo, remote, parsed_refs, p_revs):
937 force = force_push
939 fci = discovery.findcommonincoming
940 commoninc = fci(repo, remote, force=force)
941 common, _, remoteheads = commoninc
943 if not force and not checkheads(repo, remote, p_revs):
944 return None
946 cg = repo.getbundle('push', heads=list(p_revs), common=common)
948 unbundle = remote.capable('unbundle')
949 if unbundle:
950 if force:
951 remoteheads = ['force']
952 return remote.unbundle(cg, remoteheads, 'push')
953 else:
954 return remote.addchangegroup(cg, 'push', repo.url())
956 def push(repo, remote, parsed_refs, p_revs):
957 if hasattr(remote, 'canpush') and not remote.canpush():
958 print "error cannot push"
960 if not p_revs:
961 # nothing to push
962 return
964 lock = None
965 unbundle = remote.capable('unbundle')
966 if not unbundle:
967 lock = remote.lock()
968 try:
969 ret = push_unsafe(repo, remote, parsed_refs, p_revs)
970 finally:
971 if lock is not None:
972 lock.release()
974 return ret
976 def do_export(parser):
977 global parsed_refs, bmarks, peer
979 p_bmarks = []
980 p_revs = {}
982 parser.next()
984 for line in parser.each_block('done'):
985 if parser.check('blob'):
986 parse_blob(parser)
987 elif parser.check('commit'):
988 parse_commit(parser)
989 elif parser.check('reset'):
990 parse_reset(parser)
991 elif parser.check('tag'):
992 parse_tag(parser)
993 elif parser.check('feature'):
994 pass
995 else:
996 die('unhandled export command: %s' % line)
998 for ref, node in parsed_refs.iteritems():
999 bnode = hgbin(node)
1000 if ref.startswith('refs/heads/branches'):
1001 branch = ref[len('refs/heads/branches/'):]
1002 if branch in branches and bnode in branches[branch]:
1003 # up to date
1004 continue
1005 p_revs[bnode] = ref
1006 print "ok %s" % ref
1007 elif ref.startswith('refs/heads/'):
1008 bmark = ref[len('refs/heads/'):]
1009 new = node
1010 old = bmarks[bmark].hex() if bmark in bmarks else ''
1012 if old == new:
1013 continue
1015 print "ok %s" % ref
1016 if bmark != fake_bmark and \
1017 not (bmark == 'master' and bmark not in parser.repo._bookmarks):
1018 p_bmarks.append((ref, bmark, old, new))
1020 p_revs[bnode] = ref
1021 elif ref.startswith('refs/tags/'):
1022 tag = ref[len('refs/tags/'):]
1023 tag = hgref(tag)
1024 author, msg = parsed_tags.get(tag, (None, None))
1025 if mode == 'git':
1026 if not msg:
1027 msg = 'Added tag %s for changeset %s' % (tag, node[:12]);
1028 tagnode, branch = write_tag(parser.repo, tag, node, msg, author)
1029 p_revs[tagnode] = 'refs/heads/branches/' + gitref(branch)
1030 else:
1031 fp = parser.repo.opener('localtags', 'a')
1032 fp.write('%s %s\n' % (node, tag))
1033 fp.close()
1034 p_revs[bnode] = ref
1035 print "ok %s" % ref
1036 else:
1037 # transport-helper/fast-export bugs
1038 continue
1040 if peer:
1041 if not push(parser.repo, peer, parsed_refs, p_revs):
1042 # do not update bookmarks
1043 print
1044 return
1046 # update remote bookmarks
1047 remote_bmarks = peer.listkeys('bookmarks')
1048 for ref, bmark, old, new in p_bmarks:
1049 if force_push:
1050 old = remote_bmarks.get(bmark, '')
1051 if not peer.pushkey('bookmarks', bmark, old, new):
1052 print "error %s" % ref
1053 else:
1054 # update local bookmarks
1055 for ref, bmark, old, new in p_bmarks:
1056 if not bookmarks.pushbookmark(parser.repo, bmark, old, new):
1057 print "error %s" % ref
1059 print
1061 def fix_path(alias, repo, orig_url):
1062 url = urlparse.urlparse(orig_url, 'file')
1063 if url.scheme != 'file' or os.path.isabs(url.path):
1064 return
1065 abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url)
1066 cmd = ['git', 'config', 'remote.%s.url' % alias, "hg::%s" % abs_url]
1067 subprocess.call(cmd)
1069 def main(args):
1070 global prefix, gitdir, dirname, branches, bmarks
1071 global marks, blob_marks, parsed_refs
1072 global peer, mode, bad_mail, bad_name
1073 global track_branches, force_push, is_tmp
1074 global parsed_tags
1075 global filenodes
1076 global fake_bmark, hg_version
1078 alias = args[1]
1079 url = args[2]
1080 peer = None
1082 hg_git_compat = get_config_bool('remote-hg.hg-git-compat')
1083 track_branches = get_config_bool('remote-hg.track-branches', True)
1084 force_push = get_config_bool('remote-hg.force-push')
1086 if hg_git_compat:
1087 mode = 'hg'
1088 bad_mail = 'none@none'
1089 bad_name = ''
1090 else:
1091 mode = 'git'
1092 bad_mail = 'unknown'
1093 bad_name = 'Unknown'
1095 if alias[4:] == url:
1096 is_tmp = True
1097 alias = hashlib.sha1(alias).hexdigest()
1098 else:
1099 is_tmp = False
1101 gitdir = os.environ['GIT_DIR']
1102 dirname = os.path.join(gitdir, 'hg', alias)
1103 branches = {}
1104 bmarks = {}
1105 blob_marks = {}
1106 parsed_refs = {}
1107 marks = None
1108 parsed_tags = {}
1109 filenodes = {}
1110 fake_bmark = None
1111 try:
1112 hg_version = tuple(int(e) for e in util.version().split('.'))
1113 except:
1114 hg_version = None
1116 repo = get_repo(url, alias)
1117 prefix = 'refs/hg/%s' % alias
1119 if not is_tmp:
1120 fix_path(alias, peer or repo, url)
1122 marks_path = os.path.join(dirname, 'marks-hg')
1123 marks = Marks(marks_path, repo)
1125 if sys.platform == 'win32':
1126 import msvcrt
1127 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
1129 parser = Parser(repo)
1130 for line in parser:
1131 if parser.check('capabilities'):
1132 do_capabilities(parser)
1133 elif parser.check('list'):
1134 do_list(parser)
1135 elif parser.check('import'):
1136 do_import(parser)
1137 elif parser.check('export'):
1138 do_export(parser)
1139 else:
1140 die('unhandled command: %s' % line)
1141 sys.stdout.flush()
1143 def bye():
1144 if not marks:
1145 return
1146 if not is_tmp:
1147 marks.store()
1148 else:
1149 shutil.rmtree(dirname)
1151 atexit.register(bye)
1152 sys.exit(main(sys.argv))