/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 bzrlib/tree.py

Implement _bisect_recursive, which uses multiple bisect calls to
handle renames and finding entries in subdirs.
As is, this could be hooked into paths2ids() if the dirstate has not been loaded yet.
However, it doesn't quite provide enough, since the parsed entries would probably not
be saved. Further, the multiple bisect calls are less efficient then they could be,
because they do not remember the last bisect call.
We should explore switching to a caching structure, which maintains all records that
have been processed, in a structure that can be in-memory searched before going back
to disk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""Tree classes, representing directory at point in time.
 
18
"""
 
19
 
 
20
import os
 
21
from cStringIO import StringIO
 
22
 
 
23
import bzrlib
 
24
from bzrlib import (
 
25
    delta,
 
26
    osutils,
 
27
    symbol_versioning,
 
28
    )
 
29
from bzrlib.decorators import needs_read_lock
 
30
from bzrlib.errors import BzrError, BzrCheckError
 
31
from bzrlib import errors
 
32
from bzrlib.inventory import Inventory
 
33
from bzrlib.inter import InterObject
 
34
from bzrlib.osutils import fingerprint_file
 
35
import bzrlib.revision
 
36
from bzrlib.trace import mutter, note
 
37
 
 
38
 
 
39
class Tree(object):
 
40
    """Abstract file tree.
 
41
 
 
42
    There are several subclasses:
 
43
    
 
44
    * `WorkingTree` exists as files on disk editable by the user.
 
45
 
 
46
    * `RevisionTree` is a tree as recorded at some point in the past.
 
47
 
 
48
    Trees contain an `Inventory` object, and also know how to retrieve
 
49
    file texts mentioned in the inventory, either from a working
 
50
    directory or from a store.
 
51
 
 
52
    It is possible for trees to contain files that are not described
 
53
    in their inventory or vice versa; for this use `filenames()`.
 
54
 
 
55
    Trees can be compared, etc, regardless of whether they are working
 
56
    trees or versioned trees.
 
57
    """
 
58
    
 
59
    def changes_from(self, other, want_unchanged=False, specific_files=None,
 
60
        extra_trees=None, require_versioned=False, include_root=False):
 
61
        """Return a TreeDelta of the changes from other to this tree.
 
62
 
 
63
        :param other: A tree to compare with.
 
64
        :param specific_files: An optional list of file paths to restrict the
 
65
            comparison to. When mapping filenames to ids, all matches in all
 
66
            trees (including optional extra_trees) are used, and all children of
 
67
            matched directories are included.
 
68
        :param want_unchanged: An optional boolean requesting the inclusion of
 
69
            unchanged entries in the result.
 
70
        :param extra_trees: An optional list of additional trees to use when
 
71
            mapping the contents of specific_files (paths) to file_ids.
 
72
        :param require_versioned: An optional boolean (defaults to False). When
 
73
            supplied and True all the 'specific_files' must be versioned, or
 
74
            a PathsNotVersionedError will be thrown.
 
75
 
 
76
        The comparison will be performed by an InterTree object looked up on 
 
77
        self and other.
 
78
        """
 
79
        # Martin observes that Tree.changes_from returns a TreeDelta and this
 
80
        # may confuse people, because the class name of the returned object is
 
81
        # a synonym of the object referenced in the method name.
 
82
        return InterTree.get(other, self).compare(
 
83
            want_unchanged=want_unchanged,
 
84
            specific_files=specific_files,
 
85
            extra_trees=extra_trees,
 
86
            require_versioned=require_versioned,
 
87
            include_root=include_root
 
88
            )
 
89
 
 
90
    def _iter_changes(self, from_tree, include_unchanged=False,
 
91
                     specific_file_ids=None, pb=None):
 
92
        intertree = InterTree.get(from_tree, self)
 
93
        return intertree._iter_changes(include_unchanged,
 
94
                                       specific_file_ids, pb)
 
95
    
 
96
    def conflicts(self):
 
97
        """Get a list of the conflicts in the tree.
 
