/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/inventorytree.py

  • Committer: Martin
  • Date: 2017-06-11 14:36:07 UTC
  • mto: This revision was merged to the branch mainline in revision 6688.
  • Revision ID: gzlist@googlemail.com-20170611143607-iuusrtbtuvgzrcho
Make test__rio pass on Python 3

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
 
 
20
from __future__ import absolute_import
 
21
 
 
22
import os
 
23
import re
 
24
 
 
25
from . import (
 
26
    errors,
 
27
    lazy_import,
 
28
    osutils,
 
29
    revision,
 
30
    )
 
31
from .mutabletree import (
 
32
    MutableTree,
 
33
    needs_tree_write_lock,
 
34
    )
 
35
from .revisiontree import (
 
36
    RevisionTree,
 
37
    )
 
38
lazy_import.lazy_import(globals(), """
 
39
from breezy import (
 
40
    add,
 
41
    controldir,
 
42
    inventory as _mod_inventory,
 
43
    trace,
 
44
    transport as _mod_transport,
 
45
    )
 
46
""")
 
47
from .decorators import needs_read_lock
 
48
from .sixish import (
 
49
    viewvalues,
 
50
    )
 
51
from .tree import InterTree, Tree
 
52
 
 
53
 
 
54
class InventoryTree(Tree):
 
55
    """A tree that relies on an inventory for its metadata.
 
56
 
 
57
    Trees contain an `Inventory` object, and also know how to retrieve
 
58
    file texts mentioned in the inventory, either from a working
 
59
    directory or from a store.
 
60
 
 
61
    It is possible for trees to contain files that are not described
 
62
    in their inventory or vice versa; for this use `filenames()`.
 
63
 
 
64
    Subclasses should set the _inventory attribute, which is considered
 
65
    private to external API users.
 
66
    """
 
67
 
 
68
    def get_canonical_inventory_paths(self, paths):
 
69
        """Like get_canonical_inventory_path() but works on multiple items.
 
70
 
 
71
        :param paths: A sequence of paths relative to the root of the tree.
 
72
        :return: A list of paths, with each item the corresponding input path
 
73
        adjusted to account for existing elements that match case
 
74
        insensitively.
 
75
        """
 
76
        return list(self._yield_canonical_inventory_paths(paths))
 
77
 
 
78
    def get_canonical_inventory_path(self, path):
 
79
        """Returns the first inventory item that case-insensitively matches path.
 
80
 
 
81
        If a path matches exactly, it is returned. If no path matches exactly
 
82
        but more than one path matches case-insensitively, it is implementation
 
83
        defined which is returned.
 
84
 
 
85
        If no path matches case-insensitively, the input path is returned, but
 
86
        with as many path entries that do exist changed to their canonical
 
87
        form.
 
88
 
 
89
        If you need to resolve many names from the same tree, you should
 
90
        use get_canonical_inventory_paths() to avoid O(N) behaviour.
 
91
 
 
92
        :param path: A paths relative to the root of the tree.
 
93
        :return: The input path adjusted to account for existing elements
 
94
        that match case insensitively.
 
95
        """
 
96
        return next(self._yield_canonical_inventory_paths([path]))
 
97
 
 
98
    def _yield_canonical_inventory_paths(self, paths):
 
99
        for path in paths:
 
100
            # First, if the path as specified exists exactly, just use it.
 
101
            if self.path2id(path) is not None:
 
102
                yield path
 
103
                continue
 
104
            # go walkin...
 
105
            cur_id = self.get_root_id()
 
106
            cur_path = ''
 
107
            bit_iter = iter(path.split("/"))
 
108
            for elt in bit_iter:
 
109
                lelt = elt.lower()
 
110
                new_path = None
 
111
                for child in self.iter_children(cur_id):
 
112
                    try:
 
113
                        # XXX: it seem like if the child is known to be in the
 
114
                        # tree, we shouldn't need to go from its id back to
 
115
                        # its path -- mbp 2010-02-11
 
116
                        #
 
117
                        # XXX: it seems like we could be more efficient
 
118
                        # by just directly looking up the original name and
 
119
                        # only then searching all children; also by not
 
120
                        # chopping paths so much. -- mbp 2010-02-11
 
121
                        child_base = os.path.basename(self.id2path(child))
 
122
                        if (child_base == elt):
 
123
                            # if we found an exact match, we can stop now; if
 
124
                            # we found an approximate match we need to keep
 
125
                            # searching because there might be an exact match
 
126
                            # later.  
 
127
                            cur_id = child
 
128
                            new_path = osutils.pathjoin(cur_path, child_base)
 
129
                            break
 
130
                        elif child_base.lower() == lelt:
 
