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
20 if hasattr(bzrlib
, "initialize"):
24 bzrlib
.plugin
.load_plugins()
26 import bzrlib
.generate_ids
27 import bzrlib
.transport
35 import atexit
, shutil
, hashlib
37 NAME_RE
= re
.compile('^([^<>]+)')
38 AUTHOR_RE
= re
.compile('^([^<>]+?)? ?<([^<>]*)>$')
39 RAW_AUTHOR_RE
= re
.compile('^(\w+) (.+)? <(.*)> (\d+) ([+-]\d+)')
42 sys
.stderr
.write('ERROR: %s\n' % (msg
% args
))
46 sys
.stderr
.write('WARNING: %s\n' % (msg
% args
))
49 return '%+03d%02d' % (tz
/ 3600, tz
% 3600 / 60)
53 def __init__(self
, path
):
62 if not os
.path
.exists(self
.path
):
65 tmp
= json
.load(open(self
.path
))
66 self
.tips
= tmp
['tips']
67 self
.marks
= tmp
['marks']
68 self
.last_mark
= tmp
['last-mark']
70 for rev
, mark
in self
.marks
.iteritems():
71 self
.rev_marks
[mark
] = rev
74 return { 'tips': self
.tips
, 'marks': self
.marks
, 'last-mark' : self
.last_mark
}
77 json
.dump(self
.dict(), open(self
.path
, 'w'))
80 return str(self
.dict())
82 def from_rev(self
, rev
):
83 return self
.marks
[rev
]
85 def to_rev(self
, mark
):
86 return self
.rev_marks
[mark
]
92 def get_mark(self
, rev
):
94 self
.marks
[rev
] = self
.last_mark
97 def is_marked(self
, rev
):
98 return rev
in self
.marks
100 def new_mark(self
, rev
, mark
):
101 self
.marks
[rev
] = mark
102 self
.rev_marks
[mark
] = rev
103 self
.last_mark
= mark
105 def get_tip(self
, branch
):
106 return self
.tips
.get(branch
, None)
108 def set_tip(self
, branch
, tip
):
109 self
.tips
[branch
] = tip
113 def __init__(self
, repo
):
115 self
.line
= self
.get_line()
118 return sys
.stdin
.readline().strip()
120 def __getitem__(self
, i
):
121 return self
.line
.split()[i
]
123 def check(self
, word
):
124 return self
.line
.startswith(word
)
126 def each_block(self
, separator
):
127 while self
.line
!= separator
:
129 self
.line
= self
.get_line()
132 return self
.each_block('')
135 self
.line
= self
.get_line()
136 if self
.line
== 'done':
140 i
= self
.line
.index(':') + 1
141 return int(self
.line
[i
:])
144 if not self
.check('data'):
146 i
= self
.line
.index(' ') + 1
147 size
= int(self
.line
[i
:])
148 return sys
.stdin
.read(size
)
150 def get_author(self
):
151 m
= RAW_AUTHOR_RE
.match(self
.line
)
154 _
, name
, email
, date
, tz
= m
.groups()
155 committer
= '%s <%s>' % (name
, email
)
157 tz
= ((tz
/ 100) * 3600) + ((tz
% 100) * 60)
158 return (committer
, int(date
), tz
)
160 def rev_to_mark(rev
):
162 return marks
.from_rev(rev
)
164 def mark_to_rev(mark
):
166 return marks
.to_rev(mark
)
168 def fixup_user(user
):
170 user
= user
.replace('"', '')
171 m
= AUTHOR_RE
.match(user
)
174 mail
= m
.group(2).strip()
176 m
= NAME_RE
.match(user
)
178 name
= m
.group(1).strip()
180 return '%s <%s>' % (name
, mail
)
182 def get_filechanges(cur
, prev
):
186 changes
= cur
.changes_from(prev
)
189 return s
.encode('utf-8')
191 for path
, fid
, kind
in changes
.added
:
192 modified
[u(path
)] = fid
193 for path
, fid
, kind
in changes
.removed
:
194 removed
[u(path
)] = None
195 for path
, fid
, kind
, mod
, _
in changes
.modified
:
196 modified
[u(path
)] = fid
197 for oldpath
, newpath
, fid
, kind
, mod
, _
in changes
.renamed
:
198 removed
[u(oldpath
)] = None
199 if kind
== 'directory':
200 lst
= cur
.list_files(from_dir
=newpath
, recursive
=True)
201 for path
, file_class
, kind
, fid
, entry
in lst
:
202 if kind
!= 'directory':
203 modified
[u(newpath
+ '/' + path
)] = fid
205 modified
[u(newpath
)] = fid
207 return modified
, removed
209 def export_files(tree
, files
):
210 global marks
, filenodes
213 for path
, fid
in files
.iteritems():
214 kind
= tree
.kind(fid
)
216 h
= tree
.get_file_sha1(fid
)
218 if kind
== 'symlink':
219 d
= tree
.get_symlink_target(fid
)
223 if tree
.is_executable(fid
):
228 # is the blob already exported?
231 final
.append((mode
, mark
, path
))
234 d
= tree
.get_file_text(fid
)
235 elif kind
== 'directory':
238 die("Unhandled kind '%s' for path '%s'" % (kind
, path
))
240 mark
= marks
.next_mark()
244 print "mark :%u" % mark
245 print "data %d" % len(d
)
248 final
.append((mode
, mark
, path
))
252 def export_branch(branch
, name
):
255 ref
= '%s/heads/%s' % (prefix
, name
)
256 tip
= marks
.get_tip(name
)
258 repo
= branch
.repository
260 revs
= branch
.iter_merge_sorted_revisions(None, tip
, 'exclude', 'forward')
263 revs
= [revid
for revid
, _
, _
, _
in revs
if not marks
.is_marked(revid
)]
267 rev
= repo
.get_revision(revid
)
269 parents
= rev
.parent_ids
272 committer
= rev
.committer
.encode('utf-8')
273 committer
= "%s %u %s" % (fixup_user(committer
), time
, gittz(tz
))
274 authors
= rev
.get_apparent_authors()
276 author
= authors
[0].encode('utf-8')
277 author
= "%s %u %s" % (fixup_user(author
), time
, gittz(tz
))
280 msg
= rev
.message
.encode('utf-8')
284 if len(parents
) == 0:
285 parent
= bzrlib
.revision
.NULL_REVISION
289 cur_tree
= repo
.revision_tree(revid
)
290 prev
= repo
.revision_tree(parent
)
291 modified
, removed
= get_filechanges(cur_tree
, prev
)
293 modified_final
= export_files(cur_tree
, modified
)
295 if len(parents
) == 0:
296 print 'reset %s' % ref
298 print "commit %s" % ref
299 print "mark :%d" % (marks
.get_mark(revid
))
300 print "author %s" % (author
)
301 print "committer %s" % (committer
)
302 print "data %d" % (len(msg
))
305 for i
, p
in enumerate(parents
):
314 print "merge :%s" % m
318 for f
in modified_final
:
319 print "M %s :%u %s" % f
323 if (count
% 100 == 0):
324 print "progress revision %s (%d/%d)" % (revid
, count
, len(revs
))
325 print "#############################################################"
329 revid
= branch
.last_revision()
331 # make sure the ref is updated
332 print "reset %s" % ref
333 print "from :%u" % rev_to_mark(revid
)
336 marks
.set_tip(name
, revid
)
338 def export_tag(repo
, name
):
341 ref
= '%s/tags/%s' % (prefix
, name
)
342 print "reset %s" % ref
343 print "from :%u" % rev_to_mark(tags
[name
])
346 def do_import(parser
):
350 path
= os
.path
.join(dirname
, 'marks-git')
353 if os
.path
.exists(path
):
354 print "feature import-marks=%s" % path
355 print "feature export-marks=%s" % path
358 while parser
.check('import'):
360 if ref
.startswith('refs/heads/'):
361 name
= ref
[len('refs/heads/'):]
362 export_branch(branch
, name
)
363 if ref
.startswith('refs/tags/'):
364 name
= ref
[len('refs/tags/'):]
365 export_tag(branch
, name
)
372 def parse_blob(parser
):
376 mark
= parser
.get_mark()
378 data
= parser
.get_data()
379 blob_marks
[mark
] = data
384 def __init__(self
, repo
, revid
, parents
, files
):
389 self
.parents
= parents
392 def copy_tree(revid
):
393 files
= files_cache
[revid
] = {}
394 tree
= repo
.repository
.revision_tree(revid
)
397 for path
, entry
in tree
.iter_entries_by_dir():
398 files
[path
] = entry
.file_id
403 if len(parents
) == 0:
404 self
.base_id
= bzrlib
.revision
.NULL_REVISION
407 self
.base_id
= parents
[0]
408 self
.base_files
= files_cache
.get(self
.base_id
, None)
409 if not self
.base_files
:
410 self
.base_files
= copy_tree(self
.base_id
)
412 self
.files
= files_cache
[revid
] = self
.base_files
.copy()
414 for path
, f
in files
.iteritems():
415 fid
= self
.files
.get(path
, None)
417 fid
= bzrlib
.generate_ids
.gen_file_id(path
)
419 self
.updates
[fid
] = f
421 def last_revision(self
):
424 def iter_changes(self
):
427 def get_parent(dirname
, basename
):
428 parent_fid
= self
.base_files
.get(dirname
, None)
431 parent_fid
= self
.files
.get(dirname
, None)
436 fid
= bzrlib
.generate_ids
.gen_file_id(path
)
437 d
= add_entry(fid
, dirname
, 'directory')
440 def add_entry(fid
, path
, kind
, mode
= None):
441 dirname
, basename
= os
.path
.split(path
)
442 parent_fid
= get_parent(dirname
, basename
)
447 elif mode
== '120000':
458 self
.files
[path
] = change
[0]
459 changes
.append(change
)
462 def update_entry(fid
, path
, kind
, mode
= None):
463 dirname
, basename
= os
.path
.split(path
)
464 parent_fid
= get_parent(dirname
, basename
)
469 elif mode
== '120000':
480 self
.files
[path
] = change
[0]
481 changes
.append(change
)
484 def remove_entry(fid
, path
, kind
):
485 dirname
, basename
= os
.path
.split(path
)
486 parent_fid
= get_parent(dirname
, basename
)
496 changes
.append(change
)
499 for fid
, f
in self
.updates
.iteritems():
503 remove_entry(fid
, path
, 'file')
506 if path
in self
.base_files
:
507 update_entry(fid
, path
, 'file', f
['mode'])
509 add_entry(fid
, path
, 'file', f
['mode'])
513 def get_file_with_stat(self
, file_id
, path
=None):
514 return (StringIO
.StringIO(self
.updates
[file_id
]['data']), None)
516 def get_symlink_target(self
, file_id
):
517 return self
.updates
[file_id
]['data']
519 def c_style_unescape(string
):
520 if string
[0] == string
[-1] == '"':
521 return string
.decode('string-escape')[1:-1]
524 def parse_commit(parser
):
525 global marks
, blob_marks
, parsed_refs
533 if ref
!= 'refs/heads/master':
534 die("bzr doesn't support multiple branches; use 'master'")
536 commit_mark
= parser
.get_mark()
538 author
= parser
.get_author()
540 committer
= parser
.get_author()
542 data
= parser
.get_data()
544 if parser
.check('from'):
545 parents
.append(parser
.get_mark())
547 while parser
.check('merge'):
548 parents
.append(parser
.get_mark())
554 if parser
.check('M'):
555 t
, m
, mark_ref
, path
= line
.split(' ', 3)
556 mark
= int(mark_ref
[1:])
557 f
= { 'mode' : m
, 'data' : blob_marks
[mark
] }
558 elif parser
.check('D'):
559 t
, path
= line
.split(' ')
560 f
= { 'deleted' : True }
562 die('Unknown file command: %s' % line
)
563 path
= c_style_unescape(path
).decode('utf-8')
568 committer
, date
, tz
= committer
569 parents
= [str(mark_to_rev(p
)) for p
in parents
]
570 revid
= bzrlib
.generate_ids
.gen_revision_id(committer
, date
)
572 props
['branch-nick'] = repo
.nick
574 mtree
= CustomTree(repo
, revid
, parents
, files
)
575 changes
= mtree
.iter_changes()
579 builder
= repo
.get_commit_builder(parents
, None, date
, tz
, committer
, props
, revid
)
581 list(builder
.record_iter_changes(mtree
, mtree
.last_revision(), changes
))
582 builder
.finish_inventory()
583 builder
.commit(data
.decode('utf-8', 'replace'))
590 parsed_refs
[ref
] = revid
591 marks
.new_mark(revid
, commit_mark
)
593 def parse_reset(parser
):
599 if ref
!= 'refs/heads/master':
600 die("bzr doesn't support multiple branches; use 'master'")
603 if parser
.check('commit'):
606 if not parser
.check('from'):
608 from_mark
= parser
.get_mark()
611 parsed_refs
[ref
] = mark_to_rev(from_mark
)
613 def do_export(parser
):
614 global parsed_refs
, dirname
, peer
618 for line
in parser
.each_block('done'):
619 if parser
.check('blob'):
621 elif parser
.check('commit'):
623 elif parser
.check('reset'):
625 elif parser
.check('tag'):
627 elif parser
.check('feature'):
630 die('unhandled export command: %s' % line
)
634 for ref
, revid
in parsed_refs
.iteritems():
635 if ref
== 'refs/heads/master':
636 repo
.generate_revision_history(revid
, marks
.get_tip('master'))
639 repo
.push(peer
, stop_revision
=revid
)
640 except bzrlib
.errors
.DivergedBranches
:
641 print "error %s non-fast forward" % ref
644 wt
= repo
.bzrdir
.open_workingtree()
650 def do_capabilities(parser
):
655 print "refspec refs/heads/*:%s/heads/*" % prefix
656 print "refspec refs/tags/*:%s/tags/*" % prefix
658 path
= os
.path
.join(dirname
, 'marks-git')
660 if os
.path
.exists(path
):
661 print "*import-marks %s" % path
662 print "*export-marks %s" % path
666 def ref_is_valid(name
):
667 return not True in [c
in name
for c
in '~^: \\']
671 print "? refs/heads/%s" % 'master'
675 for tag
, revid
in branch
.tags
.get_tag_dict().items():
677 branch
.revision_id_to_dotted_revno(revid
)
678 except bzrlib
.errors
.NoSuchRevision
:
680 if not ref_is_valid(tag
):
682 print "? refs/tags/%s" % tag
685 print "@refs/heads/%s HEAD" % 'master'
688 def get_repo(url
, alias
):
691 origin
= bzrlib
.bzrdir
.BzrDir
.open(url
)
692 branch
= origin
.open_branch()
694 if not isinstance(origin
.transport
, bzrlib
.transport
.local
.LocalTransport
):
695 clone_path
= os
.path
.join(dirname
, 'clone')
696 remote_branch
= branch
697 if os
.path
.exists(clone_path
):
699 d
= bzrlib
.bzrdir
.BzrDir
.open(clone_path
)
700 branch
= d
.open_branch()
701 result
= branch
.pull(remote_branch
, [], None, False)
704 d
= origin
.sprout(clone_path
, None,
705 hardlink
=True, create_tree_if_local
=False,
706 source_branch
=remote_branch
)
707 branch
= d
.open_branch()
708 branch
.bind(remote_branch
)
717 global marks
, prefix
, dirname
718 global tags
, filenodes
736 alias
= hashlib
.sha1(alias
).hexdigest()
740 prefix
= 'refs/bzr/%s' % alias
741 gitdir
= os
.environ
['GIT_DIR']
742 dirname
= os
.path
.join(gitdir
, 'bzr', alias
)
744 if not os
.path
.exists(dirname
):
747 repo
= get_repo(url
, alias
)
749 marks_path
= os
.path
.join(dirname
, 'marks-int')
750 marks
= Marks(marks_path
)
752 parser
= Parser(repo
)
754 if parser
.check('capabilities'):
755 do_capabilities(parser
)
756 elif parser
.check('list'):
758 elif parser
.check('import'):
760 elif parser
.check('export'):
763 die('unhandled command: %s' % line
)
772 shutil
.rmtree(dirname
)
775 sys
.exit(main(sys
.argv
))