98
 
 
99
        Each conflict is an instance of bzrlib.conflicts.Conflict.
 
100
        """
 
101
        return []
 
102
 
 
103
    def get_parent_ids(self):
 
104
        """Get the parent ids for this tree. 
 
105
 
 
106
        :return: a list of parent ids. [] is returned to indicate
 
107
        a tree with no parents.
 
108
        :raises: BzrError if the parents are not known.
 
109
        """
 
110
        raise NotImplementedError(self.get_parent_ids)
 
111
    
 
112
    def has_filename(self, filename):
 
113
        """True if the tree has given filename."""
 
114
        raise NotImplementedError()
 
115
 
 
116
    def has_id(self, file_id):
 
117
        file_id = osutils.safe_file_id(file_id)
 
118
        return self.inventory.has_id(file_id)
 
119
 
 
120
    __contains__ = has_id
 
121
 
 
122
    def has_or_had_id(self, file_id):
 
123
        file_id = osutils.safe_file_id(file_id)
 
124
        if file_id == self.inventory.root.file_id:
 
125
            return True
 
126
        return self.inventory.has_id(file_id)
 
127
 
 
128
    def __iter__(self):
 
129
        return iter(self.inventory)
 
130
 
 
131
    def id2path(self, file_id):
 
132
        file_id = osutils.safe_file_id(file_id)
 
133
        return self.inventory.id2path(file_id)
 
134
 
 
135
    def is_control_filename(self, filename):
 
136
        """True if filename is the name of a control file in this tree.
 
137
        
 
138
        :param filename: A filename within the tree. This is a relative path
 
139
        from the root of this tree.
 
140
 
 
141
        This is true IF and ONLY IF the filename is part of the meta data
 
142
        that bzr controls in this tree. I.E. a random .bzr directory placed
 
143
        on disk will not be a control file for this tree.
 
144
        """
 
145
        return self.bzrdir.is_control_filename(filename)
 
146
 
 
147
    @needs_read_lock
 
148
    def iter_entries_by_dir(self, specific_file_ids=None):
 
149
        """Walk the tree in 'by_dir' order.
 
150
 
 
151
        This will yield each entry in the tree as a (path, entry) tuple. The
 
152
        order that they are yielded is: the contents of a directory are 
 
153
        preceeded by the parent of a directory, and all the contents of a 
 
154
        directory are grouped together.
 
155
        """
 
156
        return self.inventory.iter_entries_by_dir(
 
157
            specific_file_ids=specific_file_ids)
 
158
 
 
159
    def kind(self, file_id):
 
160
        raise NotImplementedError("subclasses must implement kind")
 
161
 
 
162
    def _comparison_data(self, entry, path):
 
163
        """Return a tuple of kind, executable, stat_value for a file.
 
164
 
 
165
        entry may be None if there is no inventory entry for the file, but
 
166
        path must always be supplied.
 
167
 
 
168
        kind is None if there is no file present (even if an inventory id is
 
169
        present).  executable is False for non-file entries.
 
170
        """
 
171
        raise NotImplementedError(self._comparison_data)
 
172
 
 
173
    def _file_size(self, entry, stat_value):
 
174
        raise NotImplementedError(self._file_size)
 
175
 
 
176
    def _get_inventory(self):
 
177
        return self._inventory
 
178
    
 
179
    def get_file(self, file_id):
 
180
        """Return a file object for the file file_id in the tree."""
 
181
        raise NotImplementedError(self.get_file)
 
182
    
 
183
    def get_file_by_path(self, path):
 
184
        return self.get_file(self._inventory.path2id(path))
 
185
 
 
186
    def annotate_iter(self, file_id):
 
187
        """Return an iterator of revision_id, line tuples
 
188
 
 
189
        For working trees (and mutable trees in general), the special
 
190
        revision_id 'current:' will be used for lines that are new in this
 
191
        tree, e.g. uncommitted changes.
 
192
        :param file_id: The file to produce an annotated version from
 
