Add new remote-bzr transport helper

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Felipe Contreras committed Nov 11, 2012 at 15:19 UTC bee118ec048911cfacdd65ad1ea2bdd6e25a6254
1 file changed +352
contrib/remote-helpers/git-remote-bzr new
+352
@@ -0,0 +1,352 @@
1 +#!/usr/bin/env python
2 +#
3 +# Copyright (c) 2012 Felipe Contreras
4 +#
5 +
6 +#
7 +# Just copy to your ~/bin, or anywhere in your $PATH.
8 +# Then you can clone with:
9 +# % git clone bzr::/path/to/bzr/repo/or/url
10 +#
11 +# For example:
12 +# % git clone bzr::$HOME/myrepo
13 +# or
14 +# % git clone bzr::lp:myrepo
15 +#
16 +
17 +import sys
18 +
19 +import bzrlib
20 +bzrlib.initialize()
21 +
22 +import bzrlib.plugin
23 +bzrlib.plugin.load_plugins()
24 +
25 +import sys
26 +import os
27 +import json
28 +import re
29 +
30 +NAME_RE = re.compile('^([^<>]+)')
31 +AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$')
32 +
33 +def die(msg, *args):
34 + sys.stderr.write('ERROR: %s\n' % (msg % args))
35 + sys.exit(1)
36 +
37 +def warn(msg, *args):
38 + sys.stderr.write('WARNING: %s\n' % (msg % args))
39 +
40 +def gittz(tz):
41 + return '%+03d%02d' % (tz / 3600, tz % 3600 / 60)
42 +
43 +class Marks:
44 +
45 + def __init__(self, path):
46 + self.path = path
47 + self.tips = {}
48 + self.marks = {}
49 + self.last_mark = 0
50 + self.load()
51 +
52 + def load(self):
53 + if not os.path.exists(self.path):
54 + return
55 +
56 + tmp = json.load(open(self.path))
57 + self.tips = tmp['tips']
58 + self.marks = tmp['marks']
59 + self.last_mark = tmp['last-mark']
60 +
61 + def dict(self):
62 + return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark }
63 +
64 + def store(self):
65 + json.dump(self.dict(), open(self.path, 'w'))
66 +
67 + def __str__(self):
68 + return str(self.dict())
69 +
70 + def from_rev(self, rev):
71 + return self.marks[rev]
72 +
73 + def next_mark(self):
74 + self.last_mark += 1
75 + return self.last_mark
76 +
77 + def get_mark(self, rev):
78 + self.last_mark += 1
79 + self.marks[rev] = self.last_mark
80 + return self.last_mark
81 +
82 + def is_marked(self, rev):
83 + return self.marks.has_key(rev)
84 +
85 + def get_tip(self, branch):
86 + return self.tips.get(branch, None)
87 +
88 + def set_tip(self, branch, tip):
89 + self.tips[branch] = tip
90 +
91 +class Parser:
92 +
93 + def __init__(self, repo):
94 + self.repo = repo
95 + self.line = self.get_line()
96 +
97 + def get_line(self):
98 + return sys.stdin.readline().strip()
99 +
100 + def __getitem__(self, i):
101 + return self.line.split()[i]
102 +
103 + def check(self, word):
104 + return self.line.startswith(word)
105 +
106 + def each_block(self, separator):
107 + while self.line != separator:
108 + yield self.line
109 + self.line = self.get_line()
110 +
111 + def __iter__(self):
112 + return self.each_block('')
113 +
114 + def next(self):
115 + self.line = self.get_line()
116 + if self.line == 'done':
117 + self.line = None
118 +
119 +def rev_to_mark(rev):
120 + global marks
121 + return marks.from_rev(rev)
122 +
123 +def fixup_user(user):
124 + name = mail = None
125 + user = user.replace('"', '')
126 + m = AUTHOR_RE.match(user)
127 + if m:
128 + name = m.group(1)
129 + mail = m.group(2).strip()
130 + else:
131 + m = NAME_RE.match(user)
132 + if m:
133 + name = m.group(1).strip()
134 +
135 + return '%s <%s>' % (name, mail)
136 +
137 +def get_filechanges(cur, prev):
138 + modified = {}
139 + removed = {}
140 +
141 + changes = cur.changes_from(prev)
142 +
143 + for path, fid, kind in changes.added:
144 + modified[path] = fid
145 + for path, fid, kind in changes.removed:
146 + removed[path] = None
147 + for path, fid, kind, mod, _ in changes.modified:
148 + modified[path] = fid
149 + for oldpath, newpath, fid, kind, mod, _ in changes.renamed:
150 + removed[oldpath] = None
151 + modified[newpath] = fid
152 +
153 + return modified, removed
154 +
155 +def export_files(tree, files):
156 + global marks, filenodes
157 +
158 + final = []
159 + for path, fid in files.iteritems():
160 + h = tree.get_file_sha1(fid)
161 +
162 + mode = '100644'
163 +
164 + # is the blob already exported?
165 + if h in filenodes:
166 + mark = filenodes[h]
167 + else:
168 + d = tree.get_file_text(fid)
169 +
170 + mark = marks.next_mark()
171 + filenodes[h] = mark
172 +
173 + print "blob"
174 + print "mark :%u" % mark
175 + print "data %d" % len(d)
176 + print d
177 +
178 + final.append((mode, mark, path))
179 +
180 + return final
181 +
182 +def export_branch(branch, name):
183 + global prefix, dirname
184 +
185 + ref = '%s/heads/%s' % (prefix, name)
186 + tip = marks.get_tip(name)
187 +
188 + repo = branch.repository
189 + repo.lock_read()
190 + revs = branch.iter_merge_sorted_revisions(None, tip, 'exclude', 'forward')
191 + count = 0
192 +
193 + revs = [revid for revid, _, _, _ in revs if not marks.is_marked(revid)]
194 +
195 + for revid in revs:
196 +
197 + rev = repo.get_revision(revid)
198 +
199 + parents = rev.parent_ids
200 + time = rev.timestamp
201 + tz = rev.timezone
202 + committer = rev.committer.encode('utf-8')
203 + committer = "%s %u %s" % (fixup_user(committer), time, gittz(tz))
204 + author = committer
205 + msg = rev.message.encode('utf-8')
206 +
207 + msg += '\n'
208 +
209 + if len(parents) == 0:
210 + parent = bzrlib.revision.NULL_REVISION
211 + else:
212 + parent = parents[0]
213 +
214 + cur_tree = repo.revision_tree(revid)
215 + prev = repo.revision_tree(parent)
216 + modified, removed = get_filechanges(cur_tree, prev)
217 +
218 + modified_final = export_files(cur_tree, modified)
219 +
220 + if len(parents) == 0:
221 + print 'reset %s' % ref
222 +
223 + print "commit %s" % ref
224 + print "mark :%d" % (marks.get_mark(revid))
225 + print "author %s" % (author)
226 + print "committer %s" % (committer)
227 + print "data %d" % (len(msg))
228 + print msg
229 +
230 + for i, p in enumerate(parents):
231 + try:
232 + m = rev_to_mark(p)
233 + except KeyError:
234 + # ghost?
235 + continue
236 + if i == 0:
237 + print "from :%s" % m
238 + else:
239 + print "merge :%s" % m
240 +
241 + for f in modified_final:
242 + print "M %s :%u %s" % f
243 + for f in removed:
244 + print "D %s" % (f)
245 + print
246 +
247 + count += 1
248 + if (count % 100 == 0):
249 + print "progress revision %s (%d/%d)" % (revid, count, len(revs))
250 + print "#############################################################"
251 +
252 + repo.unlock()
253 +
254 + revid = branch.last_revision()
255 +
256 + # make sure the ref is updated
257 + print "reset %s" % ref
258 + print "from :%u" % rev_to_mark(revid)
259 + print
260 +
261 + marks.set_tip(name, revid)
262 +
263 +def export_tag(repo, name):
264 + global tags
265 + try:
266 + print "reset refs/tags/%s" % name
267 + print "from :%u" % rev_to_mark(tags[name])
268 + print
269 + except KeyError:
270 + warn("TODO: fetch tag '%s'" % name)
271 +
272 +def do_import(parser):
273 + global dirname
274 +
275 + branch = parser.repo
276 + path = os.path.join(dirname, 'marks-git')
277 +
278 + print "feature done"
279 + if os.path.exists(path):
280 + print "feature import-marks=%s" % path
281 + print "feature export-marks=%s" % path
282 + sys.stdout.flush()
283 +
284 + while parser.check('import'):
285 + ref = parser[1]
286 + if ref.startswith('refs/heads/'):
287 + name = ref[len('refs/heads/'):]
288 + export_branch(branch, name)
289 + if ref.startswith('refs/tags/'):
290 + name = ref[len('refs/tags/'):]
291 + export_tag(branch, name)
292 + parser.next()
293 +
294 + print 'done'
295 +
296 + sys.stdout.flush()
297 +
298 +def do_capabilities(parser):
299 + print "import"
300 + print "refspec refs/heads/*:%s/heads/*" % prefix
301 + print
302 +
303 +def do_list(parser):
304 + global tags
305 + print "? refs/heads/%s" % 'master'
306 + for tag, revid in parser.repo.tags.get_tag_dict().items():
307 + print "? refs/tags/%s" % tag
308 + tags[tag] = revid
309 + print "@refs/heads/%s HEAD" % 'master'
310 + print
311 +
312 +def get_repo(url, alias):
313 + origin = bzrlib.controldir.ControlDir.open(url)
314 + return origin.open_branch()
315 +
316 +def main(args):
317 + global marks, prefix, dirname
318 + global tags, filenodes
319 +
320 + alias = args[1]
321 + url = args[2]
322 +
323 + prefix = 'refs/bzr/%s' % alias
324 + tags = {}
325 + filenodes = {}
326 +
327 + gitdir = os.environ['GIT_DIR']
328 + dirname = os.path.join(gitdir, 'bzr', alias)
329 +
330 + if not os.path.exists(dirname):
331 + os.makedirs(dirname)
332 +
333 + repo = get_repo(url, alias)
334 +
335 + marks_path = os.path.join(dirname, 'marks-int')
336 + marks = Marks(marks_path)
337 +
338 + parser = Parser(repo)
339 + for line in parser:
340 + if parser.check('capabilities'):
341 + do_capabilities(parser)
342 + elif parser.check('list'):
343 + do_list(parser)
344 + elif parser.check('import'):
345 + do_import(parser)
346 + else:
347 + die('unhandled command: %s' % line)
348 + sys.stdout.flush()
349 +
350 + marks.store()
351 +
352 +sys.exit(main(sys.argv))