~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tree.py

  • Committer: Robert Collins
  • Date: 2007-03-07 08:02:58 UTC
  • mfrom: (2255.2.239 dirstate)
  • mto: This revision was merged to the branch mainline in revision 2322.
  • Revision ID: robertc@robertcollins.net-20070307080258-fi172acyjga89fz5
(robertc) Merge dirstate and subtrees. (Robert Collins, Martin Pool, Aaaron Bentley, John A Meinel, James Westby)

Show diffs side-by-side

added added

removed removed

Lines of Context:
18
18
"""
19
19
 
20
20
import os
 
21
from collections import deque
21
22
from cStringIO import StringIO
22
23
 
23
24
import bzrlib
29
30
from bzrlib.decorators import needs_read_lock
30
31
from bzrlib.errors import BzrError, BzrCheckError
31
32
from bzrlib import errors
32
 
from bzrlib.inventory import Inventory
 
33
from bzrlib.inventory import Inventory, InventoryFile
33
34
from bzrlib.inter import InterObject
34
35
from bzrlib.osutils import fingerprint_file
35
36
import bzrlib.revision
57
58
    """
58
59
    
59
60
    def changes_from(self, other, want_unchanged=False, specific_files=None,
60
 
        extra_trees=None, require_versioned=False, include_root=False):
 
61
        extra_trees=None, require_versioned=False, include_root=False,
 
62
        want_unversioned=False):
61
63
        """Return a TreeDelta of the changes from other to this tree.
62
64
 
63
65
        :param other: A tree to compare with.
72
74
        :param require_versioned: An optional boolean (defaults to False). When
73
75
            supplied and True all the 'specific_files' must be versioned, or
74
76
            a PathsNotVersionedError will be thrown.
 
77
        :param want_unversioned: Scan for unversioned paths.
75
78
 
76
79
        The comparison will be performed by an InterTree object looked up on 
77
80
        self and other.
84
87
            specific_files=specific_files,
85
88
            extra_trees=extra_trees,
86
89
            require_versioned=require_versioned,
87
 
            include_root=include_root
 
90
            include_root=include_root,
 
91
            want_unversioned=want_unversioned,
88
92
            )
89
93
 
90
 
    def _iter_changes(self, from_tree, include_unchanged=False, 
91
 
                     specific_file_ids=None, pb=None):
 
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):
92
97
        intertree = InterTree.get(from_tree, self)
93
 
        return intertree._iter_changes(from_tree, self, include_unchanged, 
94
 
                                       specific_file_ids, pb)
 
98
        return intertree._iter_changes(include_unchanged, specific_files, pb,
 
99
            extra_trees, require_versioned, want_unversioned=want_unversioned)
95
100
    
96
101
    def conflicts(self):
97
102
        """Get a list of the conflicts in the tree.
100
105
        """
101
106
        return []
102
107
 
 
108
    def extras(self):
 
109
        """For trees that can have unversioned files, return all such paths."""
 
110
        return []
 
111
 
103
112
    def get_parent_ids(self):
104
113
        """Get the parent ids for this tree. 
105
114
 
125
134
            return True
126
135
        return self.inventory.has_id(file_id)
127
136
 
 
137
    def is_ignored(self, filename):
 
138
        """Check whether the filename is ignored by this tree.
 
139
 
 
140
        :param filename: The relative filename within the tree.
 
141
        :return: True if the filename is ignored.
 
142
        """
 
143
        return False
 
144
 
128
145
    def __iter__(self):
129
146
        return iter(self.inventory)
130
147
 
131
148
    def id2path(self, file_id):
 
149
        """Return the path for a file id.
 
150
 
 
151
        :raises NoSuchId:
 
152
        """
132
153
        file_id = osutils.safe_file_id(file_id)
133
154
        return self.inventory.id2path(file_id)
134
155
 
144
165
        """
145
166
        return self.bzrdir.is_control_filename(filename)
146
167
 
 
168
    @needs_read_lock
147
169
    def iter_entries_by_dir(self, specific_file_ids=None):
148
170
        """Walk the tree in 'by_dir' order.
149
171
 
155
177
        return self.inventory.iter_entries_by_dir(
156
178
            specific_file_ids=specific_file_ids)
