remote-hg: add support to push URLs
[alt-git.git] / contrib / remote-helpers / git-remote-hg
bloba5023c92fa834addabf29ee9d533f2dcfb58365c
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 from mercurial import hg, ui, bookmarks, context, util
14 import re
15 import sys
16 import os
17 import json
18 import shutil
20 NAME_RE = re.compile('^([^<>]+)')
21 AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]+)>$')
22 RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.+)> (\d+) ([+-]\d+)')
24 def die(msg, *args):
25 sys.stderr.write('ERROR: %s\n' % (msg % args))
26 sys.exit(1)
28 def warn(msg, *args):
29 sys.stderr.write('WARNING: %s\n' % (msg % args))
31 def gitmode(flags):
32 return 'l' in flags and '120000' or 'x' in flags and '100755' or '100644'
34 def gittz(tz):
35 return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60)
37 def hgmode(mode):
38 m = { '0100755': 'x', '0120000': 'l' }
39 return m.get(mode, '')
41 class Marks:
43 def __init__(self, path):
44 self.path = path
45 self.tips = {}
46 self.marks = {}
47 self.rev_marks = {}
48 self.last_mark = 0
50 self.load()
52 def load(self):
53 if not os.path.exists(self.path):
54 return
56 tmp = json.load(open(self.path))
58 self.tips = tmp['tips']
59 self.marks = tmp['marks']
60 self.last_mark = tmp['last-mark']
62 for rev, mark in self.marks.iteritems():
63 self.rev_marks[mark] = int(rev)
65 def dict(self):
66 return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark }
68 def store(self):
69 json.dump(self.dict(), open(self.path, 'w'))
71 def __str__(self):
72 return str(self.dict())
74 def from_rev(self, rev):
75 return self.marks[str(rev)]
77 def to_rev(self, mark):
78 return self.rev_marks[mark]
80 def get_mark(self, rev):
81 self.last_mark += 1
82 self.marks[str(rev)] = self.last_mark
83 return self.last_mark
85 def new_mark(self, rev, mark):
86 self.marks[str(rev)] = mark
87 self.rev_marks[mark] = rev
88 self.last_mark = mark
90 def is_marked(self, rev):
91 return self.marks.has_key(str(rev))
93 def get_tip(self, branch):
94 return self.tips.get(branch, 0)
96 def set_tip(self, branch, tip):
97 self.tips[branch] = tip
99 class Parser:
101 def __init__(self, repo):
102 self.repo = repo
103 self.line = self.get_line()
105 def get_line(self):
106 return sys.stdin.readline().strip()
108 def __getitem__(self, i):
109 return self.line.split()[i]
111 def check(self, word):
112 return self.line.startswith(word)
114 def each_block(self, separator):
115 while self.line != separator:
116 yield self.line
117 self.line = self.get_line()
119 def __iter__(self):
120 return self.each_block('')
122 def next(self):
123 self.line = self.get_line()
124 if self.line == 'done':
125 self.line = None
127 def get_mark(self):
128 i = self.line.index(':') + 1
129 return int(self.line[i:])
131 def get_data(self):
132 if not self.check('data'):
133 return None
134 i = self.line.index(' ') + 1
135 size = int(self.line[i:])
136 return sys.stdin.read(size)
138 def get_author(self):
139 m = RAW_AUTHOR_RE.match(self.line)
140 if not m:
141 return None
142 _, name, email, date, tz = m.groups()
144 if email != 'unknown':
145 if name:
146 user = '%s <%s>' % (name, email)
147 else:
148 user = '<%s>' % (email)
149 else:
150 user = name
152 tz = int(tz)
153 tz = ((tz / 100) * 3600) + ((tz % 100) * 60)
154 return (user, int(date), -tz)
156 def export_file(fc):
157 d = fc.data()
158 print "M %s inline %s" % (gitmode(fc.flags()), fc.path())
159 print "data %d" % len(d)
160 print d
162 def get_filechanges(repo, ctx, parent):
163 modified = set()
164 added = set()
165 removed = set()
167 cur = ctx.manifest()
168 prev = repo[parent].manifest().copy()
170 for fn in cur:
171 if fn in prev:
172 if (cur.flags(fn) != prev.flags(fn) or cur[fn] != prev[fn]):
173 modified.add(fn)
174 del prev[fn]
175 else:
176 added.add(fn)
177 removed |= set(prev.keys())
179 return added | modified, removed
181 def fixup_user(user):
182 user = user.replace('"', '')
183 name = mail = None
184 m = AUTHOR_RE.match(user)
185 if m:
186 name = m.group(1)
187 mail = m.group(2).strip()
188 else:
189 m = NAME_RE.match(user)
190 if m:
191 name = m.group(1).strip()
193 if not name:
194 name = 'Unknown'
195 if not mail:
196 mail = 'unknown'
198 return '%s <%s>' % (name, mail)
200 def get_repo(url, alias):
201 global dirname, peer
203 myui = ui.ui()
204 myui.setconfig('ui', 'interactive', 'off')
206 if hg.islocal(url):
207 repo = hg.repository(myui, url)
208 else:
209 local_path = os.path.join(dirname, 'clone')
210 if not os.path.exists(local_path):
211 peer, dstpeer = hg.clone(myui, {}, url, local_path, update=False, pull=True)
212 repo = dstpeer.local()
213 else:
214 repo = hg.repository(myui, local_path)
215 peer = hg.peer(myui, {}, url)
216 repo.pull(peer, heads=None, force=True)
218 return repo
220 def rev_to_mark(rev):
221 global marks
222 return marks.from_rev(rev)
224 def mark_to_rev(mark):
225 global marks
226 return marks.to_rev(mark)
228 def export_ref(repo, name, kind, head):
229 global prefix, marks
231 ename = '%s/%s' % (kind, name)
232 tip = marks.get_tip(ename)
234 # mercurial takes too much time checking this
235 if tip and tip == head.rev():
236 # nothing to do
237 return
238 revs = repo.revs('%u:%u' % (tip, head))
239 count = 0
241 revs = [rev for rev in revs if not marks.is_marked(rev)]
243 for rev in revs:
245 c = repo[rev]
246 (manifest, user, (time, tz), files, desc, extra) = repo.changelog.read(c.node())
247 rev_branch = extra['branch']
249 author = "%s %d %s" % (fixup_user(user), time, gittz(tz))
250 if 'committer' in extra:
251 user, time, tz = extra['committer'].rsplit(' ', 2)
252 committer = "%s %s %s" % (user, time, gittz(int(tz)))
253 else:
254 committer = author
256 parents = [p for p in repo.changelog.parentrevs(rev) if p >= 0]
258 if len(parents) == 0:
259 modified = c.manifest().keys()
260 removed = []
261 else:
262 modified, removed = get_filechanges(repo, c, parents[0])
264 if len(parents) == 0 and rev:
265 print 'reset %s/%s' % (prefix, ename)
267 print "commit %s/%s" % (prefix, ename)
268 print "mark :%d" % (marks.get_mark(rev))
269 print "author %s" % (author)
270 print "committer %s" % (committer)
271 print "data %d" % (len(desc))
272 print desc
274 if len(parents) > 0:
275 print "from :%s" % (rev_to_mark(parents[0]))
276 if len(parents) > 1:
277 print "merge :%s" % (rev_to_mark(parents[1]))
279 for f in modified:
280 export_file(c.filectx(f))
281 for f in removed:
282 print "D %s" % (f)
283 print
285 count += 1
286 if (count % 100 == 0):
287 print "progress revision %d '%s' (%d/%d)" % (rev, name, count, len(revs))
288 print "#############################################################"
290 # make sure the ref is updated
291 print "reset %s/%s" % (prefix, ename)
292 print "from :%u" % rev_to_mark(rev)
293 print
295 marks.set_tip(ename, rev)
297 def export_tag(repo, tag):
298 export_ref(repo, tag, 'tags', repo[tag])
300 def export_bookmark(repo, bmark):
301 head = bmarks[bmark]
302 export_ref(repo, bmark, 'bookmarks', head)
304 def export_branch(repo, branch):
305 tip = get_branch_tip(repo, branch)
306 head = repo[tip]
307 export_ref(repo, branch, 'branches', head)
309 def export_head(repo):
310 global g_head
311 export_ref(repo, g_head[0], 'bookmarks', g_head[1])
313 def do_capabilities(parser):
314 global prefix, dirname
316 print "import"
317 print "export"
318 print "refspec refs/heads/branches/*:%s/branches/*" % prefix
319 print "refspec refs/heads/*:%s/bookmarks/*" % prefix
320 print "refspec refs/tags/*:%s/tags/*" % prefix
322 path = os.path.join(dirname, 'marks-git')
324 if os.path.exists(path):
325 print "*import-marks %s" % path
326 print "*export-marks %s" % path
328 print
330 def get_branch_tip(repo, branch):
331 global branches
333 heads = branches.get(branch, None)
334 if not heads:
335 return None
337 # verify there's only one head
338 if (len(heads) > 1):
339 warn("Branch '%s' has more than one head, consider merging" % branch)
340 # older versions of mercurial don't have this
341 if hasattr(repo, "branchtip"):
342 return repo.branchtip(branch)
344 return heads[0]
346 def list_head(repo, cur):
347 global g_head
349 head = bookmarks.readcurrent(repo)
350 if not head:
351 return
352 node = repo[head]
353 print "@refs/heads/%s HEAD" % head
354 g_head = (head, node)
356 def do_list(parser):
357 global branches, bmarks
359 repo = parser.repo
360 for branch in repo.branchmap():
361 heads = repo.branchheads(branch)
362 if len(heads):
363 branches[branch] = heads
365 for bmark, node in bookmarks.listbookmarks(repo).iteritems():
366 bmarks[bmark] = repo[node]
368 cur = repo.dirstate.branch()
370 list_head(repo, cur)
371 for branch in branches:
372 print "? refs/heads/branches/%s" % branch
373 for bmark in bmarks:
374 print "? refs/heads/%s" % bmark
376 for tag, node in repo.tagslist():
377 if tag == 'tip':
378 continue
379 print "? refs/tags/%s" % tag
381 print
383 def do_import(parser):
384 repo = parser.repo
386 path = os.path.join(dirname, 'marks-git')
388 print "feature done"
389 if os.path.exists(path):
390 print "feature import-marks=%s" % path
391 print "feature export-marks=%s" % path
392 sys.stdout.flush()
394 # lets get all the import lines
395 while parser.check('import'):
396 ref = parser[1]
398 if (ref == 'HEAD'):
399 export_head(repo)
400 elif ref.startswith('refs/heads/branches/'):
401 branch = ref[len('refs/heads/branches/'):]
402 export_branch(repo, branch)
403 elif ref.startswith('refs/heads/'):
404 bmark = ref[len('refs/heads/'):]
405 export_bookmark(repo, bmark)
406 elif ref.startswith('refs/tags/'):
407 tag = ref[len('refs/tags/'):]
408 export_tag(repo, tag)
410 parser.next()
412 print 'done'
414 def parse_blob(parser):
415 global blob_marks
417 parser.next()
418 mark = parser.get_mark()
419 parser.next()
420 data = parser.get_data()
421 blob_marks[mark] = data
422 parser.next()
423 return
425 def parse_commit(parser):
426 global marks, blob_marks, bmarks, parsed_refs
428 from_mark = merge_mark = None
430 ref = parser[1]
431 parser.next()
433 commit_mark = parser.get_mark()
434 parser.next()
435 author = parser.get_author()
436 parser.next()
437 committer = parser.get_author()
438 parser.next()
439 data = parser.get_data()
440 parser.next()
441 if parser.check('from'):
442 from_mark = parser.get_mark()
443 parser.next()
444 if parser.check('merge'):
445 merge_mark = parser.get_mark()
446 parser.next()
447 if parser.check('merge'):
448 die('octopus merges are not supported yet')
450 files = {}
452 for line in parser:
453 if parser.check('M'):
454 t, m, mark_ref, path = line.split(' ')
455 mark = int(mark_ref[1:])
456 f = { 'mode' : hgmode(m), 'data' : blob_marks[mark] }
457 elif parser.check('D'):
458 t, path = line.split(' ')
459 f = { 'deleted' : True }
460 else:
461 die('Unknown file command: %s' % line)
462 files[path] = f
464 def getfilectx(repo, memctx, f):
465 of = files[f]
466 if 'deleted' in of:
467 raise IOError
468 is_exec = of['mode'] == 'x'
469 is_link = of['mode'] == 'l'
470 return context.memfilectx(f, of['data'], is_link, is_exec, None)
472 repo = parser.repo
474 user, date, tz = author
475 extra = {}
477 if committer != author:
478 extra['committer'] = "%s %u %u" % committer
480 if from_mark:
481 p1 = repo.changelog.node(mark_to_rev(from_mark))
482 else:
483 p1 = '\0' * 20
485 if merge_mark:
486 p2 = repo.changelog.node(mark_to_rev(merge_mark))
487 else:
488 p2 = '\0' * 20
490 ctx = context.memctx(repo, (p1, p2), data,
491 files.keys(), getfilectx,
492 user, (date, tz), extra)
494 node = repo.commitctx(ctx)
496 rev = repo[node].rev()
498 parsed_refs[ref] = node
500 marks.new_mark(rev, commit_mark)
502 def parse_reset(parser):
503 ref = parser[1]
504 parser.next()
505 # ugh
506 if parser.check('commit'):
507 parse_commit(parser)
508 return
509 if not parser.check('from'):
510 return
511 from_mark = parser.get_mark()
512 parser.next()
514 node = parser.repo.changelog.node(mark_to_rev(from_mark))
515 parsed_refs[ref] = node
517 def parse_tag(parser):
518 name = parser[1]
519 parser.next()
520 from_mark = parser.get_mark()
521 parser.next()
522 tagger = parser.get_author()
523 parser.next()
524 data = parser.get_data()
525 parser.next()
527 # nothing to do
529 def do_export(parser):
530 global parsed_refs, bmarks, peer
532 parser.next()
534 for line in parser.each_block('done'):
535 if parser.check('blob'):
536 parse_blob(parser)
537 elif parser.check('commit'):
538 parse_commit(parser)
539 elif parser.check('reset'):
540 parse_reset(parser)
541 elif parser.check('tag'):
542 parse_tag(parser)
543 elif parser.check('feature'):
544 pass
545 else:
546 die('unhandled export command: %s' % line)
548 for ref, node in parsed_refs.iteritems():
549 if ref.startswith('refs/heads/branches'):
550 pass
551 elif ref.startswith('refs/heads/'):
552 bmark = ref[len('refs/heads/'):]
553 if bmark in bmarks:
554 old = bmarks[bmark].hex()
555 else:
556 old = ''
557 if not bookmarks.pushbookmark(parser.repo, bmark, old, node):
558 continue
559 elif ref.startswith('refs/tags/'):
560 tag = ref[len('refs/tags/'):]
561 parser.repo.tag([tag], node, None, True, None, {})
562 print "ok %s" % ref
564 print
566 if peer:
567 parser.repo.push(peer, force=False)
569 def main(args):
570 global prefix, dirname, branches, bmarks
571 global marks, blob_marks, parsed_refs
572 global peer
574 alias = args[1]
575 url = args[2]
576 peer = None
578 if alias[4:] == url:
579 is_tmp = True
580 alias = util.sha1(alias).hexdigest()
581 else:
582 is_tmp = False
584 gitdir = os.environ['GIT_DIR']
585 dirname = os.path.join(gitdir, 'hg', alias)
586 branches = {}
587 bmarks = {}
588 blob_marks = {}
589 parsed_refs = {}
591 repo = get_repo(url, alias)
592 prefix = 'refs/hg/%s' % alias
594 if not os.path.exists(dirname):
595 os.makedirs(dirname)
597 marks_path = os.path.join(dirname, 'marks-hg')
598 marks = Marks(marks_path)
600 parser = Parser(repo)
601 for line in parser:
602 if parser.check('capabilities'):
603 do_capabilities(parser)
604 elif parser.check('list'):
605 do_list(parser)
606 elif parser.check('import'):
607 do_import(parser)
608 elif parser.check('export'):
609 do_export(parser)
610 else:
611 die('unhandled command: %s' % line)
612 sys.stdout.flush()
614 if not is_tmp:
615 marks.store()
616 else:
617 shutil.rmtree(dirname)
619 sys.exit(main(sys.argv))