131
                            cur_id = child
 
132
                            new_path = osutils.pathjoin(cur_path, child_base)
 
133
                    except errors.NoSuchId:
 
134
                        # before a change is committed we can see this error...
 
135
                        continue
 
136
                if new_path:
 
137
                    cur_path = new_path
 
138
                else:
 
139
                    # got to the end of this directory and no entries matched.
 
140
                    # Return what matched so far, plus the rest as specified.
 
141
                    cur_path = osutils.pathjoin(cur_path, elt, *list(bit_iter))
 
142
                    break
 
143
            yield cur_path
 
144
        # all done.
 
145
 
 
146
    def _get_root_inventory(self):
 
147
        return self._inventory
 
148
 
 
149
    root_inventory = property(_get_root_inventory,
 
150
        doc="Root inventory of this tree")
 
151
 
 
152
    def _unpack_file_id(self, file_id):
 
153
        """Find the inventory and inventory file id for a tree file id.
 
154
 
 
155
        :param file_id: The tree file id, as bytestring or tuple
 
156
        :return: Inventory and inventory file id
 
157
        """
 
158
        if isinstance(file_id, tuple):
 
159
            if len(file_id) != 1:
 
160
                raise ValueError("nested trees not yet supported: %r" % file_id)
 
161
            file_id = file_id[0]
 
162
        return self.root_inventory, file_id
 
163
 
 
164
    @needs_read_lock
 
165
    def path2id(self, path):
 
166
        """Return the id for path in this tree."""
 
167
        return self._path2inv_file_id(path)[1]
 
168
 
 
169
    def _path2inv_file_id(self, path):
 
170
        """Lookup a inventory and inventory file id by path.
 
171
 
 
172
        :param path: Path to look up
 
173
        :return: tuple with inventory and inventory file id
 
174
        """
 
175
        # FIXME: Support nested trees
 
176
        return self.root_inventory, self.root_inventory.path2id(path)
 
177
 
 
178
    def id2path(self, file_id):
 
179
        """Return the path for a file id.
 
180
 
 
181
        :raises NoSuchId:
 
182
        """
 
183
        inventory, file_id = self._unpack_file_id(file_id)
 
184
        return inventory.id2path(file_id)
 
185
 
 
186
    def has_id(self, file_id):
 
187
        inventory, file_id = self._unpack_file_id(file_id)
 
188
        return inventory.has_id(file_id)
 
189
 
 
190
    def has_or_had_id(self, file_id):
 
191
        inventory, file_id = self._unpack_file_id(file_id)
 
192
        return inventory.has_id(file_id)
 
193
 
 
194
    def all_file_ids(self):
 
195
        return {entry.file_id for path, entry in self.iter_entries_by_dir()}
 
196
 
 
197
    def filter_unversioned_files(self, paths):
 
198
        """Filter out paths that are versioned.
 
199
 
 
200
        :return: set of paths.
 
201
        """
 
202
        # NB: we specifically *don't* call self.has_filename, because for
 
203
        # WorkingTrees that can indicate files that exist on disk but that
 
204
        # are not versioned.
 
205
        return set((p for p in paths if self.path2id(p) is None))
 
206
 
 
207
    @needs_read_lock
 
208
    def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=False):
 
209
        """Walk the tree in 'by_dir' order.
 
210
 
 
211
        This will yield each entry in the tree as a (path, entry) tuple.
 
212
        The order that they are yielded is:
 
213
 
 
214
        See Tree.iter_entries_by_dir for details.
 
215
 
 
216
        :param yield_parents: If True, yield the parents from the root leading
 
217
            down to specific_file_ids that have been requested. This has no
 
218
            impact if specific_file_ids is None.
 
219
        """
 
220
        if specific_file_ids is None:
 
221
            inventory_file_ids = None
 
222
        else:
 
223
            inventory_file_ids = []
 
224
            for tree_file_id in specific_file_ids:
 
225
                inventory, inv_file_id = self._unpack_file_id(tree_file_id)
 
226
                if not inventory is self.root_inventory: # for now
 
227
                    raise AssertionError("%r != %r" % (
 
228
                        inventory, self.root_inventory))
 
229
                inventory_file_ids.append(inv_file_id)
 
230
        # FIXME: Handle nested trees
 
231
        return self.root_inventory.iter_entries_by_dir(
 
232
            specific_file_ids=inventory_file_ids, yield_parents=yield_parents)
 
233
 
 
234
    @needs_read_lock
 
235
    def iter_child_entries(self, file_id, path=None):
 
236
        inv, inv_file_id = self._unpack_file_id(file_id)
 
237
        return iter(viewvalues(inv[inv_file_id].children))
 