193
        """
 
194
        raise NotImplementedError(self.annotate_iter)
 
195
 
 
196
    inventory = property(_get_inventory,
 
197
                         doc="Inventory of this Tree")
 
198
 
 
199
    def _check_retrieved(self, ie, f):
 
200
        if not __debug__:
 
201
            return  
 
202
        fp = fingerprint_file(f)
 
203
        f.seek(0)
 
204
        
 
205
        if ie.text_size is not None:
 
206
            if ie.text_size != fp['size']:
 
207
                raise BzrError("mismatched size for file %r in %r" % (ie.file_id, self._store),
 
208
                        ["inventory expects %d bytes" % ie.text_size,
 
209
                         "file is actually %d bytes" % fp['size'],
 
210
                         "store is probably damaged/corrupt"])
 
211
 
 
212
        if ie.text_sha1 != fp['sha1']:
 
213
            raise BzrError("wrong SHA-1 for file %r in %r" % (ie.file_id, self._store),
 
214
                    ["inventory expects %s" % ie.text_sha1,
 
215
                     "file is actually %s" % fp['sha1'],
 
216
                     "store is probably damaged/corrupt"])
 
217
 
 
218
    def path2id(self, path):
 
219
        """Return the id for path in this tree."""
 
220
        return self._inventory.path2id(path)
 
221
 
 
222
    def paths2ids(self, paths, trees=[], require_versioned=True):
 
223
        """Return all the ids that can be reached by walking from paths.
 
224
        
 
225
        Each path is looked up in each this tree and any extras provided in
 
226
        trees, and this is repeated recursively: the children in an extra tree
 
227
        of a directory that has been renamed under a provided path in this tree
 
228
        are all returned, even if none exist until a provided path in this
 
229
        tree, and vice versa.
 
230
 
 
231
        :param paths: An iterable of paths to start converting to ids from.
 
232
            Alternatively, if paths is None, no ids should be calculated and None
 
233
            will be returned. This is offered to make calling the api unconditional
 
234
            for code that *might* take a list of files.
 
235
        :param trees: Additional trees to consider.
 
236
        :param require_versioned: If False, do not raise NotVersionedError if
 
237
            an element of paths is not versioned in this tree and all of trees.
 
238
        """
 
239
        return find_ids_across_trees(paths, [self] + list(trees), require_versioned)
 
240
 
 
241
    def print_file(self, file_id):
 
242
        """Print file with id `file_id` to stdout."""
 
243
        file_id = osutils.safe_file_id(file_id)
 
244
        import sys
 
245
        sys.stdout.write(self.get_file_text(file_id))
 
246
 
 
247
    def lock_read(self):
 
248
        pass
 
249
 
 
250
    def revision_tree(self, revision_id):
 
251
        """Obtain a revision tree for the revision revision_id.
 
252
 
 
253
        The intention of this method is to allow access to possibly cached
 
254
        tree data. Implementors of this method should raise NoSuchRevision if
 
255
        the tree is not locally available, even if they could obtain the 
 
256
        tree via a repository or some other means. Callers are responsible 
 
257
        for finding the ultimate source for a revision tree.
 
258
 
 
259
        :param revision_id: The revision_id of the requested tree.
 
260
        :return: A Tree.
 
261
        :raises: NoSuchRevision if the tree cannot be obtained.
 
262
        """
 
263
        raise errors.NoSuchRevisionInTree(self, revision_id)
 
264
 
 
265
    def unknowns(self):
 
266
        """What files are present in this tree and unknown.
 
267
        
 
268
        :return: an iterator over the unknown files.
 
269
        """
 
270
        return iter([])
 
271
 
 
272
    def unlock(self):
 
273
        pass
 
274
 
 
275
    def filter_unversioned_files(self, paths):
 
276
        """Filter out paths that are not versioned.
 
277
 
 
278
        :return: set of paths.
 
