~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tree.py

Optimize Tree._iter_changes with specific file_ids

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# Copyright (C) 2005 Canonical Ltd
2
 
 
 
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
 
 
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21
21
from cStringIO import StringIO
22
22
 
23
23
import bzrlib
 
24
from bzrlib import (
 
25
    delta,
 
26
    osutils,
 
27
    symbol_versioning,
 
28
    )
 
29
from bzrlib.decorators import needs_read_lock
24
30
from bzrlib.errors import BzrError, BzrCheckError
25
31
from bzrlib import errors
26
32
from bzrlib.inventory import Inventory
 
33
from bzrlib.inter import InterObject
27
34
from bzrlib.osutils import fingerprint_file
28
35
import bzrlib.revision
29
36
from bzrlib.trace import mutter, note
30
37
 
 
38
 
31
39
class Tree(object):
32
40
    """Abstract file tree.
33
41
 
37
45
 
38
46
    * `RevisionTree` is a tree as recorded at some point in the past.
39
47
 
40
 
    * `EmptyTree`
41
 
 
42
48
    Trees contain an `Inventory` object, and also know how to retrieve
43
49
    file texts mentioned in the inventory, either from a working
44
50
    directory or from a store.
50
56
    trees or versioned trees.
51
57
    """
52
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(from_tree, self, include_unchanged, 
 
94
                                       specific_file_ids, pb)
 
95
    
53
96
    def conflicts(self):
54
97
        """Get a list of the conflicts in the tree.
55
98
 
73
116
    def has_id(self, file_id):
74
117
        return self.inventory.has_id(file_id)
75
118
 
 
119
    __contains__ = has_id
 
120
 
76
121
    def has_or_had_id(self, file_id):
77
122
        if file_id == self.inventory.root.file_id:
78
123
            return True
79
124
        return self.inventory.has_id(file_id)
80
125
 
81
 
    __contains__ = has_id
82
 
 
83
126
    def __iter__(self):
84
127
        return iter(self.inventory)
85
128
 
86
129
    def id2path(self, file_id):
87
130
        return self.inventory.id2path(file_id)
88
131
 
 
132
    def is_control_filename(self, filename):
 
133
        """True if filename is the name of a control file in this tree.
 
134
        
 
135
        :param filename: A filename within the tree. This is a relative path
 
136
        from the root of this tree.
 
137
 
 
138
        This is true IF and ONLY IF the filename is part of the meta data
 
139
        that bzr controls in this tree. I.E. a random .bzr directory placed
 
140
        on disk will not be a control file for this tree.
 
141
        """
 
142
        return self.bzrdir.is_control_filename(filename)
 
143
 
 
144
    def iter_entries_by_dir(self, specific_file_ids=None):
 
145
        """Walk the tree in 'by_dir' order.
 
146
 
 
147
        This will yield each entry in the tree as a (path, entry) tuple. The
 
148
        order that they are yielded is: the contents of a directory are 
 
149
        preceeded by the parent of a directory, and all the contents of a 
 
150
        directory are grouped together.
 
151
        """
 
152
        return self.inventory.iter_entries_by_dir(
 
153
            specific_file_ids=specific_file_ids)
 
154
 
89
155
    def kind(self, file_id):
90
156
        raise NotImplementedError("subclasses must implement kind")
91
157
 
 
158
    def _comparison_data(self, entry, path):
 
159
        """Return a tuple of kind, executable, stat_value for a file.
 
160
 
 
161
        entry may be None if there is no inventory entry for the file, but
 
162
        path must always be supplied.
 
163
 
 
164
        kind is None if there is no file present (even if an inventory id is
 
165
        present).  executable is False for non-file entries.
 
166
        """
 
167
        raise NotImplementedError(self._comparison_data)
 
168
 
 
169
    def _file_size(entry, stat_value):
 
170
        raise NotImplementedError(self._file_size)
 
171
 
92
172
    def _get_inventory(self):
93
173
        return self._inventory
94
174
    
 
175
    def get_file(self, file_id):
 
176
        """Return a file object for the file file_id in the tree."""
 
177
        raise NotImplementedError(self.get_file)
 
178
    
95
179
    def get_file_by_path(self, path):
96
180
        return self.get_file(self._inventory.path2id(path))
97
181
 
 
182
    def annotate_iter(self, file_id):
 
183
        """Return an iterator of revision_id, line tuples
 
184
 
 
185
        For working trees (and mutable trees in general), the special
 
186
        revision_id 'current:' will be used for lines that are new in this
 
187
        tree, e.g. uncommitted changes.
 
188
        :param file_id: The file to produce an annotated version from
 
