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
16 # If you want to specify which branches you want track (per repo):
17 # git config remote-bzr.branches 'trunk, devel, test'
23 if hasattr(bzrlib
, "initialize"):
27 bzrlib
.plugin
.load_plugins()
29 import bzrlib
.generate_ids
30 import bzrlib
.transport
33 import bzrlib
.urlutils
41 import atexit
, shutil
, hashlib
, urlparse
, subprocess
43 NAME_RE
= re
.compile('^([^<>]+)')
44 AUTHOR_RE
= re
.compile('^([^<>]+?)? ?<([^<>]*)>$')
45 EMAIL_RE
= re
.compile('^([^<>]+[^ \\\t<>])?\\b(?:[ \\t<>]*?)\\b([^ \\t<>]+@[^ \\t<>]+)')
46 RAW_AUTHOR_RE
= re
.compile('^(\w+) (.+)? <(.*)> (\d+) ([+-]\d+)')
49 sys
.stderr
.write('ERROR: %s\n' % (msg
% args
))
53 sys
.stderr
.write('WARNING: %s\n' % (msg
% args
))
56 return '%+03d%02d' % (tz
/ 3600, tz
% 3600 / 60)
58 def get_config(config
):
59 cmd
= ['git', 'config', '--get', config
]
60 process
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
)
61 output
, _
= process
.communicate()
66 def __init__(self
, path
):
75 if not os
.path
.exists(self
.path
):
78 tmp
= json
.load(open(self
.path
))
79 self
.tips
= tmp
['tips']
80 self
.marks
= tmp
['marks']
81 self
.last_mark
= tmp
['last-mark']
83 for rev
, mark
in self
.marks
.iteritems():
84 self
.rev_marks
[mark
] = rev
87 return { 'tips': self
.tips
, 'marks': self
.marks
, 'last-mark' : self
.last_mark
}
90 json
.dump(self
.dict(), open(self
.path
, 'w'))
93 return str(self
.dict())
95 def from_rev(self
, rev
):
96 return self
.marks
[rev
]
98 def to_rev(self
, mark
):
99 return str(self
.rev_marks
[mark
])
103 return self
.last_mark
105 def get_mark(self
, rev
):
107 self
.marks
[rev
] = self
.last_mark
108 return self
.last_mark
110 def is_marked(self
, rev
):
111 return rev
in self
.marks
113 def new_mark(self
, rev
, mark
):
114 self
.marks
[rev
] = mark
115 self
.rev_marks
[mark
] = rev
116 self
.last_mark
= mark
118 def get_tip(self
, branch
):
119 return self
.tips
.get(branch
, None)
121 def set_tip(self
, branch
, tip
):
122 self
.tips
[branch
] = tip
126 def __init__(self
, repo
):
128 self
.line
= self
.get_line()
131 return sys
.stdin
.readline().strip()
133 def __getitem__(self
, i
):
134 return self
.line
.split()[i
]
136 def check(self
, word
):
137 return self
.line
.startswith(word
)
139 def each_block(self
, separator
):
140 while self
.line
!= separator
:
142 self
.line
= self
.get_line()
145 return self
.each_block('')
148 self
.line
= self
.get_line()
149 if self
.line
== 'done':
153 i
= self
.line
.index(':') + 1
154 return int(self
.line
[i
:])
157 if not self
.check('data'):
159 i
= self
.line
.index(' ') + 1
160 size
= int(self
.line
[i
:])
161 return sys
.stdin
.read(size
)
163 def get_author(self
):
164 m
= RAW_AUTHOR_RE
.match(self
.line
)
167 _
, name
, email
, date
, tz
= m
.groups()
168 committer
= '%s <%s>' % (name
, email
)
170 tz
= ((tz
/ 100) * 3600) + ((tz
% 100) * 60)
171 return (committer
, int(date
), tz
)
173 def rev_to_mark(rev
):
175 return marks
.from_rev(rev
)
177 def mark_to_rev(mark
):
179 return marks
.to_rev(mark
)
181 def fixup_user(user
):
183 user
= user
.replace('"', '')
184 m
= AUTHOR_RE
.match(user
)
187 mail
= m
.group(2).strip()
189 m
= EMAIL_RE
.match(user
)
194 m
= NAME_RE
.match(user
)
196 name
= m
.group(1).strip()
203 return '%s <%s>' % (name
, mail
)
205 def get_filechanges(cur
, prev
):
209 changes
= cur
.changes_from(prev
)
212 return s
.encode('utf-8')
214 for path
, fid
, kind
in changes
.added
:
215 modified
[u(path
)] = fid
216 for path
, fid
, kind
in changes
.removed
:
217 removed
[u(path
)] = None
218 for path
, fid
, kind
, mod
, _
in changes
.modified
:
219 modified
[u(path
)] = fid
220 for oldpath
, newpath
, fid
, kind
, mod
, _
in changes
.renamed
:
221 removed
[u(oldpath
)] = None
222 if kind
== 'directory':
223 lst
= cur
.list_files(from_dir
=newpath
, recursive
=True)
224 for path
, file_class
, kind
, fid
, entry
in lst
:
225 if kind
!= 'directory':
226 modified
[u(newpath
+ '/' + path
)] = fid
228 modified
[u(newpath
)] = fid
230 return modified
, removed
232 def export_files(tree
, files
):
233 global marks
, filenodes
236 for path
, fid
in files
.iteritems():
237 kind
= tree
.kind(fid
)
239 h
= tree
.get_file_sha1(fid
)
241 if kind
== 'symlink':
242 d
= tree
.get_symlink_target(fid
)
246 if tree
.is_executable(fid
):
251 # is the blob already exported?
254 final
.append((mode
, mark
, path
))
257 d
= tree
.get_file_text(fid
)
258 elif kind
== 'directory':
261 die("Unhandled kind '%s' for path '%s'" % (kind
, path
))
263 mark
= marks
.next_mark()
267 print "mark :%u" % mark
268 print "data %d" % len(d
)
271 final
.append((mode
, mark
, path
))
275 def export_branch(repo
, name
):
278 ref
= '%s/heads/%s' % (prefix
, name
)
279 tip
= marks
.get_tip(name
)
281 branch
= bzrlib
.branch
.Branch
.open(branches
[name
])
282 repo
= branch
.repository
285 revs
= branch
.iter_merge_sorted_revisions(None, tip
, 'exclude', 'forward')
287 tip_revno
= branch
.revision_id_to_revno(tip
)
288 last_revno
, _
= branch
.last_revision_info()
289 total
= last_revno
- tip_revno
290 except bzrlib
.errors
.NoSuchRevision
:
294 for revid
, _
, seq
, _
in revs
:
296 if marks
.is_marked(revid
):
299 rev
= repo
.get_revision(revid
)
302 parents
= rev
.parent_ids
305 committer
= rev
.committer
.encode('utf-8')
306 committer
= "%s %u %s" % (fixup_user(committer
), time
, gittz(tz
))
307 authors
= rev
.get_apparent_authors()
309 author
= authors
[0].encode('utf-8')
310 author
= "%s %u %s" % (fixup_user(author
), time
, gittz(tz
))
313 msg
= rev
.message
.encode('utf-8')
317 if len(parents
) == 0:
318 parent
= bzrlib
.revision
.NULL_REVISION
322 cur_tree
= repo
.revision_tree(revid
)
323 prev
= repo
.revision_tree(parent
)
324 modified
, removed
= get_filechanges(cur_tree
, prev
)
326 modified_final
= export_files(cur_tree
, modified
)
328 if len(parents
) == 0:
329 print 'reset %s' % ref
331 print "commit %s" % ref
332 print "mark :%d" % (marks
.get_mark(revid
))
333 print "author %s" % (author
)
334 print "committer %s" % (committer
)
335 print "data %d" % (len(msg
))
338 for i
, p
in enumerate(parents
):
347 print "merge :%s" % m
351 for f
in modified_final
:
352 print "M %s :%u %s" % f
356 # let's skip branch revisions from the progress report
359 progress
= (revno
- tip_revno
)
360 if (progress
% 100 == 0):
362 print "progress revision %d '%s' (%d/%d)" % (revno
, name
, progress
, total
)
364 print "progress revision %d '%s' (%d)" % (revno
, name
, progress
)
368 revid
= branch
.last_revision()
370 # make sure the ref is updated
371 print "reset %s" % ref
372 print "from :%u" % rev_to_mark(revid
)
375 marks
.set_tip(name
, revid
)
377 def export_tag(repo
, name
):
380 ref
= '%s/tags/%s' % (prefix
, name
)
381 print "reset %s" % ref
382 print "from :%u" % rev_to_mark(tags
[name
])
385 def do_import(parser
):
389 path
= os
.path
.join(dirname
, 'marks-git')
392 if os
.path
.exists(path
):
393 print "feature import-marks=%s" % path
394 print "feature export-marks=%s" % path
395 print "feature force"
398 while parser
.check('import'):
400 if ref
.startswith('refs/heads/'):
401 name
= ref
[len('refs/heads/'):]
402 export_branch(repo
, name
)
403 if ref
.startswith('refs/tags/'):
404 name
= ref
[len('refs/tags/'):]
405 export_tag(repo
, name
)
412 def parse_blob(parser
):
416 mark
= parser
.get_mark()
418 data
= parser
.get_data()
419 blob_marks
[mark
] = data
424 def __init__(self
, branch
, revid
, parents
, files
):
430 def copy_tree(revid
):
431 files
= files_cache
[revid
] = {}
433 tree
= branch
.repository
.revision_tree(revid
)
435 for path
, entry
in tree
.iter_entries_by_dir():
436 files
[path
] = [entry
.file_id
, None]
441 if len(parents
) == 0:
442 self
.base_id
= bzrlib
.revision
.NULL_REVISION
445 self
.base_id
= parents
[0]
446 self
.base_files
= files_cache
.get(self
.base_id
, None)
447 if not self
.base_files
:
448 self
.base_files
= copy_tree(self
.base_id
)
450 self
.files
= files_cache
[revid
] = self
.base_files
.copy()
453 for path
, data
in self
.files
.iteritems():
455 self
.rev_files
[fid
] = [path
, mark
]
457 for path
, f
in files
.iteritems():
458 fid
, mark
= self
.files
.get(path
, [None, None])
460 fid
= bzrlib
.generate_ids
.gen_file_id(path
)
462 self
.rev_files
[fid
] = [path
, mark
]
463 self
.updates
[fid
] = f
465 def last_revision(self
):
468 def iter_changes(self
):
471 def get_parent(dirname
, basename
):
472 parent_fid
, mark
= self
.base_files
.get(dirname
, [None, None])
475 parent_fid
, mark
= self
.files
.get(dirname
, [None, None])
480 fid
= bzrlib
.generate_ids
.gen_file_id(path
)
481 add_entry(fid
, dirname
, 'directory')
484 def add_entry(fid
, path
, kind
, mode
= None):
485 dirname
, basename
= os
.path
.split(path
)
486 parent_fid
= get_parent(dirname
, basename
)
491 elif mode
== '120000':
502 self
.files
[path
] = [change
[0], None]
503 changes
.append(change
)
505 def update_entry(fid
, path
, kind
, mode
= None):
506 dirname
, basename
= os
.path
.split(path
)
507 parent_fid
= get_parent(dirname
, basename
)
512 elif mode
== '120000':
523 self
.files
[path
] = [change
[0], None]
524 changes
.append(change
)
526 def remove_entry(fid
, path
, kind
):
527 dirname
, basename
= os
.path
.split(path
)
528 parent_fid
= get_parent(dirname
, basename
)
538 changes
.append(change
)
540 for fid
, f
in self
.updates
.iteritems():
544 remove_entry(fid
, path
, 'file')
547 if path
in self
.base_files
:
548 update_entry(fid
, path
, 'file', f
['mode'])
550 add_entry(fid
, path
, 'file', f
['mode'])
552 self
.files
[path
][1] = f
['mark']
553 self
.rev_files
[fid
][1] = f
['mark']
557 def get_content(self
, file_id
):
558 path
, mark
= self
.rev_files
[file_id
]
560 return blob_marks
[mark
]
563 tree
= self
.branch
.repository
.revision_tree(self
.base_id
)
564 return tree
.get_file_text(file_id
)
566 def get_file_with_stat(self
, file_id
, path
=None):
567 content
= self
.get_content(file_id
)
568 return (StringIO
.StringIO(content
), None)
570 def get_symlink_target(self
, file_id
):
571 return self
.get_content(file_id
)
573 def id2path(self
, file_id
):
574 path
, mark
= self
.rev_files
[file_id
]
577 def c_style_unescape(string
):
578 if string
[0] == string
[-1] == '"':
579 return string
.decode('string-escape')[1:-1]
582 def parse_commit(parser
):
583 global marks
, blob_marks
, parsed_refs
591 if ref
.startswith('refs/heads/'):
592 name
= ref
[len('refs/heads/'):]
593 branch
= bzrlib
.branch
.Branch
.open(branches
[name
])
597 commit_mark
= parser
.get_mark()
599 author
= parser
.get_author()
601 committer
= parser
.get_author()
603 data
= parser
.get_data()
605 if parser
.check('from'):
606 parents
.append(parser
.get_mark())
608 while parser
.check('merge'):
609 parents
.append(parser
.get_mark())
612 # fast-export adds an extra newline
619 if parser
.check('M'):
620 t
, m
, mark_ref
, path
= line
.split(' ', 3)
621 mark
= int(mark_ref
[1:])
622 f
= { 'mode' : m
, 'mark' : mark
}
623 elif parser
.check('D'):
624 t
, path
= line
.split(' ')
625 f
= { 'deleted' : True }
627 die('Unknown file command: %s' % line
)
628 path
= c_style_unescape(path
).decode('utf-8')
631 committer
, date
, tz
= committer
632 parents
= [mark_to_rev(p
) for p
in parents
]
633 revid
= bzrlib
.generate_ids
.gen_revision_id(committer
, date
)
635 props
['branch-nick'] = branch
.nick
637 mtree
= CustomTree(branch
, revid
, parents
, files
)
638 changes
= mtree
.iter_changes()
642 builder
= branch
.get_commit_builder(parents
, None, date
, tz
, committer
, props
, revid
)
644 list(builder
.record_iter_changes(mtree
, mtree
.last_revision(), changes
))
645 builder
.finish_inventory()
646 builder
.commit(data
.decode('utf-8', 'replace'))
653 parsed_refs
[ref
] = revid
654 marks
.new_mark(revid
, commit_mark
)
656 def parse_reset(parser
):
663 if parser
.check('commit'):
666 if not parser
.check('from'):
668 from_mark
= parser
.get_mark()
671 parsed_refs
[ref
] = mark_to_rev(from_mark
)
673 def do_export(parser
):
674 global parsed_refs
, dirname
678 for line
in parser
.each_block('done'):
679 if parser
.check('blob'):
681 elif parser
.check('commit'):
683 elif parser
.check('reset'):
685 elif parser
.check('tag'):
687 elif parser
.check('feature'):
690 die('unhandled export command: %s' % line
)
692 for ref
, revid
in parsed_refs
.iteritems():
693 if ref
.startswith('refs/heads/'):
694 name
= ref
[len('refs/heads/'):]
695 branch
= bzrlib
.branch
.Branch
.open(branches
[name
])
696 branch
.generate_revision_history(revid
, marks
.get_tip(name
))
699 peer
= bzrlib
.branch
.Branch
.open(peers
[name
])
701 peer
.bzrdir
.push_branch(branch
, revision_id
=revid
)
702 except bzrlib
.errors
.DivergedBranches
:
703 print "error %s non-fast forward" % ref
707 wt
= branch
.bzrdir
.open_workingtree()
709 except bzrlib
.errors
.NoWorkingTree
:
711 elif ref
.startswith('refs/tags/'):
712 # TODO: implement tag push
713 print "error %s pushing tags not supported" % ref
716 # transport-helper/fast-export bugs
723 def do_capabilities(parser
):
728 print "refspec refs/heads/*:%s/heads/*" % prefix
729 print "refspec refs/tags/*:%s/tags/*" % prefix
731 path
= os
.path
.join(dirname
, 'marks-git')
733 if os
.path
.exists(path
):
734 print "*import-marks %s" % path
735 print "*export-marks %s" % path
739 def ref_is_valid(name
):
740 return not True in [c
in name
for c
in '~^: \\']
747 for name
in branches
:
748 if not master_branch
:
750 print "? refs/heads/%s" % name
752 branch
= bzrlib
.branch
.Branch
.open(branches
[master_branch
])
754 for tag
, revid
in branch
.tags
.get_tag_dict().items():
756 branch
.revision_id_to_dotted_revno(revid
)
757 except bzrlib
.errors
.NoSuchRevision
:
759 if not ref_is_valid(tag
):
761 print "? refs/tags/%s" % tag
765 print "@refs/heads/%s HEAD" % master_branch
768 def get_remote_branch(origin
, remote_branch
, name
):
769 global dirname
, peers
771 branch_path
= os
.path
.join(dirname
, 'clone', name
)
772 if os
.path
.exists(branch_path
):
774 d
= bzrlib
.bzrdir
.BzrDir
.open(branch_path
)
775 branch
= d
.open_branch()
777 branch
.pull(remote_branch
, [], None, False)
778 except bzrlib
.errors
.DivergedBranches
:
779 # use remote branch for now
783 d
= origin
.sprout(branch_path
, None,
784 hardlink
=True, create_tree_if_local
=False,
785 force_new_repo
=False,
786 source_branch
=remote_branch
)
787 branch
= d
.open_branch()
791 def find_branches(repo
, wanted
):
792 transport
= repo
.bzrdir
.root_transport
794 for fn
in transport
.iter_files_recursive():
795 if not fn
.endswith('.bzr/branch-format'):
798 name
= subdir
= fn
[:-len('/.bzr/branch-format')]
799 name
= name
if name
!= '' else 'master'
800 name
= name
.replace('/', '+')
802 if wanted
and not name
in wanted
:
806 cur
= transport
.clone(subdir
)
807 branch
= bzrlib
.branch
.Branch
.open_from_transport(cur
)
808 except bzrlib
.errors
.NotBranchError
:
813 def get_repo(url
, alias
):
814 global dirname
, peer
, branches
816 normal_url
= bzrlib
.urlutils
.normalize_url(url
)
817 origin
= bzrlib
.bzrdir
.BzrDir
.open(url
)
818 is_local
= isinstance(origin
.transport
, bzrlib
.transport
.local
.LocalTransport
)
820 shared_path
= os
.path
.join(gitdir
, 'bzr')
822 shared_dir
= bzrlib
.bzrdir
.BzrDir
.open(shared_path
)
823 except bzrlib
.errors
.NotBranchError
:
824 shared_dir
= bzrlib
.bzrdir
.BzrDir
.create(shared_path
)
826 shared_repo
= shared_dir
.open_repository()
827 except bzrlib
.errors
.NoRepositoryPresent
:
828 shared_repo
= shared_dir
.create_repository(shared
=True)
831 clone_path
= os
.path
.join(dirname
, 'clone')
832 if not os
.path
.exists(clone_path
):
835 # check and remove old organization
837 bdir
= bzrlib
.bzrdir
.BzrDir
.open(clone_path
)
838 bdir
.destroy_repository()
839 except bzrlib
.errors
.NotBranchError
:
841 except bzrlib
.errors
.NoRepositoryPresent
:
845 repo
= origin
.open_repository()
846 if not repo
.user_transport
.listable():
847 # this repository is not usable for us
848 raise bzrlib
.errors
.NoRepositoryPresent(repo
.bzrdir
)
849 except bzrlib
.errors
.NoRepositoryPresent
:
853 remote_branch
= origin
.open_branch()
856 peers
[name
] = remote_branch
.base
857 branch
= get_remote_branch(origin
, remote_branch
, name
)
859 branch
= remote_branch
861 branches
[name
] = branch
.base
863 return branch
.repository
867 wanted
= get_config('remote-bzr.branches').rstrip().split(', ')
869 wanted
= [e
for e
in wanted
if e
]
871 for name
, remote_branch
in find_branches(repo
, wanted
):
874 peers
[name
] = remote_branch
.base
875 branch
= get_remote_branch(origin
, remote_branch
, name
)
877 branch
= remote_branch
879 branches
[name
] = branch
.base
883 def fix_path(alias
, orig_url
):
884 url
= urlparse
.urlparse(orig_url
, 'file')
885 if url
.scheme
!= 'file' or os
.path
.isabs(url
.path
):
887 abs_url
= urlparse
.urljoin("%s/" % os
.getcwd(), orig_url
)
888 cmd
= ['git', 'config', 'remote.%s.url' % alias
, "bzr::%s" % abs_url
]
892 global marks
, prefix
, gitdir
, dirname
893 global tags
, filenodes
898 global branches
, peers
914 alias
= hashlib
.sha1(alias
).hexdigest()
918 prefix
= 'refs/bzr/%s' % alias
919 gitdir
= os
.environ
['GIT_DIR']
920 dirname
= os
.path
.join(gitdir
, 'bzr', alias
)
925 if not os
.path
.exists(dirname
):
928 if hasattr(bzrlib
.ui
.ui_factory
, 'be_quiet'):
929 bzrlib
.ui
.ui_factory
.be_quiet(True)
931 repo
= get_repo(url
, alias
)
933 marks_path
= os
.path
.join(dirname
, 'marks-int')
934 marks
= Marks(marks_path
)
936 parser
= Parser(repo
)
938 if parser
.check('capabilities'):
939 do_capabilities(parser
)
940 elif parser
.check('list'):
942 elif parser
.check('import'):
944 elif parser
.check('export'):
947 die('unhandled command: %s' % line
)
956 shutil
.rmtree(dirname
)
959 sys
.exit(main(sys
.argv
))