238
 
 
239
    def iter_children(self, file_id, path=None):
 
240
        """See Tree.iter_children."""
 
241
        entry = self.iter_entries_by_dir([file_id]).next()[1]
 
242
        for child in viewvalues(getattr(entry, 'children', {})):
 
243
            yield child.file_id
 
244
 
 
245
 
 
246
class MutableInventoryTree(MutableTree, InventoryTree):
 
247
 
 
248
    @needs_tree_write_lock
 
249
    def apply_inventory_delta(self, changes):
 
250
        """Apply changes to the inventory as an atomic operation.
 
251
 
 
252
        :param changes: An inventory delta to apply to the working tree's
 
253
            inventory.
 
254
        :return None:
 
255
        :seealso Inventory.apply_delta: For details on the changes parameter.
 
256
        """
 
257
        self.flush()
 
258
        inv = self.root_inventory
 
259
        inv.apply_delta(changes)
 
260
        self._write_inventory(inv)
 
261
 
 
262
    def _fix_case_of_inventory_path(self, path):
 
263
        """If our tree isn't case sensitive, return the canonical path"""
 
264
        if not self.case_sensitive:
 
265
            path = self.get_canonical_inventory_path(path)
 
266
        return path
 
267
 
 
268
    @needs_tree_write_lock
 
269
    def smart_add(self, file_list, recurse=True, action=None, save=True):
 
270
        """Version file_list, optionally recursing into directories.
 
271
 
 
272
        This is designed more towards DWIM for humans than API clarity.
 
273
        For the specific behaviour see the help for cmd_add().
 
274
 
 
275
        :param file_list: List of zero or more paths.  *NB: these are 
 
276
            interpreted relative to the process cwd, not relative to the 
 
277
            tree.*  (Add and most other tree methods use tree-relative
 
278
            paths.)
 
279
        :param action: A reporter to be called with the inventory, parent_ie,
 
280
            path and kind of the path being added. It may return a file_id if
 
281
            a specific one should be used.
 
282
        :param save: Save the inventory after completing the adds. If False
 
283
            this provides dry-run functionality by doing the add and not saving
 
284
            the inventory.
 
285
        :return: A tuple - files_added, ignored_files. files_added is the count
 
286
            of added files, and ignored_files is a dict mapping files that were
 
287
            ignored to the rule that caused them to be ignored.
 
288
        """
 
289
        # Not all mutable trees can have conflicts
 
290
        if getattr(self, 'conflicts', None) is not None:
 
291
            # Collect all related files without checking whether they exist or
 
292
            # are versioned. It's cheaper to do that once for all conflicts
 
293
            # than trying to find the relevant conflict for each added file.
 
294
            conflicts_related = set()
 
295
            for c in self.conflicts():
 
296
                conflicts_related.update(c.associated_filenames())
 
297
        else:
 
298
            conflicts_related = None
 
299
        adder = _SmartAddHelper(self, action, conflicts_related)
 
300
        adder.add(file_list, recurse=recurse)
 
301
        if save:
 
302
            invdelta = adder.get_inventory_delta()
 
303
            self.apply_inventory_delta(invdelta)
 
304
        return adder.added, adder.ignored
 
305
 
 
306
    def update_basis_by_delta(self, new_revid, delta):
 
307
        """Update the parents of this tree after a commit.
 
308
 
 
309
        This gives the tree one parent, with revision id new_revid. The
 
310
        inventory delta is applied to the current basis tree to generate the
 
311
        inventory for the parent new_revid, and all other parent trees are
 
312
        discarded.
 
313
 
 
314
        All the changes in the delta should be changes synchronising the basis
 
315
        tree with some or all of the working tree, with a change to a directory
 
316
        requiring that its contents have been recursively included. That is,
 
317
        this is not a general purpose tree modification routine, but a helper
 
318
        for commit which is not required to handle situations that do not arise
 
319
        outside of commit.
 
320
 
 
321
        See the inventory developers documentation for the theory behind
 
322
        inventory deltas.
 
323
 
 
324
        :param new_revid: The new revision id for the trees parent.
 
325
        :param delta: An inventory delta (see apply_inventory_delta) describing
 
326
            the changes from the current left most parent revision to new_revid.
 
327
        """
 
328
        # if the tree is updated by a pull to the branch, as happens in
 
329
        # WorkingTree2, when there was no separation between branch and tree,
 
330
        # then just clear merges, efficiency is not a concern for now as this
 
331
        # is legacy environments only, and they are slow regardless.
 
332
        if self.last_revision() == new_revid:
 
333
            self.set_parent_ids([new_revid])
 
334
            return
 
335
        # generic implementation based on Inventory manipulation. See
 