157
179
 
 
180
    def iter_references(self):
 
181
        for path, entry in self.iter_entries_by_dir():
 
182
            if entry.kind == 'tree-reference':
 
183
                yield path, entry.file_id
 
184
 
158
185
    def kind(self, file_id):
159
 
        raise NotImplementedError("subclasses must implement kind")
 
186
        raise NotImplementedError("Tree subclass %s must implement kind"
 
187
            % self.__class__.__name__)
 
188
 
 
189
    def get_reference_revision(self, file_id, path=None):
 
190
        raise NotImplementedError("Tree subclass %s must implement "
 
191
                                  "get_reference_revision"
 
192
            % self.__class__.__name__)
160
193
 
161
194
    def _comparison_data(self, entry, path):
162
195
        """Return a tuple of kind, executable, stat_value for a file.
169
202
        """
170
203
        raise NotImplementedError(self._comparison_data)
171
204
 
172
 
    def _file_size(entry, stat_value):
 
205
    def _file_size(self, entry, stat_value):
173
206
        raise NotImplementedError(self._file_size)
174
207
 
175
208
    def _get_inventory(self):
178
211
    def get_file(self, file_id):
179
212
        """Return a file object for the file file_id in the tree."""
180
213
        raise NotImplementedError(self.get_file)
181
 
    
 
214
 
 
215
    def get_file_mtime(self, file_id, path=None):
 
216
        """Return the modification time for a file.
 
217
 
 
218
        :param file_id: The handle for this file.
 
219
        :param path: The path that this file can be found at.
 
220
            These must point to the same object.
 
221
        """
 
222
        raise NotImplementedError(self.get_file_mtime)
 
223
 
182
224
    def get_file_by_path(self, path):
183
225
        return self.get_file(self._inventory.path2id(path))
184
226
 
 
227
    def get_symlink_target(self, file_id):
 
228
        """Get the target for a given file_id.
 
229
 
 
230
        It is assumed that the caller already knows that file_id is referencing
 
231
        a symlink.
 
232
        :param file_id: Handle for the symlink entry.
 
233
        :return: The path the symlink points to.
 
234
        """
 
235
        raise NotImplementedError(self.get_symlink_target)
 
236
 
185
237
    def annotate_iter(self, file_id):
186
238
        """Return an iterator of revision_id, line tuples
187
239
 
218
270
        """Return the id for path in this tree."""
219
271
        return self._inventory.path2id(path)
220
272
 
 
273
    def paths2ids(self, paths, trees=[], require_versioned=True):
 
274
        """Return all the ids that can be reached by walking from paths.
 
275
        
 
276
        Each path is looked up in each this tree and any extras provided in
 
277
        trees, and this is repeated recursively: the children in an extra tree
 
278
        of a directory that has been renamed under a provided path in this tree
 
279
        are all returned, even if none exist until a provided path in this
 
280
        tree, and vice versa.
 
281
 
 
282
        :param paths: An iterable of paths to start converting to ids from.
 
283
            Alternatively, if paths is None, no ids should be calculated and None
 
284
            will be returned. This is offered to make calling the api unconditional
 
285
            for code that *might* take a list of files.
 
286
        :param trees: Additional trees to consider.
 
287
        :param require_versioned: If False, do not raise NotVersionedError if
 
288
            an element of paths is not versioned in this tree and all of trees.
 
289
        """
 
290
        return find_ids_across_trees(paths, [self] + list(trees), require_versioned)
 
291
 
221
292
    def print_file(self, file_id):
222
293
        """Print file with id `file_id` to stdout."""
223
294
        file_id = osutils.safe_file_id(file_id)
227
298
    def lock_read(self):
228
299
        pass
229
300
 
 
301
    def revision_tree(self, revision_id):
 
302
        """Obtain a revision tree for the revision revision_id.
 
303
 
 
304
        The intention of this method is to allow access to possibly cached
 
305
        tree data. Implementors of this method should raise NoSuchRevision if
 
306
        the tree is not locally available, even if they could obtain the 
 
307
        tree via a repository or some other means. Callers are responsible 
 
308
        for finding the ultimate source for a revision tree.
 
