File: //kunden/proc/self/root/lib/python3/dist-packages/breezy/mutabletree.py
# Copyright (C) 2006-2011 Canonical Ltd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
"""MutableTree object.
See MutableTree for more details.
"""
from __future__ import absolute_import
from . import (
errors,
hooks,
osutils,
trace,
tree,
)
from .sixish import (
text_type,
)
class BadReferenceTarget(errors.InternalBzrError):
_fmt = "Can't add reference to %(other_tree)s into %(tree)s." \
"%(reason)s"
def __init__(self, tree, other_tree, reason):
self.tree = tree
self.other_tree = other_tree
self.reason = reason
class MutableTree(tree.Tree):
"""A MutableTree is a specialisation of Tree which is able to be mutated.
Generally speaking these mutations are only possible within a lock_write
context, and will revert if the lock is broken abnormally - but this cannot
be guaranteed - depending on the exact implementation of the mutable state.
The most common form of Mutable Tree is WorkingTree, see breezy.workingtree.
For tests we also have MemoryTree which is a MutableTree whose contents are
entirely in memory.
For now, we are not treating MutableTree as an interface to provide
conformance tests for - rather we are testing MemoryTree specifically, and
interface testing implementations of WorkingTree.
A mutable tree always has an associated Branch and ControlDir object - the
branch and bzrdir attributes.
"""
def __init__(self, *args, **kw):
super(MutableTree, self).__init__(*args, **kw)
# Is this tree on a case-insensitive or case-preserving file-system?
# Sub-classes may initialize to False if they detect they are being
# used on media which doesn't differentiate the case of names.
self.case_sensitive = True
def is_control_filename(self, filename):
"""True if filename is the name of a control file in this tree.
:param filename: A filename within the tree. This is a relative path
from the root of this tree.
This is true IF and ONLY IF the filename is part of the meta data
that bzr controls in this tree. I.E. a random .bzr directory placed
on disk will not be a control file for this tree.
"""
raise NotImplementedError(self.is_control_filename)
def add(self, files, ids=None, kinds=None):
"""Add paths to the set of versioned paths.
Note that the command line normally calls smart_add instead,
which can automatically recurse.
This adds the files to the tree, so that they will be
recorded by the next commit.
:param files: List of paths to add, relative to the base of the tree.
:param ids: If set, use these instead of automatically generated ids.
Must be the same length as the list of files, but may
contain None for ids that are to be autogenerated.
:param kinds: Optional parameter to specify the kinds to be used for
each file.
TODO: Perhaps callback with the ids and paths as they're added.
"""
if isinstance(files, (str, text_type)):
# XXX: Passing a single string is inconsistent and should be
# deprecated.
if not (ids is None or isinstance(ids, bytes)):
raise AssertionError()
if not (kinds is None or isinstance(kinds, (str, text_type))):
raise AssertionError()
files = [files]
if ids is not None:
ids = [ids]
if kinds is not None:
kinds = [kinds]
files = [path.strip('/') for path in files]
if ids is None:
ids = [None] * len(files)
else:
if not (len(ids) == len(files)):
raise AssertionError()
if kinds is None:
kinds = [None] * len(files)
elif not len(kinds) == len(files):
raise AssertionError()
with self.lock_tree_write():
for f in files:
# generic constraint checks:
if self.is_control_filename(f):
raise errors.ForbiddenControlFileError(filename=f)
fp = osutils.splitpath(f)
# fill out file kinds for all files [not needed when we stop
# caring about the instantaneous file kind within a uncommmitted tree
#
self._gather_kinds(files, kinds)
self._add(files, ids, kinds)
def add_reference(self, sub_tree):
"""Add a TreeReference to the tree, pointing at sub_tree.
:param sub_tree: subtree to add.
"""
raise errors.UnsupportedOperation(self.add_reference, self)
def _add(self, files, ids, kinds):
"""Helper function for add - updates the inventory.
:param files: sequence of pathnames, relative to the tree root
:param ids: sequence of suggested ids for the files (may be None)
:param kinds: sequence of inventory kinds of the files (i.e. may
contain "tree-reference")
"""
raise NotImplementedError(self._add)
def commit(self, message=None, revprops=None, *args, **kwargs):
# avoid circular imports
from breezy import commit
possible_master_transports = []
with self.lock_write():
revprops = commit.Commit.update_revprops(
revprops,
self.branch,
kwargs.pop('authors', None),
kwargs.get('local', False),
possible_master_transports)
# args for wt.commit start at message from the Commit.commit method,
args = (message, ) + args
for hook in MutableTree.hooks['start_commit']:
hook(self)
committed_id = commit.Commit().commit(working_tree=self,
revprops=revprops,
possible_master_transports=possible_master_transports,
*args, **kwargs)
post_hook_params = PostCommitHookParams(self)
for hook in MutableTree.hooks['post_commit']:
hook(post_hook_params)
return committed_id
def _gather_kinds(self, files, kinds):
"""Helper function for add - sets the entries of kinds."""
raise NotImplementedError(self._gather_kinds)
def has_changes(self, _from_tree=None):
"""Quickly check that the tree contains at least one commitable change.
:param _from_tree: tree to compare against to find changes (default to
the basis tree and is intended to be used by tests).
:return: True if a change is found. False otherwise
"""
with self.lock_read():
# Check pending merges
if len(self.get_parent_ids()) > 1:
return True
if _from_tree is None:
_from_tree = self.basis_tree()
changes = self.iter_changes(_from_tree)
if self.supports_symlinks():
# Fast path for has_changes.
try:
change = next(changes)
# Exclude root (talk about black magic... --vila 20090629)
if change.parent_id == (None, None):
change = next(changes)
return True
except StopIteration:
# No changes
return False
else:
# Slow path for has_changes.
# Handle platforms that do not support symlinks in the
# conditional below. This is slower than the try/except
# approach below that but we don't have a choice as we
# need to be sure that all symlinks are removed from the
# entire changeset. This is because in platforms that
# do not support symlinks, they show up as None in the
# working copy as compared to the repository.
# Also, exclude root as mention in the above fast path.
changes = filter(
lambda c: c[6][0] != 'symlink' and c[4] != (None, None),
changes)
try:
next(iter(changes))
except StopIteration:
return False
return True
def check_changed_or_out_of_date(self, strict, opt_name,
more_error, more_warning):
"""Check the tree for uncommitted changes and branch synchronization.
If strict is None and not set in the config files, a warning is issued.
If strict is True, an error is raised.
If strict is False, no checks are done and no warning is issued.
:param strict: True, False or None, searched in branch config if None.
:param opt_name: strict option name to search in config file.
:param more_error: Details about how to avoid the check.
:param more_warning: Details about what is happening.
"""
with self.lock_read():
if strict is None:
strict = self.branch.get_config_stack().get(opt_name)
if strict is not False:
err_class = None
if (self.has_changes()):
err_class = errors.UncommittedChanges
elif self.last_revision() != self.branch.last_revision():
# The tree has lost sync with its branch, there is little
# chance that the user is aware of it but he can still
# force the action with --no-strict
err_class = errors.OutOfDateTree
if err_class is not None:
if strict is None:
err = err_class(self, more=more_warning)
# We don't want to interrupt the user if he expressed
# no preference about strict.
trace.warning('%s', err._format())
else:
err = err_class(self, more=more_error)
raise err
def last_revision(self):
"""Return the revision id of the last commit performed in this tree.
In early tree formats the result of last_revision is the same as the
branch last_revision, but that is no longer the case for modern tree
formats.
last_revision returns the left most parent id, or None if there are no
parents.
last_revision was deprecated as of 0.11. Please use get_parent_ids
instead.
"""
raise NotImplementedError(self.last_revision)
def lock_tree_write(self):
"""Lock the working tree for write, and the branch for read.
This is useful for operations which only need to mutate the working
tree. Taking out branch write locks is a relatively expensive process
and may fail if the branch is on read only media. So branch write locks
should only be taken out when we are modifying branch data - such as in
operations like commit, pull, uncommit and update.
"""
raise NotImplementedError(self.lock_tree_write)
def lock_write(self):
"""Lock the tree and its branch. This allows mutating calls to be made.
Some mutating methods will take out implicit write locks, but in
general you should always obtain a write lock before calling mutating
methods on a tree.
"""
raise NotImplementedError(self.lock_write)
def mkdir(self, path, file_id=None):
"""Create a directory in the tree. if file_id is None, one is assigned.
:param path: A unicode file path.
:param file_id: An optional file-id.
:return: the file id of the new directory.
"""
raise NotImplementedError(self.mkdir)
def _observed_sha1(self, path, sha_and_stat):
"""Tell the tree we have observed a paths sha1.
The intent of this function is to allow trees that have a hashcache to
update the hashcache during commit. If the observed file is too new
(based on the stat_value) to be safely hash-cached the tree will ignore
it.
The default implementation does nothing.
:param path: The file path
:param sha_and_stat: The sha 1 and stat result observed.
:return: None
"""
def put_file_bytes_non_atomic(self, path, bytes):
"""Update the content of a file in the tree.
Note that the file is written in-place rather than being
written to a temporary location and renamed. As a consequence,
readers can potentially see the file half-written.
:param file_id: file-id of the file
:param bytes: the new file contents
"""
raise NotImplementedError(self.put_file_bytes_non_atomic)
def set_parent_ids(self, revision_ids, allow_leftmost_as_ghost=False):
"""Set the parents ids of the working tree.
:param revision_ids: A list of revision_ids.
"""
raise NotImplementedError(self.set_parent_ids)
def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
"""Set the parents of the working tree.
:param parents_list: A list of (revision_id, tree) tuples.
If tree is None, then that element is treated as an unreachable
parent tree - i.e. a ghost.
"""
raise NotImplementedError(self.set_parent_trees)
def smart_add(self, file_list, recurse=True, action=None, save=True):
"""Version file_list, optionally recursing into directories.
This is designed more towards DWIM for humans than API clarity.
For the specific behaviour see the help for cmd_add().
:param file_list: List of zero or more paths. *NB: these are
interpreted relative to the process cwd, not relative to the
tree.* (Add and most other tree methods use tree-relative
paths.)
:param action: A reporter to be called with the working tree, parent_ie,
path and kind of the path being added. It may return a file_id if
a specific one should be used.
:param save: Save the changes after completing the adds. If False
this provides dry-run functionality by doing the add and not saving
the changes.
:return: A tuple - files_added, ignored_files. files_added is the count
of added files, and ignored_files is a dict mapping files that were
ignored to the rule that caused them to be ignored.
"""
raise NotImplementedError(self.smart_add)
def rename_one(self, from_rel, to_rel, after=False):
"""Rename one file.
This can change the directory or the filename or both.
rename_one has several 'modes' to work. First, it can rename a physical
file and change the file_id. That is the normal mode. Second, it can
only change the file_id without touching any physical file.
rename_one uses the second mode if 'after == True' and 'to_rel' is
either not versioned or newly added, and present in the working tree.
rename_one uses the second mode if 'after == False' and 'from_rel' is
versioned but no longer in the working tree, and 'to_rel' is not
versioned but present in the working tree.
rename_one uses the first mode if 'after == False' and 'from_rel' is
versioned and present in the working tree, and 'to_rel' is not
versioned and not present in the working tree.
Everything else results in an error.
"""
raise NotImplementedError(self.rename_one)
def copy_one(self, from_rel, to_rel):
"""Copy one file or directory.
This can change the directory or the filename or both.
"""
raise NotImplementedError(self.copy_one)
def get_transform(self, pb=None):
"""Return a transform object for use with this tree."""
raise NotImplementedError(self.get_transform)
class MutableTreeHooks(hooks.Hooks):
"""A dictionary mapping a hook name to a list of callables for mutabletree
hooks.
"""
def __init__(self):
"""Create the default hooks.
"""
hooks.Hooks.__init__(self, "breezy.mutabletree", "MutableTree.hooks")
self.add_hook('start_commit',
"Called before a commit is performed on a tree. The start commit "
"hook is able to change the tree before the commit takes place. "
"start_commit is called with the breezy.mutabletree.MutableTree "
"that the commit is being performed on.", (1, 4))
self.add_hook('post_commit',
"Called after a commit is performed on a tree. The hook is "
"called with a breezy.mutabletree.PostCommitHookParams object. "
"The mutable tree the commit was performed on is available via "
"the mutable_tree attribute of that object.", (2, 0))
self.add_hook('pre_transform',
"Called before a tree transform on this tree. The hook is called "
"with the tree that is being transformed and the transform.",
(2, 5))
self.add_hook('post_build_tree',
"Called after a completely new tree is built. The hook is "
"called with the tree as its only argument.", (2, 5))
self.add_hook('post_transform',
"Called after a tree transform has been performed on a tree. "
"The hook is called with the tree that is being transformed and "
"the transform.",
(2, 5))
# install the default hooks into the MutableTree class.
MutableTree.hooks = MutableTreeHooks()
class PostCommitHookParams(object):
"""Parameters for the post_commit hook.
To access the parameters, use the following attributes:
* mutable_tree - the MutableTree object
"""
def __init__(self, mutable_tree):
"""Create the parameters for the post_commit hook."""
self.mutable_tree = mutable_tree