/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/bzr/inventorytree.py

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