/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: Jelmer Vernooij
  • Date: 2017-07-20 00:00:04 UTC
  • mfrom: (6690.5.2 bundle-guess)
  • Revision ID: jelmer@jelmer.uk-20170720000004-wlknc5gthdk3tokn
Merge lp:~jelmer/brz/bundle-guess.

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