336
        # WorkingTree classes for optimised versions for specific format trees.
 
337
        basis = self.basis_tree()
 
338
        basis.lock_read()
 
339
        # TODO: Consider re-evaluating the need for this with CHKInventory
 
340
        # we don't strictly need to mutate an inventory for this
 
341
        # it only makes sense when apply_delta is cheaper than get_inventory()
 
342
        inventory = _mod_inventory.mutable_inventory_from_tree(basis)
 
343
        basis.unlock()
 
344
        inventory.apply_delta(delta)
 
345
        rev_tree = InventoryRevisionTree(self.branch.repository,
 
346
                                         inventory, new_revid)
 
347
        self.set_parent_trees([(new_revid, rev_tree)])
 
348
 
 
349
 
 
350
class _SmartAddHelper(object):
 
351
    """Helper for MutableTree.smart_add."""
 
352
 
 
353
    def get_inventory_delta(self):
 
354
        # GZ 2016-06-05: Returning view would probably be fine but currently
 
355
        # Inventory.apply_delta is documented as requiring a list of changes.
 
356
        return list(viewvalues(self._invdelta))
 
357
 
 
358
    def _get_ie(self, inv_path):
 
359
        """Retrieve the most up to date inventory entry for a path.
 
360
 
 
361
        :param inv_path: Normalized inventory path
 
362
        :return: Inventory entry (with possibly invalid .children for
 
363
            directories)
 
364
        """
 
365
        entry = self._invdelta.get(inv_path)
 
366
        if entry is not None:
 
367
            return entry[3]
 
368
        # Find a 'best fit' match if the filesystem is case-insensitive
 
369
        inv_path = self.tree._fix_case_of_inventory_path(inv_path)
 
370
        file_id = self.tree.path2id(inv_path)
 
371
        if file_id is not None:
 
372
            return self.tree.iter_entries_by_dir([file_id]).next()[1]
 
373
        return None
 
374
 
 
375
    def _convert_to_directory(self, this_ie, inv_path):
 
376
        """Convert an entry to a directory.
 
377
 
 
378
        :param this_ie: Inventory entry
 
379
        :param inv_path: Normalized path for the inventory entry
 
380
        :return: The new inventory entry
 
381
        """
 
382
        # Same as in _add_one below, if the inventory doesn't
 
383
        # think this is a directory, update the inventory
 
384
        this_ie = _mod_inventory.InventoryDirectory(
 
385
            this_ie.file_id, this_ie.name, this_ie.parent_id)
 
386
        self._invdelta[inv_path] = (inv_path, inv_path, this_ie.file_id,
 
387
            this_ie)
 
388
        return this_ie
 
389
 
 
390
    def _add_one_and_parent(self, parent_ie, path, kind, inv_path):
 
391
        """Add a new entry to the inventory and automatically add unversioned parents.
 
392
 
 
393
        :param parent_ie: Parent inventory entry if known, or None.  If
 
394
            None, the parent is looked up by name and used if present, otherwise it
 
395
            is recursively added.
 
396
        :param path: 
 
397
        :param kind: Kind of new entry (file, directory, etc)
 
398
        :param inv_path:
 
399
        :return: Inventory entry for path and a list of paths which have been added.
 
400
        """
 
401
        # Nothing to do if path is already versioned.
 
402
        # This is safe from infinite recursion because the tree root is
 
403
        # always versioned.
 
404
        inv_dirname = osutils.dirname(inv_path)
 
405
        dirname, basename = osutils.split(path)
 
406
        if parent_ie is None:
 
407
            # slower but does not need parent_ie
 
408
            this_ie = self._get_ie(inv_path)
 
409
            if this_ie is not None:
 
410
                return this_ie
 
411
            # its really not there : add the parent
 
412
            # note that the dirname use leads to some extra str copying etc but as
 
413
            # there are a limited number of dirs we can be nested under, it should
 
414
            # generally find it very fast and not recurse after that.
 
415
            parent_ie = self._add_one_and_parent(None,
 
416
                dirname, 'directory', 
 
417
                inv_dirname)
 
418
        # if the parent exists, but isn't a directory, we have to do the
 
419
        # kind change now -- really the inventory shouldn't pretend to know
 
420
        # the kind of wt files, but it does.
 
421
        if parent_ie.kind != 'directory':
 
422
            # nb: this relies on someone else checking that the path we're using
 
423
            # doesn't contain symlinks.
 
424
            parent_ie = self._convert_to_directory(parent_ie, inv_dirname)
 
425
        file_id = self.action(self.tree, parent_ie, path, kind)
 
426
        entry = _mod_inventory.make_entry(kind, basename, parent_ie.file_id,
 
427
            file_id=file_id)
 
