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
30 import bzrlib
.urlutils
37 import atexit
, shutil
, hashlib
, urlparse
, subprocess
39 NAME_RE
= re
.compile('^([^<>]+)')
40 AUTHOR_RE
= re
.compile('^([^<>]+?)? ?<([^<>]*)>$')
41 RAW_AUTHOR_RE
= re
.compile('^(\w+) (.+)? <(.*)> (\d+) ([+-]\d+)')
44 sys
.stderr
.write('ERROR: %s\n' % (msg
% args
))
48 sys
.stderr
.write('WARNING: %s\n' % (msg
% args
))
51 return '%+03d%02d' % (tz
/ 3600, tz
% 3600 / 60)
55 def __init__(self
, path
):
64 if not os
.path
.exists(self
.path
):
67 tmp
= json
.load(open(self
.path
))
68 self
.tips
= tmp
['tips']
69 self
.marks
= tmp
['marks']
70 self
.last_mark
= tmp
['last-mark']
72 for rev
, mark
in self
.marks
.iteritems():
73 self
.rev_marks
[mark
] = rev
76 return { 'tips': self
.tips
, 'marks': self
.marks
, 'last-mark' : self
.last_mark
}
79 json
.dump(self
.dict(), open(self
.path
, 'w'))
82 return str(self
.dict())
84 def from_rev(self
, rev
):
85 return self
.marks
[rev
]
87 def to_rev(self
, mark
):
88 return self
.rev_marks
[mark
]
94 def get_mark(self
, rev
):
96 self
.marks
[rev
] = self
.last_mark
99 def is_marked(self
, rev
):
100 return rev
in self
.marks
102 def new_mark(self
, rev
, mark
):
103 self
.marks
[rev
] = mark
104 self
.rev_marks
[mark
] = rev
105 self
.last_mark
= mark
107 def get_tip(self
, branch
):
108 return self
.tips
.get(branch
, None)
110 def set_tip(self
, branch
, tip
):
111 self
.tips
[branch
] = tip
115 def __init__(self
, repo
):
117 self
.line
= self
.get_line()
120 return sys
.stdin
.readline().strip()
122 def __getitem__(self
, i
):
123 return self
.line
.split()[i
]
125 def check(self
, word
):
126 return self
.line
.startswith(word
)
128 def each_block(self
, separator
):
129 while self
.line
!= separator
:
131 self
.line
= self
.get_line()
134 return self
.each_block('')
137 self
.line
= self
.get_line()
138 if self
.line
== 'done':
142 i
= self
.line
.index(':') + 1
143 return int(self
.line
[i
:])
146 if not self
.check('data'):
148 i
= self
.line
.index(' ') + 1
149 size
= int(self
.line
[i
:])
150 return sys
.stdin
.read(size
)
152 def get_author(self
):
153 m
= RAW_AUTHOR_RE
.match(self
.line
)
156 _
, name
, email
, date
, tz
= m
.groups()
157 committer
= '%s <%s>' % (name
, email
)
159 tz
= ((tz
/ 100) * 3600) + ((tz
% 100) * 60)
160 return (committer
, int(date
), tz
)
162 def rev_to_mark(rev
):
164 return marks
.from_rev(rev
)
166 def mark_to_rev(mark
):
168 return marks
.to_rev(mark
)
170 def fixup_user(user
):
172 user
= user
.replace('"', '')
173 m
= AUTHOR_RE
.match(user
)
176 mail
= m
.group(2).strip()
178 m
= NAME_RE
.match(user
)
180 name
= m
.group(1).strip()
182 return '%s <%s>' % (name
, mail
)
184 def get_filechanges(cur
, prev
):
188 changes
= cur
.changes_from(prev
)
191 return s
.encode('utf-8')
193 for path
, fid
, kind
in changes
.added
:
194 modified
[u(path
)] = fid
195 for path
, fid
, kind
in changes
.removed
:
196 removed
[u(path
)] = None
197 for path
, fid
, kind
, mod
, _
in changes
.modified
:
198 modified
[u(path
)] = fid
199 for oldpath
, newpath
, fid
, kind
, mod
, _
in changes
.renamed
:
200 removed
[u(oldpath
)] = None
201 if kind
== 'directory':
202 lst
= cur
.list_files(from_dir
=newpath
, recursive
=True)
203 for path
, file_class
, kind
, fid
, entry
in lst
:
204 if kind
!= 'directory':
205 modified
[u(newpath
+ '/' + path
)] = fid
207 modified
[u(newpath
)] = fid
209 return modified
, removed
211 def export_files(tree
, files
):
212 global marks
, filenodes
215 for path
, fid
in files
.iteritems():
216 kind
= tree
.kind(fid
)
218 h
= tree
.get_file_sha1(fid
)
220 if kind
== 'symlink':
221 d
= tree
.get_symlink_target(fid
)
225 if tree
.is_executable(fid
):
230 # is the blob already exported?
233 final
.append((mode
, mark
, path
))
236 d
= tree
.get_file_text(fid
)
237 elif kind
== 'directory':
240 die("Unhandled kind '%s' for path '%s'" % (kind
, path
))
242 mark
= marks
.next_mark()
246 print "mark :%u" % mark
247 print "data %d" % len(d
)
250 final
.append((mode
, mark
, path
))
254 def export_branch(repo
, name
):
257 ref
= '%s/heads/%s' % (prefix
, name
)
258 tip
= marks
.get_tip(name
)
260 branch
= branches
[name
]
261 repo
= branch
.repository
264 revs
= branch
.iter_merge_sorted_revisions(None, tip
, 'exclude', 'forward')
267 revs
= [revid
for revid
, _
, _
, _
in revs
if not marks
.is_marked(revid
)]
271 rev
= repo
.get_revision(revid
)
273 parents
= rev
.parent_ids
276 committer
= rev
.committer
.encode('utf-8')
277 committer
= "%s %u %s" % (fixup_user(committer
), time
, gittz(tz
))
278 authors
= rev
.get_apparent_authors()
280 author
= authors
[0].encode('utf-8')
281 author
= "%s %u %s" % (fixup_user(author
), time
, gittz(tz
))
284 msg
= rev
.message
.encode('utf-8')
288 if len(parents
) == 0:
289 parent
= bzrlib
.revision
.NULL_REVISION
293 cur_tree
= repo
.revision_tree(revid
)
294 prev
= repo
.revision_tree(parent
)
295 modified
, removed
= get_filechanges(cur_tree
, prev
)
297 modified_final
= export_files(cur_tree
, modified
)
299 if len(parents
) == 0:
300 print 'reset %s' % ref
302 print "commit %s" % ref
303 print "mark :%d" % (marks
.get_mark(revid
))
304 print "author %s" % (author
)
305 print "committer %s" % (committer
)
306 print "data %d" % (len(msg
))
309 for i
, p
in enumerate(parents
):
318 print "merge :%s" % m
322 for f
in modified_final
:
323 print "M %s :%u %s" % f
327 if (count
% 100 == 0):
328 print "progress revision %s '%s' (%d/%d)" % (revid
, name
, count
, len(revs
))
329 print "#############################################################"
333 revid
= branch
.last_revision()
335 # make sure the ref is updated
336 print "reset %s" % ref
337 print "from :%u" % rev_to_mark(revid
)
340 marks
.set_tip(name
, revid
)
342 def export_tag(repo
, name
):
345 ref
= '%s/tags/%s' % (prefix
, name
)
346 print "reset %s" % ref
347 print "from :%u" % rev_to_mark(tags
[name
])
350 def do_import(parser
):
354 path
= os
.path
.join(dirname
, 'marks-git')
357 if os
.path
.exists(path
):
358 print "feature import-marks=%s" % path
359 print "feature export-marks=%s" % path
360 print "feature force"
363 while parser
.check('import'):
365 if ref
.startswith('refs/heads/'):
366 name
= ref
[len('refs/heads/'):]
367 export_branch(repo
, name
)
368 if ref
.startswith('refs/tags/'):
369 name
= ref
[len('refs/tags/'):]
370 export_tag(repo
, name
)
377 def parse_blob(parser
):
381 mark
= parser
.get_mark()
383 data
= parser
.get_data()
384 blob_marks
[mark
] = data
389 def __init__(self
, branch
, revid
, parents
, files
):
395 def copy_tree(revid
):
396 files
= files_cache
[revid
] = {}
398 tree
= branch
.repository
.revision_tree(revid
)
400 for path
, entry
in tree
.iter_entries_by_dir():
401 files
[path
] = [entry
.file_id
, None]
406 if len(parents
) == 0:
407 self
.base_id
= bzrlib
.revision
.NULL_REVISION
410 self
.base_id
= parents
[0]
411 self
.base_files
= files_cache
.get(self
.base_id
, None)
412 if not self
.base_files
:
413 self
.base_files
= copy_tree(self
.base_id
)
415 self
.files
= files_cache
[revid
] = self
.base_files
.copy()
418 for path
, data
in self
.files
.iteritems():
420 self
.rev_files
[fid
] = [path
, mark
]
422 for path
, f
in files
.iteritems():
423 fid
, mark
= self
.files
.get(path
, [None, None])
425 fid
= bzrlib
.generate_ids
.gen_file_id(path
)
427 self
.rev_files
[fid
] = [path
, mark
]
428 self
.updates
[fid
] = f
430 def last_revision(self
):
433 def iter_changes(self
):
436 def get_parent(dirname
, basename
):
437 parent_fid
, mark
= self
.base_files
.get(dirname
, [None, None])
440 parent_fid
, mark
= self
.files
.get(dirname
, [None, None])
445 fid
= bzrlib
.generate_ids
.gen_file_id(path
)
446 add_entry(fid
, dirname
, 'directory')
449 def add_entry(fid
, path
, kind
, mode
= None):
450 dirname
, basename
= os
.path
.split(path
)
451 parent_fid
= get_parent(dirname
, basename
)
456 elif mode
== '120000':
467 self
.files
[path
] = [change
[0], None]
468 changes
.append(change
)
470 def update_entry(fid
, path
, kind
, mode
= None):
471 dirname
, basename
= os
.path
.split(path
)
472 parent_fid
= get_parent(dirname
, basename
)
477 elif mode
== '120000':
488 self
.files
[path
] = [change
[0], None]
489 changes
.append(change
)
491 def remove_entry(fid
, path
, kind
):
492 dirname
, basename
= os
.path
.split(path
)
493 parent_fid
= get_parent(dirname
, basename
)
503 changes
.append(change
)
505 for fid
, f
in self
.updates
.iteritems():
509 remove_entry(fid
, path
, 'file')
512 if path
in self
.base_files
:
513 update_entry(fid
, path
, 'file', f
['mode'])
515 add_entry(fid
, path
, 'file', f
['mode'])
517 self
.files
[path
][1] = f
['mark']
518 self
.rev_files
[fid
][1] = f
['mark']
522 def get_content(self
, file_id
):
523 path
, mark
= self
.rev_files
[file_id
]
525 return blob_marks
[mark
]
528 tree
= self
.branch
.repository
.revision_tree(self
.base_id
)
529 return tree
.get_file_text(file_id
)
531 def get_file_with_stat(self
, file_id
, path
=None):
532 content
= self
.get_content(file_id
)
533 return (StringIO
.StringIO(content
), None)
535 def get_symlink_target(self
, file_id
):
536 return self
.get_content(file_id
)
538 def id2path(self
, file_id
):
539 path
, mark
= self
.rev_files
[file_id
]
542 def c_style_unescape(string
):
543 if string
[0] == string
[-1] == '"':
544 return string
.decode('string-escape')[1:-1]
547 def parse_commit(parser
):
548 global marks
, blob_marks
, parsed_refs
556 if ref
.startswith('refs/heads/'):
557 name
= ref
[len('refs/heads/'):]
558 branch
= branches
[name
]
562 commit_mark
= parser
.get_mark()
564 author
= parser
.get_author()
566 committer
= parser
.get_author()
568 data
= parser
.get_data()
570 if parser
.check('from'):
571 parents
.append(parser
.get_mark())
573 while parser
.check('merge'):
574 parents
.append(parser
.get_mark())
577 # fast-export adds an extra newline
584 if parser
.check('M'):
585 t
, m
, mark_ref
, path
= line
.split(' ', 3)
586 mark
= int(mark_ref
[1:])
587 f
= { 'mode' : m
, 'mark' : mark
}
588 elif parser
.check('D'):
589 t
, path
= line
.split(' ')
590 f
= { 'deleted' : True }
592 die('Unknown file command: %s' % line
)
593 path
= c_style_unescape(path
).decode('utf-8')
596 committer
, date
, tz
= committer
597 parents
= [str(mark_to_rev(p
)) for p
in parents
]
598 revid
= bzrlib
.generate_ids
.gen_revision_id(committer
, date
)
600 props
['branch-nick'] = branch
.nick
602 mtree
= CustomTree(branch
, revid
, parents
, files
)
603 changes
= mtree
.iter_changes()
607 builder
= branch
.get_commit_builder(parents
, None, date
, tz
, committer
, props
, revid
)
609 list(builder
.record_iter_changes(mtree
, mtree
.last_revision(), changes
))
610 builder
.finish_inventory()
611 builder
.commit(data
.decode('utf-8', 'replace'))
618 parsed_refs
[ref
] = revid
619 marks
.new_mark(revid
, commit_mark
)
621 def parse_reset(parser
):
628 if parser
.check('commit'):
631 if not parser
.check('from'):
633 from_mark
= parser
.get_mark()
636 parsed_refs
[ref
] = mark_to_rev(from_mark
)
638 def do_export(parser
):
639 global parsed_refs
, dirname
643 for line
in parser
.each_block('done'):
644 if parser
.check('blob'):
646 elif parser
.check('commit'):
648 elif parser
.check('reset'):
650 elif parser
.check('tag'):
652 elif parser
.check('feature'):
655 die('unhandled export command: %s' % line
)
657 for ref
, revid
in parsed_refs
.iteritems():
658 name
= ref
[len('refs/heads/'):]
659 branch
= branches
[name
]
660 branch
.generate_revision_history(revid
, marks
.get_tip(name
))
665 peer
.bzrdir
.push_branch(branch
, revision_id
=revid
)
666 except bzrlib
.errors
.DivergedBranches
:
667 print "error %s non-fast forward" % ref
671 wt
= branch
.bzrdir
.open_workingtree()
673 except bzrlib
.errors
.NoWorkingTree
:
680 def do_capabilities(parser
):
685 print "refspec refs/heads/*:%s/heads/*" % prefix
686 print "refspec refs/tags/*:%s/tags/*" % prefix
688 path
= os
.path
.join(dirname
, 'marks-git')
690 if os
.path
.exists(path
):
691 print "*import-marks %s" % path
692 print "*export-marks %s" % path
696 def ref_is_valid(name
):
697 return not True in [c
in name
for c
in '~^: \\']
704 for name
in branches
:
705 if not master_branch
:
707 print "? refs/heads/%s" % name
709 branch
= branches
[master_branch
]
711 for tag
, revid
in branch
.tags
.get_tag_dict().items():
713 branch
.revision_id_to_dotted_revno(revid
)
714 except bzrlib
.errors
.NoSuchRevision
:
716 if not ref_is_valid(tag
):
718 print "? refs/tags/%s" % tag
722 print "@refs/heads/%s HEAD" % master_branch
725 def get_remote_branch(origin
, remote_branch
, name
):
726 global dirname
, peers
728 branch_path
= os
.path
.join(dirname
, 'clone', name
)
729 if os
.path
.exists(branch_path
):
731 d
= bzrlib
.bzrdir
.BzrDir
.open(branch_path
)
732 branch
= d
.open_branch()
734 branch
.pull(remote_branch
, [], None, False)
735 except bzrlib
.errors
.DivergedBranches
:
736 # use remote branch for now
740 d
= origin
.sprout(branch_path
, None,
741 hardlink
=True, create_tree_if_local
=False,
742 force_new_repo
=False,
743 source_branch
=remote_branch
)
744 branch
= d
.open_branch()
748 def get_repo(url
, alias
):
749 global dirname
, peer
, branches
751 normal_url
= bzrlib
.urlutils
.normalize_url(url
)
752 origin
= bzrlib
.bzrdir
.BzrDir
.open(url
)
753 is_local
= isinstance(origin
.transport
, bzrlib
.transport
.local
.LocalTransport
)
755 clone_path
= os
.path
.join(dirname
, 'clone')
758 repo
= origin
.open_repository()
759 except bzrlib
.errors
.NoRepositoryPresent
:
763 branch
= origin
.open_branch()
766 if not os
.path
.exists(clone_path
):
769 branches
[name
] = get_remote_branch(origin
, branch
, name
)
771 branches
[name
] = branch
773 return branch
.repository
777 if not is_local
and not os
.path
.exists(clone_path
):
778 clonedir
= bzrlib
.bzrdir
.BzrDir
.create(clone_path
)
780 for branch
in repo
.find_branches():
782 name
= repo
.user_transport
.relpath(branch
.base
)
783 name
= name
if name
!= '' else 'master'
784 name
= name
.replace('/', '+')
788 branches
[name
] = get_remote_branch(origin
, branch
, name
)
790 branches
[name
] = branch
794 def fix_path(alias
, orig_url
):
795 url
= urlparse
.urlparse(orig_url
, 'file')
796 if url
.scheme
!= 'file' or os
.path
.isabs(url
.path
):
798 abs_url
= urlparse
.urljoin("%s/" % os
.getcwd(), orig_url
)
799 cmd
= ['git', 'config', 'remote.%s.url' % alias
, "bzr::%s" % abs_url
]
803 global marks
, prefix
, dirname
804 global tags
, filenodes
809 global branches
, peers
825 alias
= hashlib
.sha1(alias
).hexdigest()
829 prefix
= 'refs/bzr/%s' % alias
830 gitdir
= os
.environ
['GIT_DIR']
831 dirname
= os
.path
.join(gitdir
, 'bzr', alias
)
836 if not os
.path
.exists(dirname
):
839 bzrlib
.ui
.ui_factory
.be_quiet(True)
841 repo
= get_repo(url
, alias
)
843 marks_path
= os
.path
.join(dirname
, 'marks-int')
844 marks
= Marks(marks_path
)
846 parser
= Parser(repo
)
848 if parser
.check('capabilities'):
849 do_capabilities(parser
)
850 elif parser
.check('list'):
852 elif parser
.check('import'):
854 elif parser
.check('export'):
857 die('unhandled command: %s' % line
)
866 shutil
.rmtree(dirname
)
869 sys
.exit(main(sys
.argv
))