git/contrib/remote-helpers/git-remote-bzr
Felipe Contreras fbd3f0e53c remote-bzr: use proper push method
Do not just randomly synchronize the revisions with no checks at
all.

I don't have any evidence that there's anything wrong with the
current code, which Bazaar seems to use, but for different purposes.
Let's use the logic Bazaar UI uses to avoid surprises.

Also, add a non-ff check.

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2013-04-25 15:34:37 -07:00

746 lines
19 KiB
Python
Executable file

#!/usr/bin/env python
#
# Copyright (c) 2012 Felipe Contreras
#
#
# Just copy to your ~/bin, or anywhere in your $PATH.
# Then you can clone with:
# % git clone bzr::/path/to/bzr/repo/or/url
#
# For example:
# % git clone bzr::$HOME/myrepo
# or
# % git clone bzr::lp:myrepo
#
import sys
import bzrlib
if hasattr(bzrlib, "initialize"):
bzrlib.initialize()
import bzrlib.plugin
bzrlib.plugin.load_plugins()
import bzrlib.generate_ids
import bzrlib.transport
import sys
import os
import json
import re
import StringIO
NAME_RE = re.compile('^([^<>]+)')
AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$')
RAW_AUTHOR_RE = re.compile('^(\w+) (.+)? <(.*)> (\d+) ([+-]\d+)')
def die(msg, *args):
sys.stderr.write('ERROR: %s\n' % (msg % args))
sys.exit(1)
def warn(msg, *args):
sys.stderr.write('WARNING: %s\n' % (msg % args))
def gittz(tz):
return '%+03d%02d' % (tz / 3600, tz % 3600 / 60)
class Marks:
def __init__(self, path):
self.path = path
self.tips = {}
self.marks = {}
self.rev_marks = {}
self.last_mark = 0
self.load()
def load(self):
if not os.path.exists(self.path):
return
tmp = json.load(open(self.path))
self.tips = tmp['tips']
self.marks = tmp['marks']
self.last_mark = tmp['last-mark']
for rev, mark in self.marks.iteritems():
self.rev_marks[mark] = rev
def dict(self):
return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark }
def store(self):
json.dump(self.dict(), open(self.path, 'w'))
def __str__(self):
return str(self.dict())
def from_rev(self, rev):
return self.marks[rev]
def to_rev(self, mark):
return self.rev_marks[mark]
def next_mark(self):
self.last_mark += 1
return self.last_mark
def get_mark(self, rev):
self.last_mark += 1
self.marks[rev] = self.last_mark
return self.last_mark
def is_marked(self, rev):
return self.marks.has_key(rev)
def new_mark(self, rev, mark):
self.marks[rev] = mark
self.rev_marks[mark] = rev
self.last_mark = mark
def get_tip(self, branch):
return self.tips.get(branch, None)
def set_tip(self, branch, tip):
self.tips[branch] = tip
class Parser:
def __init__(self, repo):
self.repo = repo
self.line = self.get_line()
def get_line(self):
return sys.stdin.readline().strip()
def __getitem__(self, i):
return self.line.split()[i]
def check(self, word):
return self.line.startswith(word)
def each_block(self, separator):
while self.line != separator:
yield self.line
self.line = self.get_line()
def __iter__(self):
return self.each_block('')
def next(self):
self.line = self.get_line()
if self.line == 'done':
self.line = None
def get_mark(self):
i = self.line.index(':') + 1
return int(self.line[i:])
def get_data(self):
if not self.check('data'):
return None
i = self.line.index(' ') + 1
size = int(self.line[i:])
return sys.stdin.read(size)
def get_author(self):
m = RAW_AUTHOR_RE.match(self.line)
if not m:
return None
_, name, email, date, tz = m.groups()
committer = '%s <%s>' % (name, email)
tz = int(tz)
tz = ((tz / 100) * 3600) + ((tz % 100) * 60)
return (committer, int(date), tz)
def rev_to_mark(rev):
global marks
return marks.from_rev(rev)
def mark_to_rev(mark):
global marks
return marks.to_rev(mark)
def fixup_user(user):
name = mail = None
user = user.replace('"', '')
m = AUTHOR_RE.match(user)
if m:
name = m.group(1)
mail = m.group(2).strip()
else:
m = NAME_RE.match(user)
if m:
name = m.group(1).strip()
return '%s <%s>' % (name, mail)
def get_filechanges(cur, prev):
modified = {}
removed = {}
changes = cur.changes_from(prev)
for path, fid, kind in changes.added:
modified[path] = fid
for path, fid, kind in changes.removed:
removed[path] = None
for path, fid, kind, mod, _ in changes.modified:
modified[path] = fid
for oldpath, newpath, fid, kind, mod, _ in changes.renamed:
removed[oldpath] = None
if kind == 'directory':
lst = cur.list_files(from_dir=newpath, recursive=True)
for path, file_class, kind, fid, entry in lst:
if kind != 'directory':
modified[newpath + '/' + path] = fid
else:
modified[newpath] = fid
return modified, removed
def export_files(tree, files):
global marks, filenodes
final = []
for path, fid in files.iteritems():
kind = tree.kind(fid)
h = tree.get_file_sha1(fid)
if kind == 'symlink':
d = tree.get_symlink_target(fid)
mode = '120000'
elif kind == 'file':
if tree.is_executable(fid):
mode = '100755'
else:
mode = '100644'
# is the blog already exported?
if h in filenodes:
mark = filenodes[h]
final.append((mode, mark, path.encode('utf-8')))
continue
d = tree.get_file_text(fid)
elif kind == 'directory':
continue
else:
die("Unhandled kind '%s' for path '%s'" % (kind, path))
mark = marks.next_mark()
filenodes[h] = mark
print "blob"
print "mark :%u" % mark
print "data %d" % len(d)
print d
final.append((mode, mark, path.encode('utf-8')))
return final
def export_branch(branch, name):
global prefix, dirname
ref = '%s/heads/%s' % (prefix, name)
tip = marks.get_tip(name)
repo = branch.repository
repo.lock_read()
revs = branch.iter_merge_sorted_revisions(None, tip, 'exclude', 'forward')
count = 0
revs = [revid for revid, _, _, _ in revs if not marks.is_marked(revid)]
for revid in revs:
rev = repo.get_revision(revid)
parents = rev.parent_ids
time = rev.timestamp
tz = rev.timezone
committer = rev.committer.encode('utf-8')
committer = "%s %u %s" % (fixup_user(committer), time, gittz(tz))
authors = rev.get_apparent_authors()
if authors:
author = authors[0].encode('utf-8')
author = "%s %u %s" % (fixup_user(author), time, gittz(tz))
else:
author = committer
msg = rev.message.encode('utf-8')
msg += '\n'
if len(parents) == 0:
parent = bzrlib.revision.NULL_REVISION
else:
parent = parents[0]
cur_tree = repo.revision_tree(revid)
prev = repo.revision_tree(parent)
modified, removed = get_filechanges(cur_tree, prev)
modified_final = export_files(cur_tree, modified)
if len(parents) == 0:
print 'reset %s' % ref
print "commit %s" % ref
print "mark :%d" % (marks.get_mark(revid))
print "author %s" % (author)
print "committer %s" % (committer)
print "data %d" % (len(msg))
print msg
for i, p in enumerate(parents):
try:
m = rev_to_mark(p)
except KeyError:
# ghost?
continue
if i == 0:
print "from :%s" % m
else:
print "merge :%s" % m
for f in removed:
print "D %s" % (f,)
for f in modified_final:
print "M %s :%u %s" % f
print
count += 1
if (count % 100 == 0):
print "progress revision %s (%d/%d)" % (revid, count, len(revs))
print "#############################################################"
repo.unlock()
revid = branch.last_revision()
# make sure the ref is updated
print "reset %s" % ref
print "from :%u" % rev_to_mark(revid)
print
marks.set_tip(name, revid)
def export_tag(repo, name):
global tags
try:
print "reset refs/tags/%s" % name
print "from :%u" % rev_to_mark(tags[name])
print
except KeyError:
warn("TODO: fetch tag '%s'" % name)
def do_import(parser):
global dirname
branch = parser.repo
path = os.path.join(dirname, 'marks-git')
print "feature done"
if os.path.exists(path):
print "feature import-marks=%s" % path
print "feature export-marks=%s" % path
sys.stdout.flush()
while parser.check('import'):
ref = parser[1]
if ref.startswith('refs/heads/'):
name = ref[len('refs/heads/'):]
export_branch(branch, name)
if ref.startswith('refs/tags/'):
name = ref[len('refs/tags/'):]
export_tag(branch, name)
parser.next()
print 'done'
sys.stdout.flush()
def parse_blob(parser):
global blob_marks
parser.next()
mark = parser.get_mark()
parser.next()
data = parser.get_data()
blob_marks[mark] = data
parser.next()
class CustomTree():
def __init__(self, repo, revid, parents, files):
global files_cache
self.repo = repo
self.revid = revid
self.parents = parents
self.updates = {}
def copy_tree(revid):
files = files_cache[revid] = {}
tree = repo.repository.revision_tree(revid)
repo.lock_read()
try:
for path, entry in tree.iter_entries_by_dir():
files[path] = entry.file_id
finally:
repo.unlock()
return files
if len(parents) == 0:
self.base_id = bzrlib.revision.NULL_REVISION
self.base_files = {}
else:
self.base_id = parents[0]
self.base_files = files_cache.get(self.base_id, None)
if not self.base_files:
self.base_files = copy_tree(self.base_id)
self.files = files_cache[revid] = self.base_files.copy()
for path, f in files.iteritems():
fid = self.files.get(path, None)
if not fid:
fid = bzrlib.generate_ids.gen_file_id(path)
f['path'] = path
self.updates[fid] = f
def last_revision(self):
return self.base_id
def iter_changes(self):
changes = []
def get_parent(dirname, basename):
parent_fid = self.base_files.get(dirname, None)
if parent_fid:
return parent_fid
parent_fid = self.files.get(dirname, None)
if parent_fid:
return parent_fid
if basename == '':
return None
fid = bzrlib.generate_ids.gen_file_id(path)
d = add_entry(fid, dirname, 'directory')
return fid
def add_entry(fid, path, kind, mode = None):
dirname, basename = os.path.split(path)
parent_fid = get_parent(dirname, basename)
executable = False
if mode == '100755':
executable = True
elif mode == '120000':
kind = 'symlink'
change = (fid,
(None, path),
True,
(False, True),
(None, parent_fid),
(None, basename),
(None, kind),
(None, executable))
self.files[path] = change[0]
changes.append(change)
return change
def update_entry(fid, path, kind, mode = None):
dirname, basename = os.path.split(path)
parent_fid = get_parent(dirname, basename)
executable = False
if mode == '100755':
executable = True
elif mode == '120000':
kind = 'symlink'
change = (fid,
(path, path),
True,
(True, True),
(None, parent_fid),
(None, basename),
(None, kind),
(None, executable))
self.files[path] = change[0]
changes.append(change)
return change
def remove_entry(fid, path, kind):
dirname, basename = os.path.split(path)
parent_fid = get_parent(dirname, basename)
change = (fid,
(path, None),
True,
(True, False),
(parent_fid, None),
(None, None),
(None, None),
(None, None))
del self.files[path]
changes.append(change)
return change
for fid, f in self.updates.iteritems():
path = f['path']
if 'deleted' in f:
remove_entry(fid, path, 'file')
continue
if path in self.base_files:
update_entry(fid, path, 'file', f['mode'])
else:
add_entry(fid, path, 'file', f['mode'])
return changes
def get_file_with_stat(self, file_id, path=None):
return (StringIO.StringIO(self.updates[file_id]['data']), None)
def get_symlink_target(self, file_id):
return self.updates[file_id]['data']
def c_style_unescape(string):
if string[0] == string[-1] == '"':
return string.decode('string-escape')[1:-1]
return string
def parse_commit(parser):
global marks, blob_marks, bmarks, parsed_refs
global mode
parents = []
ref = parser[1]
parser.next()
if ref != 'refs/heads/master':
die("bzr doesn't support multiple branches; use 'master'")
commit_mark = parser.get_mark()
parser.next()
author = parser.get_author()
parser.next()
committer = parser.get_author()
parser.next()
data = parser.get_data()
parser.next()
if parser.check('from'):
parents.append(parser.get_mark())
parser.next()
while parser.check('merge'):
parents.append(parser.get_mark())
parser.next()
files = {}
for line in parser:
if parser.check('M'):
t, m, mark_ref, path = line.split(' ', 3)
mark = int(mark_ref[1:])
f = { 'mode' : m, 'data' : blob_marks[mark] }
elif parser.check('D'):
t, path = line.split(' ')
f = { 'deleted' : True }
else:
die('Unknown file command: %s' % line)
path = c_style_unescape(path).decode('utf-8')
files[path] = f
repo = parser.repo
committer, date, tz = committer
parents = [str(mark_to_rev(p)) for p in parents]
revid = bzrlib.generate_ids.gen_revision_id(committer, date)
props = {}
props['branch-nick'] = repo.nick
mtree = CustomTree(repo, revid, parents, files)
changes = mtree.iter_changes()
repo.lock_write()
try:
builder = repo.get_commit_builder(parents, None, date, tz, committer, props, revid)
try:
list(builder.record_iter_changes(mtree, mtree.last_revision(), changes))
builder.finish_inventory()
builder.commit(data.decode('utf-8', 'replace'))
except Exception, e:
builder.abort()
raise
finally:
repo.unlock()
parsed_refs[ref] = revid
marks.new_mark(revid, commit_mark)
def parse_reset(parser):
global parsed_refs
ref = parser[1]
parser.next()
if ref != 'refs/heads/master':
die("bzr doesn't support multiple branches; use 'master'")
# ugh
if parser.check('commit'):
parse_commit(parser)
return
if not parser.check('from'):
return
from_mark = parser.get_mark()
parser.next()
parsed_refs[ref] = mark_to_rev(from_mark)
def do_export(parser):
global parsed_refs, dirname, peer
parser.next()
for line in parser.each_block('done'):
if parser.check('blob'):
parse_blob(parser)
elif parser.check('commit'):
parse_commit(parser)
elif parser.check('reset'):
parse_reset(parser)
elif parser.check('tag'):
pass
elif parser.check('feature'):
pass
else:
die('unhandled export command: %s' % line)
repo = parser.repo
for ref, revid in parsed_refs.iteritems():
if ref == 'refs/heads/master':
repo.generate_revision_history(revid, marks.get_tip('master'))
if peer:
try:
repo.push(peer, stop_revision=revid)
except bzrlib.errors.DivergedBranches:
print "error %s non-fast forward" % ref
continue
else:
wt = repo.bzrdir.open_workingtree()
wt.update()
print "ok %s" % ref
print
def do_capabilities(parser):
global dirname
print "import"
print "export"
print "refspec refs/heads/*:%s/heads/*" % prefix
path = os.path.join(dirname, 'marks-git')
if os.path.exists(path):
print "*import-marks %s" % path
print "*export-marks %s" % path
print
def do_list(parser):
global tags
print "? refs/heads/%s" % 'master'
history = parser.repo.revision_history()
for tag, revid in parser.repo.tags.get_tag_dict().items():
if revid not in history:
continue
print "? refs/tags/%s" % tag
tags[tag] = revid
print "@refs/heads/%s HEAD" % 'master'
print
def get_repo(url, alias):
global dirname, peer
origin = bzrlib.bzrdir.BzrDir.open(url)
branch = origin.open_branch()
if not isinstance(origin.transport, bzrlib.transport.local.LocalTransport):
clone_path = os.path.join(dirname, 'clone')
remote_branch = branch
if os.path.exists(clone_path):
# pull
d = bzrlib.bzrdir.BzrDir.open(clone_path)
branch = d.open_branch()
result = branch.pull(remote_branch, [], None, False)
else:
# clone
d = origin.sprout(clone_path, None,
hardlink=True, create_tree_if_local=False,
source_branch=remote_branch)
branch = d.open_branch()
branch.bind(remote_branch)
peer = remote_branch
else:
peer = None
return branch
def main(args):
global marks, prefix, dirname
global tags, filenodes
global blob_marks
global parsed_refs
global files_cache
alias = args[1]
url = args[2]
prefix = 'refs/bzr/%s' % alias
tags = {}
filenodes = {}
blob_marks = {}
parsed_refs = {}
files_cache = {}
gitdir = os.environ['GIT_DIR']
dirname = os.path.join(gitdir, 'bzr', alias)
if not os.path.exists(dirname):
os.makedirs(dirname)
repo = get_repo(url, alias)
marks_path = os.path.join(dirname, 'marks-int')
marks = Marks(marks_path)
parser = Parser(repo)
for line in parser:
if parser.check('capabilities'):
do_capabilities(parser)
elif parser.check('list'):
do_list(parser)
elif parser.check('import'):
do_import(parser)
elif parser.check('export'):
do_export(parser)
else:
die('unhandled command: %s' % line)
sys.stdout.flush()
marks.store()
sys.exit(main(sys.argv))