/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

Merge dirstate and some small cleanups

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