279
        """
 
280
        # NB: we specifically *don't* call self.has_filename, because for
 
281
        # WorkingTrees that can indicate files that exist on disk but that 
 
282
        # are not versioned.
 
283
        pred = self.inventory.has_filename
 
284
        return set((p for p in paths if not pred(p)))
 
285
 
 
286
    def walkdirs(self, prefix=""):
 
287
        """Walk the contents of this tree from path down.
 
288
 
 
289
        This yields all the data about the contents of a directory at a time.
 
290
        After each directory has been yielded, if the caller has mutated the
 
291
        list to exclude some directories, they are then not descended into.
 
292
        
 
293
        The data yielded is of the form:
 
294
        ((directory-relpath, directory-path-from-root, directory-fileid),
 
295
        [(relpath, basename, kind, lstat, path_from_tree_root, file_id, 
 
296
          versioned_kind), ...]),
 
297
         - directory-relpath is the containing dirs relpath from prefix
 
298
         - directory-path-from-root is the containing dirs path from /
 
299
         - directory-fileid is the id of the directory if it is versioned.
 
300
         - relpath is the relative path within the subtree being walked.
 
301
         - basename is the basename
 
302
         - kind is the kind of the file now. If unknonwn then the file is not
 
303
           present within the tree - but it may be recorded as versioned. See
 
304
           versioned_kind.
 
305
         - lstat is the stat data *if* the file was statted.
 
306
         - path_from_tree_root is the path from the root of the tree.
 
307
         - file_id is the file_id is the entry is versioned.
 
308
         - versioned_kind is the kind of the file as last recorded in the 
 
309
           versioning system. If 'unknown' the file is not versioned.
 
310
        One of 'kind' and 'versioned_kind' must not be 'unknown'.
 
311
 
 
312
        :param prefix: Start walking from prefix within the tree rather than
 
313
        at the root. This allows one to walk a subtree but get paths that are
 
314
        relative to a tree rooted higher up.
 
315
        :return: an iterator over the directory data.
 
316
        """
 
317
        raise NotImplementedError(self.walkdirs)
 
318
 
 
319
 
 
320
class EmptyTree(Tree):
 
321
 
 
322
    def __init__(self):
 
323
        self._inventory = Inventory(root_id=None)
 
324
        symbol_versioning.warn('EmptyTree is deprecated as of bzr 0.9 please'
 
325
                               ' use repository.revision_tree instead.',
 
326
                               DeprecationWarning, stacklevel=2)
 
327
 
 
328
    def get_parent_ids(self):
 
329
        return []
 
330
 
 
331
    def get_symlink_target(self, file_id):
 
332
        return None
 
333
 
 
334
    def has_filename(self, filename):
 
335
        return False
 
336
 
 
337
    def kind(self, file_id):
 
338
        file_id = osutils.safe_file_id(file_id)
 
339
        assert self._inventory[file_id].kind == "directory"
 
340
        return "directory"
 
341
 
 
342
    def list_files(self, include_root=False):
 
343
        return iter([])
 
344
    
 
345
    def __contains__(self, file_id):
 
346
        file_id = osutils.safe_file_id(file_id)
 
347
        return (file_id in self._inventory)
 
348
 
 
349
    def get_file_sha1(self, file_id, path=None, stat_value=None):
 
350
        return None
 
351
 
 
352
 
 
353
######################################################################
 
354
# diff
 
355
 
 
356
# TODO: Merge these two functions into a single one that can operate
 
357
# on either a whole tree or a set of files.
 
358
 
 
359
# TODO: Return the diff in order by filename, not by category or in
 
360
# random order.  Can probably be done by lock-stepping through the
 
361
# filenames from both trees.
 
362
 
 
363
 
 
364
def file_status(filename, old_tree, new_tree):
 
365
    """Return single-letter status, old and new names for a file.
 
366
 
 
367
    The complexity here is in deciding how to represent renames;
 
368
    many complex cases are possible.
 
