bzr.py
337 lines
| 12.0 KiB
| text/x-python
|
PythonLexer
|
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 | ||||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r8250 | |||
|
r48168 | # This module is for handling Breezy imports or `brz`, but it's also compatible | ||
# with Bazaar or `bzr`, that was formerly known as Bazaar-NG; | ||||
# it cannot access `bar` repositories, but they were never used very much. | ||||
|
r7053 | |||
import os | ||||
|
r29205 | |||
from mercurial.i18n import _ | ||||
|
r43375 | from mercurial import ( | ||
demandimport, | ||||
error, | ||||
r48432 | util, | |||
|
r43375 | ) | ||
|
r28411 | from . import common | ||
|
r48168 | |||
|
r7053 | # these do not work with demandimport, blacklist | ||
|
r43346 | demandimport.IGNORES.update( | ||
|
r46554 | [ | ||
|
r48168 | b'breezy.transactions', | ||
b'breezy.urlutils', | ||||
|
r46554 | b'ElementPath', | ||
] | ||||
|
r43346 | ) | ||
|
r7053 | |||
try: | ||||
# bazaar imports | ||||
|
r48168 | import breezy.bzr.bzrdir | ||
import breezy.errors | ||||
import breezy.revision | ||||
import breezy.revisionspec | ||||
|
r43346 | |||
|
r48168 | bzrdir = breezy.bzr.bzrdir | ||
errors = breezy.errors | ||||
revision = breezy.revision | ||||
revisionspec = breezy.revisionspec | ||||
|
r34489 | revisionspec.RevisionSpec | ||
|
r7053 | except ImportError: | ||
pass | ||||
|
r48168 | supportedkinds = ('file', 'symlink') | ||
|
r8045 | |||
|
r43346 | |||
|
r28411 | class bzr_source(common.converter_source): | ||
|
r7053 | """Reads Bazaar repositories by using the Bazaar Python libraries""" | ||
|
r35168 | def __init__(self, ui, repotype, path, revs=None): | ||
super(bzr_source, self).__init__(ui, repotype, path, revs=revs) | ||||
|
r7053 | |||
|
r43347 | if not os.path.exists(os.path.join(path, b'.bzr')): | ||
|
r43346 | raise common.NoRepo( | ||
|
r43347 | _(b'%s does not look like a Bazaar repository') % path | ||
|
r43346 | ) | ||
|
r7973 | |||
|
r7053 | try: | ||
|
r48168 | # access breezy stuff | ||
|
r16060 | bzrdir | ||
|
r7053 | except NameError: | ||
|
r43347 | raise common.NoRepo(_(b'Bazaar modules could not be loaded')) | ||
|
r7053 | |||
r48432 | path = util.abspath(path) | |||
|
r8470 | self._checkrepotype(path) | ||
|
r16060 | try: | ||
|
r48168 | bzr_dir = bzrdir.BzrDir.open(path.decode()) | ||
self.sourcerepo = bzr_dir.open_repository() | ||||
|
r16060 | except errors.NoRepositoryPresent: | ||
|
r43346 | raise common.NoRepo( | ||
|
r43347 | _(b'%s does not look like a Bazaar repository') % path | ||
|
r43346 | ) | ||
|
r7053 | self._parentids = {} | ||
|
r43347 | self._saverev = ui.configbool(b'convert', b'bzr.saverev') | ||
|
r7053 | |||
|
r8470 | def _checkrepotype(self, path): | ||
# Lightweight checkouts detection is informational but probably | ||||
# fragile at API level. It should not terminate the conversion. | ||||
try: | ||||
|
r48168 | dir = bzrdir.BzrDir.open_containing(path.decode())[0] | ||
|
r8470 | try: | ||
tree = dir.open_workingtree(recommend_upgrade=False) | ||||
branch = tree.branch | ||||
|
r12063 | except (errors.NoWorkingTree, errors.NotLocalUrl): | ||
|
r8470 | tree = None | ||
branch = dir.open_branch() | ||||
|
r43346 | if ( | ||
tree is not None | ||||
|
r48168 | and tree.controldir.root_transport.base | ||
!= branch.controldir.root_transport.base | ||||
|
r43346 | ): | ||
self.ui.warn( | ||||
_( | ||||
|
r43347 | b'warning: lightweight checkouts may cause ' | ||
b'conversion failures, try with a regular ' | ||||
b'branch instead.\n' | ||||
|
r43346 | ) | ||
) | ||||
|
r16689 | except Exception: | ||
|
r43347 | self.ui.note(_(b'bzr source type could not be determined\n')) | ||
|
r8470 | |||
|
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() | ||||
|
r16099 | def _bzrbranches(self): | ||
return self.sourcerepo.find_branches(using=True) | ||||
|
r7053 | def getheads(self): | ||
|
r25748 | if not self.revs: | ||
|
r16099 | # Set using=True to avoid nested repositories (see issue3254) | ||
heads = sorted([b.last_revision() for b in self._bzrbranches()]) | ||||
|
r16060 | else: | ||
revid = None | ||||
|
r16099 | for branch in self._bzrbranches(): | ||
|
r16060 | try: | ||
|
r48168 | revspec = self.revs[0].decode() | ||
r = revisionspec.RevisionSpec.from_string(revspec) | ||||
|
r16060 | info = r.in_history(branch) | ||
except errors.BzrError: | ||||
pass | ||||
revid = info.rev_id | ||||
if revid is None: | ||||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b'%s is not a valid revision') % self.revs[0] | ||
|
r43346 | ) | ||
|
r16060 | heads = [revid] | ||
|
r16061 | # Empty repositories return 'null:', which cannot be retrieved | ||
|
r43347 | heads = [h for h in heads if h != b'null:'] | ||
|
r16060 | return heads | ||
|
r7053 | |||
def getfile(self, name, rev): | ||||
|
r48168 | name = name.decode() | ||
|
r7053 | revtree = self.sourcerepo.revision_tree(rev) | ||
|
r48168 | |||
try: | ||||
kind = revtree.kind(name) | ||||
except breezy.errors.NoSuchFile: | ||||
return None, None | ||||
|
r8423 | if kind not in supportedkinds: | ||
|
r7053 | # the file is not available anymore - was deleted | ||
|
r22296 | return None, None | ||
|
r48168 | mode = self._modecache[(name.encode(), rev)] | ||
if kind == 'symlink': | ||||
target = revtree.get_symlink_target(name) | ||||
|
r8423 | if target is None: | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b'%s.%s symlink has no target') % (name, rev) | ||
|
r43346 | ) | ||
|
r48168 | return target.encode(), mode | ||
|
r8423 | else: | ||
|
r48168 | sio = revtree.get_file(name) | ||
|
r11134 | return sio.read(), mode | ||
|
r7053 | |||
|
r22300 | def getchanges(self, version, full): | ||
if full: | ||||
|
r43347 | raise error.Abort(_(b"convert from cvs does not support --full")) | ||
|
r7053 | 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]) | ||||
|
r24395 | files, changes = self._gettreechanges(self._revtree, prevtree) | ||
return files, changes, set() | ||||
|
r7053 | |||
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 | ||||
|
r48168 | branch = rev.properties.get('branch-nick', 'default') | ||
if branch == 'trunk': | ||||
branch = 'default' | ||||
|
r43346 | return common.commit( | ||
parents=parents, | ||||
|
r43347 | date=b'%d %d' % (rev.timestamp, -rev.timezone), | ||
|
r43346 | author=self.recode(rev.committer), | ||
desc=self.recode(rev.message), | ||||
|
r48168 | branch=branch.encode('utf8'), | ||
|
r43346 | rev=version, | ||
saverev=self._saverev, | ||||
) | ||||
|
r7053 | |||
def gettags(self): | ||||
bytetags = {} | ||||
|
r16099 | for branch in self._bzrbranches(): | ||
|
r16060 | if not branch.supports_tags(): | ||
return {} | ||||
tagdict = branch.tags.get_tag_dict() | ||||
|
r49768 | for name, rev in tagdict.items(): | ||
|
r16060 | bytetags[self.recode(name)] = rev | ||
|
r7053 | return bytetags | ||
def getchangedfiles(self, rev, i): | ||||
self._modecache = {} | ||||
curtree = self.sourcerepo.revision_tree(rev) | ||||
if i is not None: | ||||
|
r8165 | parentid = self._parentids[rev][i] | ||
|
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): | ||||
|
r10394 | revid = current._revision_id | ||
|
r7053 | changes = [] | ||
renames = {} | ||||
|
r15461 | seen = set() | ||
|
r35199 | |||
# Fall back to the deprecated attribute for legacy installations. | ||||
try: | ||||
inventory = origin.root_inventory | ||||
except AttributeError: | ||||
inventory = origin.inventory | ||||
|
r15461 | # Process the entries by reverse lexicographic name order to | ||
# handle nested renames correctly, most specific first. | ||||
|
r48168 | |||
def key(c): | ||||
return c.path[0] or c.path[1] or "" | ||||
|
r43346 | curchanges = sorted( | ||
current.iter_changes(origin), | ||||
|
r48168 | key=key, | ||
|
r43346 | reverse=True, | ||
) | ||||
|
r48168 | for change in curchanges: | ||
paths = change.path | ||||
kind = change.kind | ||||
executable = change.executable | ||||
|
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 | ||||
|
r48168 | if kind[1] == 'directory': | ||
if kind[0] not in (None, 'directory'): | ||||
|
r8126 | # Replacing 'something' with a directory, record it | ||
# so it can be removed. | ||||
changes.append((self.recode(paths[0]), revid)) | ||||
|
r48168 | if kind[0] == 'directory' and None not in paths: | ||
|
r15461 | renaming = paths[0] != paths[1] | ||
|
r7053 | # neither an add nor an delete - a move | ||
# rename all directory contents manually | ||||
|
r35199 | subdir = inventory.path2id(paths[0]) | ||
|
r7053 | # get all child-entries of the directory | ||
|
r35199 | for name, entry in inventory.iter_entries(subdir): | ||
|
r7053 | # hg does not track directory renames | ||
|
r48168 | if entry.kind == 'directory': | ||
|
r7053 | continue | ||
|
r48168 | frompath = self.recode(paths[0] + '/' + name) | ||
|
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 | ||||
|
r48168 | topath = self.recode(paths[1] + '/' + name) | ||
|
r7053 | # register the files as changed | ||
changes.append((frompath, revid)) | ||||
changes.append((topath, revid)) | ||||
# add to mode cache | ||||
|
r43346 | mode = ( | ||
|
r43347 | (entry.executable and b'x') | ||
|
r48168 | or (entry.kind == 'symlink' and b's') | ||
|
r43347 | or b'' | ||
|
r43346 | ) | ||
|
r7053 | self._modecache[(topath, revid)] = mode | ||
# register the change as move | ||||
renames[topath] = frompath | ||||
|
r17424 | # no further changes, go to the next change | ||
|
r7053 | continue | ||
# we got unicode paths, need to convert them | ||||
|
r16059 | path, topath = paths | ||
if path is not None: | ||||
path = self.recode(path) | ||||
if topath is not None: | ||||
topath = self.recode(topath) | ||||
|
r15461 | seen.add(path or topath) | ||
|
r7053 | |||
if topath is None: | ||||
# file deleted | ||||
changes.append((path, revid)) | ||||
continue | ||||
# renamed | ||||
if path and path != topath: | ||||
renames[topath] = path | ||||
|
r8035 | changes.append((path, revid)) | ||
|
r7053 | |||
# populate the mode cache | ||||
kind, executable = [e[1] for e in (kind, executable)] | ||||
|
r48168 | mode = (executable and b'x') or (kind == 'symlink' and b'l') or b'' | ||
|
r7053 | 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) | ||||
|
r7060 | parents = tuple([parent for parent in ids if parent in parentmap]) | ||
|
r7053 | return parents | ||