bzr.py
278 lines
| 10.7 KiB
| text/x-python
|
PythonLexer
Martin Geisler
|
r8250 | # bzr.py - bzr support for the convert extension | ||
# | ||||
# Copyright 2008, 2009 Marek Kubica <marek@xivilization.net> and others | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
Matt Mackall
|
r10263 | # GNU General Public License version 2 or any later version. | ||
Martin Geisler
|
r8250 | |||
Marek Kubica
|
r7053 | # This module is for handling 'bzr', that was formerly known as Bazaar-NG; | ||
# it cannot access 'bar' repositories, but they were never used very much | ||||
import os | ||||
from mercurial import demandimport | ||||
# these do not work with demandimport, blacklist | ||||
demandimport.ignore.extend([ | ||||
'bzrlib.transactions', | ||||
'bzrlib.urlutils', | ||||
Pierre-Yves David
|
r10560 | 'ElementPath', | ||
Marek Kubica
|
r7053 | ]) | ||
from mercurial.i18n import _ | ||||
from mercurial import util | ||||
from common import NoRepo, commit, converter_source | ||||
try: | ||||
# bazaar imports | ||||
from bzrlib import branch, revision, errors | ||||
from bzrlib.revisionspec import RevisionSpec | ||||
except ImportError: | ||||
pass | ||||
Patrick Mezard
|
r8045 | supportedkinds = ('file', 'symlink') | ||
Marek Kubica
|
r7053 | class bzr_source(converter_source): | ||
"""Reads Bazaar repositories by using the Bazaar Python libraries""" | ||||
def __init__(self, ui, path, rev=None): | ||||
super(bzr_source, self).__init__(ui, path, rev=rev) | ||||
Matt Mackall
|
r7973 | if not os.path.exists(os.path.join(path, '.bzr')): | ||
Martin Geisler
|
r10939 | raise NoRepo(_('%s does not look like a Bazaar repository') | ||
% path) | ||||
Matt Mackall
|
r7973 | |||
Marek Kubica
|
r7053 | try: | ||
# access bzrlib stuff | ||||
branch | ||||
except NameError: | ||||
Martin Geisler
|
r10939 | raise NoRepo(_('Bazaar modules could not be loaded')) | ||
Marek Kubica
|
r7053 | |||
path = os.path.abspath(path) | ||||
Patrick Mezard
|
r8470 | self._checkrepotype(path) | ||
Marek Kubica
|
r7053 | self.branch = branch.Branch.open(path) | ||
self.sourcerepo = self.branch.repository | ||||
self._parentids = {} | ||||
Patrick Mezard
|
r8470 | def _checkrepotype(self, path): | ||
# Lightweight checkouts detection is informational but probably | ||||
# fragile at API level. It should not terminate the conversion. | ||||
try: | ||||
from bzrlib import bzrdir | ||||
dir = bzrdir.BzrDir.open_containing(path)[0] | ||||
try: | ||||
tree = dir.open_workingtree(recommend_upgrade=False) | ||||
branch = tree.branch | ||||
Brodie Rao
|
r12063 | except (errors.NoWorkingTree, errors.NotLocalUrl): | ||
Patrick Mezard
|
r8470 | tree = None | ||
branch = dir.open_branch() | ||||
if (tree is not None and tree.bzrdir.root_transport.base != | ||||
branch.bzrdir.root_transport.base): | ||||
self.ui.warn(_('warning: lightweight checkouts may cause ' | ||||
'conversion failures, try with a regular ' | ||||
'branch instead.\n')) | ||||
except: | ||||
self.ui.note(_('bzr source type could not be determined\n')) | ||||
Marek Kubica
|
r7053 | def before(self): | ||
"""Before the conversion begins, acquire a read lock | ||||
for all the operations that might need it. Fortunately | ||||
read locks don't block other reads or writes to the | ||||
repository, so this shouldn't have any impact on the usage of | ||||
the source repository. | ||||
The alternative would be locking on every operation that | ||||
needs locks (there are currently two: getting the file and | ||||
getting the parent map) and releasing immediately after, | ||||
but this approach can take even 40% longer.""" | ||||
self.sourcerepo.lock_read() | ||||
def after(self): | ||||
self.sourcerepo.unlock() | ||||
def getheads(self): | ||||
if not self.rev: | ||||
return [self.branch.last_revision()] | ||||
try: | ||||
r = RevisionSpec.from_string(self.rev) | ||||
info = r.in_history(self.branch) | ||||
except errors.BzrError: | ||||
raise util.Abort(_('%s is not a valid revision in current branch') | ||||
% self.rev) | ||||
return [info.rev_id] | ||||
def getfile(self, name, rev): | ||||
revtree = self.sourcerepo.revision_tree(rev) | ||||
Brendan Cully
|
r8783 | fileid = revtree.path2id(name.decode(self.encoding or 'utf-8')) | ||
Patrick Mezard
|
r8423 | kind = None | ||
if fileid is not None: | ||||
kind = revtree.kind(fileid) | ||||
if kind not in supportedkinds: | ||||
Marek Kubica
|
r7053 | # the file is not available anymore - was deleted | ||
raise IOError(_('%s is not available in %s anymore') % | ||||
(name, rev)) | ||||
Patrick Mezard
|
r11134 | mode = self._modecache[(name, rev)] | ||
Patrick Mezard
|
r8423 | if kind == 'symlink': | ||
target = revtree.get_symlink_target(fileid) | ||||
if target is None: | ||||
raise util.Abort(_('%s.%s symlink has no target') | ||||
% (name, rev)) | ||||
Patrick Mezard
|
r11134 | return target, mode | ||
Patrick Mezard
|
r8423 | else: | ||
sio = revtree.get_file(fileid) | ||||
Patrick Mezard
|
r11134 | return sio.read(), mode | ||
Marek Kubica
|
r7053 | |||
def getchanges(self, version): | ||||
# set up caches: modecache and revtree | ||||
self._modecache = {} | ||||
self._revtree = self.sourcerepo.revision_tree(version) | ||||
# get the parentids from the cache | ||||
parentids = self._parentids.pop(version) | ||||
# only diff against first parent id | ||||
prevtree = self.sourcerepo.revision_tree(parentids[0]) | ||||
return self._gettreechanges(self._revtree, prevtree) | ||||
def getcommit(self, version): | ||||
rev = self.sourcerepo.get_revision(version) | ||||
# populate parent id cache | ||||
if not rev.parent_ids: | ||||
parents = [] | ||||
self._parentids[version] = (revision.NULL_REVISION,) | ||||
else: | ||||
parents = self._filterghosts(rev.parent_ids) | ||||
self._parentids[version] = parents | ||||
return commit(parents=parents, | ||||
Greg Ward
|
r8305 | date='%d %d' % (rev.timestamp, -rev.timezone), | ||
Marek Kubica
|
r7053 | author=self.recode(rev.committer), | ||
# bzr returns bytestrings or unicode, depending on the content | ||||
desc=self.recode(rev.message), | ||||
rev=version) | ||||
def gettags(self): | ||||
if not self.branch.supports_tags(): | ||||
return {} | ||||
tagdict = self.branch.tags.get_tag_dict() | ||||
bytetags = {} | ||||
for name, rev in tagdict.iteritems(): | ||||
bytetags[self.recode(name)] = rev | ||||
return bytetags | ||||
def getchangedfiles(self, rev, i): | ||||
self._modecache = {} | ||||
curtree = self.sourcerepo.revision_tree(rev) | ||||
if i is not None: | ||||
Patrick Mezard
|
r8165 | parentid = self._parentids[rev][i] | ||
Marek Kubica
|
r7053 | else: | ||
# no parent id, get the empty revision | ||||
parentid = revision.NULL_REVISION | ||||
prevtree = self.sourcerepo.revision_tree(parentid) | ||||
changes = [e[0] for e in self._gettreechanges(curtree, prevtree)[0]] | ||||
return changes | ||||
def _gettreechanges(self, current, origin): | ||||
Benoit Boissinot
|
r10394 | revid = current._revision_id | ||
Marek Kubica
|
r7053 | changes = [] | ||
renames = {} | ||||
Patrick Mezard
|
r15461 | seen = set() | ||
# Process the entries by reverse lexicographic name order to | ||||
# handle nested renames correctly, most specific first. | ||||
curchanges = sorted(current.iter_changes(origin), | ||||
key=lambda c: c[1][0] or c[1][1], | ||||
reverse=True) | ||||
Marek Kubica
|
r7053 | for (fileid, paths, changed_content, versioned, parent, name, | ||
Patrick Mezard
|
r15461 | kind, executable) in curchanges: | ||
Marek Kubica
|
r7053 | |||
if paths[0] == u'' or paths[1] == u'': | ||||
# ignore changes to tree root | ||||
continue | ||||
# bazaar tracks directories, mercurial does not, so | ||||
# we have to rename the directory contents | ||||
if kind[1] == 'directory': | ||||
Patrick Mezard
|
r8126 | if kind[0] not in (None, 'directory'): | ||
# Replacing 'something' with a directory, record it | ||||
# so it can be removed. | ||||
changes.append((self.recode(paths[0]), revid)) | ||||
Patrick Mezard
|
r15461 | if kind[0] == 'directory' and None not in paths: | ||
renaming = paths[0] != paths[1] | ||||
Marek Kubica
|
r7053 | # neither an add nor an delete - a move | ||
# rename all directory contents manually | ||||
subdir = origin.inventory.path2id(paths[0]) | ||||
# get all child-entries of the directory | ||||
for name, entry in origin.inventory.iter_entries(subdir): | ||||
# hg does not track directory renames | ||||
if entry.kind == 'directory': | ||||
continue | ||||
frompath = self.recode(paths[0] + '/' + name) | ||||
Patrick Mezard
|
r15461 | if frompath in seen: | ||
# Already handled by a more specific change entry | ||||
# This is important when you have: | ||||
# a => b | ||||
# a/c => a/c | ||||
# Here a/c must not be renamed into b/c | ||||
continue | ||||
seen.add(frompath) | ||||
if not renaming: | ||||
continue | ||||
Marek Kubica
|
r7053 | topath = self.recode(paths[1] + '/' + name) | ||
# register the files as changed | ||||
changes.append((frompath, revid)) | ||||
changes.append((topath, revid)) | ||||
# add to mode cache | ||||
Matt Mackall
|
r10282 | mode = ((entry.executable and 'x') | ||
or (entry.kind == 'symlink' and 's') | ||||
Marek Kubica
|
r7053 | or '') | ||
self._modecache[(topath, revid)] = mode | ||||
# register the change as move | ||||
renames[topath] = frompath | ||||
# no futher changes, go to the next change | ||||
continue | ||||
# we got unicode paths, need to convert them | ||||
path, topath = [self.recode(part) for part in paths] | ||||
Patrick Mezard
|
r15461 | seen.add(path or topath) | ||
Marek Kubica
|
r7053 | |||
if topath is None: | ||||
# file deleted | ||||
changes.append((path, revid)) | ||||
continue | ||||
# renamed | ||||
if path and path != topath: | ||||
renames[topath] = path | ||||
Xavier ALT
|
r8035 | changes.append((path, revid)) | ||
Marek Kubica
|
r7053 | |||
# populate the mode cache | ||||
kind, executable = [e[1] for e in (kind, executable)] | ||||
Patrick Mezard
|
r8148 | mode = ((executable and 'x') or (kind == 'symlink' and 'l') | ||
Marek Kubica
|
r7053 | or '') | ||
self._modecache[(topath, revid)] = mode | ||||
changes.append((topath, revid)) | ||||
return changes, renames | ||||
def _filterghosts(self, ids): | ||||
"""Filters out ghost revisions which hg does not support, see | ||||
<http://bazaar-vcs.org/GhostRevision> | ||||
""" | ||||
parentmap = self.sourcerepo.get_parent_map(ids) | ||||
Dirkjan Ochtman
|
r7060 | parents = tuple([parent for parent in ids if parent in parentmap]) | ||
Marek Kubica
|
r7053 | return parents | ||
def recode(self, s, encoding=None): | ||||
"""This version of recode tries to encode unicode to bytecode, | ||||
and preferably using the UTF-8 codec. | ||||
Other types than Unicode are silently returned, this is by | ||||
intention, e.g. the None-type is not going to be encoded but instead | ||||
just passed through | ||||
""" | ||||
if not encoding: | ||||
encoding = self.encoding or 'utf-8' | ||||
if isinstance(s, unicode): | ||||
return s.encode(encoding) | ||||
else: | ||||
# leave it alone | ||||
return s | ||||