summaryrefslogtreecommitdiff
path: root/bzrlib/workingtree.py
diff options
context:
space:
mode:
Diffstat (limited to 'bzrlib/workingtree.py')
-rw-r--r--bzrlib/workingtree.py3191
1 files changed, 3191 insertions, 0 deletions
diff --git a/bzrlib/workingtree.py b/bzrlib/workingtree.py
new file mode 100644
index 0000000..971c1ca
--- /dev/null
+++ b/bzrlib/workingtree.py
@@ -0,0 +1,3191 @@
+# Copyright (C) 2005-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
+
+"""WorkingTree object and friends.
+
+A WorkingTree represents the editable working copy of a branch.
+Operations which represent the WorkingTree are also done here,
+such as renaming or adding files. The WorkingTree has an inventory
+which is updated by these operations. A commit produces a
+new revision based on the workingtree and its inventory.
+
+At the moment every WorkingTree has its own branch. Remote
+WorkingTrees aren't supported.
+
+To get a WorkingTree, call bzrdir.open_workingtree() or
+WorkingTree.open(dir).
+"""
+
+from __future__ import absolute_import
+
+from cStringIO import StringIO
+import os
+import sys
+
+from bzrlib.lazy_import import lazy_import
+lazy_import(globals(), """
+from bisect import bisect_left
+import collections
+import errno
+import itertools
+import operator
+import stat
+import re
+
+from bzrlib import (
+ branch,
+ conflicts as _mod_conflicts,
+ controldir,
+ errors,
+ filters as _mod_filters,
+ generate_ids,
+ globbing,
+ graph as _mod_graph,
+ ignores,
+ inventory,
+ merge,
+ revision as _mod_revision,
+ revisiontree,
+ rio as _mod_rio,
+ transform,
+ transport,
+ ui,
+ views,
+ xml5,
+ xml7,
+ )
+""")
+
+# Explicitly import bzrlib.bzrdir so that the BzrProber
+# is guaranteed to be registered.
+from bzrlib import (
+ bzrdir,
+ symbol_versioning,
+ )
+
+from bzrlib.decorators import needs_read_lock, needs_write_lock
+from bzrlib.i18n import gettext
+from bzrlib.lock import LogicalLockResult
+import bzrlib.mutabletree
+from bzrlib.mutabletree import needs_tree_write_lock
+from bzrlib import osutils
+from bzrlib.osutils import (
+ file_kind,
+ isdir,
+ normpath,
+ pathjoin,
+ realpath,
+ safe_unicode,
+ splitpath,
+ )
+from bzrlib.trace import mutter, note
+from bzrlib.revision import CURRENT_REVISION
+from bzrlib.symbol_versioning import (
+ deprecated_passed,
+ DEPRECATED_PARAMETER,
+ )
+
+
+MERGE_MODIFIED_HEADER_1 = "BZR merge-modified list format 1"
+# TODO: Modifying the conflict objects or their type is currently nearly
+# impossible as there is no clear relationship between the working tree format
+# and the conflict list file format.
+CONFLICT_HEADER_1 = "BZR conflict list format 1"
+
+ERROR_PATH_NOT_FOUND = 3 # WindowsError errno code, equivalent to ENOENT
+
+
+class TreeEntry(object):
+ """An entry that implements the minimum interface used by commands.
+
+ This needs further inspection, it may be better to have
+ InventoryEntries without ids - though that seems wrong. For now,
+ this is a parallel hierarchy to InventoryEntry, and needs to become
+ one of several things: decorates to that hierarchy, children of, or
+ parents of it.
+ Another note is that these objects are currently only used when there is
+ no InventoryEntry available - i.e. for unversioned objects.
+ Perhaps they should be UnversionedEntry et al. ? - RBC 20051003
+ """
+
+ def __eq__(self, other):
+ # yes, this us ugly, TODO: best practice __eq__ style.
+ return (isinstance(other, TreeEntry)
+ and other.__class__ == self.__class__)
+
+ def kind_character(self):
+ return "???"
+
+
+class TreeDirectory(TreeEntry):
+ """See TreeEntry. This is a directory in a working tree."""
+
+ def __eq__(self, other):
+ return (isinstance(other, TreeDirectory)
+ and other.__class__ == self.__class__)
+
+ def kind_character(self):
+ return "/"
+
+
+class TreeFile(TreeEntry):
+ """See TreeEntry. This is a regular file in a working tree."""
+
+ def __eq__(self, other):
+ return (isinstance(other, TreeFile)
+ and other.__class__ == self.__class__)
+
+ def kind_character(self):
+ return ''
+
+
+class TreeLink(TreeEntry):
+ """See TreeEntry. This is a symlink in a working tree."""
+
+ def __eq__(self, other):
+ return (isinstance(other, TreeLink)
+ and other.__class__ == self.__class__)
+
+ def kind_character(self):
+ return ''
+
+
+class WorkingTree(bzrlib.mutabletree.MutableTree,
+ controldir.ControlComponent):
+ """Working copy tree.
+
+ :ivar basedir: The root of the tree on disk. This is a unicode path object
+ (as opposed to a URL).
+ """
+
+ # override this to set the strategy for storing views
+ def _make_views(self):
+ return views.DisabledViews(self)
+
+ def __init__(self, basedir='.',
+ branch=DEPRECATED_PARAMETER,
+ _internal=False,
+ _transport=None,
+ _format=None,
+ _bzrdir=None):
+ """Construct a WorkingTree instance. This is not a public API.
+
+ :param branch: A branch to override probing for the branch.
+ """
+ self._format = _format
+ self.bzrdir = _bzrdir
+ if not _internal:
+ raise errors.BzrError("Please use bzrdir.open_workingtree or "
+ "WorkingTree.open() to obtain a WorkingTree.")
+ basedir = safe_unicode(basedir)
+ mutter("opening working tree %r", basedir)
+ if deprecated_passed(branch):
+ self._branch = branch
+ else:
+ self._branch = self.bzrdir.open_branch()
+ self.basedir = realpath(basedir)
+ self._transport = _transport
+ self._rules_searcher = None
+ self.views = self._make_views()
+
+ @property
+ def user_transport(self):
+ return self.bzrdir.user_transport
+
+ @property
+ def control_transport(self):
+ return self._transport
+
+ 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.
+ """
+ return self.bzrdir.is_control_filename(filename)
+
+ branch = property(
+ fget=lambda self: self._branch,
+ doc="""The branch this WorkingTree is connected to.
+
+ This cannot be set - it is reflective of the actual disk structure
+ the working tree has been constructed from.
+ """)
+
+ def has_versioned_directories(self):
+ """See `Tree.has_versioned_directories`."""
+ return self._format.supports_versioned_directories
+
+ def _supports_executable(self):
+ if sys.platform == 'win32':
+ return False
+ # FIXME: Ideally this should check the file system
+ return True
+
+ def break_lock(self):
+ """Break a lock if one is present from another instance.
+
+ Uses the ui factory to ask for confirmation if the lock may be from
+ an active process.
+
+ This will probe the repository for its lock as well.
+ """
+ raise NotImplementedError(self.break_lock)
+
+ def requires_rich_root(self):
+ return self._format.requires_rich_root
+
+ def supports_tree_reference(self):
+ return False
+
+ def supports_content_filtering(self):
+ return self._format.supports_content_filtering()
+
+ def supports_views(self):
+ return self.views.supports_views()
+
+ def get_config_stack(self):
+ """Retrieve the config stack for this tree.
+
+ :return: A ``bzrlib.config.Stack``
+ """
+ # For the moment, just provide the branch config stack.
+ return self.branch.get_config_stack()
+
+ @staticmethod
+ def open(path=None, _unsupported=False):
+ """Open an existing working tree at path.
+
+ """
+ if path is None:
+ path = osutils.getcwd()
+ control = controldir.ControlDir.open(path, _unsupported=_unsupported)
+ return control.open_workingtree(unsupported=_unsupported)
+
+ @staticmethod
+ def open_containing(path=None):
+ """Open an existing working tree which has its root about path.
+
+ This probes for a working tree at path and searches upwards from there.
+
+ Basically we keep looking up until we find the control directory or
+ run into /. If there isn't one, raises NotBranchError.
+ TODO: give this a new exception.
+ If there is one, it is returned, along with the unused portion of path.
+
+ :return: The WorkingTree that contains 'path', and the rest of path
+ """
+ if path is None:
+ path = osutils.getcwd()
+ control, relpath = controldir.ControlDir.open_containing(path)
+ return control.open_workingtree(), relpath
+
+ @staticmethod
+ def open_containing_paths(file_list, default_directory=None,
+ canonicalize=True, apply_view=True):
+ """Open the WorkingTree that contains a set of paths.
+
+ Fail if the paths given are not all in a single tree.
+
+ This is used for the many command-line interfaces that take a list of
+ any number of files and that require they all be in the same tree.
+ """
+ if default_directory is None:
+ default_directory = u'.'
+ # recommended replacement for builtins.internal_tree_files
+ if file_list is None or len(file_list) == 0:
+ tree = WorkingTree.open_containing(default_directory)[0]
+ # XXX: doesn't really belong here, and seems to have the strange
+ # side effect of making it return a bunch of files, not the whole
+ # tree -- mbp 20100716
+ if tree.supports_views() and apply_view:
+ view_files = tree.views.lookup_view()
+ if view_files:
+ file_list = view_files
+ view_str = views.view_display_str(view_files)
+ note(gettext("Ignoring files outside view. View is %s") % view_str)
+ return tree, file_list
+ if default_directory == u'.':
+ seed = file_list[0]
+ else:
+ seed = default_directory
+ file_list = [osutils.pathjoin(default_directory, f)
+ for f in file_list]
+ tree = WorkingTree.open_containing(seed)[0]
+ return tree, tree.safe_relpath_files(file_list, canonicalize,
+ apply_view=apply_view)
+
+ def safe_relpath_files(self, file_list, canonicalize=True, apply_view=True):
+ """Convert file_list into a list of relpaths in tree.
+
+ :param self: A tree to operate on.
+ :param file_list: A list of user provided paths or None.
+ :param apply_view: if True and a view is set, apply it or check that
+ specified files are within it
+ :return: A list of relative paths.
+ :raises errors.PathNotChild: When a provided path is in a different self
+ than self.
+ """
+ if file_list is None:
+ return None
+ if self.supports_views() and apply_view:
+ view_files = self.views.lookup_view()
+ else:
+ view_files = []
+ new_list = []
+ # self.relpath exists as a "thunk" to osutils, but canonical_relpath
+ # doesn't - fix that up here before we enter the loop.
+ if canonicalize:
+ fixer = lambda p: osutils.canonical_relpath(self.basedir, p)
+ else:
+ fixer = self.relpath
+ for filename in file_list:
+ relpath = fixer(osutils.dereference_path(filename))
+ if view_files and not osutils.is_inside_any(view_files, relpath):
+ raise errors.FileOutsideView(filename, view_files)
+ new_list.append(relpath)
+ return new_list
+
+ @staticmethod
+ def open_downlevel(path=None):
+ """Open an unsupported working tree.
+
+ Only intended for advanced situations like upgrading part of a bzrdir.
+ """
+ return WorkingTree.open(path, _unsupported=True)
+
+ @staticmethod
+ def find_trees(location):
+ def list_current(transport):
+ return [d for d in transport.list_dir('') if d != '.bzr']
+ def evaluate(bzrdir):
+ try:
+ tree = bzrdir.open_workingtree()
+ except errors.NoWorkingTree:
+ return True, None
+ else:
+ return True, tree
+ t = transport.get_transport(location)
+ iterator = controldir.ControlDir.find_bzrdirs(t, evaluate=evaluate,
+ list_current=list_current)
+ return [tr for tr in iterator if tr is not None]
+
+ def __repr__(self):
+ return "<%s of %s>" % (self.__class__.__name__,
+ getattr(self, 'basedir', None))
+
+ def abspath(self, filename):
+ return pathjoin(self.basedir, filename)
+
+ def basis_tree(self):
+ """Return RevisionTree for the current last revision.
+
+ If the left most parent is a ghost then the returned tree will be an
+ empty tree - one obtained by calling
+ repository.revision_tree(NULL_REVISION).
+ """
+ try:
+ revision_id = self.get_parent_ids()[0]
+ except IndexError:
+ # no parents, return an empty revision tree.
+ # in the future this should return the tree for
+ # 'empty:' - the implicit root empty tree.
+ return self.branch.repository.revision_tree(
+ _mod_revision.NULL_REVISION)
+ try:
+ return self.revision_tree(revision_id)
+ except errors.NoSuchRevision:
+ pass
+ # No cached copy available, retrieve from the repository.
+ # FIXME? RBC 20060403 should we cache the inventory locally
+ # at this point ?
+ try:
+ return self.branch.repository.revision_tree(revision_id)
+ except (errors.RevisionNotPresent, errors.NoSuchRevision):
+ # the basis tree *may* be a ghost or a low level error may have
+ # occurred. If the revision is present, its a problem, if its not
+ # its a ghost.
+ if self.branch.repository.has_revision(revision_id):
+ raise
+ # the basis tree is a ghost so return an empty tree.
+ return self.branch.repository.revision_tree(
+ _mod_revision.NULL_REVISION)
+
+ def _cleanup(self):
+ self._flush_ignore_list_cache()
+
+ def relpath(self, path):
+ """Return the local path portion from a given path.
+
+ The path may be absolute or relative. If its a relative path it is
+ interpreted relative to the python current working directory.
+ """
+ return osutils.relpath(self.basedir, path)
+
+ def has_filename(self, filename):
+ return osutils.lexists(self.abspath(filename))
+
+ def get_file(self, file_id, path=None, filtered=True):
+ return self.get_file_with_stat(file_id, path, filtered=filtered)[0]
+
+ def get_file_with_stat(self, file_id, path=None, filtered=True,
+ _fstat=osutils.fstat):
+ """See Tree.get_file_with_stat."""
+ if path is None:
+ path = self.id2path(file_id)
+ file_obj = self.get_file_byname(path, filtered=False)
+ stat_value = _fstat(file_obj.fileno())
+ if filtered and self.supports_content_filtering():
+ filters = self._content_filter_stack(path)
+ file_obj = _mod_filters.filtered_input_file(file_obj, filters)
+ return (file_obj, stat_value)
+
+ def get_file_text(self, file_id, path=None, filtered=True):
+ my_file = self.get_file(file_id, path=path, filtered=filtered)
+ try:
+ return my_file.read()
+ finally:
+ my_file.close()
+
+ def get_file_byname(self, filename, filtered=True):
+ path = self.abspath(filename)
+ f = file(path, 'rb')
+ if filtered and self.supports_content_filtering():
+ filters = self._content_filter_stack(filename)
+ return _mod_filters.filtered_input_file(f, filters)
+ else:
+ return f
+
+ def get_file_lines(self, file_id, path=None, filtered=True):
+ """See Tree.get_file_lines()"""
+ file = self.get_file(file_id, path, filtered=filtered)
+ try:
+ return file.readlines()
+ finally:
+ file.close()
+
+ def get_parent_ids(self):
+ """See Tree.get_parent_ids.
+
+ This implementation reads the pending merges list and last_revision
+ value and uses that to decide what the parents list should be.
+ """
+ last_rev = _mod_revision.ensure_null(self._last_revision())
+ if _mod_revision.NULL_REVISION == last_rev:
+ parents = []
+ else:
+ parents = [last_rev]
+ try:
+ merges_bytes = self._transport.get_bytes('pending-merges')
+ except errors.NoSuchFile:
+ pass
+ else:
+ for l in osutils.split_lines(merges_bytes):
+ revision_id = l.rstrip('\n')
+ parents.append(revision_id)
+ return parents
+
+ def get_root_id(self):
+ """Return the id of this trees root"""
+ raise NotImplementedError(self.get_root_id)
+
+ @needs_read_lock
+ def clone(self, to_controldir, revision_id=None):
+ """Duplicate this working tree into to_bzr, including all state.
+
+ Specifically modified files are kept as modified, but
+ ignored and unknown files are discarded.
+
+ If you want to make a new line of development, see ControlDir.sprout()
+
+ revision
+ If not None, the cloned tree will have its last revision set to
+ revision, and difference between the source trees last revision
+ and this one merged in.
+ """
+ # assumes the target bzr dir format is compatible.
+ result = to_controldir.create_workingtree()
+ self.copy_content_into(result, revision_id)
+ return result
+
+ @needs_read_lock
+ def copy_content_into(self, tree, revision_id=None):
+ """Copy the current content and user files of this tree into tree."""
+ tree.set_root_id(self.get_root_id())
+ if revision_id is None:
+ merge.transform_tree(tree, self)
+ else:
+ # TODO now merge from tree.last_revision to revision (to preserve
+ # user local changes)
+ merge.transform_tree(tree, self)
+ if revision_id == _mod_revision.NULL_REVISION:
+ new_parents = []
+ else:
+ new_parents = [revision_id]
+ tree.set_parent_ids(new_parents)
+
+ def id2abspath(self, file_id):
+ return self.abspath(self.id2path(file_id))
+
+ def _check_for_tree_references(self, iterator):
+ """See if directories have become tree-references."""
+ blocked_parent_ids = set()
+ for path, ie in iterator:
+ if ie.parent_id in blocked_parent_ids:
+ # This entry was pruned because one of its parents became a
+ # TreeReference. If this is a directory, mark it as blocked.
+ if ie.kind == 'directory':
+ blocked_parent_ids.add(ie.file_id)
+ continue
+ if ie.kind == 'directory' and self._directory_is_tree_reference(path):
+ # This InventoryDirectory needs to be a TreeReference
+ ie = inventory.TreeReference(ie.file_id, ie.name, ie.parent_id)
+ blocked_parent_ids.add(ie.file_id)
+ yield path, ie
+
+ def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=False):
+ """See Tree.iter_entries_by_dir()"""
+ # The only trick here is that if we supports_tree_reference then we
+ # need to detect if a directory becomes a tree-reference.
+ iterator = super(WorkingTree, self).iter_entries_by_dir(
+ specific_file_ids=specific_file_ids,
+ yield_parents=yield_parents)
+ if not self.supports_tree_reference():
+ return iterator
+ else:
+ return self._check_for_tree_references(iterator)
+
+ def get_file_size(self, file_id):
+ """See Tree.get_file_size"""
+ # XXX: this returns the on-disk size; it should probably return the
+ # canonical size
+ try:
+ return os.path.getsize(self.id2abspath(file_id))
+ except OSError, e:
+ if e.errno != errno.ENOENT:
+ raise
+ else:
+ return None
+
+ @needs_tree_write_lock
+ def _gather_kinds(self, files, kinds):
+ """See MutableTree._gather_kinds."""
+ for pos, f in enumerate(files):
+ if kinds[pos] is None:
+ fullpath = normpath(self.abspath(f))
+ try:
+ kinds[pos] = file_kind(fullpath)
+ except OSError, e:
+ if e.errno == errno.ENOENT:
+ raise errors.NoSuchFile(fullpath)
+
+ @needs_write_lock
+ def add_parent_tree_id(self, revision_id, allow_leftmost_as_ghost=False):
+ """Add revision_id as a parent.
+
+ This is equivalent to retrieving the current list of parent ids
+ and setting the list to its value plus revision_id.
+
+ :param revision_id: The revision id to add to the parent list. It may
+ be a ghost revision as long as its not the first parent to be
+ added, or the allow_leftmost_as_ghost parameter is set True.
+ :param allow_leftmost_as_ghost: Allow the first parent to be a ghost.
+ """
+ parents = self.get_parent_ids() + [revision_id]
+ self.set_parent_ids(parents, allow_leftmost_as_ghost=len(parents) > 1
+ or allow_leftmost_as_ghost)
+
+ @needs_tree_write_lock
+ def add_parent_tree(self, parent_tuple, allow_leftmost_as_ghost=False):
+ """Add revision_id, tree tuple as a parent.
+
+ This is equivalent to retrieving the current list of parent trees
+ and setting the list to its value plus parent_tuple. See also
+ add_parent_tree_id - if you only have a parent id available it will be
+ simpler to use that api. If you have the parent already available, using
+ this api is preferred.
+
+ :param parent_tuple: The (revision id, tree) to add to the parent list.
+ If the revision_id is a ghost, pass None for the tree.
+ :param allow_leftmost_as_ghost: Allow the first parent to be a ghost.
+ """
+ parent_ids = self.get_parent_ids() + [parent_tuple[0]]
+ if len(parent_ids) > 1:
+ # the leftmost may have already been a ghost, preserve that if it
+ # was.
+ allow_leftmost_as_ghost = True
+ self.set_parent_ids(parent_ids,
+ allow_leftmost_as_ghost=allow_leftmost_as_ghost)
+
+ @needs_tree_write_lock
+ def add_pending_merge(self, *revision_ids):
+ # TODO: Perhaps should check at this point that the
+ # history of the revision is actually present?
+ parents = self.get_parent_ids()
+ updated = False
+ for rev_id in revision_ids:
+ if rev_id in parents:
+ continue
+ parents.append(rev_id)
+ updated = True
+ if updated:
+ self.set_parent_ids(parents, allow_leftmost_as_ghost=True)
+
+ def path_content_summary(self, path, _lstat=os.lstat,
+ _mapper=osutils.file_kind_from_stat_mode):
+ """See Tree.path_content_summary."""
+ abspath = self.abspath(path)
+ try:
+ stat_result = _lstat(abspath)
+ except OSError, e:
+ if getattr(e, 'errno', None) == errno.ENOENT:
+ # no file.
+ return ('missing', None, None, None)
+ # propagate other errors
+ raise
+ kind = _mapper(stat_result.st_mode)
+ if kind == 'file':
+ return self._file_content_summary(path, stat_result)
+ elif kind == 'directory':
+ # perhaps it looks like a plain directory, but it's really a
+ # reference.
+ if self._directory_is_tree_reference(path):
+ kind = 'tree-reference'
+ return kind, None, None, None
+ elif kind == 'symlink':
+ target = osutils.readlink(abspath)
+ return ('symlink', None, None, target)
+ else:
+ return (kind, None, None, None)
+
+ def _file_content_summary(self, path, stat_result):
+ size = stat_result.st_size
+ executable = self._is_executable_from_path_and_stat(path, stat_result)
+ # try for a stat cache lookup
+ return ('file', size, executable, self._sha_from_stat(
+ path, stat_result))
+
+ def _check_parents_for_ghosts(self, revision_ids, allow_leftmost_as_ghost):
+ """Common ghost checking functionality from set_parent_*.
+
+ This checks that the left hand-parent exists if there are any
+ revisions present.
+ """
+ if len(revision_ids) > 0:
+ leftmost_id = revision_ids[0]
+ if (not allow_leftmost_as_ghost and not
+ self.branch.repository.has_revision(leftmost_id)):
+ raise errors.GhostRevisionUnusableHere(leftmost_id)
+
+ def _set_merges_from_parent_ids(self, parent_ids):
+ merges = parent_ids[1:]
+ self._transport.put_bytes('pending-merges', '\n'.join(merges),
+ mode=self.bzrdir._get_file_mode())
+
+ def _filter_parent_ids_by_ancestry(self, revision_ids):
+ """Check that all merged revisions are proper 'heads'.
+
+ This will always return the first revision_id, and any merged revisions
+ which are
+ """
+ if len(revision_ids) == 0:
+ return revision_ids
+ graph = self.branch.repository.get_graph()
+ heads = graph.heads(revision_ids)
+ new_revision_ids = revision_ids[:1]
+ for revision_id in revision_ids[1:]:
+ if revision_id in heads and revision_id not in new_revision_ids:
+ new_revision_ids.append(revision_id)
+ if new_revision_ids != revision_ids:
+ mutter('requested to set revision_ids = %s,'
+ ' but filtered to %s', revision_ids, new_revision_ids)
+ return new_revision_ids
+
+ @needs_tree_write_lock
+ def set_parent_ids(self, revision_ids, allow_leftmost_as_ghost=False):
+ """Set the parent ids to revision_ids.
+
+ See also set_parent_trees. This api will try to retrieve the tree data
+ for each element of revision_ids from the trees repository. If you have
+ tree data already available, it is more efficient to use
+ set_parent_trees rather than set_parent_ids. set_parent_ids is however
+ an easier API to use.
+
+ :param revision_ids: The revision_ids to set as the parent ids of this
+ working tree. Any of these may be ghosts.
+ """
+ self._check_parents_for_ghosts(revision_ids,
+ allow_leftmost_as_ghost=allow_leftmost_as_ghost)
+ for revision_id in revision_ids:
+ _mod_revision.check_not_reserved_id(revision_id)
+
+ revision_ids = self._filter_parent_ids_by_ancestry(revision_ids)
+
+ if len(revision_ids) > 0:
+ self.set_last_revision(revision_ids[0])
+ else:
+ self.set_last_revision(_mod_revision.NULL_REVISION)
+
+ self._set_merges_from_parent_ids(revision_ids)
+
+ @needs_tree_write_lock
+ def set_pending_merges(self, rev_list):
+ parents = self.get_parent_ids()
+ leftmost = parents[:1]
+ new_parents = leftmost + rev_list
+ self.set_parent_ids(new_parents)
+
+ @needs_tree_write_lock
+ def set_merge_modified(self, modified_hashes):
+ """Set the merge modified hashes."""
+ raise NotImplementedError(self.set_merge_modified)
+
+ def _sha_from_stat(self, path, stat_result):
+ """Get a sha digest from the tree's stat cache.
+
+ The default implementation assumes no stat cache is present.
+
+ :param path: The path.
+ :param stat_result: The stat result being looked up.
+ """
+ return None
+
+ @needs_write_lock # because merge pulls data into the branch.
+ def merge_from_branch(self, branch, to_revision=None, from_revision=None,
+ merge_type=None, force=False):
+ """Merge from a branch into this working tree.
+
+ :param branch: The branch to merge from.
+ :param to_revision: If non-None, the merge will merge to to_revision,
+ but not beyond it. to_revision does not need to be in the history
+ of the branch when it is supplied. If None, to_revision defaults to
+ branch.last_revision().
+ """
+ from bzrlib.merge import Merger, Merge3Merger
+ merger = Merger(self.branch, this_tree=self)
+ # check that there are no local alterations
+ if not force and self.has_changes():
+ raise errors.UncommittedChanges(self)
+ if to_revision is None:
+ to_revision = _mod_revision.ensure_null(branch.last_revision())
+ merger.other_rev_id = to_revision
+ if _mod_revision.is_null(merger.other_rev_id):
+ raise errors.NoCommits(branch)
+ self.branch.fetch(branch, last_revision=merger.other_rev_id)
+ merger.other_basis = merger.other_rev_id
+ merger.other_tree = self.branch.repository.revision_tree(
+ merger.other_rev_id)
+ merger.other_branch = branch
+ if from_revision is None:
+ merger.find_base()
+ else:
+ merger.set_base_revision(from_revision, branch)
+ if merger.base_rev_id == merger.other_rev_id:
+ raise errors.PointlessMerge
+ merger.backup_files = False
+ if merge_type is None:
+ merger.merge_type = Merge3Merger
+ else:
+ merger.merge_type = merge_type
+ merger.set_interesting_files(None)
+ merger.show_base = False
+ merger.reprocess = False
+ conflicts = merger.do_merge()
+ merger.set_pending()
+ return conflicts
+
+ def merge_modified(self):
+ """Return a dictionary of files modified by a merge.
+
+ The list is initialized by WorkingTree.set_merge_modified, which is
+ typically called after we make some automatic updates to the tree
+ because of a merge.
+
+ This returns a map of file_id->sha1, containing only files which are
+ still in the working inventory and have that text hash.
+ """
+ raise NotImplementedError(self.merge_modified)
+
+ @needs_write_lock
+ def mkdir(self, path, file_id=None):
+ """See MutableTree.mkdir()."""
+ if file_id is None:
+ file_id = generate_ids.gen_file_id(os.path.basename(path))
+ os.mkdir(self.abspath(path))
+ self.add(path, file_id, 'directory')
+ return file_id
+
+ def get_symlink_target(self, file_id, path=None):
+ if path is not None:
+ abspath = self.abspath(path)
+ else:
+ abspath = self.id2abspath(file_id)
+ target = osutils.readlink(abspath)
+ return target
+
+ def subsume(self, other_tree):
+ raise NotImplementedError(self.subsume)
+
+ def _setup_directory_is_tree_reference(self):
+ if self._branch.repository._format.supports_tree_reference:
+ self._directory_is_tree_reference = \
+ self._directory_may_be_tree_reference
+ else:
+ self._directory_is_tree_reference = \
+ self._directory_is_never_tree_reference
+
+ def _directory_is_never_tree_reference(self, relpath):
+ return False
+
+ def _directory_may_be_tree_reference(self, relpath):
+ # as a special case, if a directory contains control files then
+ # it's a tree reference, except that the root of the tree is not
+ return relpath and osutils.isdir(self.abspath(relpath) + u"/.bzr")
+ # TODO: We could ask all the control formats whether they
+ # recognize this directory, but at the moment there's no cheap api
+ # to do that. Since we probably can only nest bzr checkouts and
+ # they always use this name it's ok for now. -- mbp 20060306
+ #
+ # FIXME: There is an unhandled case here of a subdirectory
+ # containing .bzr but not a branch; that will probably blow up
+ # when you try to commit it. It might happen if there is a
+ # checkout in a subdirectory. This can be avoided by not adding
+ # it. mbp 20070306
+
+ def extract(self, file_id, format=None):
+ """Extract a subtree from this tree.
+
+ A new branch will be created, relative to the path for this tree.
+ """
+ raise NotImplementedError(self.extract)
+
+ def flush(self):
+ """Write the in memory meta data to disk."""
+ raise NotImplementedError(self.flush)
+
+ def _kind(self, relpath):
+ return osutils.file_kind(self.abspath(relpath))
+
+ def list_files(self, include_root=False, from_dir=None, recursive=True):
+ """List all files as (path, class, kind, id, entry).
+
+ Lists, but does not descend into unversioned directories.
+ This does not include files that have been deleted in this
+ tree. Skips the control directory.
+
+ :param include_root: if True, return an entry for the root
+ :param from_dir: start from this directory or None for the root
+ :param recursive: whether to recurse into subdirectories or not
+ """
+ raise NotImplementedError(self.list_files)
+
+ def move(self, from_paths, to_dir=None, after=False):
+ """Rename files.
+
+ to_dir must be known to the working tree.
+
+ If to_dir exists and is a directory, the files are moved into
+ it, keeping their old names.
+
+ Note that to_dir is only the last component of the new name;
+ this doesn't change the directory.
+
+ For each entry in from_paths the move mode will be determined
+ independently.
+
+ The first mode moves the file in the filesystem and updates the
+ working tree metadata. The second mode only updates the working tree
+ metadata without touching the file on the filesystem.
+
+ move uses the second mode if 'after == True' and the target is not
+ versioned but present in the working tree.
+
+ move uses the second mode if 'after == False' and the source is
+ versioned but no longer in the working tree, and the target is not
+ versioned but present in the working tree.
+
+ move uses the first mode if 'after == False' and the source is
+ versioned and present in the working tree, and the target is not
+ versioned and not present in the working tree.
+
+ Everything else results in an error.
+
+ This returns a list of (from_path, to_path) pairs for each
+ entry that is moved.
+ """
+ raise NotImplementedError(self.move)
+
+ @needs_tree_write_lock
+ 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)
+
+ @needs_read_lock
+ def unknowns(self):
+ """Return all unknown files.
+
+ These are files in the working directory that are not versioned or
+ control files or ignored.
+ """
+ # force the extras method to be fully executed before returning, to
+ # prevent race conditions with the lock
+ return iter(
+ [subp for subp in self.extras() if not self.is_ignored(subp)])
+
+ def unversion(self, file_ids):
+ """Remove the file ids in file_ids from the current versioned set.
+
+ When a file_id is unversioned, all of its children are automatically
+ unversioned.
+
+ :param file_ids: The file ids to stop versioning.
+ :raises: NoSuchId if any fileid is not currently versioned.
+ """
+ raise NotImplementedError(self.unversion)
+
+ @needs_write_lock
+ def pull(self, source, overwrite=False, stop_revision=None,
+ change_reporter=None, possible_transports=None, local=False,
+ show_base=False):
+ source.lock_read()
+ try:
+ old_revision_info = self.branch.last_revision_info()
+ basis_tree = self.basis_tree()
+ count = self.branch.pull(source, overwrite, stop_revision,
+ possible_transports=possible_transports,
+ local=local)
+ new_revision_info = self.branch.last_revision_info()
+ if new_revision_info != old_revision_info:
+ repository = self.branch.repository
+ if repository._format.fast_deltas:
+ parent_ids = self.get_parent_ids()
+ if parent_ids:
+ basis_id = parent_ids[0]
+ basis_tree = repository.revision_tree(basis_id)
+ basis_tree.lock_read()
+ try:
+ new_basis_tree = self.branch.basis_tree()
+ merge.merge_inner(
+ self.branch,
+ new_basis_tree,
+ basis_tree,
+ this_tree=self,
+ pb=None,
+ change_reporter=change_reporter,
+ show_base=show_base)
+ basis_root_id = basis_tree.get_root_id()
+ new_root_id = new_basis_tree.get_root_id()
+ if new_root_id is not None and basis_root_id != new_root_id:
+ self.set_root_id(new_root_id)
+ finally:
+ basis_tree.unlock()
+ # TODO - dedup parents list with things merged by pull ?
+ # reuse the revisiontree we merged against to set the new
+ # tree data.
+ parent_trees = []
+ if self.branch.last_revision() != _mod_revision.NULL_REVISION:
+ parent_trees.append(
+ (self.branch.last_revision(), new_basis_tree))
+ # we have to pull the merge trees out again, because
+ # merge_inner has set the ids. - this corner is not yet
+ # layered well enough to prevent double handling.
+ # XXX TODO: Fix the double handling: telling the tree about
+ # the already known parent data is wasteful.
+ merges = self.get_parent_ids()[1:]
+ parent_trees.extend([
+ (parent, repository.revision_tree(parent)) for
+ parent in merges])
+ self.set_parent_trees(parent_trees)
+ return count
+ finally:
+ source.unlock()
+
+ @needs_write_lock
+ def put_file_bytes_non_atomic(self, file_id, bytes):
+ """See MutableTree.put_file_bytes_non_atomic."""
+ stream = file(self.id2abspath(file_id), 'wb')
+ try:
+ stream.write(bytes)
+ finally:
+ stream.close()
+
+ def extras(self):
+ """Yield all unversioned files in this WorkingTree.
+
+ If there are any unversioned directories then only the directory is
+ returned, not all its children. But if there are unversioned files
+ under a versioned subdirectory, they are returned.
+
+ Currently returned depth-first, sorted by name within directories.
+ This is the same order used by 'osutils.walkdirs'.
+ """
+ raise NotImplementedError(self.extras)
+
+ def ignored_files(self):
+ """Yield list of PATH, IGNORE_PATTERN"""
+ for subp in self.extras():
+ pat = self.is_ignored(subp)
+ if pat is not None:
+ yield subp, pat
+
+ def get_ignore_list(self):
+ """Return list of ignore patterns.
+
+ Cached in the Tree object after the first call.
+ """
+ ignoreset = getattr(self, '_ignoreset', None)
+ if ignoreset is not None:
+ return ignoreset
+
+ ignore_globs = set()
+ ignore_globs.update(ignores.get_runtime_ignores())
+ ignore_globs.update(ignores.get_user_ignores())
+ if self.has_filename(bzrlib.IGNORE_FILENAME):
+ f = self.get_file_byname(bzrlib.IGNORE_FILENAME)
+ try:
+ ignore_globs.update(ignores.parse_ignore_file(f))
+ finally:
+ f.close()
+ self._ignoreset = ignore_globs
+ return ignore_globs
+
+ def _flush_ignore_list_cache(self):
+ """Resets the cached ignore list to force a cache rebuild."""
+ self._ignoreset = None
+ self._ignoreglobster = None
+
+ def is_ignored(self, filename):
+ r"""Check whether the filename matches an ignore pattern.
+
+ Patterns containing '/' or '\' need to match the whole path;
+ others match against only the last component. Patterns starting
+ with '!' are ignore exceptions. Exceptions take precedence
+ over regular patterns and cause the filename to not be ignored.
+
+ If the file is ignored, returns the pattern which caused it to
+ be ignored, otherwise None. So this can simply be used as a
+ boolean if desired."""
+ if getattr(self, '_ignoreglobster', None) is None:
+ self._ignoreglobster = globbing.ExceptionGlobster(self.get_ignore_list())
+ return self._ignoreglobster.match(filename)
+
+ def kind(self, file_id):
+ return file_kind(self.id2abspath(file_id))
+
+ def stored_kind(self, file_id):
+ """See Tree.stored_kind"""
+ raise NotImplementedError(self.stored_kind)
+
+ def _comparison_data(self, entry, path):
+ abspath = self.abspath(path)
+ try:
+ stat_value = os.lstat(abspath)
+ except OSError, e:
+ if getattr(e, 'errno', None) == errno.ENOENT:
+ stat_value = None
+ kind = None
+ executable = False
+ else:
+ raise
+ else:
+ mode = stat_value.st_mode
+ kind = osutils.file_kind_from_stat_mode(mode)
+ if not self._supports_executable():
+ executable = entry is not None and entry.executable
+ else:
+ executable = bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
+ return kind, executable, stat_value
+
+ def _file_size(self, entry, stat_value):
+ return stat_value.st_size
+
+ def last_revision(self):
+ """Return the last revision of the branch for this tree.
+
+ This format tree does not support a separate marker for last-revision
+ compared to the branch.
+
+ See MutableTree.last_revision
+ """
+ return self._last_revision()
+
+ @needs_read_lock
+ def _last_revision(self):
+ """helper for get_parent_ids."""
+ return _mod_revision.ensure_null(self.branch.last_revision())
+
+ def is_locked(self):
+ """Check if this tree is locked."""
+ raise NotImplementedError(self.is_locked)
+
+ def lock_read(self):
+ """Lock the tree for reading.
+
+ This also locks the branch, and can be unlocked via self.unlock().
+
+ :return: A bzrlib.lock.LogicalLockResult.
+ """
+ raise NotImplementedError(self.lock_read)
+
+ def lock_tree_write(self):
+ """See MutableTree.lock_tree_write, and WorkingTree.unlock.
+
+ :return: A bzrlib.lock.LogicalLockResult.
+ """
+ raise NotImplementedError(self.lock_tree_write)
+
+ def lock_write(self):
+ """See MutableTree.lock_write, and WorkingTree.unlock.
+
+ :return: A bzrlib.lock.LogicalLockResult.
+ """
+ raise NotImplementedError(self.lock_write)
+
+ def get_physical_lock_status(self):
+ raise NotImplementedError(self.get_physical_lock_status)
+
+ def set_last_revision(self, new_revision):
+ """Change the last revision in the working tree."""
+ raise NotImplementedError(self.set_last_revision)
+
+ def _change_last_revision(self, new_revision):
+ """Template method part of set_last_revision to perform the change.
+
+ This is used to allow WorkingTree3 instances to not affect branch
+ when their last revision is set.
+ """
+ if _mod_revision.is_null(new_revision):
+ self.branch.set_last_revision_info(0, new_revision)
+ return False
+ _mod_revision.check_not_reserved_id(new_revision)
+ try:
+ self.branch.generate_revision_history(new_revision)
+ except errors.NoSuchRevision:
+ # not present in the repo - dont try to set it deeper than the tip
+ self.branch._set_revision_history([new_revision])
+ return True
+
+ @needs_tree_write_lock
+ def remove(self, files, verbose=False, to_file=None, keep_files=True,
+ force=False):
+ """Remove nominated files from the working tree metadata.
+
+ :files: File paths relative to the basedir.
+ :keep_files: If true, the files will also be kept.
+ :force: Delete files and directories, even if they are changed and
+ even if the directories are not empty.
+ """
+ if isinstance(files, basestring):
+ files = [files]
+
+ inv_delta = []
+
+ all_files = set() # specified and nested files
+ unknown_nested_files=set()
+ if to_file is None:
+ to_file = sys.stdout
+
+ files_to_backup = []
+
+ def recurse_directory_to_add_files(directory):
+ # Recurse directory and add all files
+ # so we can check if they have changed.
+ for parent_info, file_infos in self.walkdirs(directory):
+ for relpath, basename, kind, lstat, fileid, kind in file_infos:
+ # Is it versioned or ignored?
+ if self.path2id(relpath):
+ # Add nested content for deletion.
+ all_files.add(relpath)
+ else:
+ # Files which are not versioned
+ # should be treated as unknown.
+ files_to_backup.append(relpath)
+
+ for filename in files:
+ # Get file name into canonical form.
+ abspath = self.abspath(filename)
+ filename = self.relpath(abspath)
+ if len(filename) > 0:
+ all_files.add(filename)
+ recurse_directory_to_add_files(filename)
+
+ files = list(all_files)
+
+ if len(files) == 0:
+ return # nothing to do
+
+ # Sort needed to first handle directory content before the directory
+ files.sort(reverse=True)
+
+ # Bail out if we are going to delete files we shouldn't
+ if not keep_files and not force:
+ for (file_id, path, content_change, versioned, parent_id, name,
+ kind, executable) in self.iter_changes(self.basis_tree(),
+ include_unchanged=True, require_versioned=False,
+ want_unversioned=True, specific_files=files):
+ if versioned[0] == False:
+ # The record is unknown or newly added
+ files_to_backup.append(path[1])
+ elif (content_change and (kind[1] is not None) and
+ osutils.is_inside_any(files, path[1])):
+ # Versioned and changed, but not deleted, and still
+ # in one of the dirs to be deleted.
+ files_to_backup.append(path[1])
+
+ def backup(file_to_backup):
+ backup_name = self.bzrdir._available_backup_name(file_to_backup)
+ osutils.rename(abs_path, self.abspath(backup_name))
+ return "removed %s (but kept a copy: %s)" % (file_to_backup,
+ backup_name)
+
+ # Build inv_delta and delete files where applicable,
+ # do this before any modifications to meta data.
+ for f in files:
+ fid = self.path2id(f)
+ message = None
+ if not fid:
+ message = "%s is not versioned." % (f,)
+ else:
+ if verbose:
+ # having removed it, it must be either ignored or unknown
+ if self.is_ignored(f):
+ new_status = 'I'
+ else:
+ new_status = '?'
+ # XXX: Really should be a more abstract reporter interface
+ kind_ch = osutils.kind_marker(self.kind(fid))
+ to_file.write(new_status + ' ' + f + kind_ch + '\n')
+ # Unversion file
+ inv_delta.append((f, None, fid, None))
+ message = "removed %s" % (f,)
+
+ if not keep_files:
+ abs_path = self.abspath(f)
+ if osutils.lexists(abs_path):
+ if (osutils.isdir(abs_path) and
+ len(os.listdir(abs_path)) > 0):
+ if force:
+ osutils.rmtree(abs_path)
+ message = "deleted %s" % (f,)
+ else:
+ message = backup(f)
+ else:
+ if f in files_to_backup:
+ message = backup(f)
+ else:
+ osutils.delete_any(abs_path)
+ message = "deleted %s" % (f,)
+ elif message is not None:
+ # Only care if we haven't done anything yet.
+ message = "%s does not exist." % (f,)
+
+ # Print only one message (if any) per file.
+ if message is not None:
+ note(message)
+ self.apply_inventory_delta(inv_delta)
+
+ @needs_tree_write_lock
+ def revert(self, filenames=None, old_tree=None, backups=True,
+ pb=None, report_changes=False):
+ from bzrlib.conflicts import resolve
+ if old_tree is None:
+ basis_tree = self.basis_tree()
+ basis_tree.lock_read()
+ old_tree = basis_tree
+ else:
+ basis_tree = None
+ try:
+ conflicts = transform.revert(self, old_tree, filenames, backups, pb,
+ report_changes)
+ if filenames is None and len(self.get_parent_ids()) > 1:
+ parent_trees = []
+ last_revision = self.last_revision()
+ if last_revision != _mod_revision.NULL_REVISION:
+ if basis_tree is None:
+ basis_tree = self.basis_tree()
+ basis_tree.lock_read()
+ parent_trees.append((last_revision, basis_tree))
+ self.set_parent_trees(parent_trees)
+ resolve(self)
+ else:
+ resolve(self, filenames, ignore_misses=True, recursive=True)
+ finally:
+ if basis_tree is not None:
+ basis_tree.unlock()
+ return conflicts
+
+ def revision_tree(self, revision_id):
+ """See Tree.revision_tree.
+
+ WorkingTree can supply revision_trees for the basis revision only
+ because there is only one cached inventory in the bzr directory.
+ """
+ raise NotImplementedError(self.revision_tree)
+
+ @needs_tree_write_lock
+ def set_root_id(self, file_id):
+ """Set the root id for this tree."""
+ # for compatability
+ if file_id is None:
+ raise ValueError(
+ 'WorkingTree.set_root_id with fileid=None')
+ file_id = osutils.safe_file_id(file_id)
+ self._set_root_id(file_id)
+
+ def _set_root_id(self, file_id):
+ """Set the root id for this tree, in a format specific manner.
+
+ :param file_id: The file id to assign to the root. It must not be
+ present in the current inventory or an error will occur. It must
+ not be None, but rather a valid file id.
+ """
+ raise NotImplementedError(self._set_root_id)
+
+ def unlock(self):
+ """See Branch.unlock.
+
+ WorkingTree locking just uses the Branch locking facilities.
+ This is current because all working trees have an embedded branch
+ within them. IF in the future, we were to make branch data shareable
+ between multiple working trees, i.e. via shared storage, then we
+ would probably want to lock both the local tree, and the branch.
+ """
+ raise NotImplementedError(self.unlock)
+
+ _marker = object()
+
+ def update(self, change_reporter=None, possible_transports=None,
+ revision=None, old_tip=_marker, show_base=False):
+ """Update a working tree along its branch.
+
+ This will update the branch if its bound too, which means we have
+ multiple trees involved:
+
+ - The new basis tree of the master.
+ - The old basis tree of the branch.
+ - The old basis tree of the working tree.
+ - The current working tree state.
+
+ Pathologically, all three may be different, and non-ancestors of each
+ other. Conceptually we want to:
+
+ - Preserve the wt.basis->wt.state changes
+ - Transform the wt.basis to the new master basis.
+ - Apply a merge of the old branch basis to get any 'local' changes from
+ it into the tree.
+ - Restore the wt.basis->wt.state changes.
+
+ There isn't a single operation at the moment to do that, so we:
+
+ - Merge current state -> basis tree of the master w.r.t. the old tree
+ basis.
+ - Do a 'normal' merge of the old branch basis if it is relevant.
+
+ :param revision: The target revision to update to. Must be in the
+ revision history.
+ :param old_tip: If branch.update() has already been run, the value it
+ returned (old tip of the branch or None). _marker is used
+ otherwise.
+ """
+ if self.branch.get_bound_location() is not None:
+ self.lock_write()
+ update_branch = (old_tip is self._marker)
+ else:
+ self.lock_tree_write()
+ update_branch = False
+ try:
+ if update_branch:
+ old_tip = self.branch.update(possible_transports)
+ else:
+ if old_tip is self._marker:
+ old_tip = None
+ return self._update_tree(old_tip, change_reporter, revision, show_base)
+ finally:
+ self.unlock()
+
+ @needs_tree_write_lock
+ def _update_tree(self, old_tip=None, change_reporter=None, revision=None,
+ show_base=False):
+ """Update a tree to the master branch.
+
+ :param old_tip: if supplied, the previous tip revision the branch,
+ before it was changed to the master branch's tip.
+ """
+ # here if old_tip is not None, it is the old tip of the branch before
+ # it was updated from the master branch. This should become a pending
+ # merge in the working tree to preserve the user existing work. we
+ # cant set that until we update the working trees last revision to be
+ # one from the new branch, because it will just get absorbed by the
+ # parent de-duplication logic.
+ #
+ # We MUST save it even if an error occurs, because otherwise the users
+ # local work is unreferenced and will appear to have been lost.
+ #
+ nb_conflicts = 0
+ try:
+ last_rev = self.get_parent_ids()[0]
+ except IndexError:
+ last_rev = _mod_revision.NULL_REVISION
+ if revision is None:
+ revision = self.branch.last_revision()
+
+ old_tip = old_tip or _mod_revision.NULL_REVISION
+
+ if not _mod_revision.is_null(old_tip) and old_tip != last_rev:
+ # the branch we are bound to was updated
+ # merge those changes in first
+ base_tree = self.basis_tree()
+ other_tree = self.branch.repository.revision_tree(old_tip)
+ nb_conflicts = merge.merge_inner(self.branch, other_tree,
+ base_tree, this_tree=self,
+ change_reporter=change_reporter,
+ show_base=show_base)
+ if nb_conflicts:
+ self.add_parent_tree((old_tip, other_tree))
+ note(gettext('Rerun update after fixing the conflicts.'))
+ return nb_conflicts
+
+ if last_rev != _mod_revision.ensure_null(revision):
+ # the working tree is up to date with the branch
+ # we can merge the specified revision from master
+ to_tree = self.branch.repository.revision_tree(revision)
+ to_root_id = to_tree.get_root_id()
+
+ basis = self.basis_tree()
+ basis.lock_read()
+ try:
+ if (basis.get_root_id() is None or basis.get_root_id() != to_root_id):
+ self.set_root_id(to_root_id)
+ self.flush()
+ finally:
+ basis.unlock()
+
+ # determine the branch point
+ graph = self.branch.repository.get_graph()
+ base_rev_id = graph.find_unique_lca(self.branch.last_revision(),
+ last_rev)
+ base_tree = self.branch.repository.revision_tree(base_rev_id)
+
+ nb_conflicts = merge.merge_inner(self.branch, to_tree, base_tree,
+ this_tree=self,
+ change_reporter=change_reporter,
+ show_base=show_base)
+ self.set_last_revision(revision)
+ # TODO - dedup parents list with things merged by pull ?
+ # reuse the tree we've updated to to set the basis:
+ parent_trees = [(revision, to_tree)]
+ merges = self.get_parent_ids()[1:]
+ # Ideally we ask the tree for the trees here, that way the working
+ # tree can decide whether to give us the entire tree or give us a
+ # lazy initialised tree. dirstate for instance will have the trees
+ # in ram already, whereas a last-revision + basis-inventory tree
+ # will not, but also does not need them when setting parents.
+ for parent in merges:
+ parent_trees.append(
+ (parent, self.branch.repository.revision_tree(parent)))
+ if not _mod_revision.is_null(old_tip):
+ parent_trees.append(
+ (old_tip, self.branch.repository.revision_tree(old_tip)))
+ self.set_parent_trees(parent_trees)
+ last_rev = parent_trees[0][0]
+ return nb_conflicts
+
+ def set_conflicts(self, arg):
+ raise errors.UnsupportedOperation(self.set_conflicts, self)
+
+ def add_conflicts(self, arg):
+ raise errors.UnsupportedOperation(self.add_conflicts, self)
+
+ def conflicts(self):
+ raise NotImplementedError(self.conflicts)
+
+ def walkdirs(self, prefix=""):
+ """Walk the directories of this tree.
+
+ returns a generator which yields items in the form:
+ ((curren_directory_path, fileid),
+ [(file1_path, file1_name, file1_kind, (lstat), file1_id,
+ file1_kind), ... ])
+
+ This API returns a generator, which is only valid during the current
+ tree transaction - within a single lock_read or lock_write duration.
+
+ If the tree is not locked, it may cause an error to be raised,
+ depending on the tree implementation.
+ """
+ disk_top = self.abspath(prefix)
+ if disk_top.endswith('/'):
+ disk_top = disk_top[:-1]
+ top_strip_len = len(disk_top) + 1
+ inventory_iterator = self._walkdirs(prefix)
+ disk_iterator = osutils.walkdirs(disk_top, prefix)
+ try:
+ current_disk = disk_iterator.next()
+ disk_finished = False
+ except OSError, e:
+ if not (e.errno == errno.ENOENT or
+ (sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
+ raise
+ current_disk = None
+ disk_finished = True
+ try:
+ current_inv = inventory_iterator.next()
+ inv_finished = False
+ except StopIteration:
+ current_inv = None
+ inv_finished = True
+ while not inv_finished or not disk_finished:
+ if current_disk:
+ ((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
+ cur_disk_dir_content) = current_disk
+ else:
+ ((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
+ cur_disk_dir_content) = ((None, None), None)
+ if not disk_finished:
+ # strip out .bzr dirs
+ if (cur_disk_dir_path_from_top[top_strip_len:] == '' and
+ len(cur_disk_dir_content) > 0):
+ # osutils.walkdirs can be made nicer -
+ # yield the path-from-prefix rather than the pathjoined
+ # value.
+ bzrdir_loc = bisect_left(cur_disk_dir_content,
+ ('.bzr', '.bzr'))
+ if (bzrdir_loc < len(cur_disk_dir_content)
+ and self.bzrdir.is_control_filename(
+ cur_disk_dir_content[bzrdir_loc][0])):
+ # we dont yield the contents of, or, .bzr itself.
+ del cur_disk_dir_content[bzrdir_loc]
+ if inv_finished:
+ # everything is unknown
+ direction = 1
+ elif disk_finished:
+ # everything is missing
+ direction = -1
+ else:
+ direction = cmp(current_inv[0][0], cur_disk_dir_relpath)
+ if direction > 0:
+ # disk is before inventory - unknown
+ dirblock = [(relpath, basename, kind, stat, None, None) for
+ relpath, basename, kind, stat, top_path in
+ cur_disk_dir_content]
+ yield (cur_disk_dir_relpath, None), dirblock
+ try:
+ current_disk = disk_iterator.next()
+ except StopIteration:
+ disk_finished = True
+ elif direction < 0:
+ # inventory is before disk - missing.
+ dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
+ for relpath, basename, dkind, stat, fileid, kind in
+ current_inv[1]]
+ yield (current_inv[0][0], current_inv[0][1]), dirblock
+ try:
+ current_inv = inventory_iterator.next()
+ except StopIteration:
+ inv_finished = True
+ else:
+ # versioned present directory
+ # merge the inventory and disk data together
+ dirblock = []
+ for relpath, subiterator in itertools.groupby(sorted(
+ current_inv[1] + cur_disk_dir_content,
+ key=operator.itemgetter(0)), operator.itemgetter(1)):
+ path_elements = list(subiterator)
+ if len(path_elements) == 2:
+ inv_row, disk_row = path_elements
+ # versioned, present file
+ dirblock.append((inv_row[0],
+ inv_row[1], disk_row[2],
+ disk_row[3], inv_row[4],
+ inv_row[5]))
+ elif len(path_elements[0]) == 5:
+ # unknown disk file
+ dirblock.append((path_elements[0][0],
+ path_elements[0][1], path_elements[0][2],
+ path_elements[0][3], None, None))
+ elif len(path_elements[0]) == 6:
+ # versioned, absent file.
+ dirblock.append((path_elements[0][0],
+ path_elements[0][1], 'unknown', None,
+ path_elements[0][4], path_elements[0][5]))
+ else:
+ raise NotImplementedError('unreachable code')
+ yield current_inv[0], dirblock
+ try:
+ current_inv = inventory_iterator.next()
+ except StopIteration:
+ inv_finished = True
+ try:
+ current_disk = disk_iterator.next()
+ except StopIteration:
+ disk_finished = True
+
+ def _walkdirs(self, prefix=""):
+ """Walk the directories of this tree.
+
+ :param prefix: is used as the directrory to start with.
+ :returns: a generator which yields items in the form::
+
+ ((curren_directory_path, fileid),
+ [(file1_path, file1_name, file1_kind, None, file1_id,
+ file1_kind), ... ])
+ """
+ raise NotImplementedError(self._walkdirs)
+
+ @needs_tree_write_lock
+ def auto_resolve(self):
+ """Automatically resolve text conflicts according to contents.
+
+ Only text conflicts are auto_resolvable. Files with no conflict markers
+ are considered 'resolved', because bzr always puts conflict markers
+ into files that have text conflicts. The corresponding .THIS .BASE and
+ .OTHER files are deleted, as per 'resolve'.
+
+ :return: a tuple of ConflictLists: (un_resolved, resolved).
+ """
+ un_resolved = _mod_conflicts.ConflictList()
+ resolved = _mod_conflicts.ConflictList()
+ conflict_re = re.compile('^(<{7}|={7}|>{7})')
+ for conflict in self.conflicts():
+ if (conflict.typestring != 'text conflict' or
+ self.kind(conflict.file_id) != 'file'):
+ un_resolved.append(conflict)
+ continue
+ my_file = open(self.id2abspath(conflict.file_id), 'rb')
+ try:
+ for line in my_file:
+ if conflict_re.search(line):
+ un_resolved.append(conflict)
+ break
+ else:
+ resolved.append(conflict)
+ finally:
+ my_file.close()
+ resolved.remove_files(self)
+ self.set_conflicts(un_resolved)
+ return un_resolved, resolved
+
+ def _validate(self):
+ """Validate internal structures.
+
+ This is meant mostly for the test suite. To give it a chance to detect
+ corruption after actions have occurred. The default implementation is a
+ just a no-op.
+
+ :return: None. An exception should be raised if there is an error.
+ """
+ return
+
+ def check_state(self):
+ """Check that the working state is/isn't valid."""
+ raise NotImplementedError(self.check_state)
+
+ def reset_state(self, revision_ids=None):
+ """Reset the state of the working tree.
+
+ This does a hard-reset to a last-known-good state. This is a way to
+ fix if something got corrupted (like the .bzr/checkout/dirstate file)
+ """
+ raise NotImplementedError(self.reset_state)
+
+ def _get_rules_searcher(self, default_searcher):
+ """See Tree._get_rules_searcher."""
+ if self._rules_searcher is None:
+ self._rules_searcher = super(WorkingTree,
+ self)._get_rules_searcher(default_searcher)
+ return self._rules_searcher
+
+ def get_shelf_manager(self):
+ """Return the ShelfManager for this WorkingTree."""
+ from bzrlib.shelf import ShelfManager
+ return ShelfManager(self, self._transport)
+
+
+class InventoryWorkingTree(WorkingTree,
+ bzrlib.mutabletree.MutableInventoryTree):
+ """Base class for working trees that are inventory-oriented.
+
+ The inventory is held in the `Branch` working-inventory, and the
+ files are in a directory on disk.
+
+ It is possible for a `WorkingTree` to have a filename which is
+ not listed in the Inventory and vice versa.
+ """
+
+ def __init__(self, basedir='.',
+ branch=DEPRECATED_PARAMETER,
+ _inventory=None,
+ _control_files=None,
+ _internal=False,
+ _format=None,
+ _bzrdir=None):
+ """Construct a InventoryWorkingTree instance. This is not a public API.
+
+ :param branch: A branch to override probing for the branch.
+ """
+ super(InventoryWorkingTree, self).__init__(basedir=basedir,
+ branch=branch, _transport=_control_files._transport,
+ _internal=_internal, _format=_format, _bzrdir=_bzrdir)
+
+ self._control_files = _control_files
+ self._detect_case_handling()
+
+ if _inventory is None:
+ # This will be acquired on lock_read() or lock_write()
+ self._inventory_is_modified = False
+ self._inventory = None
+ else:
+ # the caller of __init__ has provided an inventory,
+ # we assume they know what they are doing - as its only
+ # the Format factory and creation methods that are
+ # permitted to do this.
+ self._set_inventory(_inventory, dirty=False)
+
+ def _set_inventory(self, inv, dirty):
+ """Set the internal cached inventory.
+
+ :param inv: The inventory to set.
+ :param dirty: A boolean indicating whether the inventory is the same
+ logical inventory as whats on disk. If True the inventory is not
+ the same and should be written to disk or data will be lost, if
+ False then the inventory is the same as that on disk and any
+ serialisation would be unneeded overhead.
+ """
+ self._inventory = inv
+ self._inventory_is_modified = dirty
+
+ def _detect_case_handling(self):
+ wt_trans = self.bzrdir.get_workingtree_transport(None)
+ try:
+ wt_trans.stat(self._format.case_sensitive_filename)
+ except errors.NoSuchFile:
+ self.case_sensitive = True
+ else:
+ self.case_sensitive = False
+
+ self._setup_directory_is_tree_reference()
+
+ def _serialize(self, inventory, out_file):
+ xml5.serializer_v5.write_inventory(self._inventory, out_file,
+ working=True)
+
+ def _deserialize(selt, in_file):
+ return xml5.serializer_v5.read_inventory(in_file)
+
+ def break_lock(self):
+ """Break a lock if one is present from another instance.
+
+ Uses the ui factory to ask for confirmation if the lock may be from
+ an active process.
+
+ This will probe the repository for its lock as well.
+ """
+ self._control_files.break_lock()
+ self.branch.break_lock()
+
+ def is_locked(self):
+ return self._control_files.is_locked()
+
+ def _must_be_locked(self):
+ if not self.is_locked():
+ raise errors.ObjectNotLocked(self)
+
+ def lock_read(self):
+ """Lock the tree for reading.
+
+ This also locks the branch, and can be unlocked via self.unlock().
+
+ :return: A bzrlib.lock.LogicalLockResult.
+ """
+ if not self.is_locked():
+ self._reset_data()
+ self.branch.lock_read()
+ try:
+ self._control_files.lock_read()
+ return LogicalLockResult(self.unlock)
+ except:
+ self.branch.unlock()
+ raise
+
+ def lock_tree_write(self):
+ """See MutableTree.lock_tree_write, and WorkingTree.unlock.
+
+ :return: A bzrlib.lock.LogicalLockResult.
+ """
+ if not self.is_locked():
+ self._reset_data()
+ self.branch.lock_read()
+ try:
+ self._control_files.lock_write()
+ return LogicalLockResult(self.unlock)
+ except:
+ self.branch.unlock()
+ raise
+
+ def lock_write(self):
+ """See MutableTree.lock_write, and WorkingTree.unlock.
+
+ :return: A bzrlib.lock.LogicalLockResult.
+ """
+ if not self.is_locked():
+ self._reset_data()
+ self.branch.lock_write()
+ try:
+ self._control_files.lock_write()
+ return LogicalLockResult(self.unlock)
+ except:
+ self.branch.unlock()
+ raise
+
+ def get_physical_lock_status(self):
+ return self._control_files.get_physical_lock_status()
+
+ @needs_tree_write_lock
+ def _write_inventory(self, inv):
+ """Write inventory as the current inventory."""
+ self._set_inventory(inv, dirty=True)
+ self.flush()
+
+ # XXX: This method should be deprecated in favour of taking in a proper
+ # new Inventory object.
+ @needs_tree_write_lock
+ def set_inventory(self, new_inventory_list):
+ from bzrlib.inventory import (Inventory,
+ InventoryDirectory,
+ InventoryFile,
+ InventoryLink)
+ inv = Inventory(self.get_root_id())
+ for path, file_id, parent, kind in new_inventory_list:
+ name = os.path.basename(path)
+ if name == "":
+ continue
+ # fixme, there should be a factory function inv,add_??
+ if kind == 'directory':
+ inv.add(InventoryDirectory(file_id, name, parent))
+ elif kind == 'file':
+ inv.add(InventoryFile(file_id, name, parent))
+ elif kind == 'symlink':
+ inv.add(InventoryLink(file_id, name, parent))
+ else:
+ raise errors.BzrError("unknown kind %r" % kind)
+ self._write_inventory(inv)
+
+ def _write_basis_inventory(self, xml):
+ """Write the basis inventory XML to the basis-inventory file"""
+ path = self._basis_inventory_name()
+ sio = StringIO(xml)
+ self._transport.put_file(path, sio,
+ mode=self.bzrdir._get_file_mode())
+
+ def _reset_data(self):
+ """Reset transient data that cannot be revalidated."""
+ self._inventory_is_modified = False
+ f = self._transport.get('inventory')
+ try:
+ result = self._deserialize(f)
+ finally:
+ f.close()
+ self._set_inventory(result, dirty=False)
+
+ def _set_root_id(self, file_id):
+ """Set the root id for this tree, in a format specific manner.
+
+ :param file_id: The file id to assign to the root. It must not be
+ present in the current inventory or an error will occur. It must
+ not be None, but rather a valid file id.
+ """
+ inv = self._inventory
+ orig_root_id = inv.root.file_id
+ # TODO: it might be nice to exit early if there was nothing
+ # to do, saving us from trigger a sync on unlock.
+ self._inventory_is_modified = True
+ # we preserve the root inventory entry object, but
+ # unlinkit from the byid index
+ del inv._byid[inv.root.file_id]
+ inv.root.file_id = file_id
+ # and link it into the index with the new changed id.
+ inv._byid[inv.root.file_id] = inv.root
+ # and finally update all children to reference the new id.
+ # XXX: this should be safe to just look at the root.children
+ # list, not the WHOLE INVENTORY.
+ for fid in inv:
+ entry = inv[fid]
+ if entry.parent_id == orig_root_id:
+ entry.parent_id = inv.root.file_id
+
+ @needs_tree_write_lock
+ def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
+ """See MutableTree.set_parent_trees."""
+ parent_ids = [rev for (rev, tree) in parents_list]
+ for revision_id in parent_ids:
+ _mod_revision.check_not_reserved_id(revision_id)
+
+ self._check_parents_for_ghosts(parent_ids,
+ allow_leftmost_as_ghost=allow_leftmost_as_ghost)
+
+ parent_ids = self._filter_parent_ids_by_ancestry(parent_ids)
+
+ if len(parent_ids) == 0:
+ leftmost_parent_id = _mod_revision.NULL_REVISION
+ leftmost_parent_tree = None
+ else:
+ leftmost_parent_id, leftmost_parent_tree = parents_list[0]
+
+ if self._change_last_revision(leftmost_parent_id):
+ if leftmost_parent_tree is None:
+ # If we don't have a tree, fall back to reading the
+ # parent tree from the repository.
+ self._cache_basis_inventory(leftmost_parent_id)
+ else:
+ inv = leftmost_parent_tree.root_inventory
+ xml = self._create_basis_xml_from_inventory(
+ leftmost_parent_id, inv)
+ self._write_basis_inventory(xml)
+ self._set_merges_from_parent_ids(parent_ids)
+
+ def _cache_basis_inventory(self, new_revision):
+ """Cache new_revision as the basis inventory."""
+ # TODO: this should allow the ready-to-use inventory to be passed in,
+ # as commit already has that ready-to-use [while the format is the
+ # same, that is].
+ try:
+ # this double handles the inventory - unpack and repack -
+ # but is easier to understand. We can/should put a conditional
+ # in here based on whether the inventory is in the latest format
+ # - perhaps we should repack all inventories on a repository
+ # upgrade ?
+ # the fast path is to copy the raw xml from the repository. If the
+ # xml contains 'revision_id="', then we assume the right
+ # revision_id is set. We must check for this full string, because a
+ # root node id can legitimately look like 'revision_id' but cannot
+ # contain a '"'.
+ xml = self.branch.repository._get_inventory_xml(new_revision)
+ firstline = xml.split('\n', 1)[0]
+ if (not 'revision_id="' in firstline or
+ 'format="7"' not in firstline):
+ inv = self.branch.repository._serializer.read_inventory_from_string(
+ xml, new_revision)
+ xml = self._create_basis_xml_from_inventory(new_revision, inv)
+ self._write_basis_inventory(xml)
+ except (errors.NoSuchRevision, errors.RevisionNotPresent):
+ pass
+
+ def _basis_inventory_name(self):
+ return 'basis-inventory-cache'
+
+ def _create_basis_xml_from_inventory(self, revision_id, inventory):
+ """Create the text that will be saved in basis-inventory"""
+ inventory.revision_id = revision_id
+ return xml7.serializer_v7.write_inventory_to_string(inventory)
+
+ @needs_tree_write_lock
+ def set_conflicts(self, conflicts):
+ self._put_rio('conflicts', conflicts.to_stanzas(),
+ CONFLICT_HEADER_1)
+
+ @needs_tree_write_lock
+ def add_conflicts(self, new_conflicts):
+ conflict_set = set(self.conflicts())
+ conflict_set.update(set(list(new_conflicts)))
+ self.set_conflicts(_mod_conflicts.ConflictList(sorted(conflict_set,
+ key=_mod_conflicts.Conflict.sort_key)))
+
+ @needs_read_lock
+ def conflicts(self):
+ try:
+ confile = self._transport.get('conflicts')
+ except errors.NoSuchFile:
+ return _mod_conflicts.ConflictList()
+ try:
+ try:
+ if confile.next() != CONFLICT_HEADER_1 + '\n':
+ raise errors.ConflictFormatError()
+ except StopIteration:
+ raise errors.ConflictFormatError()
+ reader = _mod_rio.RioReader(confile)
+ return _mod_conflicts.ConflictList.from_stanzas(reader)
+ finally:
+ confile.close()
+
+ def read_basis_inventory(self):
+ """Read the cached basis inventory."""
+ path = self._basis_inventory_name()
+ return self._transport.get_bytes(path)
+
+ @needs_read_lock
+ def read_working_inventory(self):
+ """Read the working inventory.
+
+ :raises errors.InventoryModified: read_working_inventory will fail
+ when the current in memory inventory has been modified.
+ """
+ # conceptually this should be an implementation detail of the tree.
+ # XXX: Deprecate this.
+ # ElementTree does its own conversion from UTF-8, so open in
+ # binary.
+ if self._inventory_is_modified:
+ raise errors.InventoryModified(self)
+ f = self._transport.get('inventory')
+ try:
+ result = self._deserialize(f)
+ finally:
+ f.close()
+ self._set_inventory(result, dirty=False)
+ return result
+
+ @needs_read_lock
+ def get_root_id(self):
+ """Return the id of this trees root"""
+ return self._inventory.root.file_id
+
+ def has_id(self, file_id):
+ # files that have been deleted are excluded
+ inv, inv_file_id = self._unpack_file_id(file_id)
+ if not inv.has_id(inv_file_id):
+ return False
+ path = inv.id2path(inv_file_id)
+ return osutils.lexists(self.abspath(path))
+
+ def has_or_had_id(self, file_id):
+ if file_id == self.get_root_id():
+ return True
+ inv, inv_file_id = self._unpack_file_id(file_id)
+ return inv.has_id(inv_file_id)
+
+ def all_file_ids(self):
+ """Iterate through file_ids for this tree.
+
+ file_ids are in a WorkingTree if they are in the working inventory
+ and the working file exists.
+ """
+ ret = set()
+ for path, ie in self.iter_entries_by_dir():
+ ret.add(ie.file_id)
+ return ret
+
+ @needs_tree_write_lock
+ def set_last_revision(self, new_revision):
+ """Change the last revision in the working tree."""
+ if self._change_last_revision(new_revision):
+ self._cache_basis_inventory(new_revision)
+
+ def _get_check_refs(self):
+ """Return the references needed to perform a check of this tree.
+
+ The default implementation returns no refs, and is only suitable for
+ trees that have no local caching and can commit on ghosts at any time.
+
+ :seealso: bzrlib.check for details about check_refs.
+ """
+ return []
+
+ @needs_read_lock
+ def _check(self, references):
+ """Check the tree for consistency.
+
+ :param references: A dict with keys matching the items returned by
+ self._get_check_refs(), and values from looking those keys up in
+ the repository.
+ """
+ tree_basis = self.basis_tree()
+ tree_basis.lock_read()
+ try:
+ repo_basis = references[('trees', self.last_revision())]
+ if len(list(repo_basis.iter_changes(tree_basis))) > 0:
+ raise errors.BzrCheckError(
+ "Mismatched basis inventory content.")
+ self._validate()
+ finally:
+ tree_basis.unlock()
+
+ @needs_read_lock
+ def check_state(self):
+ """Check that the working state is/isn't valid."""
+ check_refs = self._get_check_refs()
+ refs = {}
+ for ref in check_refs:
+ kind, value = ref
+ if kind == 'trees':
+ refs[ref] = self.branch.repository.revision_tree(value)
+ self._check(refs)
+
+ @needs_tree_write_lock
+ def reset_state(self, revision_ids=None):
+ """Reset the state of the working tree.
+
+ This does a hard-reset to a last-known-good state. This is a way to
+ fix if something got corrupted (like the .bzr/checkout/dirstate file)
+ """
+ if revision_ids is None:
+ revision_ids = self.get_parent_ids()
+ if not revision_ids:
+ rt = self.branch.repository.revision_tree(
+ _mod_revision.NULL_REVISION)
+ else:
+ rt = self.branch.repository.revision_tree(revision_ids[0])
+ self._write_inventory(rt.root_inventory)
+ self.set_parent_ids(revision_ids)
+
+ def flush(self):
+ """Write the in memory inventory to disk."""
+ # TODO: Maybe this should only write on dirty ?
+ if self._control_files._lock_mode != 'w':
+ raise errors.NotWriteLocked(self)
+ sio = StringIO()
+ self._serialize(self._inventory, sio)
+ sio.seek(0)
+ self._transport.put_file('inventory', sio,
+ mode=self.bzrdir._get_file_mode())
+ self._inventory_is_modified = False
+
+ def get_file_mtime(self, file_id, path=None):
+ """See Tree.get_file_mtime."""
+ if not path:
+ path = self.id2path(file_id)
+ try:
+ return os.lstat(self.abspath(path)).st_mtime
+ except OSError, e:
+ if e.errno == errno.ENOENT:
+ raise errors.FileTimestampUnavailable(path)
+ raise
+
+ def _is_executable_from_path_and_stat_from_basis(self, path, stat_result):
+ inv, file_id = self._path2inv_file_id(path)
+ if file_id is None:
+ # For unversioned files on win32, we just assume they are not
+ # executable
+ return False
+ return inv[file_id].executable
+
+ def _is_executable_from_path_and_stat_from_stat(self, path, stat_result):
+ mode = stat_result.st_mode
+ return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
+
+ def is_executable(self, file_id, path=None):
+ if not self._supports_executable():
+ inv, inv_file_id = self._unpack_file_id(file_id)
+ return inv[inv_file_id].executable
+ else:
+ if not path:
+ path = self.id2path(file_id)
+ mode = os.lstat(self.abspath(path)).st_mode
+ return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
+
+ def _is_executable_from_path_and_stat(self, path, stat_result):
+ if not self._supports_executable():
+ return self._is_executable_from_path_and_stat_from_basis(path, stat_result)
+ else:
+ return self._is_executable_from_path_and_stat_from_stat(path, stat_result)
+
+ @needs_tree_write_lock
+ def _add(self, files, ids, kinds):
+ """See MutableTree._add."""
+ # TODO: Re-adding a file that is removed in the working copy
+ # should probably put it back with the previous ID.
+ # the read and write working inventory should not occur in this
+ # function - they should be part of lock_write and unlock.
+ # FIXME: nested trees
+ inv = self.root_inventory
+ for f, file_id, kind in zip(files, ids, kinds):
+ if file_id is None:
+ inv.add_path(f, kind=kind)
+ else:
+ inv.add_path(f, kind=kind, file_id=file_id)
+ self._inventory_is_modified = True
+
+ def revision_tree(self, revision_id):
+ """See WorkingTree.revision_id."""
+ if revision_id == self.last_revision():
+ try:
+ xml = self.read_basis_inventory()
+ except errors.NoSuchFile:
+ pass
+ else:
+ try:
+ inv = xml7.serializer_v7.read_inventory_from_string(xml)
+ # dont use the repository revision_tree api because we want
+ # to supply the inventory.
+ if inv.revision_id == revision_id:
+ return revisiontree.InventoryRevisionTree(
+ self.branch.repository, inv, revision_id)
+ except errors.BadInventoryFormat:
+ pass
+ # raise if there was no inventory, or if we read the wrong inventory.
+ raise errors.NoSuchRevisionInTree(self, revision_id)
+
+ @needs_read_lock
+ def annotate_iter(self, file_id, default_revision=CURRENT_REVISION):
+ """See Tree.annotate_iter
+
+ This implementation will use the basis tree implementation if possible.
+ Lines not in the basis are attributed to CURRENT_REVISION
+
+ If there are pending merges, lines added by those merges will be
+ incorrectly attributed to CURRENT_REVISION (but after committing, the
+ attribution will be correct).
+ """
+ maybe_file_parent_keys = []
+ for parent_id in self.get_parent_ids():
+ try:
+ parent_tree = self.revision_tree(parent_id)
+ except errors.NoSuchRevisionInTree:
+ parent_tree = self.branch.repository.revision_tree(parent_id)
+ parent_tree.lock_read()
+ try:
+ try:
+ kind = parent_tree.kind(file_id)
+ except errors.NoSuchId:
+ continue
+ if kind != 'file':
+ # Note: this is slightly unnecessary, because symlinks and
+ # directories have a "text" which is the empty text, and we
+ # know that won't mess up annotations. But it seems cleaner
+ continue
+ parent_text_key = (
+ file_id, parent_tree.get_file_revision(file_id))
+ if parent_text_key not in maybe_file_parent_keys:
+ maybe_file_parent_keys.append(parent_text_key)
+ finally:
+ parent_tree.unlock()
+ graph = _mod_graph.Graph(self.branch.repository.texts)
+ heads = graph.heads(maybe_file_parent_keys)
+ file_parent_keys = []
+ for key in maybe_file_parent_keys:
+ if key in heads:
+ file_parent_keys.append(key)
+
+ # Now we have the parents of this content
+ annotator = self.branch.repository.texts.get_annotator()
+ text = self.get_file_text(file_id)
+ this_key =(file_id, default_revision)
+ annotator.add_special_text(this_key, file_parent_keys, text)
+ annotations = [(key[-1], line)
+ for key, line in annotator.annotate_flat(this_key)]
+ return annotations
+
+ def _put_rio(self, filename, stanzas, header):
+ self._must_be_locked()
+ my_file = _mod_rio.rio_file(stanzas, header)
+ self._transport.put_file(filename, my_file,
+ mode=self.bzrdir._get_file_mode())
+
+ @needs_tree_write_lock
+ def set_merge_modified(self, modified_hashes):
+ def iter_stanzas():
+ for file_id, hash in modified_hashes.iteritems():
+ yield _mod_rio.Stanza(file_id=file_id.decode('utf8'),
+ hash=hash)
+ self._put_rio('merge-hashes', iter_stanzas(), MERGE_MODIFIED_HEADER_1)
+
+ @needs_read_lock
+ def merge_modified(self):
+ """Return a dictionary of files modified by a merge.
+
+ The list is initialized by WorkingTree.set_merge_modified, which is
+ typically called after we make some automatic updates to the tree
+ because of a merge.
+
+ This returns a map of file_id->sha1, containing only files which are
+ still in the working inventory and have that text hash.
+ """
+ try:
+ hashfile = self._transport.get('merge-hashes')
+ except errors.NoSuchFile:
+ return {}
+ try:
+ merge_hashes = {}
+ try:
+ if hashfile.next() != MERGE_MODIFIED_HEADER_1 + '\n':
+ raise errors.MergeModifiedFormatError()
+ except StopIteration:
+ raise errors.MergeModifiedFormatError()
+ for s in _mod_rio.RioReader(hashfile):
+ # RioReader reads in Unicode, so convert file_ids back to utf8
+ file_id = osutils.safe_file_id(s.get("file_id"), warn=False)
+ if not self.has_id(file_id):
+ continue
+ text_hash = s.get("hash")
+ if text_hash == self.get_file_sha1(file_id):
+ merge_hashes[file_id] = text_hash
+ return merge_hashes
+ finally:
+ hashfile.close()
+
+ @needs_write_lock
+ def subsume(self, other_tree):
+ def add_children(inventory, entry):
+ for child_entry in entry.children.values():
+ inventory._byid[child_entry.file_id] = child_entry
+ if child_entry.kind == 'directory':
+ add_children(inventory, child_entry)
+ if other_tree.get_root_id() == self.get_root_id():
+ raise errors.BadSubsumeSource(self, other_tree,
+ 'Trees have the same root')
+ try:
+ other_tree_path = self.relpath(other_tree.basedir)
+ except errors.PathNotChild:
+ raise errors.BadSubsumeSource(self, other_tree,
+ 'Tree is not contained by the other')
+ new_root_parent = self.path2id(osutils.dirname(other_tree_path))
+ if new_root_parent is None:
+ raise errors.BadSubsumeSource(self, other_tree,
+ 'Parent directory is not versioned.')
+ # We need to ensure that the result of a fetch will have a
+ # versionedfile for the other_tree root, and only fetching into
+ # RepositoryKnit2 guarantees that.
+ if not self.branch.repository.supports_rich_root():
+ raise errors.SubsumeTargetNeedsUpgrade(other_tree)
+ other_tree.lock_tree_write()
+ try:
+ new_parents = other_tree.get_parent_ids()
+ other_root = other_tree.root_inventory.root
+ other_root.parent_id = new_root_parent
+ other_root.name = osutils.basename(other_tree_path)
+ self.root_inventory.add(other_root)
+ add_children(self.root_inventory, other_root)
+ self._write_inventory(self.root_inventory)
+ # normally we don't want to fetch whole repositories, but i think
+ # here we really do want to consolidate the whole thing.
+ for parent_id in other_tree.get_parent_ids():
+ self.branch.fetch(other_tree.branch, parent_id)
+ self.add_parent_tree_id(parent_id)
+ finally:
+ other_tree.unlock()
+ other_tree.bzrdir.retire_bzrdir()
+
+ @needs_tree_write_lock
+ def extract(self, file_id, format=None):
+ """Extract a subtree from this tree.
+
+ A new branch will be created, relative to the path for this tree.
+ """
+ self.flush()
+ def mkdirs(path):
+ segments = osutils.splitpath(path)
+ transport = self.branch.bzrdir.root_transport
+ for name in segments:
+ transport = transport.clone(name)
+ transport.ensure_base()
+ return transport
+
+ sub_path = self.id2path(file_id)
+ branch_transport = mkdirs(sub_path)
+ if format is None:
+ format = self.bzrdir.cloning_metadir()
+ branch_transport.ensure_base()
+ branch_bzrdir = format.initialize_on_transport(branch_transport)
+ try:
+ repo = branch_bzrdir.find_repository()
+ except errors.NoRepositoryPresent:
+ repo = branch_bzrdir.create_repository()
+ if not repo.supports_rich_root():
+ raise errors.RootNotRich()
+ new_branch = branch_bzrdir.create_branch()
+ new_branch.pull(self.branch)
+ for parent_id in self.get_parent_ids():
+ new_branch.fetch(self.branch, parent_id)
+ tree_transport = self.bzrdir.root_transport.clone(sub_path)
+ if tree_transport.base != branch_transport.base:
+ tree_bzrdir = format.initialize_on_transport(tree_transport)
+ tree_bzrdir.set_branch_reference(new_branch)
+ else:
+ tree_bzrdir = branch_bzrdir
+ wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
+ wt.set_parent_ids(self.get_parent_ids())
+ # FIXME: Support nested trees
+ my_inv = self.root_inventory
+ child_inv = inventory.Inventory(root_id=None)
+ new_root = my_inv[file_id]
+ my_inv.remove_recursive_id(file_id)
+ new_root.parent_id = None
+ child_inv.add(new_root)
+ self._write_inventory(my_inv)
+ wt._write_inventory(child_inv)
+ return wt
+
+ def list_files(self, include_root=False, from_dir=None, recursive=True):
+ """List all files as (path, class, kind, id, entry).
+
+ Lists, but does not descend into unversioned directories.
+ This does not include files that have been deleted in this
+ tree. Skips the control directory.
+
+ :param include_root: if True, return an entry for the root
+ :param from_dir: start from this directory or None for the root
+ :param recursive: whether to recurse into subdirectories or not
+ """
+ # list_files is an iterator, so @needs_read_lock doesn't work properly
+ # with it. So callers should be careful to always read_lock the tree.
+ if not self.is_locked():
+ raise errors.ObjectNotLocked(self)
+
+ if from_dir is None and include_root is True:
+ yield ('', 'V', 'directory', self.get_root_id(), self.root_inventory.root)
+ # Convert these into local objects to save lookup times
+ pathjoin = osutils.pathjoin
+ file_kind = self._kind
+
+ # transport.base ends in a slash, we want the piece
+ # between the last two slashes
+ transport_base_dir = self.bzrdir.transport.base.rsplit('/', 2)[1]
+
+ fk_entries = {'directory':TreeDirectory, 'file':TreeFile, 'symlink':TreeLink}
+
+ # directory file_id, relative path, absolute path, reverse sorted children
+ if from_dir is not None:
+ inv, from_dir_id = self._path2inv_file_id(from_dir)
+ if from_dir_id is None:
+ # Directory not versioned
+ return
+ from_dir_abspath = pathjoin(self.basedir, from_dir)
+ else:
+ inv = self.root_inventory
+ from_dir_id = inv.root.file_id
+ from_dir_abspath = self.basedir
+ children = os.listdir(from_dir_abspath)
+ children.sort()
+ # jam 20060527 The kernel sized tree seems equivalent whether we
+ # use a deque and popleft to keep them sorted, or if we use a plain
+ # list and just reverse() them.
+ children = collections.deque(children)
+ stack = [(from_dir_id, u'', from_dir_abspath, children)]
+ while stack:
+ from_dir_id, from_dir_relpath, from_dir_abspath, children = stack[-1]
+
+ while children:
+ f = children.popleft()
+ ## TODO: If we find a subdirectory with its own .bzr
+ ## directory, then that is a separate tree and we
+ ## should exclude it.
+
+ # the bzrdir for this tree
+ if transport_base_dir == f:
+ continue
+
+ # we know that from_dir_relpath and from_dir_abspath never end in a slash
+ # and 'f' doesn't begin with one, we can do a string op, rather
+ # than the checks of pathjoin(), all relative paths will have an extra slash
+ # at the beginning
+ fp = from_dir_relpath + '/' + f
+
+ # absolute path
+ fap = from_dir_abspath + '/' + f
+
+ dir_ie = inv[from_dir_id]
+ if dir_ie.kind == 'directory':
+ f_ie = dir_ie.children.get(f)
+ else:
+ f_ie = None
+ if f_ie:
+ c = 'V'
+ elif self.is_ignored(fp[1:]):
+ c = 'I'
+ else:
+ # we may not have found this file, because of a unicode
+ # issue, or because the directory was actually a symlink.
+ f_norm, can_access = osutils.normalized_filename(f)
+ if f == f_norm or not can_access:
+ # No change, so treat this file normally
+ c = '?'
+ else:
+ # this file can be accessed by a normalized path
+ # check again if it is versioned
+ # these lines are repeated here for performance
+ f = f_norm
+ fp = from_dir_relpath + '/' + f
+ fap = from_dir_abspath + '/' + f
+ f_ie = inv.get_child(from_dir_id, f)
+ if f_ie:
+ c = 'V'
+ elif self.is_ignored(fp[1:]):
+ c = 'I'
+ else:
+ c = '?'
+
+ fk = file_kind(fap)
+
+ # make a last minute entry
+ if f_ie:
+ yield fp[1:], c, fk, f_ie.file_id, f_ie
+ else:
+ try:
+ yield fp[1:], c, fk, None, fk_entries[fk]()
+ except KeyError:
+ yield fp[1:], c, fk, None, TreeEntry()
+ continue
+
+ if fk != 'directory':
+ continue
+
+ # But do this child first if recursing down
+ if recursive:
+ new_children = os.listdir(fap)
+ new_children.sort()
+ new_children = collections.deque(new_children)
+ stack.append((f_ie.file_id, fp, fap, new_children))
+ # Break out of inner loop,
+ # so that we start outer loop with child
+ break
+ else:
+ # if we finished all children, pop it off the stack
+ stack.pop()
+
+ @needs_tree_write_lock
+ def move(self, from_paths, to_dir=None, after=False):
+ """Rename files.
+
+ to_dir must exist in the inventory.
+
+ If to_dir exists and is a directory, the files are moved into
+ it, keeping their old names.
+
+ Note that to_dir is only the last component of the new name;
+ this doesn't change the directory.
+
+ For each entry in from_paths the move mode will be determined
+ independently.
+
+ The first mode moves the file in the filesystem and updates the
+ inventory. The second mode only updates the inventory without
+ touching the file on the filesystem.
+
+ move uses the second mode if 'after == True' and the target is
+ either not versioned or newly added, and present in the working tree.
+
+ move uses the second mode if 'after == False' and the source is
+ versioned but no longer in the working tree, and the target is not
+ versioned but present in the working tree.
+
+ move uses the first mode if 'after == False' and the source is
+ versioned and present in the working tree, and the target is not
+ versioned and not present in the working tree.
+
+ Everything else results in an error.
+
+ This returns a list of (from_path, to_path) pairs for each
+ entry that is moved.
+ """
+ rename_entries = []
+ rename_tuples = []
+
+ invs_to_write = set()
+
+ # check for deprecated use of signature
+ if to_dir is None:
+ raise TypeError('You must supply a target directory')
+ # check destination directory
+ if isinstance(from_paths, basestring):
+ raise ValueError()
+ to_abs = self.abspath(to_dir)
+ if not isdir(to_abs):
+ raise errors.BzrMoveFailedError('',to_dir,
+ errors.NotADirectory(to_abs))
+ if not self.has_filename(to_dir):
+ raise errors.BzrMoveFailedError('',to_dir,
+ errors.NotInWorkingDirectory(to_dir))
+ to_inv, to_dir_id = self._path2inv_file_id(to_dir)
+ if to_dir_id is None:
+ raise errors.BzrMoveFailedError('',to_dir,
+ errors.NotVersionedError(path=to_dir))
+
+ to_dir_ie = to_inv[to_dir_id]
+ if to_dir_ie.kind != 'directory':
+ raise errors.BzrMoveFailedError('',to_dir,
+ errors.NotADirectory(to_abs))
+
+ # create rename entries and tuples
+ for from_rel in from_paths:
+ from_tail = splitpath(from_rel)[-1]
+ from_inv, from_id = self._path2inv_file_id(from_rel)
+ if from_id is None:
+ raise errors.BzrMoveFailedError(from_rel,to_dir,
+ errors.NotVersionedError(path=from_rel))
+
+ from_entry = from_inv[from_id]
+ from_parent_id = from_entry.parent_id
+ to_rel = pathjoin(to_dir, from_tail)
+ rename_entry = InventoryWorkingTree._RenameEntry(
+ from_rel=from_rel,
+ from_id=from_id,
+ from_tail=from_tail,
+ from_parent_id=from_parent_id,
+ to_rel=to_rel, to_tail=from_tail,
+ to_parent_id=to_dir_id)
+ rename_entries.append(rename_entry)
+ rename_tuples.append((from_rel, to_rel))
+
+ # determine which move mode to use. checks also for movability
+ rename_entries = self._determine_mv_mode(rename_entries, after)
+
+ original_modified = self._inventory_is_modified
+ try:
+ if len(from_paths):
+ self._inventory_is_modified = True
+ self._move(rename_entries)
+ except:
+ # restore the inventory on error
+ self._inventory_is_modified = original_modified
+ raise
+ #FIXME: Should potentially also write the from_invs
+ self._write_inventory(to_inv)
+ return rename_tuples
+
+ @needs_tree_write_lock
+ 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 not
+ versioned but 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.
+ """
+ rename_entries = []
+
+ # create rename entries and tuples
+ from_tail = splitpath(from_rel)[-1]
+ from_inv, from_id = self._path2inv_file_id(from_rel)
+ if from_id is None:
+ # if file is missing in the inventory maybe it's in the basis_tree
+ basis_tree = self.branch.basis_tree()
+ from_id = basis_tree.path2id(from_rel)
+ if from_id is None:
+ raise errors.BzrRenameFailedError(from_rel,to_rel,
+ errors.NotVersionedError(path=from_rel))
+ # put entry back in the inventory so we can rename it
+ from_entry = basis_tree.root_inventory[from_id].copy()
+ from_inv.add(from_entry)
+ else:
+ from_inv, from_inv_id = self._unpack_file_id(from_id)
+ from_entry = from_inv[from_inv_id]
+ from_parent_id = from_entry.parent_id
+ to_dir, to_tail = os.path.split(to_rel)
+ to_inv, to_dir_id = self._path2inv_file_id(to_dir)
+ rename_entry = InventoryWorkingTree._RenameEntry(from_rel=from_rel,
+ from_id=from_id,
+ from_tail=from_tail,
+ from_parent_id=from_parent_id,
+ to_rel=to_rel, to_tail=to_tail,
+ to_parent_id=to_dir_id)
+ rename_entries.append(rename_entry)
+
+ # determine which move mode to use. checks also for movability
+ rename_entries = self._determine_mv_mode(rename_entries, after)
+
+ # check if the target changed directory and if the target directory is
+ # versioned
+ if to_dir_id is None:
+ raise errors.BzrMoveFailedError(from_rel,to_rel,
+ errors.NotVersionedError(path=to_dir))
+
+ # all checks done. now we can continue with our actual work
+ mutter('rename_one:\n'
+ ' from_id {%s}\n'
+ ' from_rel: %r\n'
+ ' to_rel: %r\n'
+ ' to_dir %r\n'
+ ' to_dir_id {%s}\n',
+ from_id, from_rel, to_rel, to_dir, to_dir_id)
+
+ self._move(rename_entries)
+ self._write_inventory(to_inv)
+
+ class _RenameEntry(object):
+ def __init__(self, from_rel, from_id, from_tail, from_parent_id,
+ to_rel, to_tail, to_parent_id, only_change_inv=False,
+ change_id=False):
+ self.from_rel = from_rel
+ self.from_id = from_id
+ self.from_tail = from_tail
+ self.from_parent_id = from_parent_id
+ self.to_rel = to_rel
+ self.to_tail = to_tail
+ self.to_parent_id = to_parent_id
+ self.change_id = change_id
+ self.only_change_inv = only_change_inv
+
+ def _determine_mv_mode(self, rename_entries, after=False):
+ """Determines for each from-to pair if both inventory and working tree
+ or only the inventory has to be changed.
+
+ Also does basic plausability tests.
+ """
+ # FIXME: Handling of nested trees
+ inv = self.root_inventory
+
+ for rename_entry in rename_entries:
+ # store to local variables for easier reference
+ from_rel = rename_entry.from_rel
+ from_id = rename_entry.from_id
+ to_rel = rename_entry.to_rel
+ to_id = inv.path2id(to_rel)
+ only_change_inv = False
+ change_id = False
+
+ # check the inventory for source and destination
+ if from_id is None:
+ raise errors.BzrMoveFailedError(from_rel,to_rel,
+ errors.NotVersionedError(path=from_rel))
+ if to_id is not None:
+ allowed = False
+ # allow it with --after but only if dest is newly added
+ if after:
+ basis = self.basis_tree()
+ basis.lock_read()
+ try:
+ if not basis.has_id(to_id):
+ rename_entry.change_id = True
+ allowed = True
+ finally:
+ basis.unlock()
+ if not allowed:
+ raise errors.BzrMoveFailedError(from_rel,to_rel,
+ errors.AlreadyVersionedError(path=to_rel))
+
+ # try to determine the mode for rename (only change inv or change
+ # inv and file system)
+ if after:
+ if not self.has_filename(to_rel):
+ raise errors.BzrMoveFailedError(from_id,to_rel,
+ errors.NoSuchFile(path=to_rel,
+ extra="New file has not been created yet"))
+ only_change_inv = True
+ elif not self.has_filename(from_rel) and self.has_filename(to_rel):
+ only_change_inv = True
+ elif self.has_filename(from_rel) and not self.has_filename(to_rel):
+ only_change_inv = False
+ elif (not self.case_sensitive
+ and from_rel.lower() == to_rel.lower()
+ and self.has_filename(from_rel)):
+ only_change_inv = False
+ else:
+ # something is wrong, so lets determine what exactly
+ if not self.has_filename(from_rel) and \
+ not self.has_filename(to_rel):
+ raise errors.BzrRenameFailedError(from_rel, to_rel,
+ errors.PathsDoNotExist(paths=(from_rel, to_rel)))
+ else:
+ raise errors.RenameFailedFilesExist(from_rel, to_rel)
+ rename_entry.only_change_inv = only_change_inv
+ return rename_entries
+
+ def _move(self, rename_entries):
+ """Moves a list of files.
+
+ Depending on the value of the flag 'only_change_inv', the
+ file will be moved on the file system or not.
+ """
+ moved = []
+
+ for entry in rename_entries:
+ try:
+ self._move_entry(entry)
+ except:
+ self._rollback_move(moved)
+ raise
+ moved.append(entry)
+
+ def _rollback_move(self, moved):
+ """Try to rollback a previous move in case of an filesystem error."""
+ for entry in moved:
+ try:
+ self._move_entry(WorkingTree._RenameEntry(
+ entry.to_rel, entry.from_id,
+ entry.to_tail, entry.to_parent_id, entry.from_rel,
+ entry.from_tail, entry.from_parent_id,
+ entry.only_change_inv))
+ except errors.BzrMoveFailedError, e:
+ raise errors.BzrMoveFailedError( '', '', "Rollback failed."
+ " The working tree is in an inconsistent state."
+ " Please consider doing a 'bzr revert'."
+ " Error message is: %s" % e)
+
+ def _move_entry(self, entry):
+ inv = self.root_inventory
+ from_rel_abs = self.abspath(entry.from_rel)
+ to_rel_abs = self.abspath(entry.to_rel)
+ if from_rel_abs == to_rel_abs:
+ raise errors.BzrMoveFailedError(entry.from_rel, entry.to_rel,
+ "Source and target are identical.")
+
+ if not entry.only_change_inv:
+ try:
+ osutils.rename(from_rel_abs, to_rel_abs)
+ except OSError, e:
+ raise errors.BzrMoveFailedError(entry.from_rel,
+ entry.to_rel, e[1])
+ if entry.change_id:
+ to_id = inv.path2id(entry.to_rel)
+ inv.remove_recursive_id(to_id)
+ inv.rename(entry.from_id, entry.to_parent_id, entry.to_tail)
+
+ @needs_tree_write_lock
+ def unversion(self, file_ids):
+ """Remove the file ids in file_ids from the current versioned set.
+
+ When a file_id is unversioned, all of its children are automatically
+ unversioned.
+
+ :param file_ids: The file ids to stop versioning.
+ :raises: NoSuchId if any fileid is not currently versioned.
+ """
+ for file_id in file_ids:
+ if not self._inventory.has_id(file_id):
+ raise errors.NoSuchId(self, file_id)
+ for file_id in file_ids:
+ if self._inventory.has_id(file_id):
+ self._inventory.remove_recursive_id(file_id)
+ if len(file_ids):
+ # in the future this should just set a dirty bit to wait for the
+ # final unlock. However, until all methods of workingtree start
+ # with the current in -memory inventory rather than triggering
+ # a read, it is more complex - we need to teach read_inventory
+ # to know when to read, and when to not read first... and possibly
+ # to save first when the in memory one may be corrupted.
+ # so for now, we just only write it if it is indeed dirty.
+ # - RBC 20060907
+ self._write_inventory(self._inventory)
+
+ def stored_kind(self, file_id):
+ """See Tree.stored_kind"""
+ inv, inv_file_id = self._unpack_file_id(file_id)
+ return inv[inv_file_id].kind
+
+ def extras(self):
+ """Yield all unversioned files in this WorkingTree.
+
+ If there are any unversioned directories then only the directory is
+ returned, not all its children. But if there are unversioned files
+ under a versioned subdirectory, they are returned.
+
+ Currently returned depth-first, sorted by name within directories.
+ This is the same order used by 'osutils.walkdirs'.
+ """
+ ## TODO: Work from given directory downwards
+ for path, dir_entry in self.iter_entries_by_dir():
+ if dir_entry.kind != 'directory':
+ continue
+ # mutter("search for unknowns in %r", path)
+ dirabs = self.abspath(path)
+ if not isdir(dirabs):
+ # e.g. directory deleted
+ continue
+
+ fl = []
+ for subf in os.listdir(dirabs):
+ if self.bzrdir.is_control_filename(subf):
+ continue
+ if subf not in dir_entry.children:
+ try:
+ (subf_norm,
+ can_access) = osutils.normalized_filename(subf)
+ except UnicodeDecodeError:
+ path_os_enc = path.encode(osutils._fs_enc)
+ relpath = path_os_enc + '/' + subf
+ raise errors.BadFilenameEncoding(relpath,
+ osutils._fs_enc)
+ if subf_norm != subf and can_access:
+ if subf_norm not in dir_entry.children:
+ fl.append(subf_norm)
+ else:
+ fl.append(subf)
+
+ fl.sort()
+ for subf in fl:
+ subp = pathjoin(path, subf)
+ yield subp
+
+ def _walkdirs(self, prefix=""):
+ """Walk the directories of this tree.
+
+ :param prefix: is used as the directrory to start with.
+ :returns: a generator which yields items in the form::
+
+ ((curren_directory_path, fileid),
+ [(file1_path, file1_name, file1_kind, None, file1_id,
+ file1_kind), ... ])
+ """
+ _directory = 'directory'
+ # get the root in the inventory
+ inv, top_id = self._path2inv_file_id(prefix)
+ if top_id is None:
+ pending = []
+ else:
+ pending = [(prefix, '', _directory, None, top_id, None)]
+ while pending:
+ dirblock = []
+ currentdir = pending.pop()
+ # 0 - relpath, 1- basename, 2- kind, 3- stat, 4-id, 5-kind
+ top_id = currentdir[4]
+ if currentdir[0]:
+ relroot = currentdir[0] + '/'
+ else:
+ relroot = ""
+ # FIXME: stash the node in pending
+ entry = inv[top_id]
+ if entry.kind == 'directory':
+ for name, child in entry.sorted_children():
+ dirblock.append((relroot + name, name, child.kind, None,
+ child.file_id, child.kind
+ ))
+ yield (currentdir[0], entry.file_id), dirblock
+ # push the user specified dirs from dirblock
+ for dir in reversed(dirblock):
+ if dir[2] == _directory:
+ pending.append(dir)
+
+ @needs_write_lock
+ def update_feature_flags(self, updated_flags):
+ """Update the feature flags for this branch.
+
+ :param updated_flags: Dictionary mapping feature names to necessities
+ A necessity can be None to indicate the feature should be removed
+ """
+ self._format._update_feature_flags(updated_flags)
+ self.control_transport.put_bytes('format', self._format.as_string())
+
+
+class WorkingTreeFormatRegistry(controldir.ControlComponentFormatRegistry):
+ """Registry for working tree formats."""
+
+ def __init__(self, other_registry=None):
+ super(WorkingTreeFormatRegistry, self).__init__(other_registry)
+ self._default_format = None
+ self._default_format_key = None
+
+ def get_default(self):
+ """Return the current default format."""
+ if (self._default_format_key is not None and
+ self._default_format is None):
+ self._default_format = self.get(self._default_format_key)
+ return self._default_format
+
+ def set_default(self, format):
+ """Set the default format."""
+ self._default_format = format
+ self._default_format_key = None
+
+ def set_default_key(self, format_string):
+ """Set the default format by its format string."""
+ self._default_format_key = format_string
+ self._default_format = None
+
+
+format_registry = WorkingTreeFormatRegistry()
+
+
+class WorkingTreeFormat(controldir.ControlComponentFormat):
+ """An encapsulation of the initialization and open routines for a format.
+
+ Formats provide three things:
+ * An initialization routine,
+ * a format string,
+ * an open routine.
+
+ Formats are placed in an dict by their format string for reference
+ during workingtree opening. Its not required that these be instances, they
+ can be classes themselves with class methods - it simply depends on
+ whether state is needed for a given format or not.
+
+ Once a format is deprecated, just deprecate the initialize and open
+ methods on the format class. Do not deprecate the object, as the
+ object will be created every time regardless.
+ """
+
+ requires_rich_root = False
+
+ upgrade_recommended = False
+
+ requires_normalized_unicode_filenames = False
+
+ case_sensitive_filename = "FoRMaT"
+
+ missing_parent_conflicts = False
+ """If this format supports missing parent conflicts."""
+
+ supports_versioned_directories = None
+
+ def initialize(self, controldir, revision_id=None, from_branch=None,
+ accelerator_tree=None, hardlink=False):
+ """Initialize a new working tree in controldir.
+
+ :param controldir: ControlDir to initialize the working tree in.
+ :param revision_id: allows creating a working tree at a different
+ revision than the branch is at.
+ :param from_branch: Branch to checkout
+ :param accelerator_tree: A tree which can be used for retrieving file
+ contents more quickly than the revision tree, i.e. a workingtree.
+ The revision tree will be used for cases where accelerator_tree's
+ content is different.
+ :param hardlink: If true, hard-link files from accelerator_tree,
+ where possible.
+ """
+ raise NotImplementedError(self.initialize)
+
+ def __eq__(self, other):
+ return self.__class__ is other.__class__
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def get_format_description(self):
+ """Return the short description for this format."""
+ raise NotImplementedError(self.get_format_description)
+
+ def is_supported(self):
+ """Is this format supported?
+
+ Supported formats can be initialized and opened.
+ Unsupported formats may not support initialization or committing or
+ some other features depending on the reason for not being supported.
+ """
+ return True
+
+ def supports_content_filtering(self):
+ """True if this format supports content filtering."""
+ return False
+
+ def supports_views(self):
+ """True if this format supports stored views."""
+ return False
+
+ def get_controldir_for_branch(self):
+ """Get the control directory format for creating branches.
+
+ This is to support testing of working tree formats that can not exist
+ in the same control directory as a branch.
+ """
+ return self._matchingbzrdir
+
+
+class WorkingTreeFormatMetaDir(bzrdir.BzrFormat, WorkingTreeFormat):
+ """Base class for working trees that live in bzr meta directories."""
+
+ def __init__(self):
+ WorkingTreeFormat.__init__(self)
+ bzrdir.BzrFormat.__init__(self)
+
+ @classmethod
+ def find_format_string(klass, controldir):
+ """Return format name for the working tree object in controldir."""
+ try:
+ transport = controldir.get_workingtree_transport(None)
+ return transport.get_bytes("format")
+ except errors.NoSuchFile:
+ raise errors.NoWorkingTree(base=transport.base)
+
+ @classmethod
+ def find_format(klass, controldir):
+ """Return the format for the working tree object in controldir."""
+ format_string = klass.find_format_string(controldir)
+ return klass._find_format(format_registry, 'working tree',
+ format_string)
+
+ def check_support_status(self, allow_unsupported, recommend_upgrade=True,
+ basedir=None):
+ WorkingTreeFormat.check_support_status(self,
+ allow_unsupported=allow_unsupported, recommend_upgrade=recommend_upgrade,
+ basedir=basedir)
+ bzrdir.BzrFormat.check_support_status(self, allow_unsupported=allow_unsupported,
+ recommend_upgrade=recommend_upgrade, basedir=basedir)
+
+ def get_controldir_for_branch(self):
+ """Get the control directory format for creating branches.
+
+ This is to support testing of working tree formats that can not exist
+ in the same control directory as a branch.
+ """
+ return self._matchingbzrdir
+
+
+class WorkingTreeFormatMetaDir(bzrdir.BzrFormat, WorkingTreeFormat):
+ """Base class for working trees that live in bzr meta directories."""
+
+ def __init__(self):
+ WorkingTreeFormat.__init__(self)
+ bzrdir.BzrFormat.__init__(self)
+
+ @classmethod
+ def find_format_string(klass, controldir):
+ """Return format name for the working tree object in controldir."""
+ try:
+ transport = controldir.get_workingtree_transport(None)
+ return transport.get_bytes("format")
+ except errors.NoSuchFile:
+ raise errors.NoWorkingTree(base=transport.base)
+
+ @classmethod
+ def find_format(klass, controldir):
+ """Return the format for the working tree object in controldir."""
+ format_string = klass.find_format_string(controldir)
+ return klass._find_format(format_registry, 'working tree',
+ format_string)
+
+ def check_support_status(self, allow_unsupported, recommend_upgrade=True,
+ basedir=None):
+ WorkingTreeFormat.check_support_status(self,
+ allow_unsupported=allow_unsupported, recommend_upgrade=recommend_upgrade,
+ basedir=basedir)
+ bzrdir.BzrFormat.check_support_status(self, allow_unsupported=allow_unsupported,
+ recommend_upgrade=recommend_upgrade, basedir=basedir)
+
+
+format_registry.register_lazy("Bazaar Working Tree Format 4 (bzr 0.15)\n",
+ "bzrlib.workingtree_4", "WorkingTreeFormat4")
+format_registry.register_lazy("Bazaar Working Tree Format 5 (bzr 1.11)\n",
+ "bzrlib.workingtree_4", "WorkingTreeFormat5")
+format_registry.register_lazy("Bazaar Working Tree Format 6 (bzr 1.14)\n",
+ "bzrlib.workingtree_4", "WorkingTreeFormat6")
+format_registry.register_lazy("Bazaar-NG Working Tree format 3",
+ "bzrlib.workingtree_3", "WorkingTreeFormat3")
+format_registry.set_default_key("Bazaar Working Tree Format 6 (bzr 1.14)\n")