369
    """
 
370
    old_inv = old_tree.inventory
 
371
    new_inv = new_tree.inventory
 
372
    new_id = new_inv.path2id(filename)
 
373
    old_id = old_inv.path2id(filename)
 
374
 
 
375
    if not new_id and not old_id:
 
376
        # easy: doesn't exist in either; not versioned at all
 
377
        if new_tree.is_ignored(filename):
 
378
            return 'I', None, None
 
379
        else:
 
380
            return '?', None, None
 
381
    elif new_id:
 
382
        # There is now a file of this name, great.
 
383
        pass
 
384
    else:
 
385
        # There is no longer a file of this name, but we can describe
 
386
        # what happened to the file that used to have
 
387
        # this name.  There are two possibilities: either it was
 
388
        # deleted entirely, or renamed.
 
389
        assert old_id
 
390
        if new_inv.has_id(old_id):
 
391
            return 'X', old_inv.id2path(old_id), new_inv.id2path(old_id)
 
392
        else:
 
393
            return 'D', old_inv.id2path(old_id), None
 
394
 
 
395
    # if the file_id is new in this revision, it is added
 
396
    if new_id and not old_inv.has_id(new_id):
 
397
        return 'A'
 
398
 
 
399
    # if there used to be a file of this name, but that ID has now
 
400
    # disappeared, it is deleted
 
401
    if old_id and not new_inv.has_id(old_id):
 
402
        return 'D'
 
403
 
 
404
    return 'wtf?'
 
405
 
 
406
    
 
407
 
 
408
def find_renames(old_inv, new_inv):
 
409
    for file_id in old_inv:
 
410
        if file_id not in new_inv:
 
411
            continue
 
412
        old_name = old_inv.id2path(file_id)
 
413
        new_name = new_inv.id2path(file_id)
 
414
        if old_name != new_name:
 
415
            yield (old_name, new_name)
 
416
            
 
417
 
 
418
def find_ids_across_trees(filenames, trees, require_versioned=True):
 
419
    """Find the ids corresponding to specified filenames.
 
420
    
 
421
    All matches in all trees will be used, and all children of matched
 
422
    directories will be used.
 
423
 
 
424
    :param filenames: The filenames to find file_ids for (if None, returns
 
425
        None)
 
426
    :param trees: The trees to find file_ids within
 
427
    :param require_versioned: if true, all specified filenames must occur in
 
428
    at least one tree.
 
429
    :return: a set of file ids for the specified filenames and their children.
 
430
    """
 
431
    if not filenames:
 
432
        return None
 
433
    specified_path_ids = _find_ids_across_trees(filenames, trees,
 
434
        require_versioned)
 
435
    return _find_children_across_trees(specified_path_ids, trees)
 
436
#    specified_ids = [id for path, id in _find_path_ids_across_trees(filenames, trees, require_versioned)]
 
437
#    return _find_children_across_trees(specified_ids, trees)
 
438
 
 
439
def find_path_ids_across_trees(filenames, trees, require_versioned=True):
 
440
    """Find the paths and ids corresponding to specified filenames.
 
441
    
 
442
    All matches in all trees will be used, and all children of matched
 
443
    directories will be included
 
444
 
 
445
    :param filenames: The filenames to find file_ids for
 
446
    :param trees: The trees to find file_ids within
 
447
    :param require_versioned: if true, all specified filenames must occur in
 
448
        at least one tree.
 
449
    :return: a set of (path, file ids) for the specified filenames and their
 
450
        children. The returned path is the path of the id in the first tree
 
451
        that contains it. This matters when files have been moved 
 