189
        """
 
190
        raise NotImplementedError(self.annotate_iter)
 
191
 
98
192
    inventory = property(_get_inventory,
99
193
                         doc="Inventory of this Tree")
100
194
 
104
198
        fp = fingerprint_file(f)
105
199
        f.seek(0)
106
200
        
107
 
        if ie.text_size != None:
 
201
        if ie.text_size is not None:
108
202
            if ie.text_size != fp['size']:
109
203
                raise BzrError("mismatched size for file %r in %r" % (ie.file_id, self._store),
110
204
                        ["inventory expects %d bytes" % ie.text_size,
117
211
                     "file is actually %s" % fp['sha1'],
118
212
                     "store is probably damaged/corrupt"])
119
213
 
 
214
    def path2id(self, path):
 
215
        """Return the id for path in this tree."""
 
216
        return self._inventory.path2id(path)
120
217
 
121
218
    def print_file(self, file_id):
122
219
        """Print file with id `file_id` to stdout."""
146
243
        # are not versioned.
147
244
        pred = self.inventory.has_filename
148
245
        return set((p for p in paths if not pred(p)))
149
 
        
150
 
        
151
 
class RevisionTree(Tree):
152
 
    """Tree viewing a previous revision.
153
 
 
154
 
    File text can be retrieved from the text store.
155
 
 
156
 
    TODO: Some kind of `__repr__` method, but a good one
157
 
           probably means knowing the branch and revision number,
158
 
           or at least passing a description to the constructor.
159
 
    """
160
 
    
161
 
    def __init__(self, branch, inv, revision_id):
162
 
        # for compatability the 'branch' parameter has not been renamed to 
163
 
        # repository at this point. However, we should change RevisionTree's
164
 
        # construction to always be via Repository and not via direct 
165
 
        # construction - this will mean that we can change the constructor
166
 
        # with much less chance of breaking client code.
167
 
        self._repository = branch
168
 
        self._weave_store = branch.weave_store
169
 
        self._inventory = inv
170
 
        self._revision_id = revision_id
171
 
 
172
 
    def get_parent_ids(self):
173
 
        """See Tree.get_parent_ids.
174
 
 
175
 
        A RevisionTree's parents match the revision graph.
176
 
        """
177
 
        parent_ids = self._repository.get_revision(self._revision_id).parent_ids
178
 
        return parent_ids
179
 
        
180
 
    def get_revision_id(self):
181
 
        """Return the revision id associated with this tree."""
182
 
        return self._revision_id
183
 
 
184
 
    def get_weave(self, file_id):
185
 
        return self._weave_store.get_weave(file_id,
186
 
                self._repository.get_transaction())
187
 
 
188
 
    def get_file_lines(self, file_id):
189
 
        ie = self._inventory[file_id]
190
 
        weave = self.get_weave(file_id)
191
 
        return weave.get_lines(ie.revision)
192
 
 
193
 
    def get_file_text(self, file_id):
194
 
        return ''.join(self.get_file_lines(file_id))
195
 
 
196
 
    def get_file(self, file_id):
197
 
        return StringIO(self.get_file_text(file_id))
198
 
 
199
 
    def get_file_size(self, file_id):
200
 
        return self._inventory[file_id].text_size
201
 
 
202
 
    def get_file_sha1(self, file_id, path=None):
203
 
        ie = self._inventory[file_id]
204
 
        if ie.kind == "file":
205
 
            return ie.text_sha1
206
 
        return None
207
 
 
208
 
    def get_file_mtime(self, file_id, path=None):
209
 
        ie = self._inventory[file_id]
210
 
        revision = self._repository.get_revision(ie.revision)
211
 
        return revision.timestamp
212
 
 
213
 
    def is_executable(self, file_id, path=None):
214
 
        ie = self._inventory[file_id]
215
 
        if ie.kind != "file":
216
 
            return None 
217
 
        return self._inventory[file_id].executable
218
 
 
219
 
    def has_filename(self, filename):
220
 
        return bool(self.inventory.path2id(filename))
221
 
 
222
 
    def list_files(self):
223
 
        # The only files returned by this are those from the version
224
 
        for path, entry in self.inventory.iter_entries():
225
 
            yield path, 'V', entry.kind, entry.file_id, entry
226
 
 
227
 
    def get_symlink_target(self, file_id):
228
 
        ie = self._inventory[file_id]
229
 
        return ie.symlink_target;
230
 
 
231
 
    def kind(self, file_id):
232
 
        return self._inventory[file_id].kind
233
 
 
234
 
    def lock_read(self):
235
 
        self._repository.lock_read()
236
 
 
237
 
    def unlock(self):
238
 
        self._repository.unlock()
239
246
 
240
247
 
241
248
class EmptyTree(Tree):
242
249
 
243
250
    def __init__(self):
244
 
        self._inventory = Inventory()
 
251
        self._inventory = Inventory(root_id=None)
 
252
        symbol_versioning.warn('EmptyTree is deprecated as of bzr 0.9 please'
 
253
                               ' use repository.revision_tree instead.',
 
254
                               DeprecationWarning, stacklevel=2)
245
255
 
246
256
    def get_parent_ids(self):
247
 
        """See Tree.get_parent_ids.
