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
17import sys
18
19import bzrlib
20bzrlib.initialize()
21
22import bzrlib.plugin
23bzrlib.plugin.load_plugins()
24
25import sys
26import os
27import json
28import re
29
30NAME_RE = re.compile('^([^<>]+)')
31AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$')
32
33def die(msg, *args):
34 sys.stderr.write('ERROR: %s\n' % (msg % args))
35 sys.exit(1)
36
37def warn(msg, *args):
38 sys.stderr.write('WARNING: %s\n' % (msg % args))
39
40def gittz(tz):
41 return '%+03d%02d' % (tz / 3600, tz % 3600 / 60)
42
43class 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
91class 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
119def rev_to_mark(rev):
120 global marks
121 return marks.from_rev(rev)
122
123def 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
137def 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
155def 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
182def 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
263def 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
272def 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
298def do_capabilities(parser):
299 print "import"
300 print "refspec refs/heads/*:%s/heads/*" % prefix
301 print
302
303def 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
312def get_repo(url, alias):
313 origin = bzrlib.controldir.ControlDir.open(url)
314 return origin.open_branch()
315
316def 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
352sys.exit(main(sys.argv))