428
        self._invdelta[inv_path] = (None, inv_path, entry.file_id, entry)
 
429
        self.added.append(inv_path)
 
430
        return entry
 
431
 
 
432
    def _gather_dirs_to_add(self, user_dirs):
 
433
        # only walk the minimal parents needed: we have user_dirs to override
 
434
        # ignores.
 
435
        prev_dir = None
 
436
 
 
437
        is_inside = osutils.is_inside_or_parent_of_any
 
438
        for path in sorted(user_dirs):
 
439
            if (prev_dir is None or not is_inside([prev_dir], path)):
 
440
                inv_path, this_ie = user_dirs[path]
 
441
                yield (path, inv_path, this_ie, None)
 
442
            prev_dir = path
 
443
 
 
444
    def __init__(self, tree, action, conflicts_related=None):
 
445
        self.tree = tree
 
446
        if action is None:
 
447
            self.action = add.AddAction()
 
448
        else:
 
449
            self.action = action
 
450
        self._invdelta = {}
 
451
        self.added = []
 
452
        self.ignored = {}
 
453
        if conflicts_related is None:
 
454
            self.conflicts_related = frozenset()
 
455
        else:
 
456
            self.conflicts_related = conflicts_related
 
457
 
 
458
    def add(self, file_list, recurse=True):
 
459
        from breezy.inventory import InventoryEntry
 
460
        if not file_list:
 
461
            # no paths supplied: add the entire tree.
 
462
            # FIXME: this assumes we are running in a working tree subdir :-/
 
463
            # -- vila 20100208
 
464
            file_list = [u'.']
 
465
 
 
466
        # expand any symlinks in the directory part, while leaving the
 
467
        # filename alone
 
468
        # only expanding if symlinks are supported avoids windows path bugs
 
469
        if osutils.has_symlinks():
 
470
            file_list = list(map(osutils.normalizepath, file_list))
 
471
 
 
472
        user_dirs = {}
 
473
        # validate user file paths and convert all paths to tree
 
474
        # relative : it's cheaper to make a tree relative path an abspath
 
475
        # than to convert an abspath to tree relative, and it's cheaper to
 
476
        # perform the canonicalization in bulk.
 
477
        for filepath in osutils.canonical_relpaths(self.tree.basedir, file_list):
 
478
            # validate user parameters. Our recursive code avoids adding new
 
479
            # files that need such validation
 
480
            if self.tree.is_control_filename(filepath):
 
481
                raise errors.ForbiddenControlFileError(filename=filepath)
 
482
 
 
483
            abspath = self.tree.abspath(filepath)
 
484
            kind = osutils.file_kind(abspath)
 
485
            # ensure the named path is added, so that ignore rules in the later
 
486
            # directory walk dont skip it.
 
487
            # we dont have a parent ie known yet.: use the relatively slower
 
488
            # inventory probing method
 
489
            inv_path, _ = osutils.normalized_filename(filepath)
 
490
            this_ie = self._get_ie(inv_path)
 
491
            if this_ie is None:
 
492
                this_ie = self._add_one_and_parent(None, filepath, kind, inv_path)
 
493
            if kind == 'directory':
 
494
                # schedule the dir for scanning
 
495
                user_dirs[filepath] = (inv_path, this_ie)
 
496
 
 
497
        if not recurse:
 
498
            # no need to walk any directories at all.
 
499
            return
 
500
 
 
501
        things_to_add = list(self._gather_dirs_to_add(user_dirs))
 
502
 
 
503
        illegalpath_re = re.compile(r'[\r\n]')
 
504
        for directory, inv_path, this_ie, parent_ie in things_to_add:
 
505
            # directory is tree-relative
 
506
            abspath = self.tree.abspath(directory)
 
507
 
 
508
            # get the contents of this directory.
 
509
 
 
510
            # find the kind of the path being added, and save stat_value
 
511
            # for reuse
 
512
            stat_value = None
 
513
            if this_ie is None:
 
514
                stat_value = osutils.file_stat(abspath)
 
515
                kind = osutils.file_kind_from_stat_mode(stat_value.st_mode)
 
516
            else:
 
517
                kind = this_ie.kind
 
518
            
 
519
            # allow AddAction to skip this file
 
520
            if self.action.skip_file(self.tree,  abspath,  kind,  stat_value):
 
521
                continue
 
522
            if not InventoryEntry.versionable_kind(kind):
 
523
                trace.warning("skipping %s (can't add file of kind '%s')",
 
524
                              abspath, kind)
 
525
                continue
 
526
            if illegalpath_re.search(directory):
 
527
                trace.warning("skipping %r (contains \\n or \\r)" % abspath)
 