248
 
 
249
 
        An EmptyTree always has NULL_REVISION as the only parent.
250
 
        """
251
257
        return []
252
258
 
253
259
    def get_symlink_target(self, file_id):
257
263
        return False
258
264
 
259
265
    def kind(self, file_id):
260
 
        assert self._inventory[file_id].kind == "root_directory"
261
 
        return "root_directory"
 
266
        assert self._inventory[file_id].kind == "directory"
 
267
        return "directory"
262
268
 
263
 
    def list_files(self):
 
269
    def list_files(self, include_root=False):
264
270
        return iter([])
265
271
    
266
272
    def __contains__(self, file_id):
267
 
        return file_id in self._inventory
 
273
        return (file_id in self._inventory)
268
274
 
269
 
    def get_file_sha1(self, file_id, path=None):
270
 
        assert self._inventory[file_id].kind == "root_directory"
 
275
    def get_file_sha1(self, file_id, path=None, stat_value=None):
271
276
        return None
272
277
 
273
278
 
407
412
        interesting_ids.update(new_pending)
408
413
        pending = new_pending
409
414
    return interesting_ids
 
415
 
 
416
 
 
417
class InterTree(InterObject):
 
418
    """This class represents operations taking place between two Trees.
 
419
 
 
420
    Its instances have methods like 'compare' and contain references to the
 
421
    source and target trees these operations are to be carried out on.
 
422
 
 
423
    clients of bzrlib should not need to use InterTree directly, rather they
 
424
    should use the convenience methods on Tree such as 'Tree.compare()' which
 
425
    will pass through to InterTree as appropriate.
 
426
    """
 
427
 
 
428
    _optimisers = []
 
429
 
 
430
    @needs_read_lock
 
431
    def compare(self, want_unchanged=False, specific_files=None,
 
432
        extra_trees=None, require_versioned=False, include_root=False):
 
433
        """Return the changes from source to target.
 
434
 
 
435
        :return: A TreeDelta.
 
436
        :param specific_files: An optional list of file paths to restrict the
 
437
            comparison to. When mapping filenames to ids, all matches in all
 
438
            trees (including optional extra_trees) are used, and all children of
 
439
            matched directories are included.
 
440
        :param want_unchanged: An optional boolean requesting the inclusion of
 
441
            unchanged entries in the result.
 
442
        :param extra_trees: An optional list of additional trees to use when
 
443
            mapping the contents of specific_files (paths) to file_ids.
 
444
        :param require_versioned: An optional boolean (defaults to False). When
 
445
            supplied and True all the 'specific_files' must be versioned, or
 
446
            a PathsNotVersionedError will be thrown.
 
447
        """
 
448
        # NB: show_status depends on being able to pass in non-versioned files and
 
449
        # report them as unknown
 
450
        trees = (self.source, self.target)
 
451
        if extra_trees is not None:
 
452
            trees = trees + tuple(extra_trees)
 
453
        specific_file_ids = find_ids_across_trees(specific_files,
 
454
            trees, require_versioned=require_versioned)
 
455
        if specific_files and not specific_file_ids:
 
456
            # All files are unversioned, so just return an empty delta
 
457
            # _compare_trees would think we want a complete delta
 
458
            return delta.TreeDelta()
 
459
        return delta._compare_trees(self.source, self.target, want_unchanged,
 
460
            specific_file_ids, include_root)
 
461
 
 
462
    def _iter_changes(self, from_tree, to_tree, include_unchanged, 
 
463
                      specific_file_ids, pb):
 
464
        """Generate an iterator of changes between trees.
 
465
 
 
466
        A tuple is returned:
 
467
        (file_id, path, changed_content, versioned, parent, name, kind,
 
468
         executable)
 
469
 
 
470
        Path is relative to the to_tree.  changed_content is True if the file's
 
471
        content has changed.  This includes changes to its kind, and to
 
472
        a symlink's target.
 
473
 
 
474
        versioned, parent, name, kind, executable are tuples of (from, to).
 
475
        If a file is missing in a tree, its kind is None.
 
476
 
 
477
        Iteration is done in parent-to-child order, relative to the to_tree.
 