309
 
 
310
        :param revision_id: The revision_id of the requested tree.
 
311
        :return: A Tree.
 
312
        :raises: NoSuchRevision if the tree cannot be obtained.
 
313
        """
 
314
        raise errors.NoSuchRevisionInTree(self, revision_id)
 
315
 
230
316
    def unknowns(self):
231
317
        """What files are present in this tree and unknown.
232
318
        
238
324
        pass
239
325
 
240
326
    def filter_unversioned_files(self, paths):
241
 
        """Filter out paths that are not versioned.
 
327
        """Filter out paths that are versioned.
242
328
 
243
329
        :return: set of paths.
244
330
        """
248
334
        pred = self.inventory.has_filename
249
335
        return set((p for p in paths if not pred(p)))
250
336
 
 
337
    def walkdirs(self, prefix=""):
 
338
        """Walk the contents of this tree from path down.
 
339
 
 
340
        This yields all the data about the contents of a directory at a time.
 
341
        After each directory has been yielded, if the caller has mutated the
 
342
        list to exclude some directories, they are then not descended into.
 
343
        
 
344
        The data yielded is of the form:
 
345
        ((directory-relpath, directory-path-from-root, directory-fileid),
 
346
        [(relpath, basename, kind, lstat, path_from_tree_root, file_id, 
 
347
          versioned_kind), ...]),
 
348
         - directory-relpath is the containing dirs relpath from prefix
 
349
         - directory-path-from-root is the containing dirs path from /
 
350
         - directory-fileid is the id of the directory if it is versioned.
 
351
         - relpath is the relative path within the subtree being walked.
 
352
         - basename is the basename
 
353
         - kind is the kind of the file now. If unknonwn then the file is not
 
354
           present within the tree - but it may be recorded as versioned. See
 
355
           versioned_kind.
 
356
         - lstat is the stat data *if* the file was statted.
 
357
         - path_from_tree_root is the path from the root of the tree.
 
358
         - file_id is the file_id is the entry is versioned.
 
359
         - versioned_kind is the kind of the file as last recorded in the 
 
360
           versioning system. If 'unknown' the file is not versioned.
 
361
        One of 'kind' and 'versioned_kind' must not be 'unknown'.
 
362
 
 
363
        :param prefix: Start walking from prefix within the tree rather than
 
364
        at the root. This allows one to walk a subtree but get paths that are
 
365
        relative to a tree rooted higher up.
 
366
        :return: an iterator over the directory data.
 
367
        """
 
368
        raise NotImplementedError(self.walkdirs)
 
369
 
251
370
 
252
371
class EmptyTree(Tree):
253
372
 
362
481
    """
363
482
    if not filenames:
364
483
        return None
365
 
    specified_ids = _find_filename_ids_across_trees(filenames, trees, 
366
 
                                                    require_versioned)
367
 
    return _find_children_across_trees(specified_ids, trees)
368
 
 
369
 
 
370
 
def _find_filename_ids_across_trees(filenames, trees, require_versioned):
 
484
    specified_path_ids = _find_ids_across_trees(filenames, trees,
 
485
        require_versioned)
 
486
    return _find_children_across_trees(specified_path_ids, trees)
 
487
 
 
488
 
 
489
def _find_ids_across_trees(filenames, trees, require_versioned):
371
490
    """Find the ids corresponding to specified filenames.
372
491
    
373
 
    All matches in all trees will be used.
 
492
    All matches in all trees will be used, but subdirectories are not scanned.
374
493
 
375
494
    :param filenames: The filenames to find file_ids for
376
495
    :param trees: The trees to find file_ids within
377
496
    :param require_versioned: if true, all specified filenames must occur in
378
 
    at least one tree.
379
 
    :return: a set of file ids for the specified filenames
 
497
        at least one tree.
 
498
    :return: a set of (path, file ids) for the specified filenames
380
499
    """
381
500
    not_versioned = []
382
501
    interesting_ids = set()
383
502
    for tree_path in filenames:
384
503
        not_found = True
385
504
        for tree in trees:
386
 
            file_id = tree.inventory.path2id(tree_path)
 
505
            file_id = tree.path2id(tree_path)
