~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/dirstate.py

(robertc) Make iter_changes produce output that is always safe to
        generate inventory deltas of in the same direction as the
        changes. (Robert Collins, #347649)

Show diffs side-by-side

added added

removed removed

Lines of Context:
3166
3166
 
3167
3167
    __slots__ = ["old_dirname_to_file_id", "new_dirname_to_file_id",
3168
3168
        "last_source_parent", "last_target_parent", "include_unchanged",
3169
 
        "use_filesystem_for_exec", "utf8_decode", "searched_specific_files",
3170
 
        "search_specific_files", "state", "source_index", "target_index",
3171
 
        "want_unversioned", "tree"]
 
3169
        "partial", "use_filesystem_for_exec", "utf8_decode",
 
3170
        "searched_specific_files", "search_specific_files",
 
3171
        "searched_exact_paths", "search_specific_file_parents", "seen_ids",
 
3172
        "state", "source_index", "target_index", "want_unversioned", "tree"]
3172
3173
 
3173
3174
    def __init__(self, include_unchanged, use_filesystem_for_exec,
3174
3175
        search_specific_files, state, source_index, target_index,
3175
3176
        want_unversioned, tree):
3176
3177
        self.old_dirname_to_file_id = {}
3177
3178
        self.new_dirname_to_file_id = {}
 
3179
        # Are we doing a partial iter_changes?
 
3180
        self.partial = search_specific_files != set([''])
3178
3181
        # Using a list so that we can access the values and change them in
3179
3182
        # nested scope. Each one is [path, file_id, entry]
3180
3183
        self.last_source_parent = [None, None]
3183
3186
        self.use_filesystem_for_exec = use_filesystem_for_exec
3184
3187
        self.utf8_decode = cache_utf8._utf8_decode
3185
3188
        # for all search_indexs in each path at or under each element of
3186
 
        # search_specific_files, if the detail is relocated: add the id, and add the
3187
 
        # relocated path as one to search if its not searched already. If the
3188
 
        # detail is not relocated, add the id.
 
3189
        # search_specific_files, if the detail is relocated: add the id, and
 
3190
        # add the relocated path as one to search if its not searched already.
 
3191
        # If the detail is not relocated, add the id.
3189
3192
        self.searched_specific_files = set()
 
3193
        # When we search exact paths without expanding downwards, we record
 
3194
        # that here.
 
3195
        self.searched_exact_paths = set()
3190
3196
        self.search_specific_files = search_specific_files
 
3197
        # The parents up to the root of the paths we are searching.
 
3198
        # After all normal paths are returned, these specific items are returned.
 
3199
        self.search_specific_file_parents = set()
 
3200
        # The ids we've sent out in the delta.
 
3201
        self.seen_ids = set()
3191
3202
        self.state = state
3192
3203
        self.source_index = source_index
3193
3204
        self.target_index = target_index
 
3205
        if target_index != 0:
 
3206
            # A lot of code in here depends on target_index == 0
 
3207
            raise errors.BzrError('unsupported target index')
3194
3208
        self.want_unversioned = want_unversioned
3195
3209
        self.tree = tree
3196
3210
 
3198
3212
        """Compare an entry and real disk to generate delta information.
3199
3213
 
3200
3214
        :param path_info: top_relpath, basename, kind, lstat, abspath for
3201
 
            the path of entry. If None, then the path is considered absent.
3202
 
            (Perhaps we should pass in a concrete entry for this ?)
 
3215
            the path of entry. If None, then the path is considered absent in 
 
3216
            the target (Perhaps we should pass in a concrete entry for this ?)
3203
3217
            Basename is returned as a utf8 string because we expect this
3204
3218
            tuple will be ignored, and don't want to take the time to
3205
3219
            decode.
3206
3220
        :return: (iter_changes_result, changed). If the entry has not been
3207
3221
            handled then changed is None. Otherwise it is False if no content
3208
 
            or metadata changes have occured, and None if any content or
3209
 
            metadata change has occured. If self.include_unchanged is True then
 
3222
            or metadata changes have occurred, and True if any content or
 
3223
            metadata change has occurred. If self.include_unchanged is True then
3210
3224
            if changed is not None, iter_changes_result will always be a result
3211
3225
            tuple. Otherwise, iter_changes_result is None unless changed is
3212
3226
            True.
3463
3477
    def __iter__(self):
3464
3478
        return self
3465
3479
 
 
3480
    def _gather_result_for_consistency(self, result):
 
3481
        """Check a result we will yield to make sure we are consistent later.
 
