1# Copyright (C) 2005-2011 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17"""Tree classes, representing directory at point in time.
18"""
19
20try:
21    from collections.abc import deque
22except ImportError:  # python < 3.7
23    from collections import deque
24
25import os
26import re
27
28
29from .. import (
30    branch as _mod_branch,
31    debug,
32    errors,
33    lazy_import,
34    osutils,
35    revision,
36    )
37from ..controldir import (
38    ControlDir,
39    )
40from ..mutabletree import (
41    MutableTree,
42    )
43from ..repository import (
44    Repository,
45    )
46from ..revisiontree import (
47    RevisionTree,
48    )
49lazy_import.lazy_import(globals(), """
50from breezy import (
51    add,
52    controldir,
53    trace,
54    transport as _mod_transport,
55    )
56from breezy.bzr import (
57    inventory as _mod_inventory,
58    )
59""")
60from ..tree import (
61    FileTimestampUnavailable,
62    InterTree,
63    MissingNestedTree,
64    Tree,
65    TreeChange,
66    TreeFile,
67    )
68
69
70class InventoryTreeChange(TreeChange):
71
72    __slots__ = TreeChange.__slots__ + ['file_id', 'parent_id']
73
74    def __init__(self, file_id, path, changed_content, versioned, parent_id,
75                 name, kind, executable, copied=False):
76        self.file_id = file_id
77        self.parent_id = parent_id
78        super(InventoryTreeChange, self).__init__(
79            path=path, changed_content=changed_content, versioned=versioned,
80            name=name, kind=kind, executable=executable, copied=copied)
81
82    def __repr__(self):
83        return "%s%r" % (self.__class__.__name__, self._as_tuple())
84
85    def _as_tuple(self):
86        return (self.file_id, self.path, self.changed_content, self.versioned,
87                self.parent_id, self.name, self.kind, self.executable, self.copied)
88
89    def __eq__(self, other):
90        if isinstance(other, TreeChange):
91            return self._as_tuple() == other._as_tuple()
92        if isinstance(other, tuple):
93            return self._as_tuple() == other
94        return False
95
96    def __lt__(self, other):
97        return self._as_tuple() < other._as_tuple()
98
99    def meta_modified(self):
100        if self.versioned == (True, True):
101            return (self.executable[0] != self.executable[1])
102        return False
103
104    def is_reparented(self):
105        return self.parent_id[0] != self.parent_id[1]
106
107    @property
108    def renamed(self):
109        return (
110            not self.copied and
111            None not in self.name and
112            None not in self.parent_id and
113            (self.name[0] != self.name[1] or self.parent_id[0] != self.parent_id[1]))
114
115    def discard_new(self):
116        return self.__class__(
117            self.file_id, (self.path[0], None), self.changed_content,
118            (self.versioned[0], None), (self.parent_id[0], None),
119            (self.name[0], None), (self.kind[0], None),
120            (self.executable[0], None),
121            copied=False)
122
123
124class InventoryTree(Tree):
125    """A tree that relies on an inventory for its metadata.
126
127    Trees contain an `Inventory` object, and also know how to retrieve
128    file texts mentioned in the inventory, either from a working
129    directory or from a store.
130
131    It is possible for trees to contain files that are not described
132    in their inventory or vice versa; for this use `filenames()`.
133
134    Subclasses should set the _inventory attribute, which is considered
135    private to external API users.
136    """
137
138    def _get_root_inventory(self):
139        return self._inventory
140
141    root_inventory = property(_get_root_inventory,
142                              doc="Root inventory of this tree")
143
144    def _unpack_file_id(self, file_id):
145        """Find the inventory and inventory file id for a tree file id.
146
147        :param file_id: The tree file id, as bytestring or tuple
148        :return: Inventory and inventory file id
149        """
150        if isinstance(file_id, tuple):
151            if len(file_id) != 1:
152                raise ValueError(
153                    "nested trees not yet supported: %r" % file_id)
154            file_id = file_id[0]
155        return self.root_inventory, file_id
156
157    def find_related_paths_across_trees(self, paths, trees=[],
158                                        require_versioned=True):
159        """Find related paths in tree corresponding to specified filenames in any
160        of `lookup_trees`.
161
162        All matches in all trees will be used, and all children of matched
163        directories will be used.
164
165        :param paths: The filenames to find related paths for (if None, returns
166            None)
167        :param trees: The trees to find file_ids within
168        :param require_versioned: if true, all specified filenames must occur in
169            at least one tree.
170        :return: a set of paths for the specified filenames and their children
171            in `tree`
172        """
173        if paths is None:
174            return None
175        file_ids = self.paths2ids(
176            paths, trees, require_versioned=require_versioned)
177        ret = set()
178        for file_id in file_ids:
179            try:
180                ret.add(self.id2path(file_id))
181            except errors.NoSuchId:
182                pass
183        return ret
184
185    def paths2ids(self, paths, trees=[], require_versioned=True):
186        """Return all the ids that can be reached by walking from paths.
187
188        Each path is looked up in this tree and any extras provided in
189        trees, and this is repeated recursively: the children in an extra tree
190        of a directory that has been renamed under a provided path in this tree
191        are all returned, even if none exist under a provided path in this
192        tree, and vice versa.
193
194        :param paths: An iterable of paths to start converting to ids from.
195            Alternatively, if paths is None, no ids should be calculated and None
196            will be returned. This is offered to make calling the api unconditional
197            for code that *might* take a list of files.
198        :param trees: Additional trees to consider.
199        :param require_versioned: If False, do not raise NotVersionedError if
200            an element of paths is not versioned in this tree and all of trees.
201        """
202        return find_ids_across_trees(paths, [self] + list(trees), require_versioned)
203
204    def path2id(self, path):
205        """Return the id for path in this tree."""
206        with self.lock_read():
207            return self._path2inv_file_id(path)[1]
208
209    def _path2ie(self, path):
210        """Lookup an inventory entry by path.
211
212        :param path: Path to look up
213        :return: InventoryEntry
214        """
215        inv, ie = self._path2inv_ie(path)
216        if ie is None:
217            raise errors.NoSuchFile(path)
218        return ie
219
220    def _path2inv_ie(self, path):
221        inv = self.root_inventory
222        if isinstance(path, list):
223            remaining = path
224        else:
225            remaining = osutils.splitpath(path)
226        ie = inv.root
227        while remaining:
228            ie, base, remaining = inv.get_entry_by_path_partial(remaining)
229            if remaining:
230                inv = self._get_nested_tree('/'.join(base), ie.file_id, ie.reference_revision).root_inventory
231        if ie is None:
232            return None, None
233        return inv, ie
234
235    def _path2inv_file_id(self, path):
236        """Lookup a inventory and inventory file id by path.
237
238        :param path: Path to look up
239        :return: tuple with inventory and inventory file id
240        """
241        inv, ie = self._path2inv_ie(path)
242        if ie is None:
243            return None, None
244        return inv, ie.file_id
245
246    def id2path(self, file_id, recurse='down'):
247        """Return the path for a file id.
248
249        :raises NoSuchId:
250        """
251        inventory, file_id = self._unpack_file_id(file_id)
252        try:
253            return inventory.id2path(file_id)
254        except errors.NoSuchId:
255            if recurse == 'down':
256                if 'evil' in debug.debug_flags:
257                    trace.mutter_callsite(
258                        2, "id2path with nested trees scales with tree size.")
259                for path in self.iter_references():
260                    subtree = self.get_nested_tree(path)
261                    try:
262                        return osutils.pathjoin(path, subtree.id2path(file_id))
263                    except errors.NoSuchId:
264                        pass
265            raise errors.NoSuchId(self, file_id)
266
267    def all_file_ids(self):
268        return {entry.file_id for path, entry in self.iter_entries_by_dir()}
269
270    def all_versioned_paths(self):
271        return {path for path, entry in self.iter_entries_by_dir()}
272
273    def iter_entries_by_dir(self, specific_files=None,
274                            recurse_nested=False):
275        """Walk the tree in 'by_dir' order.
276
277        This will yield each entry in the tree as a (path, entry) tuple.
278        The order that they are yielded is:
279
280        See Tree.iter_entries_by_dir for details.
281        """
282        with self.lock_read():
283            if specific_files is not None:
284                inventory_file_ids = []
285                for path in specific_files:
286                    inventory, inv_file_id = self._path2inv_file_id(path)
287                    if inventory and inventory is not self.root_inventory:
288                        raise AssertionError("%r != %r" % (
289                            inventory, self.root_inventory))
290                    inventory_file_ids.append(inv_file_id)
291            else:
292                inventory_file_ids = None
293            def iter_entries(inv):
294                for p, e in inv.iter_entries_by_dir(specific_file_ids=inventory_file_ids):
295                    if e.kind == 'tree-reference' and recurse_nested:
296                        subinv = self._get_nested_tree(p, e.file_id, e.reference_revision).root_inventory
297                        for subp, e in iter_entries(subinv):
298                            yield (osutils.pathjoin(p, subp) if subp else p), e
299                    else:
300                        yield p, e
301            return iter_entries(self.root_inventory)
302
303    def iter_child_entries(self, path):
304        with self.lock_read():
305            ie = self._path2ie(path)
306            if ie.kind != 'directory':
307                raise errors.NotADirectory(path)
308            return ie.children.values()
309
310    def _get_plan_merge_data(self, path, other, base):
311        from . import versionedfile
312        file_id = self.path2id(path)
313        vf = versionedfile._PlanMergeVersionedFile(file_id)
314        last_revision_a = self._get_file_revision(
315            path, file_id, vf, b'this:')
316        last_revision_b = other._get_file_revision(
317            other.id2path(file_id), file_id, vf, b'other:')
318        if base is None:
319            last_revision_base = None
320        else:
321            last_revision_base = base._get_file_revision(
322                base.id2path(file_id), file_id, vf, b'base:')
323        return vf, last_revision_a, last_revision_b, last_revision_base
324
325    def plan_file_merge(self, path, other, base=None):
326        """Generate a merge plan based on annotations.
327
328        If the file contains uncommitted changes in this tree, they will be
329        attributed to the 'current:' pseudo-revision.  If the file contains
330        uncommitted changes in the other tree, they will be assigned to the
331        'other:' pseudo-revision.
332        """
333        data = self._get_plan_merge_data(path, other, base)
334        vf, last_revision_a, last_revision_b, last_revision_base = data
335        return vf.plan_merge(last_revision_a, last_revision_b,
336                             last_revision_base)
337
338    def plan_file_lca_merge(self, path, other, base=None):
339        """Generate a merge plan based lca-newness.
340
341        If the file contains uncommitted changes in this tree, they will be
342        attributed to the 'current:' pseudo-revision.  If the file contains
343        uncommitted changes in the other tree, they will be assigned to the
344        'other:' pseudo-revision.
345        """
346        data = self._get_plan_merge_data(path, other, base)
347        vf, last_revision_a, last_revision_b, last_revision_base = data
348        return vf.plan_lca_merge(last_revision_a, last_revision_b,
349                                 last_revision_base)
350
351    def _iter_parent_trees(self):
352        """Iterate through parent trees, defaulting to Tree.revision_tree."""
353        for revision_id in self.get_parent_ids():
354            try:
355                yield self.revision_tree(revision_id)
356            except errors.NoSuchRevisionInTree:
357                yield self.branch.repository.revision_tree(revision_id)
358
359    def _get_file_revision(self, path, file_id, vf, tree_revision):
360        """Ensure that file_id, tree_revision is in vf to plan the merge."""
361        from . import versionedfile
362        last_revision = tree_revision
363        parent_keys = [
364            (file_id, t.get_file_revision(path)) for t in
365            self._iter_parent_trees()]
366        with self.get_file(path) as f:
367            vf.add_content(
368                versionedfile.FileContentFactory(
369                    (file_id, last_revision), parent_keys, f, size=osutils.filesize(f)))
370        repo = self.branch.repository
371        base_vf = repo.texts
372        if base_vf not in vf.fallback_versionedfiles:
373            vf.fallback_versionedfiles.append(base_vf)
374        return last_revision
375
376    def preview_transform(self, pb=None):
377        from .transform import TransformPreview
378        return TransformPreview(self, pb=pb)
379
380
381def find_ids_across_trees(filenames, trees, require_versioned=True):
382    """Find the ids corresponding to specified filenames.
383
384    All matches in all trees will be used, and all children of matched
385    directories will be used.
386
387    :param filenames: The filenames to find file_ids for (if None, returns
388        None)
389    :param trees: The trees to find file_ids within
390    :param require_versioned: if true, all specified filenames must occur in
391        at least one tree.
392    :return: a set of file ids for the specified filenames and their children.
393    """
394    if not filenames:
395        return None
396    specified_path_ids = _find_ids_across_trees(filenames, trees,
397                                                require_versioned)
398    return _find_children_across_trees(specified_path_ids, trees)
399
400
401def _find_ids_across_trees(filenames, trees, require_versioned):
402    """Find the ids corresponding to specified filenames.
403
404    All matches in all trees will be used, but subdirectories are not scanned.
405
406    :param filenames: The filenames to find file_ids for
407    :param trees: The trees to find file_ids within
408    :param require_versioned: if true, all specified filenames must occur in
409        at least one tree.
410    :return: a set of file ids for the specified filenames
411    """
412    not_versioned = []
413    interesting_ids = set()
414    for tree_path in filenames:
415        not_found = True
416        for tree in trees:
417            file_id = tree.path2id(tree_path)
418            if file_id is not None:
419                interesting_ids.add(file_id)
420                not_found = False
421        if not_found:
422            not_versioned.append(tree_path)
423    if len(not_versioned) > 0 and require_versioned:
424        raise errors.PathsNotVersionedError(not_versioned)
425    return interesting_ids
426
427
428def _find_children_across_trees(specified_ids, trees):
429    """Return a set including specified ids and their children.
430
431    All matches in all trees will be used.
432
433    :param trees: The trees to find file_ids within
434    :return: a set containing all specified ids and their children
435    """
436    interesting_ids = set(specified_ids)
437    pending = interesting_ids
438    # now handle children of interesting ids
439    # we loop so that we handle all children of each id in both trees
440    while len(pending) > 0:
441        new_pending = set()
442        for file_id in pending:
443            for tree in trees:
444                try:
445                    path = tree.id2path(file_id)
446                except errors.NoSuchId:
447                    continue
448                try:
449                    for child in tree.iter_child_entries(path):
450                        if child.file_id not in interesting_ids:
451                            new_pending.add(child.file_id)
452                except errors.NotADirectory:
453                    pass
454        interesting_ids.update(new_pending)
455        pending = new_pending
456    return interesting_ids
457
458
459class MutableInventoryTree(MutableTree, InventoryTree):
460
461    def apply_inventory_delta(self, changes):
462        """Apply changes to the inventory as an atomic operation.
463
464        :param changes: An inventory delta to apply to the working tree's
465            inventory.
466        :return None:
467        :seealso Inventory.apply_delta: For details on the changes parameter.
468        """
469        with self.lock_tree_write():
470            self.flush()
471            inv = self.root_inventory
472            inv.apply_delta(changes)
473            self._write_inventory(inv)
474
475    def has_changes(self, _from_tree=None):
476        """Quickly check that the tree contains at least one commitable change.
477
478        :param _from_tree: tree to compare against to find changes (default to
479            the basis tree and is intended to be used by tests).
480
481        :return: True if a change is found. False otherwise
482        """
483        with self.lock_read():
484            # Check pending merges
485            if len(self.get_parent_ids()) > 1:
486                return True
487            if _from_tree is None:
488                _from_tree = self.basis_tree()
489            changes = self.iter_changes(_from_tree)
490            if self.supports_symlinks():
491                # Fast path for has_changes.
492                try:
493                    change = next(changes)
494                    # Exclude root (talk about black magic... --vila 20090629)
495                    if change.parent_id == (None, None):
496                        change = next(changes)
497                    return True
498                except StopIteration:
499                    # No changes
500                    return False
501            else:
502                # Slow path for has_changes.
503                # Handle platforms that do not support symlinks in the
504                # conditional below. This is slower than the try/except
505                # approach below that but we don't have a choice as we
506                # need to be sure that all symlinks are removed from the
507                # entire changeset. This is because in platforms that
508                # do not support symlinks, they show up as None in the
509                # working copy as compared to the repository.
510                # Also, exclude root as mention in the above fast path.
511                changes = filter(
512                    lambda c: c[6][0] != 'symlink' and c[4] != (None, None),
513                    changes)
514                try:
515                    next(iter(changes))
516                except StopIteration:
517                    return False
518                return True
519
520    def _fix_case_of_inventory_path(self, path):
521        """If our tree isn't case sensitive, return the canonical path"""
522        if not self.case_sensitive:
523            path = self.get_canonical_path(path)
524        return path
525
526    def smart_add(self, file_list, recurse=True, action=None, save=True):
527        """Version file_list, optionally recursing into directories.
528
529        This is designed more towards DWIM for humans than API clarity.
530        For the specific behaviour see the help for cmd_add().
531
532        :param file_list: List of zero or more paths.  *NB: these are
533            interpreted relative to the process cwd, not relative to the
534            tree.*  (Add and most other tree methods use tree-relative
535            paths.)
536        :param action: A reporter to be called with the inventory, parent_ie,
537            path and kind of the path being added. It may return a file_id if
538            a specific one should be used.
539        :param save: Save the inventory after completing the adds. If False
540            this provides dry-run functionality by doing the add and not saving
541            the inventory.
542        :return: A tuple - files_added, ignored_files. files_added is the count
543            of added files, and ignored_files is a dict mapping files that were
544            ignored to the rule that caused them to be ignored.
545        """
546        with self.lock_tree_write():
547            # Not all mutable trees can have conflicts
548            if getattr(self, 'conflicts', None) is not None:
549                # Collect all related files without checking whether they exist or
550                # are versioned. It's cheaper to do that once for all conflicts
551                # than trying to find the relevant conflict for each added file.
552                conflicts_related = set()
553                for c in self.conflicts():
554                    conflicts_related.update(c.associated_filenames())
555            else:
556                conflicts_related = None
557            adder = _SmartAddHelper(self, action, conflicts_related)
558            adder.add(file_list, recurse=recurse)
559            if save:
560                invdelta = adder.get_inventory_delta()
561                self.apply_inventory_delta(invdelta)
562            return adder.added, adder.ignored
563
564    def update_basis_by_delta(self, new_revid, delta):
565        """Update the parents of this tree after a commit.
566
567        This gives the tree one parent, with revision id new_revid. The
568        inventory delta is applied to the current basis tree to generate the
569        inventory for the parent new_revid, and all other parent trees are
570        discarded.
571
572        All the changes in the delta should be changes synchronising the basis
573        tree with some or all of the working tree, with a change to a directory
574        requiring that its contents have been recursively included. That is,
575        this is not a general purpose tree modification routine, but a helper
576        for commit which is not required to handle situations that do not arise
577        outside of commit.
578
579        See the inventory developers documentation for the theory behind
580        inventory deltas.
581
582        :param new_revid: The new revision id for the trees parent.
583        :param delta: An inventory delta (see apply_inventory_delta) describing
584            the changes from the current left most parent revision to new_revid.
585        """
586        # if the tree is updated by a pull to the branch, as happens in
587        # WorkingTree2, when there was no separation between branch and tree,
588        # then just clear merges, efficiency is not a concern for now as this
589        # is legacy environments only, and they are slow regardless.
590        if self.last_revision() == new_revid:
591            self.set_parent_ids([new_revid])
592            return
593        # generic implementation based on Inventory manipulation. See
594        # WorkingTree classes for optimised versions for specific format trees.
595        basis = self.basis_tree()
596        with basis.lock_read():
597            # TODO: Consider re-evaluating the need for this with CHKInventory
598            # we don't strictly need to mutate an inventory for this
599            # it only makes sense when apply_delta is cheaper than get_inventory()
600            inventory = _mod_inventory.mutable_inventory_from_tree(basis)
601        inventory.apply_delta(delta)
602        rev_tree = InventoryRevisionTree(self.branch.repository,
603                                         inventory, new_revid)
604        self.set_parent_trees([(new_revid, rev_tree)])
605
606    def transform(self, pb=None):
607        from .transform import InventoryTreeTransform
608        return InventoryTreeTransform(self, pb=pb)
609
610
611class _SmartAddHelper(object):
612    """Helper for MutableTree.smart_add."""
613
614    def get_inventory_delta(self):
615        # GZ 2016-06-05: Returning view would probably be fine but currently
616        # Inventory.apply_delta is documented as requiring a list of changes.
617        return list(self._invdelta.values())
618
619    def _get_ie(self, inv_path):
620        """Retrieve the most up to date inventory entry for a path.
621
622        :param inv_path: Normalized inventory path
623        :return: Inventory entry (with possibly invalid .children for
624            directories)
625        """
626        entry = self._invdelta.get(inv_path)
627        if entry is not None:
628            return entry[3]
629        # Find a 'best fit' match if the filesystem is case-insensitive
630        inv_path = self.tree._fix_case_of_inventory_path(inv_path)
631        try:
632            return next(self.tree.iter_entries_by_dir(
633                specific_files=[inv_path]))[1]
634        except StopIteration:
635            return None
636
637    def _convert_to_directory(self, this_ie, inv_path):
638        """Convert an entry to a directory.
639
640        :param this_ie: Inventory entry
641        :param inv_path: Normalized path for the inventory entry
642        :return: The new inventory entry
643        """
644        # Same as in _add_one below, if the inventory doesn't
645        # think this is a directory, update the inventory
646        this_ie = _mod_inventory.InventoryDirectory(
647            this_ie.file_id, this_ie.name, this_ie.parent_id)
648        self._invdelta[inv_path] = (inv_path, inv_path, this_ie.file_id,
649                                    this_ie)
650        return this_ie
651
652    def _add_one_and_parent(self, parent_ie, path, kind, inv_path):
653        """Add a new entry to the inventory and automatically add unversioned parents.
654
655        :param parent_ie: Parent inventory entry if known, or None.  If
656            None, the parent is looked up by name and used if present, otherwise it
657            is recursively added.
658        :param path: Filesystem path to add
659        :param kind: Kind of new entry (file, directory, etc)
660        :param inv_path: Inventory path
661        :return: Inventory entry for path and a list of paths which have been added.
662        """
663        # Nothing to do if path is already versioned.
664        # This is safe from infinite recursion because the tree root is
665        # always versioned.
666        inv_dirname = osutils.dirname(inv_path)
667        dirname, basename = osutils.split(path)
668        if parent_ie is None:
669            # slower but does not need parent_ie
670            this_ie = self._get_ie(inv_path)
671            if this_ie is not None:
672                return this_ie
673            # its really not there : add the parent
674            # note that the dirname use leads to some extra str copying etc but as
675            # there are a limited number of dirs we can be nested under, it should
676            # generally find it very fast and not recurse after that.
677            parent_ie = self._add_one_and_parent(None,
678                                                 dirname, 'directory',
679                                                 inv_dirname)
680        # if the parent exists, but isn't a directory, we have to do the
681        # kind change now -- really the inventory shouldn't pretend to know
682        # the kind of wt files, but it does.
683        if parent_ie.kind != 'directory':
684            # nb: this relies on someone else checking that the path we're using
685            # doesn't contain symlinks.
686            parent_ie = self._convert_to_directory(parent_ie, inv_dirname)
687        file_id = self.action(self.tree, parent_ie, path, kind)
688        entry = _mod_inventory.make_entry(kind, basename, parent_ie.file_id,
689                                          file_id=file_id)
690        self._invdelta[inv_path] = (None, inv_path, entry.file_id, entry)
691        self.added.append(inv_path)
692        return entry
693
694    def _gather_dirs_to_add(self, user_dirs):
695        # only walk the minimal parents needed: we have user_dirs to override
696        # ignores.
697        prev_dir = None
698
699        is_inside = osutils.is_inside_or_parent_of_any
700        for path in sorted(user_dirs):
701            if (prev_dir is None or not is_inside([prev_dir], path)):
702                inv_path, this_ie = user_dirs[path]
703                yield (path, inv_path, this_ie, None)
704            prev_dir = path
705
706    def __init__(self, tree, action, conflicts_related=None):
707        self.tree = tree
708        if action is None:
709            self.action = add.AddAction()
710        else:
711            self.action = action
712        self._invdelta = {}
713        self.added = []
714        self.ignored = {}
715        if conflicts_related is None:
716            self.conflicts_related = frozenset()
717        else:
718            self.conflicts_related = conflicts_related
719
720    def add(self, file_list, recurse=True):
721        if not file_list:
722            # no paths supplied: add the entire tree.
723            # FIXME: this assumes we are running in a working tree subdir :-/
724            # -- vila 20100208
725            file_list = [u'.']
726
727        # expand any symlinks in the directory part, while leaving the
728        # filename alone
729        # only expanding if symlinks are supported avoids windows path bugs
730        if self.tree.supports_symlinks():
731            file_list = list(map(osutils.normalizepath, file_list))
732
733        user_dirs = {}
734        # validate user file paths and convert all paths to tree
735        # relative : it's cheaper to make a tree relative path an abspath
736        # than to convert an abspath to tree relative, and it's cheaper to
737        # perform the canonicalization in bulk.
738        for filepath in osutils.canonical_relpaths(self.tree.basedir, file_list):
739            # validate user parameters. Our recursive code avoids adding new
740            # files that need such validation
741            if self.tree.is_control_filename(filepath):
742                raise errors.ForbiddenControlFileError(filename=filepath)
743
744            abspath = self.tree.abspath(filepath)
745            kind = osutils.file_kind(abspath)
746            # ensure the named path is added, so that ignore rules in the later
747            # directory walk dont skip it.
748            # we dont have a parent ie known yet.: use the relatively slower
749            # inventory probing method
750            inv_path, _ = osutils.normalized_filename(filepath)
751            this_ie = self._get_ie(inv_path)
752            if this_ie is None:
753                this_ie = self._add_one_and_parent(
754                    None, filepath, kind, inv_path)
755            if kind == 'directory':
756                # schedule the dir for scanning
757                user_dirs[filepath] = (inv_path, this_ie)
758
759        if not recurse:
760            # no need to walk any directories at all.
761            return
762
763        things_to_add = list(self._gather_dirs_to_add(user_dirs))
764
765        illegalpath_re = re.compile(r'[\r\n]')
766        for directory, inv_path, this_ie, parent_ie in things_to_add:
767            # directory is tree-relative
768            abspath = self.tree.abspath(directory)
769
770            # get the contents of this directory.
771
772            # find the kind of the path being added, and save stat_value
773            # for reuse
774            stat_value = None
775            if this_ie is None:
776                stat_value = osutils.file_stat(abspath)
777                kind = osutils.file_kind_from_stat_mode(stat_value.st_mode)
778            else:
779                kind = this_ie.kind
780
781            # allow AddAction to skip this file
782            if self.action.skip_file(self.tree, abspath, kind, stat_value):
783                continue
784            if not _mod_inventory.InventoryEntry.versionable_kind(kind):
785                trace.warning("skipping %s (can't add file of kind '%s')",
786                              abspath, kind)
787                continue
788            if illegalpath_re.search(directory):
789                trace.warning("skipping %r (contains \\n or \\r)" % abspath)
790                continue
791            if directory in self.conflicts_related:
792                # If the file looks like one generated for a conflict, don't
793                # add it.
794                trace.warning(
795                    'skipping %s (generated to help resolve conflicts)',
796                    abspath)
797                continue
798
799            if kind == 'directory' and directory != '':
800                try:
801                    transport = _mod_transport.get_transport_from_path(abspath)
802                    controldir.ControlDirFormat.find_format(transport)
803                    sub_tree = True
804                except errors.NotBranchError:
805                    sub_tree = False
806                except errors.UnsupportedFormatError:
807                    sub_tree = True
808            else:
809                sub_tree = False
810
811            if this_ie is not None:
812                pass
813            elif sub_tree:
814                # XXX: This is wrong; people *might* reasonably be trying to
815                # add subtrees as subtrees.  This should probably only be done
816                # in formats which can represent subtrees, and even then
817                # perhaps only when the user asked to add subtrees.  At the
818                # moment you can add them specially through 'join --reference',
819                # which is perhaps reasonable: adding a new reference is a
820                # special operation and can have a special behaviour.  mbp
821                # 20070306
822                trace.warning("skipping nested tree %r", abspath)
823            else:
824                this_ie = self._add_one_and_parent(parent_ie, directory, kind,
825                                                   inv_path)
826
827            if kind == 'directory' and not sub_tree:
828                if this_ie.kind != 'directory':
829                    this_ie = self._convert_to_directory(this_ie, inv_path)
830
831                for subf in sorted(os.listdir(abspath)):
832                    inv_f, _ = osutils.normalized_filename(subf)
833                    # here we could use TreeDirectory rather than
834                    # string concatenation.
835                    subp = osutils.pathjoin(directory, subf)
836                    # TODO: is_control_filename is very slow. Make it faster.
837                    # TreeDirectory.is_control_filename could also make this
838                    # faster - its impossible for a non root dir to have a
839                    # control file.
840                    if self.tree.is_control_filename(subp):
841                        trace.mutter("skip control directory %r", subp)
842                        continue
843                    sub_invp = osutils.pathjoin(inv_path, inv_f)
844                    entry = self._invdelta.get(sub_invp)
845                    if entry is not None:
846                        sub_ie = entry[3]
847                    else:
848                        sub_ie = this_ie.children.get(inv_f)
849                    if sub_ie is not None:
850                        # recurse into this already versioned subdir.
851                        things_to_add.append((subp, sub_invp, sub_ie, this_ie))
852                    else:
853                        # user selection overrides ignores
854                        # ignore while selecting files - if we globbed in the
855                        # outer loop we would ignore user files.
856                        ignore_glob = self.tree.is_ignored(subp)
857                        if ignore_glob is not None:
858                            self.ignored.setdefault(
859                                ignore_glob, []).append(subp)
860                        else:
861                            things_to_add.append(
862                                (subp, sub_invp, None, this_ie))
863
864
865class InventoryRevisionTree(RevisionTree, InventoryTree):
866
867    def __init__(self, repository, inv, revision_id):
868        RevisionTree.__init__(self, repository, revision_id)
869        self._inventory = inv
870
871    def _get_file_revision(self, path, file_id, vf, tree_revision):
872        """Ensure that file_id, tree_revision is in vf to plan the merge."""
873        last_revision = self.get_file_revision(path)
874        base_vf = self._repository.texts
875        if base_vf not in vf.fallback_versionedfiles:
876            vf.fallback_versionedfiles.append(base_vf)
877        return last_revision
878
879    def get_file_mtime(self, path):
880        ie = self._path2ie(path)
881        try:
882            revision = self._repository.get_revision(ie.revision)
883        except errors.NoSuchRevision:
884            raise FileTimestampUnavailable(path)
885        return revision.timestamp
886
887    def get_file_size(self, path):
888        return self._path2ie(path).text_size
889
890    def get_file_sha1(self, path, stat_value=None):
891        ie = self._path2ie(path)
892        if ie.kind == "file":
893            return ie.text_sha1
894        return None
895
896    def get_file_revision(self, path):
897        return self._path2ie(path).revision
898
899    def is_executable(self, path):
900        ie = self._path2ie(path)
901        if ie.kind != "file":
902            return False
903        return ie.executable
904
905    def has_filename(self, filename):
906        return bool(self.path2id(filename))
907
908    def reference_parent(self, path, branch=None, possible_transports=None):
909        if branch is not None:
910            file_id = self.path2id(path)
911            parent_url = branch.get_reference_info(file_id)[0]
912        else:
913            subdir = ControlDir.open_from_transport(
914                self._repository.user_transport.clone(path))
915            parent_url = subdir.open_branch().get_parent()
916        if parent_url is None:
917            return None
918        return _mod_branch.Branch.open(
919            parent_url,
920            possible_transports=possible_transports)
921
922    def get_reference_info(self, path, branch=None):
923        return branch.get_reference_info(self.path2id(path))[0]
924
925    def list_files(self, include_root=False, from_dir=None, recursive=True,
926                   recurse_nested=False):
927        # The only files returned by this are those from the version
928        if from_dir is None:
929            from_dir_id = None
930            inv = self.root_inventory
931        else:
932            inv, from_dir_id = self._path2inv_file_id(from_dir)
933            if from_dir_id is None:
934                # Directory not versioned
935                return
936        entries = inv.iter_entries(from_dir=from_dir_id, recursive=recursive)
937        if inv.root is not None and not include_root and from_dir is None:
938            # skip the root for compatibility with the current apis.
939            next(entries)
940        for path, entry in entries:
941            if entry.kind == 'tree-reference' and recurse_nested:
942                subtree = self._get_nested_tree(
943                    path, entry.file_id, entry.reference_revision)
944                for subpath, status, kind, entry in subtree.list_files(
945                        include_root=True, recurse_nested=recurse_nested,
946                        recursive=recursive):
947                    if subpath:
948                        full_subpath = osutils.pathjoin(path, subpath)
949                    else:
950                        full_subpath = path
951                    yield full_subpath, status, kind, entry
952            else:
953                yield path, 'V', entry.kind, entry
954
955    def get_symlink_target(self, path):
956        # Inventories store symlink targets in unicode
957        return self._path2ie(path).symlink_target
958
959    def get_reference_revision(self, path):
960        return self._path2ie(path).reference_revision
961
962    def _get_nested_tree(self, path, file_id, reference_revision):
963        # Just a guess..
964        subdir = ControlDir.open_from_transport(
965            self._repository.user_transport.clone(path))
966        subrepo = subdir.find_repository()
967        try:
968            revtree = subrepo.revision_tree(reference_revision)
969        except errors.NoSuchRevision:
970            raise MissingNestedTree(path)
971        if file_id is not None and file_id != revtree.path2id(''):
972            raise AssertionError('invalid root id: %r != %r' % (
973                file_id, revtree.path2id('')))
974        return revtree
975
976    def get_nested_tree(self, path):
977        nested_revid = self.get_reference_revision(path)
978        return self._get_nested_tree(path, None, nested_revid)
979
980    def kind(self, path):
981        return self._path2ie(path).kind
982
983    def path_content_summary(self, path):
984        """See Tree.path_content_summary."""
985        try:
986            entry = self._path2ie(path)
987        except errors.NoSuchFile:
988            return ('missing', None, None, None)
989        kind = entry.kind
990        if kind == 'file':
991            return (kind, entry.text_size, entry.executable, entry.text_sha1)
992        elif kind == 'symlink':
993            return (kind, None, None, entry.symlink_target)
994        else:
995            return (kind, None, None, None)
996
997    def _comparison_data(self, entry, path):
998        if entry is None:
999            return None, False, None
1000        return entry.kind, entry.executable, None
1001
1002    def walkdirs(self, prefix=""):
1003        _directory = 'directory'
1004        inv, top_id = self._path2inv_file_id(prefix)
1005        if top_id is None:
1006            pending = []
1007        else:
1008            pending = [(prefix, top_id)]
1009        while pending:
1010            dirblock = []
1011            root, file_id = pending.pop()
1012            if root:
1013                relroot = root + '/'
1014            else:
1015                relroot = ""
1016            # FIXME: stash the node in pending
1017            entry = inv.get_entry(file_id)
1018            subdirs = []
1019            for name, child in entry.sorted_children():
1020                toppath = relroot + name
1021                dirblock.append((toppath, name, child.kind, None, child.kind))
1022                if child.kind == _directory:
1023                    subdirs.append((toppath, child.file_id))
1024            yield root, dirblock
1025            # push the user specified dirs from dirblock
1026            pending.extend(reversed(subdirs))
1027
1028    def iter_files_bytes(self, desired_files):
1029        """See Tree.iter_files_bytes.
1030
1031        This version is implemented on top of Repository.iter_files_bytes"""
1032        repo_desired_files = [(self.path2id(f), self.get_file_revision(f), i)
1033                              for f, i in desired_files]
1034        try:
1035            for result in self._repository.iter_files_bytes(repo_desired_files):
1036                yield result
1037        except errors.RevisionNotPresent as e:
1038            raise errors.NoSuchFile(e.file_id)
1039
1040    def annotate_iter(self, path, default_revision=revision.CURRENT_REVISION):
1041        """See Tree.annotate_iter"""
1042        file_id = self.path2id(path)
1043        text_key = (file_id, self.get_file_revision(path))
1044        annotator = self._repository.texts.get_annotator()
1045        annotations = annotator.annotate_flat(text_key)
1046        return [(key[-1], line) for key, line in annotations]
1047
1048    def __eq__(self, other):
1049        if self is other:
1050            return True
1051        if isinstance(other, InventoryRevisionTree):
1052            return (self.root_inventory == other.root_inventory)
1053        return False
1054
1055    def __ne__(self, other):
1056        return not (self == other)
1057
1058    def __hash__(self):
1059        raise ValueError('not hashable')
1060
1061
1062class InterInventoryTree(InterTree):
1063    """InterTree implementation for InventoryTree objects.
1064
1065    """
1066    # Formats that will be used to test this InterTree. If both are
1067    # None, this InterTree will not be tested (e.g. because a complex
1068    # setup is required)
1069    _matching_from_tree_format = None
1070    _matching_to_tree_format = None
1071
1072    @classmethod
1073    def is_compatible(kls, source, target):
1074        # The default implementation is naive and uses the public API, so
1075        # it works for all trees.
1076        return (isinstance(source, InventoryTree) and
1077                isinstance(target, InventoryTree))
1078
1079    def _changes_from_entries(self, source_entry, target_entry, source_path,
1080                              target_path):
1081        """Generate a iter_changes tuple between source_entry and target_entry.
1082
1083        :param source_entry: An inventory entry from self.source, or None.
1084        :param target_entry: An inventory entry from self.target, or None.
1085        :param source_path: The path of source_entry.
1086        :param target_path: The path of target_entry.
1087        :return: A tuple, item 0 of which is an iter_changes result tuple, and
1088            item 1 is True if there are any changes in the result tuple.
1089        """
1090        if source_entry is None:
1091            if target_entry is None:
1092                return None
1093            file_id = target_entry.file_id
1094        else:
1095            file_id = source_entry.file_id
1096        if source_entry is not None:
1097            source_versioned = True
1098            source_name = source_entry.name
1099            source_parent = source_entry.parent_id
1100            source_kind, source_executable, source_stat = \
1101                self.source._comparison_data(source_entry, source_path)
1102        else:
1103            source_versioned = False
1104            source_name = None
1105            source_parent = None
1106            source_kind = None
1107            source_executable = None
1108        if target_entry is not None:
1109            target_versioned = True
1110            target_name = target_entry.name
1111            target_parent = target_entry.parent_id
1112            target_kind, target_executable, target_stat = \
1113                self.target._comparison_data(target_entry, target_path)
1114        else:
1115            target_versioned = False
1116            target_name = None
1117            target_parent = None
1118            target_kind = None
1119            target_executable = None
1120        versioned = (source_versioned, target_versioned)
1121        kind = (source_kind, target_kind)
1122        changed_content = False
1123        if source_kind != target_kind:
1124            changed_content = True
1125        elif source_kind == 'file':
1126            if not self.file_content_matches(
1127                    source_path, target_path,
1128                    source_stat, target_stat):
1129                changed_content = True
1130        elif source_kind == 'symlink':
1131            if (self.source.get_symlink_target(source_path) !=
1132                    self.target.get_symlink_target(target_path)):
1133                changed_content = True
1134        elif source_kind == 'tree-reference':
1135            if (self.source.get_reference_revision(source_path)
1136                    != self.target.get_reference_revision(target_path)):
1137                changed_content = True
1138        parent = (source_parent, target_parent)
1139        name = (source_name, target_name)
1140        executable = (source_executable, target_executable)
1141        if (changed_content is not False or versioned[0] != versioned[1] or
1142            parent[0] != parent[1] or name[0] != name[1] or
1143                executable[0] != executable[1]):
1144            changes = True
1145        else:
1146            changes = False
1147        return InventoryTreeChange(
1148            file_id, (source_path, target_path), changed_content,
1149            versioned, parent, name, kind, executable), changes
1150
1151    def iter_changes(self, include_unchanged=False,
1152                     specific_files=None, pb=None, extra_trees=[],
1153                     require_versioned=True, want_unversioned=False):
1154        """Generate an iterator of changes between trees.
1155
1156        A tuple is returned:
1157        (file_id, (path_in_source, path_in_target),
1158         changed_content, versioned, parent, name, kind,
1159         executable)
1160
1161        Changed_content is True if the file's content has changed.  This
1162        includes changes to its kind, and to a symlink's target.
1163
1164        versioned, parent, name, kind, executable are tuples of (from, to).
1165        If a file is missing in a tree, its kind is None.
1166
1167        Iteration is done in parent-to-child order, relative to the target
1168        tree.
1169
1170        There is no guarantee that all paths are in sorted order: the
1171        requirement to expand the search due to renames may result in children
1172        that should be found early being found late in the search, after
1173        lexically later results have been returned.
1174        :param require_versioned: Raise errors.PathsNotVersionedError if a
1175            path in the specific_files list is not versioned in one of
1176            source, target or extra_trees.
1177        :param specific_files: An optional list of file paths to restrict the
1178            comparison to. When mapping filenames to ids, all matches in all
1179            trees (including optional extra_trees) are used, and all children
1180            of matched directories are included. The parents in the target tree
1181            of the specific files up to and including the root of the tree are
1182            always evaluated for changes too.
1183        :param want_unversioned: Should unversioned files be returned in the
1184            output. An unversioned file is defined as one with (False, False)
1185            for the versioned pair.
1186        """
1187        if not extra_trees:
1188            extra_trees = []
1189        else:
1190            extra_trees = list(extra_trees)
1191        # The ids of items we need to examine to insure delta consistency.
1192        precise_file_ids = set()
1193        changed_file_ids = []
1194        if specific_files == []:
1195            target_specific_files = []
1196            source_specific_files = []
1197        else:
1198            target_specific_files = self.target.find_related_paths_across_trees(
1199                specific_files, [self.source] + extra_trees,
1200                require_versioned=require_versioned)
1201            source_specific_files = self.source.find_related_paths_across_trees(
1202                specific_files, [self.target] + extra_trees,
1203                require_versioned=require_versioned)
1204        if specific_files is not None:
1205            # reparented or added entries must have their parents included
1206            # so that valid deltas can be created. The seen_parents set
1207            # tracks the parents that we need to have.
1208            # The seen_dirs set tracks directory entries we've yielded.
1209            # After outputting version object in to_entries we set difference
1210            # the two seen sets and start checking parents.
1211            seen_parents = set()
1212            seen_dirs = set()
1213        if want_unversioned:
1214            all_unversioned = sorted([(p.split('/'), p) for p in
1215                                      self.target.extras()
1216                                      if specific_files is None or
1217                                      osutils.is_inside_any(specific_files, p)])
1218            all_unversioned = deque(all_unversioned)
1219        else:
1220            all_unversioned = deque()
1221        to_paths = {}
1222        from_entries_by_dir = list(self.source.iter_entries_by_dir(
1223            specific_files=source_specific_files))
1224        from_data = dict(from_entries_by_dir)
1225        to_entries_by_dir = list(self.target.iter_entries_by_dir(
1226            specific_files=target_specific_files))
1227        path_equivs = self.find_source_paths([p for p, e in to_entries_by_dir])
1228        num_entries = len(from_entries_by_dir) + len(to_entries_by_dir)
1229        entry_count = 0
1230        # the unversioned path lookup only occurs on real trees - where there
1231        # can be extras. So the fake_entry is solely used to look up
1232        # executable it values when execute is not supported.
1233        fake_entry = TreeFile()
1234        for target_path, target_entry in to_entries_by_dir:
1235            while (all_unversioned and
1236                   all_unversioned[0][0] < target_path.split('/')):
1237                unversioned_path = all_unversioned.popleft()
1238                target_kind, target_executable, target_stat = \
1239                    self.target._comparison_data(
1240                        fake_entry, unversioned_path[1])
1241                yield InventoryTreeChange(
1242                    None, (None, unversioned_path[1]), True, (False, False),
1243                    (None, None),
1244                    (None, unversioned_path[0][-1]),
1245                    (None, target_kind),
1246                    (None, target_executable))
1247            source_path = path_equivs[target_path]
1248            if source_path is not None:
1249                source_entry = from_data.get(source_path)
1250            else:
1251                source_entry = None
1252            result, changes = self._changes_from_entries(
1253                source_entry, target_entry, source_path=source_path, target_path=target_path)
1254            to_paths[result.file_id] = result.path[1]
1255            entry_count += 1
1256            if result.versioned[0]:
1257                entry_count += 1
1258            if pb is not None:
1259                pb.update('comparing files', entry_count, num_entries)
1260            if changes or include_unchanged:
1261                if specific_files is not None:
1262                    precise_file_ids.add(result.parent_id[1])
1263                    changed_file_ids.append(result.file_id)
1264                yield result
1265            # Ensure correct behaviour for reparented/added specific files.
1266            if specific_files is not None:
1267                # Record output dirs
1268                if result.kind[1] == 'directory':
1269                    seen_dirs.add(result.file_id)
1270                # Record parents of reparented/added entries.
1271                if not result.versioned[0] or result.is_reparented():
1272                    seen_parents.add(result.parent_id[1])
1273        while all_unversioned:
1274            # yield any trailing unversioned paths
1275            unversioned_path = all_unversioned.popleft()
1276            to_kind, to_executable, to_stat = \
1277                self.target._comparison_data(fake_entry, unversioned_path[1])
1278            yield InventoryTreeChange(
1279                None, (None, unversioned_path[1]), True, (False, False),
1280                (None, None),
1281                (None, unversioned_path[0][-1]),
1282                (None, to_kind),
1283                (None, to_executable))
1284        # Yield all remaining source paths
1285        for path, from_entry in from_entries_by_dir:
1286            file_id = from_entry.file_id
1287            if file_id in to_paths:
1288                # already returned
1289                continue
1290            to_path = self.find_target_path(path)
1291            entry_count += 1
1292            if pb is not None:
1293                pb.update('comparing files', entry_count, num_entries)
1294            versioned = (True, False)
1295            parent = (from_entry.parent_id, None)
1296            name = (from_entry.name, None)
1297            from_kind, from_executable, stat_value = \
1298                self.source._comparison_data(from_entry, path)
1299            kind = (from_kind, None)
1300            executable = (from_executable, None)
1301            changed_content = from_kind is not None
1302            # the parent's path is necessarily known at this point.
1303            changed_file_ids.append(file_id)
1304            yield InventoryTreeChange(
1305                file_id, (path, to_path), changed_content, versioned, parent,
1306                name, kind, executable)
1307        changed_file_ids = set(changed_file_ids)
1308        if specific_files is not None:
1309            for result in self._handle_precise_ids(precise_file_ids,
1310                                                   changed_file_ids):
1311                yield result
1312
1313    @staticmethod
1314    def _get_entry(tree, path):
1315        """Get an inventory entry from a tree, with missing entries as None.
1316
1317        If the tree raises NotImplementedError on accessing .inventory, then
1318        this is worked around using iter_entries_by_dir on just the file id
1319        desired.
1320
1321        :param tree: The tree to lookup the entry in.
1322        :param path: The path to look up
1323        """
1324        # No inventory available.
1325        try:
1326            iterator = tree.iter_entries_by_dir(specific_files=[path])
1327            return next(iterator)[1]
1328        except StopIteration:
1329            return None
1330
1331    def _handle_precise_ids(self, precise_file_ids, changed_file_ids,
1332                            discarded_changes=None):
1333        """Fill out a partial iter_changes to be consistent.
1334
1335        :param precise_file_ids: The file ids of parents that were seen during
1336            the iter_changes.
1337        :param changed_file_ids: The file ids of already emitted items.
1338        :param discarded_changes: An optional dict of precalculated
1339            iter_changes items which the partial iter_changes had not output
1340            but had calculated.
1341        :return: A generator of iter_changes items to output.
1342        """
1343        # process parents of things that had changed under the users
1344        # requested paths to prevent incorrect paths or parent ids which
1345        # aren't in the tree.
1346        while precise_file_ids:
1347            precise_file_ids.discard(None)
1348            # Don't emit file_ids twice
1349            precise_file_ids.difference_update(changed_file_ids)
1350            if not precise_file_ids:
1351                break
1352            # If the there was something at a given output path in source, we
1353            # have to include the entry from source in the delta, or we would
1354            # be putting this entry into a used path.
1355            paths = []
1356            for parent_id in precise_file_ids:
1357                try:
1358                    paths.append(self.target.id2path(parent_id))
1359                except errors.NoSuchId:
1360                    # This id has been dragged in from the source by delta
1361                    # expansion and isn't present in target at all: we don't
1362                    # need to check for path collisions on it.
1363                    pass
1364            for path in paths:
1365                old_id = self.source.path2id(path)
1366                precise_file_ids.add(old_id)
1367            precise_file_ids.discard(None)
1368            current_ids = precise_file_ids
1369            precise_file_ids = set()
1370            # We have to emit all of precise_file_ids that have been altered.
1371            # We may have to output the children of some of those ids if any
1372            # directories have stopped being directories.
1373            for file_id in current_ids:
1374                # Examine file_id
1375                if discarded_changes:
1376                    result = discarded_changes.get(file_id)
1377                    source_entry = None
1378                else:
1379                    result = None
1380                if result is None:
1381                    try:
1382                        source_path = self.source.id2path(file_id)
1383                    except errors.NoSuchId:
1384                        source_path = None
1385                        source_entry = None
1386                    else:
1387                        source_entry = self._get_entry(
1388                            self.source, source_path)
1389                    try:
1390                        target_path = self.target.id2path(file_id)
1391                    except errors.NoSuchId:
1392                        target_path = None
1393                        target_entry = None
1394                    else:
1395                        target_entry = self._get_entry(
1396                            self.target, target_path)
1397                    result, changes = self._changes_from_entries(
1398                        source_entry, target_entry, source_path, target_path)
1399                else:
1400                    changes = True
1401                # Get this parents parent to examine.
1402                new_parent_id = result.parent_id[1]
1403                precise_file_ids.add(new_parent_id)
1404                if changes:
1405                    if (result.kind[0] == 'directory' and
1406                            result.kind[1] != 'directory'):
1407                        # This stopped being a directory, the old children have
1408                        # to be included.
1409                        if source_entry is None:
1410                            # Reusing a discarded change.
1411                            source_entry = self._get_entry(
1412                                self.source, result.path[0])
1413                        precise_file_ids.update(
1414                            child.file_id
1415                            for child in self.source.iter_child_entries(result.path[0]))
1416                    changed_file_ids.add(result.file_id)
1417                    yield result
1418
1419    def find_target_path(self, path, recurse='none'):
1420        """Find target tree path.
1421
1422        :param path: Path to search for (exists in source)
1423        :return: path in target, or None if there is no equivalent path.
1424        :raise NoSuchFile: If the path doesn't exist in source
1425        """
1426        file_id = self.source.path2id(path)
1427        if file_id is None:
1428            raise errors.NoSuchFile(path)
1429        try:
1430            return self.target.id2path(file_id, recurse=recurse)
1431        except errors.NoSuchId:
1432            return None
1433
1434    def find_source_path(self, path, recurse='none'):
1435        """Find the source tree path.
1436
1437        :param path: Path to search for (exists in target)
1438        :return: path in source, or None if there is no equivalent path.
1439        :raise NoSuchFile: if the path doesn't exist in target
1440        """
1441        file_id = self.target.path2id(path)
1442        if file_id is None:
1443            raise errors.NoSuchFile(path)
1444        try:
1445            return self.source.id2path(file_id, recurse=recurse)
1446        except errors.NoSuchId:
1447            return None
1448
1449
1450InterTree.register_optimiser(InterInventoryTree)
1451
1452
1453class InterCHKRevisionTree(InterInventoryTree):
1454    """Fast path optimiser for RevisionTrees with CHK inventories."""
1455
1456    @staticmethod
1457    def is_compatible(source, target):
1458        if (isinstance(source, RevisionTree) and
1459                isinstance(target, RevisionTree)):
1460            try:
1461                # Only CHK inventories have id_to_entry attribute
1462                source.root_inventory.id_to_entry
1463                target.root_inventory.id_to_entry
1464                return True
1465            except AttributeError:
1466                pass
1467        return False
1468
1469    def iter_changes(self, include_unchanged=False,
1470                     specific_files=None, pb=None, extra_trees=[],
1471                     require_versioned=True, want_unversioned=False):
1472        lookup_trees = [self.source]
1473        if extra_trees:
1474            lookup_trees.extend(extra_trees)
1475        # The ids of items we need to examine to insure delta consistency.
1476        precise_file_ids = set()
1477        discarded_changes = {}
1478        if specific_files == []:
1479            specific_file_ids = []
1480        else:
1481            specific_file_ids = self.target.paths2ids(specific_files,
1482                                                      lookup_trees, require_versioned=require_versioned)
1483        # FIXME: It should be possible to delegate include_unchanged handling
1484        # to CHKInventory.iter_changes and do a better job there -- vila
1485        # 20090304
1486        changed_file_ids = set()
1487        # FIXME: nested tree support
1488        for result in self.target.root_inventory.iter_changes(
1489                self.source.root_inventory):
1490            result = InventoryTreeChange(*result)
1491            if specific_file_ids is not None:
1492                if result.file_id not in specific_file_ids:
1493                    # A change from the whole tree that we don't want to show yet.
1494                    # We may find that we need to show it for delta consistency, so
1495                    # stash it.
1496                    discarded_changes[result.file_id] = result
1497                    continue
1498                precise_file_ids.add(result.parent_id[1])
1499            yield result
1500            changed_file_ids.add(result.file_id)
1501        if specific_file_ids is not None:
1502            for result in self._handle_precise_ids(precise_file_ids,
1503                                                   changed_file_ids, discarded_changes=discarded_changes):
1504                yield result
1505        if include_unchanged:
1506            # CHKMap avoid being O(tree), so we go to O(tree) only if
1507            # required to.
1508            # Now walk the whole inventory, excluding the already yielded
1509            # file ids
1510            # FIXME: Support nested trees
1511            changed_file_ids = set(changed_file_ids)
1512            for relpath, entry in self.target.root_inventory.iter_entries():
1513                if (specific_file_ids is not None and
1514                        entry.file_id not in specific_file_ids):
1515                    continue
1516                if entry.file_id not in changed_file_ids:
1517                    yield InventoryTreeChange(
1518                        entry.file_id,
1519                        (relpath, relpath),  # Not renamed
1520                        False,  # Not modified
1521                        (True, True),  # Still  versioned
1522                        (entry.parent_id, entry.parent_id),
1523                        (entry.name, entry.name),
1524                        (entry.kind, entry.kind),
1525                        (entry.executable, entry.executable))
1526
1527
1528InterTree.register_optimiser(InterCHKRevisionTree)
1529