Add new remote-hg transport helper
[git.git] / contrib / remote-helpers / git-remote-hg
blobe37e278c26d6f9f3cd0bd897212deb61152d9df7
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
14 import re
15 import sys
16 import os
17 import json
19 NAME_RE = re.compile('^([^<>]+)')
20 AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]+)>$')
22 def die(msg, *args):
23 sys.stderr.write('ERROR: %s\n' % (msg % args))
24 sys.exit(1)
26 def warn(msg, *args):
27 sys.stderr.write('WARNING: %s\n' % (msg % args))
29 def gitmode(flags):
30 return 'l' in flags and '120000' or 'x' in flags and '100755' or '100644'
32 def gittz(tz):
33 return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60)
35 class Marks:
37 def __init__(self, path):
38 self.path = path
39 self.tips = {}
40 self.marks = {}
41 self.last_mark = 0
43 self.load()
45 def load(self):
46 if not os.path.exists(self.path):
47 return
49 tmp = json.load(open(self.path))
51 self.tips = tmp['tips']
52 self.marks = tmp['marks']
53 self.last_mark = tmp['last-mark']
55 def dict(self):
56 return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark }
58 def store(self):
59 json.dump(self.dict(), open(self.path, 'w'))
61 def __str__(self):
62 return str(self.dict())
64 def from_rev(self, rev):
65 return self.marks[str(rev)]
67 def get_mark(self, rev):
68 self.last_mark += 1
69 self.marks[str(rev)] = self.last_mark
70 return self.last_mark
72 def is_marked(self, rev):
73 return self.marks.has_key(str(rev))
75 def get_tip(self, branch):
76 return self.tips.get(branch, 0)
78 def set_tip(self, branch, tip):
79 self.tips[branch] = tip
81 class Parser:
83 def __init__(self, repo):
84 self.repo = repo
85 self.line = self.get_line()
87 def get_line(self):
88 return sys.stdin.readline().strip()
90 def __getitem__(self, i):
91 return self.line.split()[i]
93 def check(self, word):
94 return self.line.startswith(word)
96 def each_block(self, separator):
97 while self.line != separator:
98 yield self.line
99 self.line = self.get_line()
101 def __iter__(self):
102 return self.each_block('')
104 def next(self):
105 self.line = self.get_line()
106 if self.line == 'done':
107 self.line = None
109 def export_file(fc):
110 d = fc.data()
111 print "M %s inline %s" % (gitmode(fc.flags()), fc.path())
112 print "data %d" % len(d)
113 print d
115 def get_filechanges(repo, ctx, parent):
116 modified = set()
117 added = set()
118 removed = set()
120 cur = ctx.manifest()
121 prev = repo[parent].manifest().copy()
123 for fn in cur:
124 if fn in prev:
125 if (cur.flags(fn) != prev.flags(fn) or cur[fn] != prev[fn]):
126 modified.add(fn)
127 del prev[fn]
128 else:
129 added.add(fn)
130 removed |= set(prev.keys())
132 return added | modified, removed
134 def fixup_user(user):
135 user = user.replace('"', '')
136 name = mail = None
137 m = AUTHOR_RE.match(user)
138 if m:
139 name = m.group(1)
140 mail = m.group(2).strip()
141 else:
142 m = NAME_RE.match(user)
143 if m:
144 name = m.group(1).strip()
146 if not name:
147 name = 'Unknown'
148 if not mail:
149 mail = 'unknown'
151 return '%s <%s>' % (name, mail)
153 def get_repo(url, alias):
154 global dirname
156 myui = ui.ui()
157 myui.setconfig('ui', 'interactive', 'off')
159 if hg.islocal(url):
160 repo = hg.repository(myui, url)
161 else:
162 local_path = os.path.join(dirname, 'clone')
163 if not os.path.exists(local_path):
164 peer, dstpeer = hg.clone(myui, {}, url, local_path, update=False, pull=True)
165 repo = dstpeer.local()
166 else:
167 repo = hg.repository(myui, local_path)
168 peer = hg.peer(myui, {}, url)
169 repo.pull(peer, heads=None, force=True)
171 return repo
173 def rev_to_mark(rev):
174 global marks
175 return marks.from_rev(rev)
177 def export_ref(repo, name, kind, head):
178 global prefix, marks
180 ename = '%s/%s' % (kind, name)
181 tip = marks.get_tip(ename)
183 # mercurial takes too much time checking this
184 if tip and tip == head.rev():
185 # nothing to do
186 return
187 revs = repo.revs('%u:%u' % (tip, head))
188 count = 0
190 revs = [rev for rev in revs if not marks.is_marked(rev)]
192 for rev in revs:
194 c = repo[rev]
195 (manifest, user, (time, tz), files, desc, extra) = repo.changelog.read(c.node())
196 rev_branch = extra['branch']
198 author = "%s %d %s" % (fixup_user(user), time, gittz(tz))
199 if 'committer' in extra:
200 user, time, tz = extra['committer'].rsplit(' ', 2)
201 committer = "%s %s %s" % (user, time, gittz(int(tz)))
202 else:
203 committer = author
205 parents = [p for p in repo.changelog.parentrevs(rev) if p >= 0]
207 if len(parents) == 0:
208 modified = c.manifest().keys()
209 removed = []
210 else:
211 modified, removed = get_filechanges(repo, c, parents[0])
213 if len(parents) == 0 and rev:
214 print 'reset %s/%s' % (prefix, ename)
216 print "commit %s/%s" % (prefix, ename)
217 print "mark :%d" % (marks.get_mark(rev))
218 print "author %s" % (author)
219 print "committer %s" % (committer)
220 print "data %d" % (len(desc))
221 print desc
223 if len(parents) > 0:
224 print "from :%s" % (rev_to_mark(parents[0]))
225 if len(parents) > 1:
226 print "merge :%s" % (rev_to_mark(parents[1]))
228 for f in modified:
229 export_file(c.filectx(f))
230 for f in removed:
231 print "D %s" % (f)
232 print
234 count += 1
235 if (count % 100 == 0):
236 print "progress revision %d '%s' (%d/%d)" % (rev, name, count, len(revs))
237 print "#############################################################"
239 # make sure the ref is updated
240 print "reset %s/%s" % (prefix, ename)
241 print "from :%u" % rev_to_mark(rev)
242 print
244 marks.set_tip(ename, rev)
246 def export_tag(repo, tag):
247 export_ref(repo, tag, 'tags', repo[tag])
249 def export_bookmark(repo, bmark):
250 head = bmarks[bmark]
251 export_ref(repo, bmark, 'bookmarks', head)
253 def export_branch(repo, branch):
254 tip = get_branch_tip(repo, branch)
255 head = repo[tip]
256 export_ref(repo, branch, 'branches', head)
258 def export_head(repo):
259 global g_head
260 export_ref(repo, g_head[0], 'bookmarks', g_head[1])
262 def do_capabilities(parser):
263 global prefix, dirname
265 print "import"
266 print "refspec refs/heads/branches/*:%s/branches/*" % prefix
267 print "refspec refs/heads/*:%s/bookmarks/*" % prefix
268 print "refspec refs/tags/*:%s/tags/*" % prefix
269 print
271 def get_branch_tip(repo, branch):
272 global branches
274 heads = branches.get(branch, None)
275 if not heads:
276 return None
278 # verify there's only one head
279 if (len(heads) > 1):
280 warn("Branch '%s' has more than one head, consider merging" % branch)
281 # older versions of mercurial don't have this
282 if hasattr(repo, "branchtip"):
283 return repo.branchtip(branch)
285 return heads[0]
287 def list_head(repo, cur):
288 global g_head
290 head = bookmarks.readcurrent(repo)
291 if not head:
292 return
293 node = repo[head]
294 print "@refs/heads/%s HEAD" % head
295 g_head = (head, node)
297 def do_list(parser):
298 global branches, bmarks
300 repo = parser.repo
301 for branch in repo.branchmap():
302 heads = repo.branchheads(branch)
303 if len(heads):
304 branches[branch] = heads
306 for bmark, node in bookmarks.listbookmarks(repo).iteritems():
307 bmarks[bmark] = repo[node]
309 cur = repo.dirstate.branch()
311 list_head(repo, cur)
312 for branch in branches:
313 print "? refs/heads/branches/%s" % branch
314 for bmark in bmarks:
315 print "? refs/heads/%s" % bmark
317 for tag, node in repo.tagslist():
318 if tag == 'tip':
319 continue
320 print "? refs/tags/%s" % tag
322 print
324 def do_import(parser):
325 repo = parser.repo
327 path = os.path.join(dirname, 'marks-git')
329 print "feature done"
330 if os.path.exists(path):
331 print "feature import-marks=%s" % path
332 print "feature export-marks=%s" % path
333 sys.stdout.flush()
335 # lets get all the import lines
336 while parser.check('import'):
337 ref = parser[1]
339 if (ref == 'HEAD'):
340 export_head(repo)
341 elif ref.startswith('refs/heads/branches/'):
342 branch = ref[len('refs/heads/branches/'):]
343 export_branch(repo, branch)
344 elif ref.startswith('refs/heads/'):
345 bmark = ref[len('refs/heads/'):]
346 export_bookmark(repo, bmark)
347 elif ref.startswith('refs/tags/'):
348 tag = ref[len('refs/tags/'):]
349 export_tag(repo, tag)
351 parser.next()
353 print 'done'
355 def main(args):
356 global prefix, dirname, marks, branches, bmarks
358 alias = args[1]
359 url = args[2]
361 gitdir = os.environ['GIT_DIR']
362 dirname = os.path.join(gitdir, 'hg', alias)
363 branches = {}
364 bmarks = {}
366 repo = get_repo(url, alias)
367 prefix = 'refs/hg/%s' % alias
369 if not os.path.exists(dirname):
370 os.makedirs(dirname)
372 marks_path = os.path.join(dirname, 'marks-hg')
373 marks = Marks(marks_path)
375 parser = Parser(repo)
376 for line in parser:
377 if parser.check('capabilities'):
378 do_capabilities(parser)
379 elif parser.check('list'):
380 do_list(parser)
381 elif parser.check('import'):
382 do_import(parser)
383 elif parser.check('export'):
384 do_export(parser)
385 else:
386 die('unhandled command: %s' % line)
387 sys.stdout.flush()
389 marks.store()
391 sys.exit(main(sys.argv))