528
                continue
 
529
            if directory in self.conflicts_related:
 
530
                # If the file looks like one generated for a conflict, don't
 
531
                # add it.
 
532
                trace.warning(
 
533
                    'skipping %s (generated to help resolve conflicts)',
 
534
                    abspath)
 
535
                continue
 
536
 
 
537
            if kind == 'directory' and directory != '':
 
538
                try:
 
539
                    transport = _mod_transport.get_transport_from_path(abspath)
 
540
                    controldir.ControlDirFormat.find_format(transport)
 
541
                    sub_tree = True
 
542
                except errors.NotBranchError:
 
543
                    sub_tree = False
 
544
                except errors.UnsupportedFormatError:
 
545
                    sub_tree = True
 
546
            else:
 
547
                sub_tree = False
 
548
 
 
549
            if this_ie is not None:
 
550
                pass
 
551
            elif sub_tree:
 
552
                # XXX: This is wrong; people *might* reasonably be trying to
 
553
                # add subtrees as subtrees.  This should probably only be done
 
554
                # in formats which can represent subtrees, and even then
 
555
                # perhaps only when the user asked to add subtrees.  At the
 
556
                # moment you can add them specially through 'join --reference',
 
557
                # which is perhaps reasonable: adding a new reference is a
 
558
                # special operation and can have a special behaviour.  mbp
 
559
                # 20070306
 
560
                trace.warning("skipping nested tree %r", abspath)
 
561
            else:
 
562
                this_ie = self._add_one_and_parent(parent_ie, directory, kind,
 
563
                    inv_path)
 
564
 
 
565
            if kind == 'directory' and not sub_tree:
 
566
                if this_ie.kind != 'directory':
 
567
                    this_ie = self._convert_to_directory(this_ie, inv_path)
 
568
 
 
569
                for subf in sorted(os.listdir(abspath)):
 
570
                    inv_f, _ = osutils.normalized_filename(subf)
 
571
                    # here we could use TreeDirectory rather than
 
572
                    # string concatenation.
 
573
                    subp = osutils.pathjoin(directory, subf)
 
574
                    # TODO: is_control_filename is very slow. Make it faster.
 
575
                    # TreeDirectory.is_control_filename could also make this
 
576
                    # faster - its impossible for a non root dir to have a
 
577
                    # control file.
 
578
                    if self.tree.is_control_filename(subp):
 
579
                        trace.mutter("skip control directory %r", subp)
 
580
                        continue
 
581
                    sub_invp = osutils.pathjoin(inv_path, inv_f)
 
582
                    entry = self._invdelta.get(sub_invp)
 
583
                    if entry is not None:
 
584
                        sub_ie = entry[3]
 
585
                    else:
 
586
                        sub_ie = this_ie.children.get(inv_f)
 
587
                    if sub_ie is not None:
 
588
                        # recurse into this already versioned subdir.
 
589
                        things_to_add.append((subp, sub_invp, sub_ie, this_ie))
 
590
                    else:
 
591
                        # user selection overrides ignores
 
592
                        # ignore while selecting files - if we globbed in the
 
593
                        # outer loop we would ignore user files.
 
594
                        ignore_glob = self.tree.is_ignored(subp)
 
595
                        if ignore_glob is not None:
 
596
                            self.ignored.setdefault(ignore_glob, []).append(subp)
 
597
                        else:
 
598
                            things_to_add.append((subp, sub_invp, None, this_ie))
 
599
 
 
600
 
 
601
class InventoryRevisionTree(RevisionTree,InventoryTree):
 
602
 
 
603
    def __init__(self, repository, inv, revision_id):
 
604
        RevisionTree.__init__(self, repository, revision_id)
 
605
        self._inventory = inv
 
606
 
 
607
    def get_file_mtime(self, file_id, path=None):
 
608
        inv, inv_file_id = self._unpack_file_id(file_id)
 
609
        ie = inv[inv_file_id]
 
610
        try:
 
611
            revision = self._repository.get_revision(ie.revision)
 
612
        except errors.NoSuchRevision:
 
613
            raise errors.FileTimestampUnavailable(self.id2path(file_id))
 
614
        return revision.timestamp
 
615
 
 
616
    def get_file_size(self, file_id):
 
617
        inv, inv_file_id = self._unpack_file_id(file_id)
 
618
        return inv[inv_file_id].text_size
 
619
 
 
620
    def get_file_sha1(self, file_id, path=None, stat_value=None):
 
621
        inv, inv_file_id = self._unpack_file_id(file_id)
 
622
        ie = inv[inv_file_id]
 
623
        if ie.kind == "file":
 
624
            return ie.text_sha1
 
625
        return None
 