478
        """
 
479
        to_paths = {}
 
480
        from_entries_by_dir = list(from_tree.inventory.iter_entries_by_dir(
 
481
            specific_file_ids=specific_file_ids))
 
482
        from_data = dict((e.file_id, (p, e)) for p, e in from_entries_by_dir)
 
483
        to_entries_by_dir = list(to_tree.inventory.iter_entries_by_dir(specific_file_ids=specific_file_ids))
 
484
        num_entries = len(from_entries_by_dir) + len(to_entries_by_dir)
 
485
        entry_count = 0
 
486
        for to_path, to_entry in to_entries_by_dir:
 
487
            file_id = to_entry.file_id
 
488
            to_paths[file_id] = to_path
 
489
            entry_count += 1
 
490
            changed_content = False
 
491
            from_path, from_entry = from_data.get(file_id, (None, None))
 
492
            from_versioned = (from_entry is not None)
 
493
            if from_entry is not None:
 
494
                from_versioned = True
 
495
                from_name = from_entry.name
 
496
                from_parent = from_entry.parent_id
 
497
                from_kind, from_executable, from_stat = \
 
498
                    from_tree._comparison_data(from_entry, from_path)
 
499
                entry_count += 1
 
500
            else:
 
501
                from_versioned = False
 
502
                from_kind = None
 
503
                from_parent = None
 
504
                from_name = None
 
505
                from_executable = None
 
506
            versioned = (from_versioned, True)
 
507
            to_kind, to_executable, to_stat = \
 
508
                to_tree._comparison_data(to_entry, to_path)
 
509
            kind = (from_kind, to_kind)
 
510
            if kind[0] != kind[1]:
 
511
                changed_content = True
 
512
            elif from_kind == 'file':
 
513
                from_size = from_tree._file_size(from_entry, from_stat)
 
514
                to_size = to_tree._file_size(to_entry, to_stat)
 
515
                if from_size != to_size:
 
516
                    changed_content = True
 
517
                elif (from_tree.get_file_sha1(file_id, from_path, from_stat) !=
 
518
                    to_tree.get_file_sha1(file_id, to_path, to_stat)):
 
519
                    changed_content = True
 
520
            elif from_kind == 'symlink':
 
521
                if (from_tree.get_symlink_target(file_id) != 
 
522
                    to_tree.get_symlink_target(file_id)):
 
523
                    changed_content = True
 
524
            parent = (from_parent, to_entry.parent_id)
 
525
            name = (from_name, to_entry.name)
 
526
            executable = (from_executable, to_executable)
 
527
            if pb is not None:
 
528
                pb.update('comparing files', entry_count, num_entries)
 
529
            if (changed_content is not False or versioned[0] != versioned[1] 
 
530
                or parent[0] != parent[1] or name[0] != name[1] or 
 
531
                executable[0] != executable[1] or include_unchanged):
 
532
                yield (file_id, to_path, changed_content, versioned, parent,
 
533
                       name, kind, executable)
 
534
 
 
535
        for path, from_entry in from_entries_by_dir:
 
536
            file_id = from_entry.file_id
 
537
            if file_id in to_paths:
 
538
                continue
 
539
            if from_entry.parent_id is None:
 
540
                to_path = ''
 
541
            else:
 
542
                to_path = osutils.pathjoin(to_paths[from_entry.parent_id],
 
543
                                           from_entry.name)
 
544
            to_paths[file_id] = to_path
 
545
            entry_count += 1
 
546
            if pb is not None:
 
547
                pb.update('comparing files', entry_count, num_entries)
 
548
            versioned = (True, False)
 
549
            parent = (from_entry.parent_id, None)
 
550
            name = (from_entry.name, None)
 
551
            from_kind, from_executable, stat_value = \
 
552
                from_tree._comparison_data(from_entry, path)
 
553
            kind = (from_kind, None)
 
554
            executable = (from_executable, None)
 
555
            changed_content = True
 
556
            # the parent's path is necessarily known at this point.
 
557
            yield(file_id, to_path, changed_content, versioned, parent,
 
558
                  name, kind, executable)
 
559
 
 
560
 
 
561
# This was deprecated before 0.12, but did not have an official warning
 
562
@symbol_versioning.deprecated_function(symbol_versioning.zero_twelve)
 
563
def RevisionTree(*args, **kwargs):
 
564
    """RevisionTree has moved to bzrlib.revisiontree.RevisionTree()
 
565
 
 
566
    Accessing it as bzrlib.tree.RevisionTree has been deprecated as of
 
567
    bzr 0.12.
 
568
    """
 
569
    from bzrlib.revisiontree import RevisionTree as _RevisionTree
 
570
    return _RevisionTree(*args, **kwargs)
 
571
 
 
572