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 to track (per repo):
17 # % git config remote.origin.bzr-branches 'trunk, devel, test'
19 # Where 'origin' is the name of the repository you want to specify the
26 if hasattr(bzrlib
, "initialize"):
30 bzrlib
.plugin
.load_plugins()
32 import bzrlib
.generate_ids
33 import bzrlib
.transport
36 import bzrlib
.urlutils
44 import atexit
, shutil
, hashlib
, urlparse
, subprocess
46 sys
.stderr
.write('WARNING: git-remote-bzr is now maintained independently.\n')
47 sys
.stderr
.write('WARNING: For more information visit https://github.com/felipec/git-remote-bzr\n')
49 NAME_RE
= re
.compile('^([^<>]+)')
50 AUTHOR_RE
= re
.compile('^([^<>]+?)? ?[<>]([^<>]*)(?:$|>)')
51 EMAIL_RE
= re
.compile(r
'([^ \t<>]+@[^ \t<>]+)')
52 RAW_AUTHOR_RE
= re
.compile('^(\w+) (.+)? <(.*)> (\d+) ([+-]\d+)')
55 sys
.stderr
.write('ERROR: %s\n' % (msg
% args
))
59 sys
.stderr
.write('WARNING: %s\n' % (msg
% args
))
62 return '%+03d%02d' % (tz
/ 3600, tz
% 3600 / 60)
64 def get_config(config
):
65 cmd
= ['git', 'config', '--get', config
]
66 process
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
)
67 output
, _
= process
.communicate()
72 def __init__(self
, path
):
81 if not os
.path
.exists(self
.path
):
84 tmp
= json
.load(open(self
.path
))
85 self
.tips
= tmp
['tips']
86 self
.marks
= tmp
['marks']
87 self
.last_mark
= tmp
['last-mark']
89 for rev
, mark
in self
.marks
.iteritems():
90 self
.rev_marks
[mark
] = rev
93 return { 'tips': self
.tips
, 'marks': self
.marks
, 'last-mark' : self
.last_mark
}
96 json
.dump(self
.dict(), open(self
.path
, 'w'))
99 return str(self
.dict())
101 def from_rev(self
, rev
):
102 return self
.marks
[rev
]
104 def to_rev(self
, mark
):
105 return str(self
.rev_marks
[mark
])
109 return self
.last_mark
111 def get_mark(self
, rev
):
113 self
.marks
[rev
] = self
.last_mark
114 return self
.last_mark
116 def is_marked(self
, rev
):
117 return rev
in self
.marks
119 def new_mark(self
, rev
, mark
):
120 self
.marks
[rev
] = mark
121 self
.rev_marks
[mark
] = rev
122 self
.last_mark
= mark
124 def get_tip(self
, branch
):
126 return str(self
.tips
[branch
])
130 def set_tip(self
, branch
, tip
):
131 self
.tips
[branch
] = tip
135 def __init__(self
, repo
):
137 self
.line
= self
.get_line()
140 return sys
.stdin
.readline().strip()
142 def __getitem__(self
, i
):
143 return self
.line
.split()[i
]
145 def check(self
, word
):
146 return self
.line
.startswith(word
)
148 def each_block(self
, separator
):
149 while self
.line
!= separator
:
151 self
.line
= self
.get_line()
154 return self
.each_block('')
157 self
.line
= self
.get_line()
158 if self
.line
== 'done':
162 i
= self
.line
.index(':') + 1
163 return int(self
.line
[i
:])
166 if not self
.check('data'):
168 i
= self
.line
.index(' ') + 1
169 size
= int(self
.line
[i
:])
170 return sys
.stdin
.read(size
)
172 def get_author(self
):
173 m
= RAW_AUTHOR_RE
.match(self
.line
)
176 _
, name
, email
, date
, tz
= m
.groups()
177 name
= name
.decode('utf-8')
178 committer
= '%s <%s>' % (name
, email
)
180 tz
= ((tz
/ 100) * 3600) + ((tz
% 100) * 60)
181 return (committer
, int(date
), tz
)
183 def rev_to_mark(rev
):
184 return marks
.from_rev(rev
)
186 def mark_to_rev(mark
):
187 return marks
.to_rev(mark
)
189 def fixup_user(user
):
191 user
= user
.replace('"', '')
192 m
= AUTHOR_RE
.match(user
)
195 mail
= m
.group(2).strip()
197 m
= EMAIL_RE
.match(user
)
201 m
= NAME_RE
.match(user
)
203 name
= m
.group(1).strip()
210 return '%s <%s>' % (name
, mail
)
212 def get_filechanges(cur
, prev
):
216 changes
= cur
.changes_from(prev
)
219 return s
.encode('utf-8')
221 for path
, fid
, kind
in changes
.added
:
222 modified
[u(path
)] = fid
223 for path
, fid
, kind
in changes
.removed
:
224 removed
[u(path
)] = None
225 for path
, fid
, kind
, mod
, _
in changes
.modified
:
226 modified
[u(path
)] = fid
227 for oldpath
, newpath
, fid
, kind
, mod
, _
in changes
.renamed
:
228 removed
[u(oldpath
)] = None
229 if kind
== 'directory':
230 lst
= cur
.list_files(from_dir
=newpath
, recursive
=True)
231 for path
, file_class
, kind
, fid
, entry
in lst
:
232 if kind
!= 'directory':
233 modified
[u(newpath
+ '/' + path
)] = fid
235 modified
[u(newpath
)] = fid
237 return modified
, removed
239 def export_files(tree
, files
):
241 for path
, fid
in files
.iteritems():
242 kind
= tree
.kind(fid
)
244 h
= tree
.get_file_sha1(fid
)
246 if kind
== 'symlink':
247 d
= tree
.get_symlink_target(fid
)
251 if tree
.is_executable(fid
):
256 # is the blob already exported?
259 final
.append((mode
, mark
, path
))
262 d
= tree
.get_file_text(fid
)
263 elif kind
== 'directory':
266 die("Unhandled kind '%s' for path '%s'" % (kind
, path
))
268 mark
= marks
.next_mark()
272 print "mark :%u" % mark
273 print "data %d" % len(d
)
276 final
.append((mode
, mark
, path
))
280 def export_branch(repo
, name
):
281 ref
= '%s/heads/%s' % (prefix
, name
)
282 tip
= marks
.get_tip(name
)
284 branch
= get_remote_branch(name
)
285 repo
= branch
.repository
288 revs
= branch
.iter_merge_sorted_revisions(None, tip
, 'exclude', 'forward')
290 tip_revno
= branch
.revision_id_to_revno(tip
)
291 last_revno
, _
= branch
.last_revision_info()
292 total
= last_revno
- tip_revno
293 except bzrlib
.errors
.NoSuchRevision
:
297 for revid
, _
, seq
, _
in revs
:
299 if marks
.is_marked(revid
):
302 rev
= repo
.get_revision(revid
)
305 parents
= rev
.parent_ids
308 committer
= rev
.committer
.encode('utf-8')
309 committer
= "%s %u %s" % (fixup_user(committer
), time
, gittz(tz
))
310 authors
= rev
.get_apparent_authors()
312 author
= authors
[0].encode('utf-8')
313 author
= "%s %u %s" % (fixup_user(author
), time
, gittz(tz
))
316 msg
= rev
.message
.encode('utf-8')
320 if len(parents
) == 0:
321 parent
= bzrlib
.revision
.NULL_REVISION
325 cur_tree
= repo
.revision_tree(revid
)
326 prev
= repo
.revision_tree(parent
)
327 modified
, removed
= get_filechanges(cur_tree
, prev
)
329 modified_final
= export_files(cur_tree
, modified
)
331 if len(parents
) == 0:
332 print 'reset %s' % ref
334 print "commit %s" % ref
335 print "mark :%d" % (marks
.get_mark(revid
))
336 print "author %s" % (author
)
337 print "committer %s" % (committer
)
338 print "data %d" % (len(msg
))
341 for i
, p
in enumerate(parents
):
350 print "merge :%s" % m
354 for f
in modified_final
:
355 print "M %s :%u %s" % f
359 # let's skip branch revisions from the progress report
362 progress
= (revno
- tip_revno
)
363 if (progress
% 100 == 0):
365 print "progress revision %d '%s' (%d/%d)" % (revno
, name
, progress
, total
)
367 print "progress revision %d '%s' (%d)" % (revno
, name
, progress
)
371 revid
= branch
.last_revision()
373 # make sure the ref is updated
374 print "reset %s" % ref
375 print "from :%u" % rev_to_mark(revid
)
378 marks
.set_tip(name
, revid
)
380 def export_tag(repo
, name
):
381 ref
= '%s/tags/%s' % (prefix
, name
)
382 print "reset %s" % ref
383 print "from :%u" % rev_to_mark(tags
[name
])
386 def do_import(parser
):
388 path
= os
.path
.join(dirname
, 'marks-git')
391 if os
.path
.exists(path
):
392 print "feature import-marks=%s" % path
393 print "feature export-marks=%s" % path
394 print "feature force"
397 while parser
.check('import'):
399 if ref
.startswith('refs/heads/'):
400 name
= ref
[len('refs/heads/'):]
401 export_branch(repo
, name
)
402 if ref
.startswith('refs/tags/'):
403 name
= ref
[len('refs/tags/'):]
404 export_tag(repo
, name
)
411 def parse_blob(parser
):
413 mark
= parser
.get_mark()
415 data
= parser
.get_data()
416 blob_marks
[mark
] = data
421 def __init__(self
, branch
, revid
, parents
, files
):
425 def copy_tree(revid
):
426 files
= files_cache
[revid
] = {}
428 tree
= branch
.repository
.revision_tree(revid
)
430 for path
, entry
in tree
.iter_entries_by_dir():
431 files
[path
] = [entry
.file_id
, None]
436 if len(parents
) == 0:
437 self
.base_id
= bzrlib
.revision
.NULL_REVISION
440 self
.base_id
= parents
[0]
441 self
.base_files
= files_cache
.get(self
.base_id
, None)
442 if not self
.base_files
:
443 self
.base_files
= copy_tree(self
.base_id
)
445 self
.files
= files_cache
[revid
] = self
.base_files
.copy()
448 for path
, data
in self
.files
.iteritems():
450 self
.rev_files
[fid
] = [path
, mark
]
452 for path
, f
in files
.iteritems():
453 fid
, mark
= self
.files
.get(path
, [None, None])
455 fid
= bzrlib
.generate_ids
.gen_file_id(path
)
457 self
.rev_files
[fid
] = [path
, mark
]
458 self
.updates
[fid
] = f
460 def last_revision(self
):
463 def iter_changes(self
):
466 def get_parent(dirname
, basename
):
467 parent_fid
, mark
= self
.base_files
.get(dirname
, [None, None])
470 parent_fid
, mark
= self
.files
.get(dirname
, [None, None])
475 fid
= bzrlib
.generate_ids
.gen_file_id(path
)
476 add_entry(fid
, dirname
, 'directory')
479 def add_entry(fid
, path
, kind
, mode
=None):
480 dirname
, basename
= os
.path
.split(path
)
481 parent_fid
= get_parent(dirname
, basename
)
486 elif mode
== '120000':
497 self
.files
[path
] = [change
[0], None]
498 changes
.append(change
)
500 def update_entry(fid
, path
, kind
, mode
=None):
501 dirname
, basename
= os
.path
.split(path
)
502 parent_fid
= get_parent(dirname
, basename
)
507 elif mode
== '120000':
518 self
.files
[path
] = [change
[0], None]
519 changes
.append(change
)
521 def remove_entry(fid
, path
, kind
):
522 dirname
, basename
= os
.path
.split(path
)
523 parent_fid
= get_parent(dirname
, basename
)
533 changes
.append(change
)
535 for fid
, f
in self
.updates
.iteritems():
539 remove_entry(fid
, path
, 'file')
542 if path
in self
.base_files
:
543 update_entry(fid
, path
, 'file', f
['mode'])
545 add_entry(fid
, path
, 'file', f
['mode'])
547 self
.files
[path
][1] = f
['mark']
548 self
.rev_files
[fid
][1] = f
['mark']
552 def get_content(self
, file_id
):
553 path
, mark
= self
.rev_files
[file_id
]
555 return blob_marks
[mark
]
558 tree
= self
.branch
.repository
.revision_tree(self
.base_id
)
559 return tree
.get_file_text(file_id
)
561 def get_file_with_stat(self
, file_id
, path
=None):
562 content
= self
.get_content(file_id
)
563 return (StringIO
.StringIO(content
), None)
565 def get_symlink_target(self
, file_id
):
566 return self
.get_content(file_id
)
568 def id2path(self
, file_id
):
569 path
, mark
= self
.rev_files
[file_id
]
572 def c_style_unescape(string
):
573 if string
[0] == string
[-1] == '"':
574 return string
.decode('string-escape')[1:-1]
577 def parse_commit(parser
):
583 if ref
.startswith('refs/heads/'):
584 name
= ref
[len('refs/heads/'):]
585 branch
= get_remote_branch(name
)
589 commit_mark
= parser
.get_mark()
591 author
= parser
.get_author()
593 committer
= parser
.get_author()
595 data
= parser
.get_data()
597 if parser
.check('from'):
598 parents
.append(parser
.get_mark())
600 while parser
.check('merge'):
601 parents
.append(parser
.get_mark())
604 # fast-export adds an extra newline
611 if parser
.check('M'):
612 t
, m
, mark_ref
, path
= line
.split(' ', 3)
613 mark
= int(mark_ref
[1:])
614 f
= { 'mode' : m
, 'mark' : mark
}
615 elif parser
.check('D'):
616 t
, path
= line
.split(' ', 1)
617 f
= { 'deleted' : True }
619 die('Unknown file command: %s' % line
)
620 path
= c_style_unescape(path
).decode('utf-8')
623 committer
, date
, tz
= committer
624 author
, _
, _
= author
625 parents
= [mark_to_rev(p
) for p
in parents
]
626 revid
= bzrlib
.generate_ids
.gen_revision_id(committer
, date
)
628 props
['branch-nick'] = branch
.nick
629 props
['authors'] = author
631 mtree
= CustomTree(branch
, revid
, parents
, files
)
632 changes
= mtree
.iter_changes()
636 builder
= branch
.get_commit_builder(parents
, None, date
, tz
, committer
, props
, revid
)
638 list(builder
.record_iter_changes(mtree
, mtree
.last_revision(), changes
))
639 builder
.finish_inventory()
640 builder
.commit(data
.decode('utf-8', 'replace'))
647 parsed_refs
[ref
] = revid
648 marks
.new_mark(revid
, commit_mark
)
650 def parse_reset(parser
):
655 if parser
.check('commit'):
658 if not parser
.check('from'):
660 from_mark
= parser
.get_mark()
663 parsed_refs
[ref
] = mark_to_rev(from_mark
)
665 def do_export(parser
):
668 for line
in parser
.each_block('done'):
669 if parser
.check('blob'):
671 elif parser
.check('commit'):
673 elif parser
.check('reset'):
675 elif parser
.check('tag'):
677 elif parser
.check('feature'):
680 die('unhandled export command: %s' % line
)
682 for ref
, revid
in parsed_refs
.iteritems():
683 if ref
.startswith('refs/heads/'):
684 name
= ref
[len('refs/heads/'):]
685 branch
= get_remote_branch(name
)
686 branch
.generate_revision_history(revid
, marks
.get_tip(name
))
689 peer
= bzrlib
.branch
.Branch
.open(peers
[name
],
690 possible_transports
=transports
)
692 peer
.bzrdir
.push_branch(branch
, revision_id
=revid
,
694 except bzrlib
.errors
.DivergedBranches
:
695 print "error %s non-fast forward" % ref
699 wt
= branch
.bzrdir
.open_workingtree()
701 except bzrlib
.errors
.NoWorkingTree
:
703 elif ref
.startswith('refs/tags/'):
704 # TODO: implement tag push
705 print "error %s pushing tags not supported" % ref
708 # transport-helper/fast-export bugs
715 def do_capabilities(parser
):
718 print "refspec refs/heads/*:%s/heads/*" % prefix
719 print "refspec refs/tags/*:%s/tags/*" % prefix
721 path
= os
.path
.join(dirname
, 'marks-git')
723 if os
.path
.exists(path
):
724 print "*import-marks %s" % path
725 print "*export-marks %s" % path
730 class InvalidOptionValue(Exception):
733 def get_bool_option(val
):
739 raise InvalidOptionValue()
741 def do_option(parser
):
743 opt
, val
= parser
[1:3]
746 force
= get_bool_option(val
)
750 except InvalidOptionValue
:
751 print "error '%s' is not a valid value for option '%s'" % (val
, opt
)
753 def ref_is_valid(name
):
754 return not True in [c
in name
for c
in '~^: \\']
759 for name
in branches
:
760 if not master_branch
:
762 print "? refs/heads/%s" % name
764 branch
= get_remote_branch(master_branch
)
766 for tag
, revid
in branch
.tags
.get_tag_dict().items():
768 branch
.revision_id_to_dotted_revno(revid
)
769 except bzrlib
.errors
.NoSuchRevision
:
771 if not ref_is_valid(tag
):
773 print "? refs/tags/%s" % tag
777 print "@refs/heads/%s HEAD" % master_branch
780 def clone(path
, remote_branch
):
782 bdir
= bzrlib
.bzrdir
.BzrDir
.create(path
, possible_transports
=transports
)
783 except bzrlib
.errors
.AlreadyControlDirError
:
784 bdir
= bzrlib
.bzrdir
.BzrDir
.open(path
, possible_transports
=transports
)
785 repo
= bdir
.find_repository()
786 repo
.fetch(remote_branch
.repository
)
787 return remote_branch
.sprout(bdir
, repository
=repo
)
789 def get_remote_branch(name
):
790 remote_branch
= bzrlib
.branch
.Branch
.open(branches
[name
],
791 possible_transports
=transports
)
792 if isinstance(remote_branch
.bzrdir
.root_transport
, bzrlib
.transport
.local
.LocalTransport
):
795 branch_path
= os
.path
.join(dirname
, 'clone', name
)
798 branch
= bzrlib
.branch
.Branch
.open(branch_path
,
799 possible_transports
=transports
)
800 except bzrlib
.errors
.NotBranchError
:
802 branch
= clone(branch_path
, remote_branch
)
806 branch
.pull(remote_branch
, overwrite
=True)
807 except bzrlib
.errors
.DivergedBranches
:
808 # use remote branch for now
813 def find_branches(repo
):
814 transport
= repo
.bzrdir
.root_transport
816 for fn
in transport
.iter_files_recursive():
817 if not fn
.endswith('.bzr/branch-format'):
820 name
= subdir
= fn
[:-len('/.bzr/branch-format')]
821 name
= name
if name
!= '' else 'master'
822 name
= name
.replace('/', '+')
825 cur
= transport
.clone(subdir
)
826 branch
= bzrlib
.branch
.Branch
.open_from_transport(cur
)
827 except bzrlib
.errors
.NotBranchError
:
830 yield name
, branch
.base
832 def get_repo(url
, alias
):
833 normal_url
= bzrlib
.urlutils
.normalize_url(url
)
834 origin
= bzrlib
.bzrdir
.BzrDir
.open(url
, possible_transports
=transports
)
835 is_local
= isinstance(origin
.transport
, bzrlib
.transport
.local
.LocalTransport
)
837 shared_path
= os
.path
.join(gitdir
, 'bzr')
839 shared_dir
= bzrlib
.bzrdir
.BzrDir
.open(shared_path
,
840 possible_transports
=transports
)
841 except bzrlib
.errors
.NotBranchError
:
842 shared_dir
= bzrlib
.bzrdir
.BzrDir
.create(shared_path
,
843 possible_transports
=transports
)
845 shared_repo
= shared_dir
.open_repository()
846 except bzrlib
.errors
.NoRepositoryPresent
:
847 shared_repo
= shared_dir
.create_repository(shared
=True)
850 clone_path
= os
.path
.join(dirname
, 'clone')
851 if not os
.path
.exists(clone_path
):
854 # check and remove old organization
856 bdir
= bzrlib
.bzrdir
.BzrDir
.open(clone_path
,
857 possible_transports
=transports
)
858 bdir
.destroy_repository()
859 except bzrlib
.errors
.NotBranchError
:
861 except bzrlib
.errors
.NoRepositoryPresent
:
864 wanted
= get_config('remote.%s.bzr-branches' % alias
).rstrip().split(', ')
866 wanted
= [e
for e
in wanted
if e
]
868 wanted
= get_config('remote-bzr.branches').rstrip().split(', ')
870 wanted
= [e
for e
in wanted
if e
]
874 repo
= origin
.open_repository()
875 if not repo
.bzrdir
.root_transport
.listable():
876 # this repository is not usable for us
877 raise bzrlib
.errors
.NoRepositoryPresent(repo
.bzrdir
)
878 except bzrlib
.errors
.NoRepositoryPresent
:
882 def list_wanted(url
, wanted
):
884 subdir
= name
if name
!= 'master' else ''
885 yield name
, bzrlib
.urlutils
.join(url
, subdir
)
887 branch_list
= list_wanted(url
, wanted
)
889 branch_list
= find_branches(repo
)
891 for name
, url
in branch_list
:
898 def fix_path(alias
, orig_url
):
899 url
= urlparse
.urlparse(orig_url
, 'file')
900 if url
.scheme
!= 'file' or os
.path
.isabs(url
.path
):
902 abs_url
= urlparse
.urljoin("%s/" % os
.getcwd(), orig_url
)
903 cmd
= ['git', 'config', 'remote.%s.url' % alias
, "bzr::%s" % abs_url
]
907 global marks
, prefix
, gitdir
, dirname
908 global tags
, filenodes
913 global branches
, peers
919 gitdir
= os
.environ
.get('GIT_DIR', None)
922 die('Not enough arguments.')
925 die('GIT_DIR not set')
942 alias
= hashlib
.sha1(alias
).hexdigest()
944 prefix
= 'refs/bzr/%s' % alias
945 dirname
= os
.path
.join(gitdir
, 'bzr', alias
)
950 if not os
.path
.exists(dirname
):
953 if hasattr(bzrlib
.ui
.ui_factory
, 'be_quiet'):
954 bzrlib
.ui
.ui_factory
.be_quiet(True)
956 repo
= get_repo(url
, alias
)
958 marks_path
= os
.path
.join(dirname
, 'marks-int')
959 marks
= Marks(marks_path
)
961 parser
= Parser(repo
)
963 if parser
.check('capabilities'):
964 do_capabilities(parser
)
965 elif parser
.check('list'):
967 elif parser
.check('import'):
969 elif parser
.check('export'):
971 elif parser
.check('option'):
974 die('unhandled command: %s' % line
)
983 shutil
.rmtree(dirname
)
986 sys
.exit(main(sys
.argv
))