626
 
 
627
    def get_file_revision(self, file_id, path=None):
 
628
        inv, inv_file_id = self._unpack_file_id(file_id)
 
629
        ie = inv[inv_file_id]
 
630
        return ie.revision
 
631
 
 
632
    def is_executable(self, file_id, path=None):
 
633
        inv, inv_file_id = self._unpack_file_id(file_id)
 
634
        ie = inv[inv_file_id]
 
635
        if ie.kind != "file":
 
636
            return False
 
637
        return ie.executable
 
638
 
 
639
    def has_filename(self, filename):
 
640
        return bool(self.path2id(filename))
 
641
 
 
642
    def list_files(self, include_root=False, from_dir=None, recursive=True):
 
643
        # The only files returned by this are those from the version
 
644
        if from_dir is None:
 
645
            from_dir_id = None
 
646
            inv = self.root_inventory
 
647
        else:
 
648
            inv, from_dir_id = self._path2inv_file_id(from_dir)
 
649
            if from_dir_id is None:
 
650
                # Directory not versioned
 
651
                return
 
652
        entries = inv.iter_entries(from_dir=from_dir_id, recursive=recursive)
 
653
        if inv.root is not None and not include_root and from_dir is None:
 
654
            # skip the root for compatability with the current apis.
 
655
            next(entries)
 
656
        for path, entry in entries:
 
657
            yield path, 'V', entry.kind, entry.file_id, entry
 
658
 
 
659
    def get_symlink_target(self, file_id, path=None):
 
660
        inv, inv_file_id = self._unpack_file_id(file_id)
 
661
        ie = inv[inv_file_id]
 
662
        # Inventories store symlink targets in unicode
 
663
        return ie.symlink_target
 
664
 
 
665
    def get_reference_revision(self, file_id, path=None):
 
666
        inv, inv_file_id = self._unpack_file_id(file_id)
 
667
        return inv[inv_file_id].reference_revision
 
668
 
 
669
    def get_root_id(self):
 
670
        if self.root_inventory.root:
 
671
            return self.root_inventory.root.file_id
 
672
 
 
673
    def kind(self, file_id):
 
674
        inv, inv_file_id = self._unpack_file_id(file_id)
 
675
        return inv[inv_file_id].kind
 
676
 
 
677
    def path_content_summary(self, path):
 
678
        """See Tree.path_content_summary."""
 
679
        inv, file_id = self._path2inv_file_id(path)
 
680
        if file_id is None:
 
681
            return ('missing', None, None, None)
 
682
        entry = inv[file_id]
 
683
        kind = entry.kind
 
684
        if kind == 'file':
 
685
            return (kind, entry.text_size, entry.executable, entry.text_sha1)
 
686
        elif kind == 'symlink':
 
687
            return (kind, None, None, entry.symlink_target)
 
688
        else:
 
689
            return (kind, None, None, None)
 
690
 
 
691
    def _comparison_data(self, entry, path):
 
692
        if entry is None:
 
693
            return None, False, None
 
694
        return entry.kind, entry.executable, None
 
695
 
 
696
    def _file_size(self, entry, stat_value):
 
697
        return entry.text_size
 
698
 
 
699
    def walkdirs(self, prefix=""):
 
700
        _directory = 'directory'
 
701
        inv, top_id = self._path2inv_file_id(prefix)
 
702
        if top_id is None:
 
703
            pending = []
 
704
        else:
 
705
            pending = [(prefix, '', _directory, None, top_id, None)]
 
706
        while pending:
 
707
            dirblock = []
 
708
            currentdir = pending.pop()
 
709
            # 0 - relpath, 1- basename, 2- kind, 3- stat, id, v-kind
 
710
            if currentdir[0]:
 
711
                relroot = currentdir[0] + '/'
 
712
            else:
 
713
                relroot = ""
 
714
            # FIXME: stash the node in pending
 
715
            entry = inv[currentdir[4]]
 
716
            for name, child in entry.sorted_children():
 
717
                toppath = relroot + name
 
718
                dirblock.append((toppath, name, child.kind, None,
 
719
                    child.file_id, child.kind
 
720
                    ))
 
721
            yield (currentdir[0], entry.file_id), dirblock
 
722
            # push the user specified dirs from dirblock
 
723
            for dir in reversed(dirblock):
 
724
                if dir[2] == _directory:
 
725
                    pending.append(dir)
 
726
 
 
727
    def iter_files_bytes(self, desired_files):
 
728
        """See Tree.iter_files_bytes.
 
729
 
 
730
        This version is implemented on top of Repository.iter_files_bytes"""
 
731
        repo_desired_files = [(f, self.get_file_revision(f), i)
 
732
                              for f, i in desired_files]
 
