3 # Copyright (c) 2012 Felipe Contreras
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
12 # % git clone bzr::$HOME/myrepo
14 # % git clone bzr::lp:myrepo
23 bzrlib
.plugin
.load_plugins()
25 import bzrlib
.generate_ids
33 NAME_RE
= re
.compile('^([^<>]+)')
34 AUTHOR_RE
= re
.compile('^([^<>]+?)? ?<([^<>]*)>$')
35 RAW_AUTHOR_RE
= re
.compile('^(\w+) (.+)? <(.*)> (\d+) ([+-]\d+)')
38 sys
.stderr
.write('ERROR: %s\n' % (msg
% args
))
42 sys
.stderr
.write('WARNING: %s\n' % (msg
% args
))
45 return '%+03d%02d' % (tz
/ 3600, tz
% 3600 / 60)
49 def __init__(self
, path
):
58 if not os
.path
.exists(self
.path
):
61 tmp
= json
.load(open(self
.path
))
62 self
.tips
= tmp
['tips']
63 self
.marks
= tmp
['marks']
64 self
.last_mark
= tmp
['last-mark']
66 for rev
, mark
in self
.marks
.iteritems():
67 self
.rev_marks
[mark
] = rev
70 return { 'tips': self
.tips
, 'marks': self
.marks
, 'last-mark' : self
.last_mark
}
73 json
.dump(self
.dict(), open(self
.path
, 'w'))
76 return str(self
.dict())
78 def from_rev(self
, rev
):
79 return self
.marks
[rev
]
81 def to_rev(self
, mark
):
82 return self
.rev_marks
[mark
]
88 def get_mark(self
, rev
):
90 self
.marks
[rev
] = self
.last_mark
93 def is_marked(self
, rev
):
94 return self
.marks
.has_key(rev
)
96 def new_mark(self
, rev
, mark
):
97 self
.marks
[rev
] = mark
98 self
.rev_marks
[mark
] = rev
101 def get_tip(self
, branch
):
102 return self
.tips
.get(branch
, None)
104 def set_tip(self
, branch
, tip
):
105 self
.tips
[branch
] = tip
109 def __init__(self
, repo
):
111 self
.line
= self
.get_line()
114 return sys
.stdin
.readline().strip()
116 def __getitem__(self
, i
):
117 return self
.line
.split()[i
]
119 def check(self
, word
):
120 return self
.line
.startswith(word
)
122 def each_block(self
, separator
):
123 while self
.line
!= separator
:
125 self
.line
= self
.get_line()
128 return self
.each_block('')
131 self
.line
= self
.get_line()
132 if self
.line
== 'done':
136 i
= self
.line
.index(':') + 1
137 return int(self
.line
[i
:])
140 if not self
.check('data'):
142 i
= self
.line
.index(' ') + 1
143 size
= int(self
.line
[i
:])
144 return sys
.stdin
.read(size
)
146 def get_author(self
):
147 m
= RAW_AUTHOR_RE
.match(self
.line
)
150 _
, name
, email
, date
, tz
= m
.groups()
151 committer
= '%s <%s>' % (name
, email
)
153 tz
= ((tz
/ 100) * 3600) + ((tz
% 100) * 60)
154 return (committer
, int(date
), tz
)
156 def rev_to_mark(rev
):
158 return marks
.from_rev(rev
)
160 def mark_to_rev(mark
):
162 return marks
.to_rev(mark
)
164 def fixup_user(user
):
166 user
= user
.replace('"', '')
167 m
= AUTHOR_RE
.match(user
)
170 mail
= m
.group(2).strip()
172 m
= NAME_RE
.match(user
)
174 name
= m
.group(1).strip()
176 return '%s <%s>' % (name
, mail
)
178 def get_filechanges(cur
, prev
):
182 changes
= cur
.changes_from(prev
)
184 for path
, fid
, kind
in changes
.added
:
186 for path
, fid
, kind
in changes
.removed
:
188 for path
, fid
, kind
, mod
, _
in changes
.modified
:
190 for oldpath
, newpath
, fid
, kind
, mod
, _
in changes
.renamed
:
191 removed
[oldpath
] = None
192 modified
[newpath
] = fid
194 return modified
, removed
196 def export_files(tree
, files
):
197 global marks
, filenodes
200 for path
, fid
in files
.iteritems():
201 h
= tree
.get_file_sha1(fid
)
205 # is the blob already exported?
209 d
= tree
.get_file_text(fid
)
211 mark
= marks
.next_mark()
215 print "mark :%u" % mark
216 print "data %d" % len(d
)
219 final
.append((mode
, mark
, path
))
223 def export_branch(branch
, name
):
224 global prefix
, dirname
226 ref
= '%s/heads/%s' % (prefix
, name
)
227 tip
= marks
.get_tip(name
)
229 repo
= branch
.repository
231 revs
= branch
.iter_merge_sorted_revisions(None, tip
, 'exclude', 'forward')
234 revs
= [revid
for revid
, _
, _
, _
in revs
if not marks
.is_marked(revid
)]
238 rev
= repo
.get_revision(revid
)
240 parents
= rev
.parent_ids
243 committer
= rev
.committer
.encode('utf-8')
244 committer
= "%s %u %s" % (fixup_user(committer
), time
, gittz(tz
))
246 msg
= rev
.message
.encode('utf-8')
250 if len(parents
) == 0:
251 parent
= bzrlib
.revision
.NULL_REVISION
255 cur_tree
= repo
.revision_tree(revid
)
256 prev
= repo
.revision_tree(parent
)
257 modified
, removed
= get_filechanges(cur_tree
, prev
)
259 modified_final
= export_files(cur_tree
, modified
)
261 if len(parents
) == 0:
262 print 'reset %s' % ref
264 print "commit %s" % ref
265 print "mark :%d" % (marks
.get_mark(revid
))
266 print "author %s" % (author
)
267 print "committer %s" % (committer
)
268 print "data %d" % (len(msg
))
271 for i
, p
in enumerate(parents
):
280 print "merge :%s" % m
282 for f
in modified_final
:
283 print "M %s :%u %s" % f
289 if (count
% 100 == 0):
290 print "progress revision %s (%d/%d)" % (revid
, count
, len(revs
))
291 print "#############################################################"
295 revid
= branch
.last_revision()
297 # make sure the ref is updated
298 print "reset %s" % ref
299 print "from :%u" % rev_to_mark(revid
)
302 marks
.set_tip(name
, revid
)
304 def export_tag(repo
, name
):
307 print "reset refs/tags/%s" % name
308 print "from :%u" % rev_to_mark(tags
[name
])
311 warn("TODO: fetch tag '%s'" % name
)
313 def do_import(parser
):
317 path
= os
.path
.join(dirname
, 'marks-git')
320 if os
.path
.exists(path
):
321 print "feature import-marks=%s" % path
322 print "feature export-marks=%s" % path
325 while parser
.check('import'):
327 if ref
.startswith('refs/heads/'):
328 name
= ref
[len('refs/heads/'):]
329 export_branch(branch
, name
)
330 if ref
.startswith('refs/tags/'):
331 name
= ref
[len('refs/tags/'):]
332 export_tag(branch
, name
)
339 def parse_blob(parser
):
343 mark
= parser
.get_mark()
345 data
= parser
.get_data()
346 blob_marks
[mark
] = data
351 def __init__(self
, repo
, revid
, parents
, files
):
356 self
.parents
= parents
359 def copy_tree(revid
):
360 files
= files_cache
[revid
] = {}
361 tree
= repo
.repository
.revision_tree(revid
)
364 for path
, entry
in tree
.iter_entries_by_dir():
365 files
[path
] = entry
.file_id
370 if len(parents
) == 0:
371 self
.base_id
= bzrlib
.revision
.NULL_REVISION
374 self
.base_id
= parents
[0]
375 self
.base_files
= files_cache
.get(self
.base_id
, None)
376 if not self
.base_files
:
377 self
.base_files
= copy_tree(self
.base_id
)
379 self
.files
= files_cache
[revid
] = self
.base_files
.copy()
381 def last_revision(self
):
384 def iter_changes(self
):
387 def get_parent(dirname
, basename
):
388 parent_fid
= self
.base_files
.get(dirname
, None)
391 parent_fid
= self
.files
.get(dirname
, None)
396 d
= add_entry(dirname
, 'directory')
399 def add_entry(path
, kind
):
400 dirname
, basename
= os
.path
.split(path
)
401 parent_fid
= get_parent(dirname
, basename
)
402 fid
= bzrlib
.generate_ids
.gen_file_id(path
)
411 self
.files
[path
] = change
[0]
412 changes
.append(change
)
415 def update_entry(path
, kind
):
416 dirname
, basename
= os
.path
.split(path
)
417 fid
= self
.base_files
[path
]
418 parent_fid
= get_parent(dirname
, basename
)
427 self
.files
[path
] = change
[0]
428 changes
.append(change
)
431 def remove_entry(path
, kind
):
432 dirname
, basename
= os
.path
.split(path
)
433 fid
= self
.base_files
[path
]
434 parent_fid
= get_parent(dirname
, basename
)
444 changes
.append(change
)
447 for path
, f
in self
.updates
.iteritems():
449 remove_entry(path
, 'file')
450 elif path
in self
.base_files
:
451 update_entry(path
, 'file')
453 add_entry(path
, 'file')
457 def get_file_with_stat(self
, file_id
, path
=None):
458 return (StringIO
.StringIO(self
.updates
[path
]['data']), None)
460 def parse_commit(parser
):
461 global marks
, blob_marks
, bmarks
, parsed_refs
469 if ref
!= 'refs/heads/master':
470 die("bzr doesn't support multiple branches; use 'master'")
472 commit_mark
= parser
.get_mark()
474 author
= parser
.get_author()
476 committer
= parser
.get_author()
478 data
= parser
.get_data()
480 if parser
.check('from'):
481 parents
.append(parser
.get_mark())
483 while parser
.check('merge'):
484 parents
.append(parser
.get_mark())
490 if parser
.check('M'):
491 t
, m
, mark_ref
, path
= line
.split(' ', 3)
492 mark
= int(mark_ref
[1:])
493 f
= { 'mode' : m
, 'data' : blob_marks
[mark
] }
494 elif parser
.check('D'):
495 t
, path
= line
.split(' ')
496 f
= { 'deleted' : True }
498 die('Unknown file command: %s' % line
)
503 committer
, date
, tz
= committer
504 parents
= [str(mark_to_rev(p
)) for p
in parents
]
505 revid
= bzrlib
.generate_ids
.gen_revision_id(committer
, date
)
507 props
['branch-nick'] = repo
.nick
509 mtree
= CustomTree(repo
, revid
, parents
, files
)
510 changes
= mtree
.iter_changes()
514 builder
= repo
.get_commit_builder(parents
, None, date
, tz
, committer
, props
, revid
, False)
516 list(builder
.record_iter_changes(mtree
, mtree
.last_revision(), changes
))
517 builder
.finish_inventory()
518 builder
.commit(data
.decode('utf-8', 'replace'))
525 parsed_refs
[ref
] = revid
526 marks
.new_mark(revid
, commit_mark
)
528 def parse_reset(parser
):
534 if ref
!= 'refs/heads/master':
535 die("bzr doesn't support multiple branches; use 'master'")
538 if parser
.check('commit'):
541 if not parser
.check('from'):
543 from_mark
= parser
.get_mark()
546 parsed_refs
[ref
] = mark_to_rev(from_mark
)
548 def do_export(parser
):
549 global parsed_refs
, dirname
, peer
553 for line
in parser
.each_block('done'):
554 if parser
.check('blob'):
556 elif parser
.check('commit'):
558 elif parser
.check('reset'):
560 elif parser
.check('tag'):
562 elif parser
.check('feature'):
565 die('unhandled export command: %s' % line
)
569 for ref
, revid
in parsed_refs
.iteritems():
570 if ref
== 'refs/heads/master':
571 repo
.generate_revision_history(revid
, marks
.get_tip('master'))
572 revno
, revid
= repo
.last_revision_info()
573 peer
.import_last_revision_info_and_tags(repo
, revno
, revid
)
577 def do_capabilities(parser
):
582 print "refspec refs/heads/*:%s/heads/*" % prefix
584 path
= os
.path
.join(dirname
, 'marks-git')
586 if os
.path
.exists(path
):
587 print "*import-marks %s" % path
588 print "*export-marks %s" % path
594 print "? refs/heads/%s" % 'master'
595 for tag
, revid
in parser
.repo
.tags
.get_tag_dict().items():
596 print "? refs/tags/%s" % tag
598 print "@refs/heads/%s HEAD" % 'master'
601 def get_repo(url
, alias
):
604 clone_path
= os
.path
.join(dirname
, 'clone')
605 origin
= bzrlib
.controldir
.ControlDir
.open(url
)
606 remote_branch
= origin
.open_branch()
608 if os
.path
.exists(clone_path
):
610 d
= bzrlib
.controldir
.ControlDir
.open(clone_path
)
611 branch
= d
.open_branch()
612 result
= branch
.pull(remote_branch
, [], None, False)
615 d
= origin
.sprout(clone_path
, None,
616 hardlink
=True, create_tree_if_local
=False,
617 source_branch
=remote_branch
)
618 branch
= d
.open_branch()
619 branch
.bind(remote_branch
)
626 global marks
, prefix
, dirname
627 global tags
, filenodes
635 prefix
= 'refs/bzr/%s' % alias
642 gitdir
= os
.environ
['GIT_DIR']
643 dirname
= os
.path
.join(gitdir
, 'bzr', alias
)
645 if not os
.path
.exists(dirname
):
648 repo
= get_repo(url
, alias
)
650 marks_path
= os
.path
.join(dirname
, 'marks-int')
651 marks
= Marks(marks_path
)
653 parser
= Parser(repo
)
655 if parser
.check('capabilities'):
656 do_capabilities(parser
)
657 elif parser
.check('list'):
659 elif parser
.check('import'):
661 elif parser
.check('export'):
664 die('unhandled command: %s' % line
)
669 sys
.exit(main(sys
.argv
))