1
0
Fork 0
mirror of https://github.com/git/git.git synced 2024-11-14 21:23:03 +01:00
git/contrib/remote-helpers/git-remote-bzr
Richard Hansen bd5424f0d6 remote-bzr: reuse bzrlib transports when possible
Pass a list of open bzrlib.transport.Transport objects to each bzrlib
function that might create a transport.  This enables bzrlib to reuse
existing transports when possible, avoiding multiple concurrent
connections to the same remote server.

If the remote server is accessed via ssh, this fixes a couple of
problems:
  * If the user does not have keys loaded into an ssh agent, the user
    may be prompted for a password multiple times.
  * If the user is using OpenSSH and the ControlMaster setting is set
    to auto, git-remote-bzr might hang.  This is because bzrlib closes
    the multiple ssh sessions in an undefined order and might try to
    close the master ssh session before the other sessions.  The
    master ssh process will not exit until the other sessions have
    exited, causing a deadlock.  (The ssh sessions are closed in an
    undefined order because bzrlib relies on the Python garbage
    collector to trigger ssh session termination.)

Signed-off-by: Richard Hansen <rhansen@bbn.com>
Acked-by: Felipe Contreras <felipe.contreras@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2013-09-08 11:15:33 -07:00

969 lines
26 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
#
# If you want to specify which branches you want track (per repo):
# git config remote-bzr.branches 'trunk, devel, test'
#
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 bzrlib.errors
import bzrlib.ui
import bzrlib.urlutils
import bzrlib.branch
import sys
import os
import json
import re
import StringIO
import atexit, shutil, hashlib, urlparse, subprocess
NAME_RE = re.compile('^([^<>]+)')
AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$')
EMAIL_RE = re.compile('^([^<>]+[^ \\\t<>])?\\b(?:[ \\t<>]*?)\\b([^ \\t<>]+@[^ \\t<>]+)')
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)
def get_config(config):
cmd = ['git', 'config', '--get', config]
process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
output, _ = process.communicate()
return output
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 str(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 rev in self.marks
def new_mark(self, rev, mark):
self.marks[rev] = mark
self.rev_marks[mark] = rev
self.last_mark = mark
def get_tip(self, branch):
try:
return str(self.tips[branch])
except KeyError:
return 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 = EMAIL_RE.match(user)
if m:
name = m.group(1)
mail = m.group(2)
else:
m = NAME_RE.match(user)
if m:
name = m.group(1).strip()
if not name:
name = 'unknown'
if not mail:
mail = 'Unknown'
return '%s <%s>' % (name, mail)
def get_filechanges(cur, prev):
modified = {}
removed = {}
changes = cur.changes_from(prev)
def u(s):
return s.encode('utf-8')
for path, fid, kind in changes.added:
modified[u(path)] = fid
for path, fid, kind in changes.removed:
removed[u(path)] = None
for path, fid, kind, mod, _ in changes.modified:
modified[u(path)] = fid
for oldpath, newpath, fid, kind, mod, _ in changes.renamed:
removed[u(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[u(newpath + '/' + path)] = fid
else:
modified[u(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 blob already exported?
if h in filenodes:
mark = filenodes[h]
final.append((mode, mark, path))
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))
return final
def export_branch(repo, name):
global prefix
ref = '%s/heads/%s' % (prefix, name)
tip = marks.get_tip(name)
branch = get_remote_branch(name)
repo = branch.repository
branch.lock_read()
revs = branch.iter_merge_sorted_revisions(None, tip, 'exclude', 'forward')
try:
tip_revno = branch.revision_id_to_revno(tip)
last_revno, _ = branch.last_revision_info()
total = last_revno - tip_revno
except bzrlib.errors.NoSuchRevision:
tip_revno = 0
total = 0
for revid, _, seq, _ in revs:
if marks.is_marked(revid):
continue
rev = repo.get_revision(revid)
revno = seq[0]
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
if len(seq) > 1:
# let's skip branch revisions from the progress report
continue
progress = (revno - tip_revno)
if (progress % 100 == 0):
if total:
print "progress revision %d '%s' (%d/%d)" % (revno, name, progress, total)
else:
print "progress revision %d '%s' (%d)" % (revno, name, progress)
branch.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, prefix
ref = '%s/tags/%s' % (prefix, name)
print "reset %s" % ref
print "from :%u" % rev_to_mark(tags[name])
print
def do_import(parser):
global dirname
repo = 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
print "feature force"
sys.stdout.flush()
while parser.check('import'):
ref = parser[1]
if ref.startswith('refs/heads/'):
name = ref[len('refs/heads/'):]
export_branch(repo, name)
if ref.startswith('refs/tags/'):
name = ref[len('refs/tags/'):]
export_tag(repo, 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, branch, revid, parents, files):
global files_cache
self.updates = {}
self.branch = branch
def copy_tree(revid):
files = files_cache[revid] = {}
branch.lock_read()
tree = branch.repository.revision_tree(revid)
try:
for path, entry in tree.iter_entries_by_dir():
files[path] = [entry.file_id, None]
finally:
branch.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()
self.rev_files = {}
for path, data in self.files.iteritems():
fid, mark = data
self.rev_files[fid] = [path, mark]
for path, f in files.iteritems():
fid, mark = self.files.get(path, [None, None])
if not fid:
fid = bzrlib.generate_ids.gen_file_id(path)
f['path'] = path
self.rev_files[fid] = [path, mark]
self.updates[fid] = f
def last_revision(self):
return self.base_id
def iter_changes(self):
changes = []
def get_parent(dirname, basename):
parent_fid, mark = self.base_files.get(dirname, [None, None])
if parent_fid:
return parent_fid
parent_fid, mark = self.files.get(dirname, [None, None])
if parent_fid:
return parent_fid
if basename == '':
return None
fid = bzrlib.generate_ids.gen_file_id(path)
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], None]
changes.append(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], None]
changes.append(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)
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'])
self.files[path][1] = f['mark']
self.rev_files[fid][1] = f['mark']
return changes
def get_content(self, file_id):
path, mark = self.rev_files[file_id]
if mark:
return blob_marks[mark]
# last resort
tree = self.branch.repository.revision_tree(self.base_id)
return tree.get_file_text(file_id)
def get_file_with_stat(self, file_id, path=None):
content = self.get_content(file_id)
return (StringIO.StringIO(content), None)
def get_symlink_target(self, file_id):
return self.get_content(file_id)
def id2path(self, file_id):
path, mark = self.rev_files[file_id]
return path
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, parsed_refs
global mode
parents = []
ref = parser[1]
parser.next()
if ref.startswith('refs/heads/'):
name = ref[len('refs/heads/'):]
branch = get_remote_branch(name)
else:
die('unknown ref')
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()
# fast-export adds an extra newline
if data[-1] == '\n':
data = data[:-1]
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, 'mark' : mark }
elif parser.check('D'):
t, path = line.split(' ', 1)
f = { 'deleted' : True }
else:
die('Unknown file command: %s' % line)
path = c_style_unescape(path).decode('utf-8')
files[path] = f
committer, date, tz = committer
parents = [mark_to_rev(p) for p in parents]
revid = bzrlib.generate_ids.gen_revision_id(committer, date)
props = {}
props['branch-nick'] = branch.nick
mtree = CustomTree(branch, revid, parents, files)
changes = mtree.iter_changes()
branch.lock_write()
try:
builder = branch.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:
branch.unlock()
parsed_refs[ref] = revid
marks.new_mark(revid, commit_mark)
def parse_reset(parser):
global parsed_refs
ref = parser[1]
parser.next()
# 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, transports
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)
for ref, revid in parsed_refs.iteritems():
if ref.startswith('refs/heads/'):
name = ref[len('refs/heads/'):]
branch = get_remote_branch(name)
branch.generate_revision_history(revid, marks.get_tip(name))
if name in peers:
peer = bzrlib.branch.Branch.open(peers[name],
possible_transports=transports)
try:
peer.bzrdir.push_branch(branch, revision_id=revid)
except bzrlib.errors.DivergedBranches:
print "error %s non-fast forward" % ref
continue
try:
wt = branch.bzrdir.open_workingtree()
wt.update()
except bzrlib.errors.NoWorkingTree:
pass
elif ref.startswith('refs/tags/'):
# TODO: implement tag push
print "error %s pushing tags not supported" % ref
continue
else:
# transport-helper/fast-export bugs
continue
print "ok %s" % ref
print
def do_capabilities(parser):
global dirname
print "import"
print "export"
print "refspec refs/heads/*:%s/heads/*" % prefix
print "refspec refs/tags/*:%s/tags/*" % 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 ref_is_valid(name):
return not True in [c in name for c in '~^: \\']
def do_list(parser):
global tags
master_branch = None
for name in branches:
if not master_branch:
master_branch = name
print "? refs/heads/%s" % name
branch = get_remote_branch(master_branch)
branch.lock_read()
for tag, revid in branch.tags.get_tag_dict().items():
try:
branch.revision_id_to_dotted_revno(revid)
except bzrlib.errors.NoSuchRevision:
continue
if not ref_is_valid(tag):
continue
print "? refs/tags/%s" % tag
tags[tag] = revid
branch.unlock()
print "@refs/heads/%s HEAD" % master_branch
print
def clone(path, remote_branch):
global transports
try:
bdir = bzrlib.bzrdir.BzrDir.create(path, possible_transports=transports)
except bzrlib.errors.AlreadyControlDirError:
bdir = bzrlib.bzrdir.BzrDir.open(path, possible_transports=transports)
repo = bdir.find_repository()
repo.fetch(remote_branch.repository)
return remote_branch.sprout(bdir, repository=repo)
def get_remote_branch(name):
global dirname, branches, transports
remote_branch = bzrlib.branch.Branch.open(branches[name],
possible_transports=transports)
if isinstance(remote_branch.user_transport, bzrlib.transport.local.LocalTransport):
return remote_branch
branch_path = os.path.join(dirname, 'clone', name)
try:
branch = bzrlib.branch.Branch.open(branch_path,
possible_transports=transports)
except bzrlib.errors.NotBranchError:
# clone
branch = clone(branch_path, remote_branch)
else:
# pull
try:
branch.pull(remote_branch, overwrite=True)
except bzrlib.errors.DivergedBranches:
# use remote branch for now
return remote_branch
return branch
def find_branches(repo):
transport = repo.bzrdir.root_transport
for fn in transport.iter_files_recursive():
if not fn.endswith('.bzr/branch-format'):
continue
name = subdir = fn[:-len('/.bzr/branch-format')]
name = name if name != '' else 'master'
name = name.replace('/', '+')
try:
cur = transport.clone(subdir)
branch = bzrlib.branch.Branch.open_from_transport(cur)
except bzrlib.errors.NotBranchError:
continue
else:
yield name, branch.base
def get_repo(url, alias):
global dirname, peer, branches, transports
normal_url = bzrlib.urlutils.normalize_url(url)
origin = bzrlib.bzrdir.BzrDir.open(url, possible_transports=transports)
is_local = isinstance(origin.transport, bzrlib.transport.local.LocalTransport)
shared_path = os.path.join(gitdir, 'bzr')
try:
shared_dir = bzrlib.bzrdir.BzrDir.open(shared_path,
possible_transports=transports)
except bzrlib.errors.NotBranchError:
shared_dir = bzrlib.bzrdir.BzrDir.create(shared_path,
possible_transports=transports)
try:
shared_repo = shared_dir.open_repository()
except bzrlib.errors.NoRepositoryPresent:
shared_repo = shared_dir.create_repository(shared=True)
if not is_local:
clone_path = os.path.join(dirname, 'clone')
if not os.path.exists(clone_path):
os.mkdir(clone_path)
else:
# check and remove old organization
try:
bdir = bzrlib.bzrdir.BzrDir.open(clone_path,
possible_transports=transports)
bdir.destroy_repository()
except bzrlib.errors.NotBranchError:
pass
except bzrlib.errors.NoRepositoryPresent:
pass
wanted = get_config('remote-bzr.branches').rstrip().split(', ')
# stupid python
wanted = [e for e in wanted if e]
if not wanted:
try:
repo = origin.open_repository()
if not repo.user_transport.listable():
# this repository is not usable for us
raise bzrlib.errors.NoRepositoryPresent(repo.bzrdir)
except bzrlib.errors.NoRepositoryPresent:
wanted = ['master']
if wanted:
def list_wanted(url, wanted):
for name in wanted:
subdir = name if name != 'master' else ''
yield name, bzrlib.urlutils.join(url, subdir)
branch_list = list_wanted(url, wanted)
else:
branch_list = find_branches(repo)
for name, url in branch_list:
if not is_local:
peers[name] = url
branches[name] = url
return origin
def fix_path(alias, orig_url):
url = urlparse.urlparse(orig_url, 'file')
if url.scheme != 'file' or os.path.isabs(url.path):
return
abs_url = urlparse.urljoin("%s/" % os.getcwd(), orig_url)
cmd = ['git', 'config', 'remote.%s.url' % alias, "bzr::%s" % abs_url]
subprocess.call(cmd)
def main(args):
global marks, prefix, gitdir, dirname
global tags, filenodes
global blob_marks
global parsed_refs
global files_cache
global is_tmp
global branches, peers
global transports
alias = args[1]
url = args[2]
tags = {}
filenodes = {}
blob_marks = {}
parsed_refs = {}
files_cache = {}
marks = None
branches = {}
peers = {}
transports = []
if alias[5:] == url:
is_tmp = True
alias = hashlib.sha1(alias).hexdigest()
else:
is_tmp = False
prefix = 'refs/bzr/%s' % alias
gitdir = os.environ['GIT_DIR']
dirname = os.path.join(gitdir, 'bzr', alias)
if not is_tmp:
fix_path(alias, url)
if not os.path.exists(dirname):
os.makedirs(dirname)
if hasattr(bzrlib.ui.ui_factory, 'be_quiet'):
bzrlib.ui.ui_factory.be_quiet(True)
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()
def bye():
if not marks:
return
if not is_tmp:
marks.store()
else:
shutil.rmtree(dirname)
atexit.register(bye)
sys.exit(main(sys.argv))