3482
        
 
3483
        This gathers result's parents into a set to output later.
 
3484
 
 
3485
        :param result: A result tuple.
 
3486
        """
 
3487
        if not self.partial or not result[0]:
 
3488
            return
 
3489
        self.seen_ids.add(result[0])
 
3490
        new_path = result[1][1]
 
3491
        if new_path:
 
3492
            # Not the root and not a delete: queue up the parents of the path.
 
3493
            self.search_specific_file_parents.update(
 
3494
                osutils.parent_directories(new_path.encode('utf8')))
 
3495
            # Add the root directory which parent_directories does not
 
3496
            # provide.
 
3497
            self.search_specific_file_parents.add('')
 
3498
 
3466
3499
    def iter_changes(self):
3467
3500
        """Iterate over the changes."""
3468
3501
        utf8_decode = cache_utf8._utf8_decode
3546
3579
                result, changed = _process_entry(entry, root_dir_info)
3547
3580
                if changed is not None:
3548
3581
                    path_handled = True
 
3582
                    if changed:
 
3583
                        self._gather_result_for_consistency(result)
3549
3584
                    if changed or self.include_unchanged:
3550
3585
                        yield result
3551
3586
            if self.want_unversioned and not path_handled and root_dir_info:
3664
3699
                            # advance the entry only, after processing.
3665
3700
                            result, changed = _process_entry(current_entry, None)
3666
3701
                            if changed is not None:
 
3702
                                if changed:
 
3703
                                    self._gather_result_for_consistency(result)
3667
3704
                                if changed or self.include_unchanged:
3668
3705
                                    yield result
3669
3706
                        block_index +=1
3702
3739
                        # no path is fine: the per entry code will handle it.
3703
3740
                        result, changed = _process_entry(current_entry, current_path_info)
3704
3741
                        if changed is not None:
 
3742
                            if changed:
 
3743
                                self._gather_result_for_consistency(result)
3705
3744
                            if changed or self.include_unchanged:
3706
3745
                                yield result
3707
3746
                    elif (current_entry[0][1] != current_path_info[1]
3723
3762
                            # advance the entry only, after processing.
3724
3763
                            result, changed = _process_entry(current_entry, None)
3725
3764
                            if changed is not None:
 
3765
                                if changed:
 
3766
                                    self._gather_result_for_consistency(result)
3726
3767
                                if changed or self.include_unchanged:
3727
3768
                                    yield result
3728
3769
                            advance_path = False
3730
3771
                        result, changed = _process_entry(current_entry, current_path_info)
3731
3772
                        if changed is not None:
3732
3773
                            path_handled = True
 
3774
                            if changed:
 
3775
                                self._gather_result_for_consistency(result)
3733
3776
                            if changed or self.include_unchanged:
3734
3777
                                yield result
3735
3778
                    if advance_entry and current_entry is not None:
3795
3838
                        current_dir_info = dir_iterator.next()
3796
3839
                    except StopIteration:
3797
3840
                        current_dir_info = None
 
3841
        for result in self._iter_specific_file_parents():
 
3842
            yield result
 
3843
 
 
3844
    def _iter_specific_file_parents(self):
 
3845
        """Iter over the specific file parents."""
 
3846
        while self.search_specific_file_parents:
 
3847
            # Process the parent directories for the paths we were iterating.
 
3848
            # Even in extremely large trees this should be modest, so currently
 
3849
            # no attempt is made to optimise.
 
3850
            path_utf8 = self.search_specific_file_parents.pop()
 
3851
            if osutils.is_inside_any(self.searched_specific_files, path_utf8):
 
3852
                # We've examined this path.
 
3853
                continue
 
3854
            if path_utf8 in self.searched_exact_paths:
 
3855
                # We've examined this path.
 
3856
                continue
 
3857
            path_entries = self.state._entries_for_path(path_utf8)
 
3858
            # We need either one or two entries. If the path in
 
3859
            # self.target_index has moved (so the entry in source_index is in
 
3860
            # 'ar') then we need to also look for the entry for this path in
 
3861
            # self.source_index, to output the appropriate delete-or-rename.
 
3862
            selected_entries = []
 
3863
            found_item = False
 
3864
            for candidate_entry in path_entries:
 
3865
                # Find entries present in target at this path:
 
3866
                if candidate_entry[1][self.target_index][0] not in 'ar':
 
3867
                    found_item = True
 
3868
                    selected_entries.append(candidate_entry)
 
3869
                # Find entries present in source at this path:
 
3870
                elif (self.source_index is not None and
 
3871
                    candidate_entry[1][self.source_index][0] not in 'ar'):
 
3872
                    found_item = True
 
3873
                    if candidate_entry[1][self.target_index][0] == 'a':
 
3874
                        # Deleted, emit it here.
 
3875
                        selected_entries.append(candidate_entry)
 
3876
                    else:
 
3877
                        # renamed, emit it when we process the directory it
 
3878
                        # ended up at.
 
3879
                        self.search_specific_file_parents.add(
 
3880
                            candidate_entry[1][self.target_index][1])
 
3881
            if not found_item:
 
3882
                raise AssertionError(
 
3883
                    "Missing entry for specific path parent %r, %r" % (
 
3884
                    path_utf8, path_entries))
 
3885
            path_info = self._path_info(path_utf8, path_utf8.decode('utf8'))
 
3886
            for entry in selected_entries:
 
3887
                if entry[0][2] in self.seen_ids:
 
3888
                    continue
 
3889
                result, changed = self._process_entry(entry, path_info)
 
3890
                if changed is None:
 
3891
                    raise AssertionError(
 
3892
                        "Got entry<->path mismatch for specific path "
 
3893
                        "%r entry %r path_info %r " % (
 
3894
                        path_utf8, entry, path_info))
 
3895
                # Only include changes - we're outside the users requested
 
3896
                # expansion.
 
3897
                if changed:
 
3898
                    self._gather_result_for_consistency(result)
 
3899
                    if (result[6][0] == 'directory' and
 
3900
                        result[6][1] != 'directory'):
 
3901
                        # This stopped being a directory, the old children have
 
3902
                        # to be included.
 
3903
                        if entry[1][self.source_index][0] == 'r':
 
3904
                            # renamed, take the source path
 
3905
                            entry_path_utf8 = entry[1][self.source_index][1]
 
3906
                        else:
 
3907
                            entry_path_utf8 = path_utf8
 
3908
                        initial_key = (entry_path_utf8, '', '')
 
3909
                        block_index, _ = self.state._find_block_index_from_key(
 
3910
                            initial_key)
 
3911
                        if block_index == 0:
 
3912
                            # The children of the root are in block index 1.
 
3913
                            block_index +=1
 
3914
                        current_block = None
 
3915
                        if block_index < len(self.state._dirblocks):
 
3916
                            current_block = self.state._dirblocks[block_index]
 
3917
                            if not osutils.is_inside(
 
3918
                                entry_path_utf8, current_block[0]):
 
3919
                                # No entries for this directory at all.
 
3920
                                current_block = None
 
3921
                        if current_block is not None:
 
3922
                            for entry in current_block[1]:
 
3923
                                if entry[1][self.source_index][0] in 'ar':
 
3924
                                    # Not in the source tree, so doesn't have to be
 
3925
                                    # included.
 
3926
                                    continue
 
3927
                                # Path of the entry itself.
 
3928
 
 
3929
                                self.search_specific_file_parents.add(
 
3930
                                    osutils.pathjoin(*entry[0][:2]))
 
3931
                if changed or self.include_unchanged:
 
3932
                    yield result
 
3933
            self.searched_exact_paths.add(path_utf8)
 
3934
 
 
3935
    def _path_info(self, utf8_path, unicode_path):
 
3936
        """Generate path_info for unicode_path.
 
3937
 
 
3938
        :return: None if unicode_path does not exist, or a path_info tuple.
 
3939
        """
 
3940
        abspath = self.tree.abspath(unicode_path)
 
3941
        try:
 
3942
            stat = os.lstat(abspath)
 
3943
        except OSError, e:
 
3944
            if e.errno == errno.ENOENT:
 
3945
                # the path does not exist.
 
3946
                return None
 
3947
            else:
 
3948
                raise
 
3949
        utf8_basename = utf8_path.rsplit('/', 1)[-1]
 
3950
        dir_info = (utf8_path, utf8_basename,
 
3951
            osutils.file_kind_from_stat_mode(stat.st_mode), stat,
 
3952
            abspath)
 
3953
        if dir_info[2] == 'directory':
 
3954
            if self.tree._directory_is_tree_reference(
 
3955
                unicode_path):
 
3956
                self.root_dir_info = self.root_dir_info[:2] + \
 
3957
                    ('tree-reference',) + self.root_dir_info[3:]
 
3958
        return dir_info
3798
3959
 
3799
3960
 
3800
3961
# Try to load the compiled form if possible