452
    """
 
453
    if not filenames:
 
454
        return set()
 
455
    # This function needs to know the ids for filenames in all trees, then
 
456
    # search for those same files and children in all the other trees.
 
457
    # it is complicated by the same path in two trees being able to have
 
458
    # different ids, which might both be present in both trees.
 
459
    # consider two trees, which have had 'mv foo bar' and 'mv baz foo' done
 
460
    # in this case, a diff of 'foo' should should changes to both the current
 
461
    # 'bar' and the current 'foo' which was baz. Its arguable that if 
 
462
    # the situation is 'mv parent/foo bar' and 'mv baz parent/foo', that 
 
463
    # we should return the current bar and the current parent/foo' - at the 
 
464
    # moment we do, but we loop around all ids and all trees: I*T checks.
 
465
    
 
466
    # Updating this algorithm to be fast in the common case:
 
467
    # nothing has moved, all files have the same id in parent, child and there
 
468
    # are only two trees (or one is working tree and the others are parents).
 
469
    # walk the dirstate. as we find each path, gather the paths of that
 
470
    # id in all trees. add a mapping from the id to the path in those trees.
 
471
    # now lookup children by id, again in all trees; for these trees that
 
472
    # nothing has moved in, the id->path mapping will allow us to find the
 
473
    # parent trivially. To answer 'has anything been moved' in one of the
 
474
    # dirstate parent trees though, we will need to stare harder at it.
 
475
 
 
476
    #  Now, given a path index, that is trivial for any one tree, and given
 
477
    #  that we can ask for additional data from a dirstate tree, its a single
 
478
    #  pass, though it will require scanning the entire tree to find paths
 
479
    #  that were at the current location.
 
480
    # ideal results?: There are three things: tree, path, id. Pathologically
 
481
    # we can have completely disjoint ids for each tree; but we cannot have 
 
482
    # disjoin paths for each tree, except if we scan each tree for the 
 
483
    # different ids from other trees.
 
484
 
 
485
    specified_path_ids = _find_ids_across_trees(filenames, trees,
 
486
        require_versioned)
 
487
    return _find_path_id_children_across_trees(specified_path_ids, trees)
 
488
 
 
489
 
 
490
def _find_ids_across_trees(filenames, trees, require_versioned):
 
491
    """Find the ids corresponding to specified filenames.
 
492
    
 
493
    All matches in all trees will be used, but subdirectories are not scanned.
 
494
 
 
495
    :param filenames: The filenames to find file_ids for
 
496
    :param trees: The trees to find file_ids within
 
497
    :param require_versioned: if true, all specified filenames must occur in
 
498
        at least one tree.
 
499
    :return: a set of (path, file ids) for the specified filenames
 
500
    """
 
501
    not_versioned = []
 
502
    interesting_ids = set()
 
503
    for tree_path in filenames:
 
504
        not_found = True
 
505
        for tree in trees:
 
506
            file_id = tree.path2id(tree_path)
 
507
            if file_id is not None:
 
508
                interesting_ids.add(file_id)
 
509
                not_found = False
 
510
        if not_found:
 
511
            not_versioned.append(tree_path)
 
512
    if len(not_versioned) > 0 and require_versioned:
 
513
        raise errors.PathsNotVersionedError(not_versioned)
 
514
    return interesting_ids
 
515
 
 
516
 
 
517
def _find_children_across_trees(specified_ids, trees):
 
518
    """Return a set including specified ids and their children
 
519
    
 
520
    All matches in all trees will be used.
 
521
 
 
522
    :param trees: The trees to find file_ids within
 
523
    :return: a set containing all specified ids and their children 
 
524
    """
 
525
    interesting_ids = set(specified_ids)
 
526
    pending = interesting_ids
 
527
    # now handle children of interesting ids
 
528
    # we loop so that we handle all children of each id in both trees
 
529
    while len(pending) > 0:
 
530
        new_pending = set()
 
531
        for file_id in pending:
 
532
            for tree in trees:
 
533
                if not tree.has_id(file_id):
 
534
                    continue
 
535
                entry = tree.inventory[file_id]
 
536
                for child in getattr(entry, 'children', {}).itervalues():
 
537
                    if child.file_id not in interesting_ids:
 
538
                        new_pending.add(child.file_id)
 
539
        interesting_ids.update(new_pending)
 
540
        pending = new_pending
 
541
    return interesting_ids
 
542
 
 
543
 
 
544
class InterTree(InterObject):
 
545
    """This class represents operations taking place between two Trees.
 