387
506
            if file_id is not None:
388
507
                interesting_ids.add(file_id)
389
508
                not_found = False
410
529
        new_pending = set()
411
530
        for file_id in pending:
412
531
            for tree in trees:
413
 
                if file_id not in tree:
 
532
                if not tree.has_id(file_id):
414
533
                    continue
415
534
                entry = tree.inventory[file_id]
416
535
                for child in getattr(entry, 'children', {}).itervalues():
436
555
 
437
556
    @needs_read_lock
438
557
    def compare(self, want_unchanged=False, specific_files=None,
439
 
        extra_trees=None, require_versioned=False, include_root=False):
 
558
        extra_trees=None, require_versioned=False, include_root=False,
 
559
        want_unversioned=False):
440
560
        """Return the changes from source to target.
441
561
 
442
562
        :return: A TreeDelta.
451
571
        :param require_versioned: An optional boolean (defaults to False). When
452
572
            supplied and True all the 'specific_files' must be versioned, or
453
573
            a PathsNotVersionedError will be thrown.
 
574
        :param want_unversioned: Scan for unversioned paths.
454
575
        """
455
 
        # NB: show_status depends on being able to pass in non-versioned files and
456
 
        # report them as unknown
457
 
        trees = (self.source, self.target)
 
576
        # NB: show_status depends on being able to pass in non-versioned files
 
577
        # and report them as unknown
 
578
        trees = (self.source,)
458
579
        if extra_trees is not None:
459
580
            trees = trees + tuple(extra_trees)
460
 
        specific_file_ids = find_ids_across_trees(specific_files,
461
 
            trees, require_versioned=require_versioned)
 
581
        # target is usually the newer tree:
 
582
        specific_file_ids = self.target.paths2ids(specific_files, trees,
 
583
            require_versioned=require_versioned)
462
584
        if specific_files and not specific_file_ids:
463
585
            # All files are unversioned, so just return an empty delta
464
586
            # _compare_trees would think we want a complete delta
465
 
            return delta.TreeDelta()
 
587
            result = delta.TreeDelta()
 
588
            fake_entry = InventoryFile('unused', 'unused', 'unused')
 
589
            result.unversioned = [(path, None,
 
590
                self.target._comparison_data(fake_entry, path)[0]) for path in
 
591
                specific_files]
 
592
            return result
466
593
        return delta._compare_trees(self.source, self.target, want_unchanged,
467
 
            specific_file_ids, include_root)
 
594
            specific_files, include_root, extra_trees=extra_trees,
 
595
            want_unversioned=want_unversioned)
468
596
 
469
 
    def _iter_changes(self, from_tree, to_tree, include_unchanged, 
470
 
                      specific_file_ids, pb):
 
597
    def _iter_changes(self, include_unchanged=False,
 
598
                      specific_files=None, pb=None, extra_trees=[],
 
599
                      require_versioned=True, want_unversioned=False):
471
600
        """Generate an iterator of changes between trees.
472
601
 
473
602
        A tuple is returned:
474
 
        (file_id, path, changed_content, versioned, parent, name, kind,
 
603
        (file_id, (path_in_source, path_in_target),
 
604
         changed_content, versioned, parent, name, kind,
475
605
         executable)
476
606
 
477
 
        Path is relative to the to_tree.  changed_content is True if the file's
478
 
        content has changed.  This includes changes to its kind, and to
479
 
        a symlink's target.
 
607
        Changed_content is True if the file's content has changed.  This
 
608
        includes changes to its kind, and to a symlink's target.
480
609
 
481
610
        versioned, parent, name, kind, executable are tuples of (from, to).
482
611
        If a file is missing in a tree, its kind is None.
483
612
 
484
 
        Iteration is done in parent-to-child order, relative to the to_tree.
 
613
        Iteration is done in parent-to-child order, relative to the target
 
614
        tree.
 
615
 
 
616
        There is no guarantee that all paths are in sorted order: the
 
617
        requirement to expand the search due to renames may result in children
 
618
        that should be found early being found late in the search, after
 
619
        lexically later results have been returned.
 
620
        :param require_versioned: Raise errors.PathsNotVersionedError if a
 
621
            path in the specific_files list is not versioned in one of
 
622
            source, target or extra_trees.
 
623
        :param want_unversioned: Should unversioned files be returned in the
 
624
            output. An unversioned file is defined as one with (False, False)
 
625
            for the versioned pair.
485
626
        """
 