733
        try:
 
734
            for result in self._repository.iter_files_bytes(repo_desired_files):
 
735
                yield result
 
736
        except errors.RevisionNotPresent as e:
 
737
            raise errors.NoSuchFile(e.file_id)
 
738
 
 
739
    def annotate_iter(self, file_id,
 
740
                      default_revision=revision.CURRENT_REVISION):
 
741
        """See Tree.annotate_iter"""
 
742
        text_key = (file_id, self.get_file_revision(file_id))
 
743
        annotator = self._repository.texts.get_annotator()
 
744
        annotations = annotator.annotate_flat(text_key)
 
745
        return [(key[-1], line) for key, line in annotations]
 
746
 
 
747
    def __eq__(self, other):
 
748
        if self is other:
 
749
            return True
 
750
        if isinstance(other, InventoryRevisionTree):
 
751
            return (self.root_inventory == other.root_inventory)
 
752
        return False
 
753
 
 
754
    def __ne__(self, other):
 
755
        return not (self == other)
 
756
 
 
757
    def __hash__(self):
 
758
        raise ValueError('not hashable')
 
759
 
 
760
 
 
761
class InterCHKRevisionTree(InterTree):
 
762
    """Fast path optimiser for RevisionTrees with CHK inventories."""
 
763
 
 
764
    @staticmethod
 
765
    def is_compatible(source, target):
 
766
        if (isinstance(source, RevisionTree)
 
767
            and isinstance(target, RevisionTree)):
 
768
            try:
 
769
                # Only CHK inventories have id_to_entry attribute
 
770
                source.root_inventory.id_to_entry
 
771
                target.root_inventory.id_to_entry
 
772
                return True
 
773
            except AttributeError:
 
774
                pass
 
775
        return False
 
776
 
 
777
    def iter_changes(self, include_unchanged=False,
 
778
                     specific_files=None, pb=None, extra_trees=[],
 
779
                     require_versioned=True, want_unversioned=False):
 
780
        lookup_trees = [self.source]
 
781
        if extra_trees:
 
782
             lookup_trees.extend(extra_trees)
 
783
        # The ids of items we need to examine to insure delta consistency.
 
784
        precise_file_ids = set()
 
785
        discarded_changes = {}
 
786
        if specific_files == []:
 
787
            specific_file_ids = []
 
788
        else:
 
789
            specific_file_ids = self.target.paths2ids(specific_files,
 
790
                lookup_trees, require_versioned=require_versioned)
 
791
        # FIXME: It should be possible to delegate include_unchanged handling
 
792
        # to CHKInventory.iter_changes and do a better job there -- vila
 
793
        # 20090304
 
794
        changed_file_ids = set()
 
795
        # FIXME: nested tree support
 
796
        for result in self.target.root_inventory.iter_changes(
 
797
                self.source.root_inventory):
 
798
            if specific_file_ids is not None:
 
799
                file_id = result[0]
 
800
                if file_id not in specific_file_ids:
 
801
                    # A change from the whole tree that we don't want to show yet.
 
802
                    # We may find that we need to show it for delta consistency, so
 
803
                    # stash it.
 
804
                    discarded_changes[result[0]] = result
 
805
                    continue
 
806
                new_parent_id = result[4][1]
 
807
                precise_file_ids.add(new_parent_id)
 
808
            yield result
 
809
            changed_file_ids.add(result[0])
 
810
        if specific_file_ids is not None:
 
811
            for result in self._handle_precise_ids(precise_file_ids,
 
812
                changed_file_ids, discarded_changes=discarded_changes):
 
813
                yield result
 
814
        if include_unchanged:
 
815
            # CHKMap avoid being O(tree), so we go to O(tree) only if
 
816
            # required to.
 
817
            # Now walk the whole inventory, excluding the already yielded
 
818
            # file ids
 
819
            # FIXME: Support nested trees
 
820
            changed_file_ids = set(changed_file_ids)
 
821
            for relpath, entry in self.target.root_inventory.iter_entries():
 
822
                if (specific_file_ids is not None
 
823
                    and not entry.file_id in specific_file_ids):
 
824
                    continue
 
825
                if not entry.file_id in changed_file_ids:
 
826
                    yield (entry.file_id,
 
827
                           (relpath, relpath), # Not renamed
 
828
                           False, # Not modified
 
829
                           (True, True), # Still  versioned
 
830
                           (entry.parent_id, entry.parent_id),
 
831
                           (entry.name, entry.name),
 
832
                           (entry.kind, entry.kind),
 
833
                           (entry.executable, entry.executable))
 
834
 
 
835
 
 
836
InterTree.register_optimiser(InterCHKRevisionTree)