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
36 import atexit
, shutil
, hashlib
, urlparse
, subprocess
38 NAME_RE
= re
.compile('^([^<>]+)')
39 AUTHOR_RE
= re
.compile('^([^<>]+?)? ?<([^<>]*)>$')
40 RAW_AUTHOR_RE
= re
.compile('^(\w+) (.+)? <(.*)> (\d+) ([+-]\d+)')
43 sys
.stderr
.write('ERROR: %s\n' % (msg
% args
))
47 sys
.stderr
.write('WARNING: %s\n' % (msg
% args
))
50 return '%+03d%02d' % (tz
/ 3600, tz
% 3600 / 60)
54 def __init__(self
, path
):
63 if not os
.path
.exists(self
.path
):
66 tmp
= json
.load(open(self
.path
))
67 self
.tips
= tmp
['tips']
68 self
.marks
= tmp
['marks']
69 self
.last_mark
= tmp
['last-mark']
71 for rev
, mark
in self
.marks
.iteritems():
72 self
.rev_marks
[mark
] = rev
75 return { 'tips': self
.tips
, 'marks': self
.marks
, 'last-mark' : self
.last_mark
}
78 json
.dump(self
.dict(), open(self
.path
, 'w'))
81 return str(self
.dict())
83 def from_rev(self
, rev
):
84 return self
.marks
[rev
]
86 def to_rev(self
, mark
):
87 return self
.rev_marks
[mark
]
93 def get_mark(self
, rev
):
95 self
.marks
[rev
] = self
.last_mark
98 def is_marked(self
, rev
):
99 return rev
in self
.marks
101 def new_mark(self
, rev
, mark
):
102 self
.marks
[rev
] = mark
103 self
.rev_marks
[mark
] = rev
104 self
.last_mark
= mark
106 def get_tip(self
, branch
):
107 return self
.tips
.get(branch
, None)
109 def set_tip(self
, branch
, tip
):
110 self
.tips
[branch
] = tip
114 def __init__(self
, repo
):
116 self
.line
= self
.get_line()
119 return sys
.stdin
.readline().strip()
121 def __getitem__(self
, i
):
122 return self
.line
.split()[i
]
124 def check(self
, word
):
125 return self
.line
.startswith(word
)
127 def each_block(self
, separator
):
128 while self
.line
!= separator
:
130 self
.line
= self
.get_line()
133 return self
.each_block('')
136 self
.line
= self
.get_line()
137 if self
.line
== 'done':
141 i
= self
.line
.index(':') + 1
142 return int(self
.line
[i
:])
145 if not self
.check('data'):
147 i
= self
.line
.index(' ') + 1
148 size
= int(self
.line
[i
:])
149 return sys
.stdin
.read(size
)
151 def get_author(self
):
152 m
= RAW_AUTHOR_RE
.match(self
.line
)
155 _
, name
, email
, date
, tz
= m
.groups()
156 committer
= '%s <%s>' % (name
, email
)
158 tz
= ((tz
/ 100) * 3600) + ((tz
% 100) * 60)
159 return (committer
, int(date
), tz
)
161 def rev_to_mark(rev
):
163 return marks
.from_rev(rev
)
165 def mark_to_rev(mark
):
167 return marks
.to_rev(mark
)
169 def fixup_user(user
):
171 user
= user
.replace('"', '')
172 m
= AUTHOR_RE
.match(user
)
175 mail
= m
.group(2).strip()
177 m
= NAME_RE
.match(user
)
179 name
= m
.group(1).strip()
181 return '%s <%s>' % (name
, mail
)
183 def get_filechanges(cur
, prev
):
187 changes
= cur
.changes_from(prev
)
190 return s
.encode('utf-8')
192 for path
, fid
, kind
in changes
.added
:
193 modified
[u(path
)] = fid
194 for path
, fid
, kind
in changes
.removed
:
195 removed
[u(path
)] = None
196 for path
, fid
, kind
, mod
, _
in changes
.modified
:
197 modified
[u(path
)] = fid
198 for oldpath
, newpath
, fid
, kind
, mod
, _
in changes
.renamed
:
199 removed
[u(oldpath
)] = None
200 if kind
== 'directory':
201 lst
= cur
.list_files(from_dir
=newpath
, recursive
=True)
202 for path
, file_class
, kind
, fid
, entry
in lst
:
203 if kind
!= 'directory':
204 modified
[u(newpath
+ '/' + path
)] = fid
206 modified
[u(newpath
)] = fid
208 return modified
, removed
210 def export_files(tree
, files
):
211 global marks
, filenodes
214 for path
, fid
in files
.iteritems():
215 kind
= tree
.kind(fid
)
217 h
= tree
.get_file_sha1(fid
)
219 if kind
== 'symlink':
220 d
= tree
.get_symlink_target(fid
)
224 if tree
.is_executable(fid
):
229 # is the blob already exported?
232 final
.append((mode
, mark
, path
))
235 d
= tree
.get_file_text(fid
)
236 elif kind
== 'directory':
239 die("Unhandled kind '%s' for path '%s'" % (kind
, path
))
241 mark
= marks
.next_mark()
245 print "mark :%u" % mark
246 print "data %d" % len(d
)
249 final
.append((mode
, mark
, path
))
253 def export_branch(branch
, name
):
256 ref
= '%s/heads/%s' % (prefix
, name
)
257 tip
= marks
.get_tip(name
)
259 repo
= branch
.repository
261 revs
= branch
.iter_merge_sorted_revisions(None, tip
, 'exclude', 'forward')
264 revs
= [revid
for revid
, _
, _
, _
in revs
if not marks
.is_marked(revid
)]
268 rev
= repo
.get_revision(revid
)
270 parents
= rev
.parent_ids
273 committer
= rev
.committer
.encode('utf-8')
274 committer
= "%s %u %s" % (fixup_user(committer
), time
, gittz(tz
))
275 authors
= rev
.get_apparent_authors()
277 author
= authors
[0].encode('utf-8')
278 author
= "%s %u %s" % (fixup_user(author
), time
, gittz(tz
))
281 msg
= rev
.message
.encode('utf-8')
285 if len(parents
) == 0:
286 parent
= bzrlib
.revision
.NULL_REVISION
290 cur_tree
= repo
.revision_tree(revid
)
291 prev
= repo
.revision_tree(parent
)
292 modified
, removed
= get_filechanges(cur_tree
, prev
)
294 modified_final
= export_files(cur_tree
, modified
)
296 if len(parents
) == 0:
297 print 'reset %s' % ref
299 print "commit %s" % ref
300 print "mark :%d" % (marks
.get_mark(revid
))
301 print "author %s" % (author
)
302 print "committer %s" % (committer
)
303 print "data %d" % (len(msg
))
306 for i
, p
in enumerate(parents
):
315 print "merge :%s" % m
319 for f
in modified_final
:
320 print "M %s :%u %s" % f
324 if (count
% 100 == 0):
325 print "progress revision %s (%d/%d)" % (revid
, count
, len(revs
))
326 print "#############################################################"
330 revid
= branch
.last_revision()
332 # make sure the ref is updated
333 print "reset %s" % ref
334 print "from :%u" % rev_to_mark(revid
)
337 marks
.set_tip(name
, revid
)
339 def export_tag(repo
, name
):
342 ref
= '%s/tags/%s' % (prefix
, name
)
343 print "reset %s" % ref
344 print "from :%u" % rev_to_mark(tags
[name
])
347 def do_import(parser
):
351 path
= os
.path
.join(dirname
, 'marks-git')
354 if os
.path
.exists(path
):
355 print "feature import-marks=%s" % path
356 print "feature export-marks=%s" % path
359 while parser
.check('import'):
361 if ref
.startswith('refs/heads/'):
362 name
= ref
[len('refs/heads/'):]
363 export_branch(branch
, name
)
364 if ref
.startswith('refs/tags/'):
365 name
= ref
[len('refs/tags/'):]
366 export_tag(branch
, name
)
373 def parse_blob(parser
):
377 mark
= parser
.get_mark()
379 data
= parser
.get_data()
380 blob_marks
[mark
] = data
385 def __init__(self
, repo
, revid
, parents
, files
):
390 self
.parents
= parents
393 def copy_tree(revid
):
394 files
= files_cache
[revid
] = {}
395 tree
= repo
.repository
.revision_tree(revid
)
398 for path
, entry
in tree
.iter_entries_by_dir():
399 files
[path
] = entry
.file_id
404 if len(parents
) == 0:
405 self
.base_id
= bzrlib
.revision
.NULL_REVISION
408 self
.base_id
= parents
[0]
409 self
.base_files
= files_cache
.get(self
.base_id
, None)
410 if not self
.base_files
:
411 self
.base_files
= copy_tree(self
.base_id
)
413 self
.files
= files_cache
[revid
] = self
.base_files
.copy()
415 for path
, f
in files
.iteritems():
416 fid
= self
.files
.get(path
, None)
418 fid
= bzrlib
.generate_ids
.gen_file_id(path
)
420 self
.updates
[fid
] = f
422 def last_revision(self
):
425 def iter_changes(self
):
428 def get_parent(dirname
, basename
):
429 parent_fid
= self
.base_files
.get(dirname
, None)
432 parent_fid
= self
.files
.get(dirname
, None)
437 fid
= bzrlib
.generate_ids
.gen_file_id(path
)
438 d
= add_entry(fid
, dirname
, 'directory')
441 def add_entry(fid
, path
, kind
, mode
= None):
442 dirname
, basename
= os
.path
.split(path
)
443 parent_fid
= get_parent(dirname
, basename
)
448 elif mode
== '120000':
459 self
.files
[path
] = change
[0]
460 changes
.append(change
)
463 def update_entry(fid
, path
, kind
, mode
= None):
464 dirname
, basename
= os
.path
.split(path
)
465 parent_fid
= get_parent(dirname
, basename
)
470 elif mode
== '120000':
481 self
.files
[path
] = change
[0]
482 changes
.append(change
)
485 def remove_entry(fid
, path
, kind
):
486 dirname
, basename
= os
.path
.split(path
)
487 parent_fid
= get_parent(dirname
, basename
)
497 changes
.append(change
)
500 for fid
, f
in self
.updates
.iteritems():
504 remove_entry(fid
, path
, 'file')
507 if path
in self
.base_files
:
508 update_entry(fid
, path
, 'file', f
['mode'])
510 add_entry(fid
, path
, 'file', f
['mode'])
514 def get_file_with_stat(self
, file_id
, path
=None):
515 return (StringIO
.StringIO(self
.updates
[file_id
]['data']), None)
517 def get_symlink_target(self
, file_id
):
518 return self
.updates
[file_id
]['data']
520 def c_style_unescape(string
):
521 if string
[0] == string
[-1] == '"':
522 return string
.decode('string-escape')[1:-1]
525 def parse_commit(parser
):
526 global marks
, blob_marks
, parsed_refs
534 if ref
!= 'refs/heads/master':
535 die("bzr doesn't support multiple branches; use 'master'")
537 commit_mark
= parser
.get_mark()
539 author
= parser
.get_author()
541 committer
= parser
.get_author()
543 data
= parser
.get_data()
545 if parser
.check('from'):
546 parents
.append(parser
.get_mark())
548 while parser
.check('merge'):
549 parents
.append(parser
.get_mark())
552 # fast-export adds an extra newline
559 if parser
.check('M'):
560 t
, m
, mark_ref
, path
= line
.split(' ', 3)
561 mark
= int(mark_ref
[1:])
562 f
= { 'mode' : m
, 'data' : blob_marks
[mark
] }
563 elif parser
.check('D'):
564 t
, path
= line
.split(' ')
565 f
= { 'deleted' : True }
567 die('Unknown file command: %s' % line
)
568 path
= c_style_unescape(path
).decode('utf-8')
573 committer
, date
, tz
= committer
574 parents
= [str(mark_to_rev(p
)) for p
in parents
]
575 revid
= bzrlib
.generate_ids
.gen_revision_id(committer
, date
)
577 props
['branch-nick'] = repo
.nick
579 mtree
= CustomTree(repo
, revid
, parents
, files
)
580 changes
= mtree
.iter_changes()
584 builder
= repo
.get_commit_builder(parents
, None, date
, tz
, committer
, props
, revid
)
586 list(builder
.record_iter_changes(mtree
, mtree
.last_revision(), changes
))
587 builder
.finish_inventory()
588 builder
.commit(data
.decode('utf-8', 'replace'))
595 parsed_refs
[ref
] = revid
596 marks
.new_mark(revid
, commit_mark
)
598 def parse_reset(parser
):
604 if ref
!= 'refs/heads/master':
605 die("bzr doesn't support multiple branches; use 'master'")
608 if parser
.check('commit'):
611 if not parser
.check('from'):
613 from_mark
= parser
.get_mark()
616 parsed_refs
[ref
] = mark_to_rev(from_mark
)
618 def do_export(parser
):
619 global parsed_refs
, dirname
, peer
623 for line
in parser
.each_block('done'):
624 if parser
.check('blob'):
626 elif parser
.check('commit'):
628 elif parser
.check('reset'):
630 elif parser
.check('tag'):
632 elif parser
.check('feature'):
635 die('unhandled export command: %s' % line
)
639 for ref
, revid
in parsed_refs
.iteritems():
640 if ref
== 'refs/heads/master':
641 repo
.generate_revision_history(revid
, marks
.get_tip('master'))
644 repo
.push(peer
, stop_revision
=revid
)
645 except bzrlib
.errors
.DivergedBranches
:
646 print "error %s non-fast forward" % ref
649 wt
= repo
.bzrdir
.open_workingtree()
655 def do_capabilities(parser
):
660 print "refspec refs/heads/*:%s/heads/*" % prefix
661 print "refspec refs/tags/*:%s/tags/*" % prefix
663 path
= os
.path
.join(dirname
, 'marks-git')
665 if os
.path
.exists(path
):
666 print "*import-marks %s" % path
667 print "*export-marks %s" % path
671 def ref_is_valid(name
):
672 return not True in [c
in name
for c
in '~^: \\']
676 print "? refs/heads/%s" % 'master'
680 for tag
, revid
in branch
.tags
.get_tag_dict().items():
682 branch
.revision_id_to_dotted_revno(revid
)
683 except bzrlib
.errors
.NoSuchRevision
:
685 if not ref_is_valid(tag
):
687 print "? refs/tags/%s" % tag
690 print "@refs/heads/%s HEAD" % 'master'
693 def get_repo(url
, alias
):
696 origin
= bzrlib
.bzrdir
.BzrDir
.open(url
)
697 branch
= origin
.open_branch()
699 if not isinstance(origin
.transport
, bzrlib
.transport
.local
.LocalTransport
):
700 clone_path
= os
.path
.join(dirname
, 'clone')
701 remote_branch
= branch
702 if os
.path
.exists(clone_path
):
704 d
= bzrlib
.bzrdir
.BzrDir
.open(clone_path
)
705 branch
= d
.open_branch()
706 result
= branch
.pull(remote_branch
, [], None, False)
709 d
= origin
.sprout(clone_path
, None,
710 hardlink
=True, create_tree_if_local
=False,
711 source_branch
=remote_branch
)
712 branch
= d
.open_branch()
713 branch
.bind(remote_branch
)
721 def fix_path(alias
, orig_url
):
722 url
= urlparse
.urlparse(orig_url
, 'file')
723 if url
.scheme
!= 'file' or os
.path
.isabs(url
.path
):
725 abs_url
= urlparse
.urljoin("%s/" % os
.getcwd(), orig_url
)
726 cmd
= ['git', 'config', 'remote.%s.url' % alias
, "bzr::%s" % abs_url
]
730 global marks
, prefix
, dirname
731 global tags
, filenodes
749 alias
= hashlib
.sha1(alias
).hexdigest()
753 prefix
= 'refs/bzr/%s' % alias
754 gitdir
= os
.environ
['GIT_DIR']
755 dirname
= os
.path
.join(gitdir
, 'bzr', alias
)
760 if not os
.path
.exists(dirname
):
763 bzrlib
.ui
.ui_factory
.be_quiet(True)
765 repo
= get_repo(url
, alias
)
767 marks_path
= os
.path
.join(dirname
, 'marks-int')
768 marks
= Marks(marks_path
)
770 parser
= Parser(repo
)
772 if parser
.check('capabilities'):
773 do_capabilities(parser
)
774 elif parser
.check('list'):
776 elif parser
.check('import'):
778 elif parser
.check('export'):
781 die('unhandled command: %s' % line
)
790 shutil
.rmtree(dirname
)
793 sys
.exit(main(sys
.argv
))