546
 
 
547
    Its instances have methods like 'compare' and contain references to the
 
548
    source and target trees these operations are to be carried out on.
 
549
 
 
550
    clients of bzrlib should not need to use InterTree directly, rather they
 
551
    should use the convenience methods on Tree such as 'Tree.compare()' which
 
552
    will pass through to InterTree as appropriate.
 
553
    """
 
554
 
 
555
    _optimisers = []
 
556
 
 
557
    @needs_read_lock
 
558
    def compare(self, want_unchanged=False, specific_files=None,
 
559
        extra_trees=None, require_versioned=False, include_root=False):
 
560
        """Return the changes from source to target.
 
561
 
 
562
        :return: A TreeDelta.
 
563
        :param specific_files: An optional list of file paths to restrict the
 
564
            comparison to. When mapping filenames to ids, all matches in all
 
565
            trees (including optional extra_trees) are used, and all children of
 
566
            matched directories are included.
 
567
        :param want_unchanged: An optional boolean requesting the inclusion of
 
568
            unchanged entries in the result.
 
569
        :param extra_trees: An optional list of additional trees to use when
 
570
            mapping the contents of specific_files (paths) to file_ids.
 
571
        :param require_versioned: An optional boolean (defaults to False). When
 
572
            supplied and True all the 'specific_files' must be versioned, or
 
573
            a PathsNotVersionedError will be thrown.
 
574
        """
 
575
        # NB: show_status depends on being able to pass in non-versioned files
 
576
        # and report them as unknown
 
577
        trees = (self.source,)
 
578
        if extra_trees is not None:
 
579
            trees = trees + tuple(extra_trees)
 
580
        # target is usually the newer tree:
 
581
        specific_file_ids = self.target.paths2ids(specific_files, trees,
 
582
            require_versioned=require_versioned)
 
583
        if specific_files and not specific_file_ids:
 
584
            # All files are unversioned, so just return an empty delta
 
585
            # _compare_trees would think we want a complete delta
 
586
            return delta.TreeDelta()
 
587
        return delta._compare_trees(self.source, self.target, want_unchanged,
 
588
            specific_file_ids, include_root)
 
589
 
 
590
    def _iter_changes(self, include_unchanged=False,
 
591
                      specific_file_ids=None, pb=None):
 
592
        """Generate an iterator of changes between trees.
 
593
 
 
594
        A tuple is returned:
 
595
        (file_id, path, changed_content, versioned, parent, name, kind,
 
596
         executable)
 
597
 
 
598
        Path is relative to the target tree.  changed_content is True if the
 
599
        file's content has changed.  This includes changes to its kind, and to
 
600
        a symlink's target.
 
601
 
 
602
        versioned, parent, name, kind, executable are tuples of (from, to).
 
603
        If a file is missing in a tree, its kind is None.
 
604
 
 
605
        Iteration is done in parent-to-child order, relative to the target
 
606
        tree.
 
