Add new remote-hg transport helper

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com> Signed-off-by: Jeff King <peff@peff.net>

Felipe Contreras committed Nov 4, 2012 at 03:13 UTC 5085a4e2e81f295a93df9b4bd98813d771f18a3c
1 file changed +391
contrib/remote-helpers/git-remote-hg new
+391
@@ -0,0 +1,391 @@
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 +from mercurial import hg, ui, bookmarks
13 +
14 +import re
15 +import sys
16 +import os
17 +import json
18 +
19 +NAME_RE = re.compile('^([^<>]+)')
20 +AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]+)>$')
21 +
22 +def die(msg, *args):
23 + sys.stderr.write('ERROR: %s\n' % (msg % args))
24 + sys.exit(1)
25 +
26 +def warn(msg, *args):
27 + sys.stderr.write('WARNING: %s\n' % (msg % args))
28 +
29 +def gitmode(flags):
30 + return 'l' in flags and '120000' or 'x' in flags and '100755' or '100644'
31 +
32 +def gittz(tz):
33 + return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60)
34 +
35 +class Marks:
36 +
37 + def __init__(self, path):
38 + self.path = path
39 + self.tips = {}
40 + self.marks = {}
41 + self.last_mark = 0
42 +
43 + self.load()
44 +
45 + def load(self):
46 + if not os.path.exists(self.path):
47 + return
48 +
49 + tmp = json.load(open(self.path))
50 +
51 + self.tips = tmp['tips']
52 + self.marks = tmp['marks']
53 + self.last_mark = tmp['last-mark']
54 +
55 + def dict(self):
56 + return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark }
57 +
58 + def store(self):
59 + json.dump(self.dict(), open(self.path, 'w'))
60 +
61 + def __str__(self):
62 + return str(self.dict())
63 +
64 + def from_rev(self, rev):
65 + return self.marks[str(rev)]
66 +
67 + def get_mark(self, rev):
68 + self.last_mark += 1
69 + self.marks[str(rev)] = self.last_mark
70 + return self.last_mark
71 +
72 + def is_marked(self, rev):
73 + return self.marks.has_key(str(rev))
74 +
75 + def get_tip(self, branch):
76 + return self.tips.get(branch, 0)
77 +
78 + def set_tip(self, branch, tip):
79 + self.tips[branch] = tip
80 +
81 +class Parser:
82 +
83 + def __init__(self, repo):
84 + self.repo = repo
85 + self.line = self.get_line()
86 +
87 + def get_line(self):
88 + return sys.stdin.readline().strip()
89 +
90 + def __getitem__(self, i):
91 + return self.line.split()[i]
92 +
93 + def check(self, word):
94 + return self.line.startswith(word)
95 +
96 + def each_block(self, separator):
97 + while self.line != separator:
98 + yield self.line
99 + self.line = self.get_line()
100 +
101 + def __iter__(self):
102 + return self.each_block('')
103 +
104 + def next(self):
105 + self.line = self.get_line()
106 + if self.line == 'done':
107 + self.line = None
108 +
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
114 +
115 +def get_filechanges(repo, ctx, parent):
116 + modified = set()
117 + added = set()
118 + removed = set()
119 +
120 + cur = ctx.manifest()
121 + prev = repo[parent].manifest().copy()
122 +
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())
131 +
132 + return added | modified, removed
133 +
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()
145 +
146 + if not name:
147 + name = 'Unknown'
148 + if not mail:
149 + mail = 'unknown'
150 +
151 + return '%s <%s>' % (name, mail)
152 +
153 +def get_repo(url, alias):
154 + global dirname
155 +
156 + myui = ui.ui()
157 + myui.setconfig('ui', 'interactive', 'off')
158 +
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)
170 +
171 + return repo
172 +
173 +def rev_to_mark(rev):
174 + global marks
175 + return marks.from_rev(rev)
176 +
177 +def export_ref(repo, name, kind, head):
178 + global prefix, marks
179 +
180 + ename = '%s/%s' % (kind, name)
181 + tip = marks.get_tip(ename)
182 +
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
189 +
190 + revs = [rev for rev in revs if not marks.is_marked(rev)]
191 +
192 + for rev in revs:
193 +
194 + c = repo[rev]
195 + (manifest, user, (time, tz), files, desc, extra) = repo.changelog.read(c.node())
196 + rev_branch = extra['branch']
197 +
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
204 +
205 + parents = [p for p in repo.changelog.parentrevs(rev) if p >= 0]
206 +
207 + if len(parents) == 0:
208 + modified = c.manifest().keys()
209 + removed = []
210 + else:
211 + modified, removed = get_filechanges(repo, c, parents[0])
212 +
213 + if len(parents) == 0 and rev:
214 + print 'reset %s/%s' % (prefix, ename)
215 +
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
222 +
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]))
227 +
228 + for f in modified:
229 + export_file(c.filectx(f))
230 + for f in removed:
231 + print "D %s" % (f)
232 + print
233 +
234 + count += 1
235 + if (count % 100 == 0):
236 + print "progress revision %d '%s' (%d/%d)" % (rev, name, count, len(revs))
237 + print "#############################################################"
238 +
239 + # make sure the ref is updated
240 + print "reset %s/%s" % (prefix, ename)
241 + print "from :%u" % rev_to_mark(rev)
242 + print
243 +
244 + marks.set_tip(ename, rev)
245 +
246 +def export_tag(repo, tag):
247 + export_ref(repo, tag, 'tags', repo[tag])
248 +
249 +def export_bookmark(repo, bmark):
250 + head = bmarks[bmark]
251 + export_ref(repo, bmark, 'bookmarks', head)
252 +
253 +def export_branch(repo, branch):
254 + tip = get_branch_tip(repo, branch)
255 + head = repo[tip]
256 + export_ref(repo, branch, 'branches', head)
257 +
258 +def export_head(repo):
259 + global g_head
260 + export_ref(repo, g_head[0], 'bookmarks', g_head[1])
261 +
262 +def do_capabilities(parser):
263 + global prefix, dirname
264 +
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
270 +
271 +def get_branch_tip(repo, branch):
272 + global branches
273 +
274 + heads = branches.get(branch, None)
275 + if not heads:
276 + return None
277 +
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)
284 +
285 + return heads[0]
286 +
287 +def list_head(repo, cur):
288 + global g_head
289 +
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)
296 +
297 +def do_list(parser):
298 + global branches, bmarks
299 +
300 + repo = parser.repo
301 + for branch in repo.branchmap():
302 + heads = repo.branchheads(branch)
303 + if len(heads):
304 + branches[branch] = heads
305 +
306 + for bmark, node in bookmarks.listbookmarks(repo).iteritems():
307 + bmarks[bmark] = repo[node]
308 +
309 + cur = repo.dirstate.branch()
310 +
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
316 +
317 + for tag, node in repo.tagslist():
318 + if tag == 'tip':
319 + continue
320 + print "? refs/tags/%s" % tag
321 +
322 + print
323 +
324 +def do_import(parser):
325 + repo = parser.repo
326 +
327 + path = os.path.join(dirname, 'marks-git')
328 +
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()
334 +
335 + # lets get all the import lines
336 + while parser.check('import'):
337 + ref = parser[1]
338 +
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)
350 +
351 + parser.next()
352 +
353 + print 'done'
354 +
355 +def main(args):
356 + global prefix, dirname, marks, branches, bmarks
357 +
358 + alias = args[1]
359 + url = args[2]
360 +
361 + gitdir = os.environ['GIT_DIR']
362 + dirname = os.path.join(gitdir, 'hg', alias)
363 + branches = {}
364 + bmarks = {}
365 +
366 + repo = get_repo(url, alias)
367 + prefix = 'refs/hg/%s' % alias
368 +
369 + if not os.path.exists(dirname):
370 + os.makedirs(dirname)
371 +
372 + marks_path = os.path.join(dirname, 'marks-hg')
373 + marks = Marks(marks_path)
374 +
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()
388 +
389 + marks.store()
390 +
391 +sys.exit(main(sys.argv))