627
        result = []
 
628
        lookup_trees = [self.source]
 
629
        if extra_trees:
 
630
             lookup_trees.extend(extra_trees)
 
631
        specific_file_ids = self.target.paths2ids(specific_files,
 
632
            lookup_trees, require_versioned=require_versioned)
 
633
        if want_unversioned:
 
634
            all_unversioned = sorted([(p.split('/'), p) for p in self.target.extras()
 
635
                if not specific_files or
 
636
                    osutils.is_inside_any(specific_files, p)])
 
637
            all_unversioned = deque(all_unversioned)
 
638
        else:
 
639
            all_unversioned = deque()
486
640
        to_paths = {}
487
 
        from_entries_by_dir = list(from_tree.inventory.iter_entries_by_dir(
 
641
        from_entries_by_dir = list(self.source.inventory.iter_entries_by_dir(
488
642
            specific_file_ids=specific_file_ids))
489
643
        from_data = dict((e.file_id, (p, e)) for p, e in from_entries_by_dir)
490
 
        to_entries_by_dir = list(to_tree.inventory.iter_entries_by_dir(
 
644
        to_entries_by_dir = list(self.target.inventory.iter_entries_by_dir(
491
645
            specific_file_ids=specific_file_ids))
492
646
        num_entries = len(from_entries_by_dir) + len(to_entries_by_dir)
493
647
        entry_count = 0
 
648
        # the unversioned path lookup only occurs on real trees - where there 
 
649
        # can be extras. So the fake_entry is solely used to look up
 
650
        # executable it values when execute is not supported.
 
651
        fake_entry = InventoryFile('unused', 'unused', 'unused')
494
652
        for to_path, to_entry in to_entries_by_dir:
 
653
            while all_unversioned and all_unversioned[0][0] < to_path.split('/'):
 
654
                unversioned_path = all_unversioned.popleft()
 
655
                to_kind, to_executable, to_stat = \
 
656
                    self.target._comparison_data(fake_entry, unversioned_path[1])
 
657
                yield (None, (None, unversioned_path[1]), True, (False, False),
 
658
                    (None, None),
 
659
                    (None, unversioned_path[0][-1]),
 
660
                    (None, to_kind),
 
661
                    (None, to_executable))
495
662
            file_id = to_entry.file_id
496
663
            to_paths[file_id] = to_path
497
664
            entry_count += 1
503
670
                from_name = from_entry.name
504
671
                from_parent = from_entry.parent_id
505
672
                from_kind, from_executable, from_stat = \
506
 
                    from_tree._comparison_data(from_entry, from_path)
 
673
                    self.source._comparison_data(from_entry, from_path)
507
674
                entry_count += 1
508
675
            else:
509
676
                from_versioned = False
513
680
                from_executable = None
514
681
            versioned = (from_versioned, True)
515
682
            to_kind, to_executable, to_stat = \
516
 
                to_tree._comparison_data(to_entry, to_path)
 
683
                self.target._comparison_data(to_entry, to_path)
517
684
            kind = (from_kind, to_kind)
518
685
            if kind[0] != kind[1]:
519
686
                changed_content = True
520
687
            elif from_kind == 'file':
521
 
                from_size = from_tree._file_size(from_entry, from_stat)
522
 
                to_size = to_tree._file_size(to_entry, to_stat)
 
688
                from_size = self.source._file_size(from_entry, from_stat)
 
689
                to_size = self.target._file_size(to_entry, to_stat)
523
690
                if from_size != to_size:
524
691
                    changed_content = True
525
 
                elif (from_tree.get_file_sha1(file_id, from_path, from_stat) !=
526
 
                    to_tree.get_file_sha1(file_id, to_path, to_stat)):
 
692
                elif (self.source.get_file_sha1(file_id, from_path, from_stat) !=
 
693
                    self.target.get_file_sha1(file_id, to_path, to_stat)):
527
694
                    changed_content = True
528
695
            elif from_kind == 'symlink':
529
 
                if (from_tree.get_symlink_target(file_id) != 
530
 
                    to_tree.get_symlink_target(file_id)):
 
696
                if (self.source.get_symlink_target(file_id) !=
 
697
                    self.target.get_symlink_target(file_id)):
531
698
                    changed_content = True
 
699
                elif from_kind == 'tree-reference':
 
700
                    if (self.source.get_reference_revision(file_id, from_path)
 
701
                        != self.target.get_reference_revision(file_id, to_path)):
 
702
                        changed_content = True 
532
703
            parent = (from_parent, to_entry.parent_id)
533
704
            name = (from_name, to_entry.name)
534
705
            executable = (from_executable, to_executable)
535
706
            if pb is not None:
536
707
                pb.update('comparing files', entry_count, num_entries)
537
 
            if (changed_content is not False or versioned[0] != versioned[1] 
 
708
            if (changed_content is not False or versioned[0] != versioned[1]
538
709
                or parent[0] != parent[1] or name[0] != name[1] or 
539
710
                executable[0] != executable[1] or include_unchanged):
540
 
                yield (file_id, to_path, changed_content, versioned, parent,
541
 
                       name, kind, executable)
542
 
 
543
 
        def get_to_path(from_entry):
544
 
            if from_entry.parent_id is None:
545
 
                to_path = ''
 
711
                yield (file_id, (from_path, to_path), changed_content,
 
712
                    versioned, parent, name, kind, executable)
 
713
 
 
714
        while all_unversioned:
 
715
            # yield any trailing unversioned paths
 
716
            unversioned_path = all_unversioned.popleft()
 
717
            to_kind, to_executable, to_stat = \
 
718
                self.target._comparison_data(fake_entry, unversioned_path[1])
 
719
            yield (None, (None, unversioned_path[1]), True, (False, False),
 
720
                (None, None),
 
721
                (None, unversioned_path[0][-1]),
 
722
                (None, to_kind),
 
723
                (None, to_executable))
 
724
 
 
725
        def get_to_path(to_entry):
 
726
            if to_entry.parent_id is None:
 
727
                to_path = '' # the root
546
728
            else:
547
 
                if from_entry.parent_id not in to_paths:
548
 
                    get_to_path(from_tree.inventory[from_entry.parent_id])
549
 
                to_path = osutils.pathjoin(to_paths[from_entry.parent_id],
550
 
                                           from_entry.name)
551
 
            to_paths[from_entry.file_id] = to_path
 
729
                if to_entry.parent_id not in to_paths:
 
730
                    # recurse up
 
731
                    return get_to_path(self.target.inventory[to_entry.parent_id])
 
732
                to_path = osutils.pathjoin(to_paths[to_entry.parent_id],
 
733
                                           to_entry.name)
 
734
            to_paths[to_entry.file_id] = to_path
552
735
            return to_path
553
736
 
554
737
        for path, from_entry in from_entries_by_dir:
555
738
            file_id = from_entry.file_id
556
739
            if file_id in to_paths:
 
740
                # already returned
557
741
                continue
558
 
            to_path = get_to_path(from_entry)
 
742
            if not file_id in self.target.inventory:
 
743
                # common case - paths we have not emitted are not present in
 
744
                # target.
 
745
                to_path = None
 
746
            else:
 
747
                to_path = get_to_path(self.target.inventory[file_id])
559
748
            entry_count += 1
560
749
            if pb is not None:
561
750
                pb.update('comparing files', entry_count, num_entries)
563
752
            parent = (from_entry.parent_id, None)
564
753
            name = (from_entry.name, None)
565
754
            from_kind, from_executable, stat_value = \
566
 
                from_tree._comparison_data(from_entry, path)
 
755
                self.source._comparison_data(from_entry, path)
567
756
            kind = (from_kind, None)
568
757
            executable = (from_executable, None)
569
758
            changed_content = True
570
759
            # the parent's path is necessarily known at this point.
571
 
            yield(file_id, to_path, changed_content, versioned, parent,
 
760
            yield(file_id, (path, to_path), changed_content, versioned, parent,
572
761
                  name, kind, executable)
573
762
 
574
763