607
        """
 
608
        to_paths = {}
 
609
        from_entries_by_dir = list(self.source.inventory.iter_entries_by_dir(
 
610
            specific_file_ids=specific_file_ids))
 
611
        from_data = dict((e.file_id, (p, e)) for p, e in from_entries_by_dir)
 
612
        to_entries_by_dir = list(self.target.inventory.iter_entries_by_dir(
 
613
            specific_file_ids=specific_file_ids))
 
614
        num_entries = len(from_entries_by_dir) + len(to_entries_by_dir)
 
615
        entry_count = 0
 
616
        for to_path, to_entry in to_entries_by_dir:
 
617
            file_id = to_entry.file_id
 
618
            to_paths[file_id] = to_path
 
619
            entry_count += 1
 
620
            changed_content = False
 
621
            from_path, from_entry = from_data.get(file_id, (None, None))
 
622
            from_versioned = (from_entry is not None)
 
623
            if from_entry is not None:
 
624
                from_versioned = True
 
625
                from_name = from_entry.name
 
626
                from_parent = from_entry.parent_id
 
627
                from_kind, from_executable, from_stat = \
 
628
                    self.source._comparison_data(from_entry, from_path)
 
629
                entry_count += 1
 
630
            else:
 
631
                from_versioned = False
 
632
                from_kind = None
 
633
                from_parent = None
 
634
                from_name = None
 
635
                from_executable = None
 
636
            versioned = (from_versioned, True)
 
637
            to_kind, to_executable, to_stat = \
 
638
                self.target._comparison_data(to_entry, to_path)
 
639
            kind = (from_kind, to_kind)
 
640
            if kind[0] != kind[1]:
 
641
                changed_content = True
 
642
            elif from_kind == 'file':
 
643
                from_size = self.source._file_size(from_entry, from_stat)
 
644
                to_size = self.target._file_size(to_entry, to_stat)
 
645
                if from_size != to_size:
 
646
                    changed_content = True
 
647
                elif (self.source.get_file_sha1(file_id, from_path, from_stat) !=
 
648
                    self.target.get_file_sha1(file_id, to_path, to_stat)):
 
649
                    changed_content = True
 
650
            elif from_kind == 'symlink':
 
651
                if (self.source.get_symlink_target(file_id) != 
 
652
                    self.target.get_symlink_target(file_id)):
 
653
                    changed_content = True
 
654
            parent = (from_parent, to_entry.parent_id)
 
655
            name = (from_name, to_entry.name)
 
656
            executable = (from_executable, to_executable)
 
657
            if pb is not None:
 
658
                pb.update('comparing files', entry_count, num_entries)
 
659
            if (changed_content is not False or versioned[0] != versioned[1] 
 
660
                or parent[0] != parent[1] or name[0] != name[1] or 
 
661
                executable[0] != executable[1] or include_unchanged):
 
662
                yield (file_id, to_path, changed_content, versioned, parent,
 
663
                       name, kind, executable)
 
664
 
 
665
        def get_to_path(from_entry):
 
666
            if from_entry.parent_id is None:
 
667
                to_path = ''
 
668
            else:
 
669
                if from_entry.parent_id not in to_paths:
 
670
                    get_to_path(self.source.inventory[from_entry.parent_id])
 
671
                to_path = osutils.pathjoin(to_paths[from_entry.parent_id],
 
672
                                           from_entry.name)
 
673
            to_paths[from_entry.file_id] = to_path
 
674
            return to_path
 
675
 
 
676
        for path, from_entry in from_entries_by_dir:
 
677
            file_id = from_entry.file_id
 
678
            if file_id in to_paths:
 
679
                continue
 
680
            to_path = get_to_path(from_entry)
 
681
            entry_count += 1
 
682
            if pb is not None:
 
683
                pb.update('comparing files', entry_count, num_entries)
 
684
            versioned = (True, False)
 
685
            parent = (from_entry.parent_id, None)
 
686
            name = (from_entry.name, None)
 
687
            from_kind, from_executable, stat_value = \
 
688
                self.source._comparison_data(from_entry, path)
 
689
            kind = (from_kind, None)
 
690
            executable = (from_executable, None)
 
691
            changed_content = True
 
692
            # the parent's path is necessarily known at this point.
 
693
            yield(file_id, to_path, changed_content, versioned, parent,
 
694
                  name, kind, executable)
 
695
 
 
696
 
 
697
# This was deprecated before 0.12, but did not have an official warning
 
698
@symbol_versioning.deprecated_function(symbol_versioning.zero_twelve)
 
699
def RevisionTree(*args, **kwargs):
 
700
    """RevisionTree has moved to bzrlib.revisiontree.RevisionTree()
 
701
 
 
702
    Accessing it as bzrlib.tree.RevisionTree has been deprecated as of
 
703
    bzr 0.12.
 
704
    """
 
705
    from bzrlib.revisiontree import RevisionTree as _RevisionTree
 
706
    return _RevisionTree(*args, **kwargs)
 
707
 
 
708