def _record_unselected(self): # If specific files are selected, then all un-selected files must be # recorded in their previous state. For more details, see # https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html. if self.specific_files or self.exclude: specific_files = self.specific_files or [] for path, old_ie in self.basis_inv.iter_entries(): if self.builder.new_inventory.has_id(old_ie.file_id): # already added - skip. continue if (is_inside_any(specific_files, path) and not is_inside_any(self.exclude, path)): # was inside the selected path, and not excluded - if not # present it has been deleted so skip. continue # From here down it was either not selected, or was excluded: # We preserve the entry unaltered. ie = old_ie.copy() # Note: specific file commits after a merge are currently # prohibited. This test is for sanity/safety in case it's # required after that changes. if len(self.parents) > 1: ie.revision = None self.builder.record_entry_contents(ie, self.parent_invs, path, self.basis_tree, None)
def _update_builder_with_changes(self): """Update the commit builder with the data about what has changed. """ # Build the revision inventory. # # This starts by creating a new empty inventory. Depending on # which files are selected for commit, and what is present in the # current tree, the new inventory is populated. inventory entries # which are candidates for modification have their revision set to # None; inventory entries that are carried over untouched have their # revision set to their prior value. # # ESEPARATIONOFCONCERNS: this function is diffing and using the diff # results to create a new inventory at the same time, which results # in bugs like #46635. Any reason not to use/enhance Tree.changes_from? # ADHB 11-07-2006 exclude = self.exclude specific_files = self.specific_files or [] mutter("Selecting files for commit with filter %s", specific_files) # Build the new inventory self._populate_from_inventory() # If specific files are selected, then all un-selected files must be # recorded in their previous state. For more details, see # https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html. if specific_files or exclude: for path, old_ie in self.basis_inv.iter_entries(): if old_ie.file_id in self.builder.new_inventory: # already added - skip. continue if (is_inside_any(specific_files, path) and not is_inside_any(exclude, path)): # was inside the selected path, and not excluded - if not # present it has been deleted so skip. continue # From here down it was either not selected, or was excluded: if old_ie.kind == 'directory': self._next_progress_entry() # We preserve the entry unaltered. ie = old_ie.copy() # Note: specific file commits after a merge are currently # prohibited. This test is for sanity/safety in case it's # required after that changes. if len(self.parents) > 1: ie.revision = None delta, version_recorded = self.builder.record_entry_contents( ie, self.parent_invs, path, self.basis_tree, None) if version_recorded: self.any_entries_changed = True if delta: self._basis_delta.append(delta)
def report(self, file_id, paths, versioned, renamed, modified, exe_change, kind): """Report one change to a file :param file_id: The file_id of the file :param path: The old and new paths as generated by Tree.iter_changes. :param versioned: may be 'added', 'removed', 'unchanged', or 'unversioned. :param renamed: may be True or False :param modified: may be 'created', 'deleted', 'kind changed', 'modified' or 'unchanged'. :param exe_change: True if the execute bit has changed :param kind: A pair of file kinds, as generated by Tree.iter_changes. None indicates no file present. """ if is_quiet(): return if paths[1] == '' and versioned == 'added' and self.suppress_root_add: return if self.view_files and not osutils.is_inside_any(self.view_files, paths[1]): return if versioned == 'unversioned': # skip ignored unversioned files if needed. if self.unversioned_filter is not None: if self.unversioned_filter(paths[1]): return # dont show a content change in the output. modified = 'unchanged' # we show both paths in the following situations: # the file versioning is unchanged AND # ( the path is different OR # the kind is different) if (versioned == 'unchanged' and (renamed or modified == 'kind changed')): if renamed: # on a rename, we show old and new old_path, path = paths else: # if it's not renamed, we're showing both for kind changes # so only show the new path old_path, path = paths[1], paths[1] # if the file is not missing in the source, we show its kind # when we show two paths. if kind[0] is not None: old_path += self.kind_marker(kind[0]) old_path += " => " elif versioned == 'removed': # not present in target old_path = "" path = paths[0] else: old_path = "" path = paths[1] if renamed: rename = "R" else: rename = self.versioned_map[versioned] # we show the old kind on the new path when the content is deleted. if modified == 'deleted': path += self.kind_marker(kind[0]) # otherwise we always show the current kind when there is one elif kind[1] is not None: path += self.kind_marker(kind[1]) if exe_change: exe = '*' else: exe = ' ' self.output("%s%s%s %s%s", rename, self.modified_map[modified], exe, old_path, path)
def check_path_in_view(tree, relpath): """If a working tree has a view enabled, check the path is within it.""" if tree.supports_views(): view_files = tree.views.lookup_view() if view_files and not osutils.is_inside_any(view_files, relpath): raise errors.FileOutsideView(relpath, view_files)
def _populate_from_inventory(self): """Populate the CommitBuilder by walking the working tree inventory.""" if self.strict: # raise an exception as soon as we find a single unknown. for unknown in self.work_tree.unknowns(): raise StrictCommitFailed() specific_files = self.specific_files exclude = self.exclude report_changes = self.reporter.is_verbose() deleted_ids = [] # A tree of paths that have been deleted. E.g. if foo/bar has been # deleted, then we have {'foo':{'bar':{}}} deleted_paths = {} # XXX: Note that entries may have the wrong kind because the entry does # not reflect the status on disk. work_inv = self.work_tree.inventory # NB: entries will include entries within the excluded ids/paths # because iter_entries_by_dir has no 'exclude' facility today. entries = work_inv.iter_entries_by_dir( specific_file_ids=self.specific_file_ids, yield_parents=True) for path, existing_ie in entries: file_id = existing_ie.file_id name = existing_ie.name parent_id = existing_ie.parent_id kind = existing_ie.kind if kind == 'directory': self._next_progress_entry() # Skip files that have been deleted from the working tree. # The deleted path ids are also recorded so they can be explicitly # unversioned later. if deleted_paths: path_segments = splitpath(path) deleted_dict = deleted_paths for segment in path_segments: deleted_dict = deleted_dict.get(segment, None) if not deleted_dict: # We either took a path not present in the dict # (deleted_dict was None), or we've reached an empty # child dir in the dict, so are now a sub-path. break else: deleted_dict = None if deleted_dict is not None: # the path has a deleted parent, do not add it. continue if exclude and is_inside_any(exclude, path): # Skip excluded paths. Excluded paths are processed by # _update_builder_with_changes. continue content_summary = self.work_tree.path_content_summary(path) # Note that when a filter of specific files is given, we must only # skip/record deleted files matching that filter. if not specific_files or is_inside_any(specific_files, path): if content_summary[0] == 'missing': if not deleted_paths: # path won't have been split yet. path_segments = splitpath(path) deleted_dict = deleted_paths for segment in path_segments: deleted_dict = deleted_dict.setdefault(segment, {}) self.reporter.missing(path) deleted_ids.append(file_id) continue # TODO: have the builder do the nested commit just-in-time IF and # only if needed. if content_summary[0] == 'tree-reference': # enforce repository nested tree policy. if (not self.work_tree.supports_tree_reference() or # repository does not support it either. not self.branch.repository._format.supports_tree_reference): content_summary = ('directory',) + content_summary[1:] kind = content_summary[0] # TODO: specific_files filtering before nested tree processing if kind == 'tree-reference': if self.recursive == 'down': nested_revision_id = self._commit_nested_tree( file_id, path) content_summary = content_summary[:3] + ( nested_revision_id,) else: content_summary = content_summary[:3] + ( self.work_tree.get_reference_revision(file_id),) # Record an entry for this item # Note: I don't particularly want to have the existing_ie # parameter but the test suite currently (28-Jun-07) breaks # without it thanks to a unicode normalisation issue. :-( definitely_changed = kind != existing_ie.kind self._record_entry(path, file_id, specific_files, kind, name, parent_id, definitely_changed, existing_ie, report_changes, content_summary) # Unversion IDs that were found to be deleted self.work_tree.unversion(deleted_ids)
def _populate_from_inventory(self): """Populate the CommitBuilder by walking the working tree inventory.""" # Build the revision inventory. # # This starts by creating a new empty inventory. Depending on # which files are selected for commit, and what is present in the # current tree, the new inventory is populated. inventory entries # which are candidates for modification have their revision set to # None; inventory entries that are carried over untouched have their # revision set to their prior value. # # ESEPARATIONOFCONCERNS: this function is diffing and using the diff # results to create a new inventory at the same time, which results # in bugs like #46635. Any reason not to use/enhance Tree.changes_from? # ADHB 11-07-2006 specific_files = self.specific_files exclude = self.exclude report_changes = self.reporter.is_verbose() deleted_ids = [] # A tree of paths that have been deleted. E.g. if foo/bar has been # deleted, then we have {'foo':{'bar':{}}} deleted_paths = {} # XXX: Note that entries may have the wrong kind because the entry does # not reflect the status on disk. # NB: entries will include entries within the excluded ids/paths # because iter_entries_by_dir has no 'exclude' facility today. entries = self.work_tree.iter_entries_by_dir( specific_file_ids=self.specific_file_ids, yield_parents=True) for path, existing_ie in entries: file_id = existing_ie.file_id name = existing_ie.name parent_id = existing_ie.parent_id kind = existing_ie.kind # Skip files that have been deleted from the working tree. # The deleted path ids are also recorded so they can be explicitly # unversioned later. if deleted_paths: path_segments = splitpath(path) deleted_dict = deleted_paths for segment in path_segments: deleted_dict = deleted_dict.get(segment, None) if not deleted_dict: # We either took a path not present in the dict # (deleted_dict was None), or we've reached an empty # child dir in the dict, so are now a sub-path. break else: deleted_dict = None if deleted_dict is not None: # the path has a deleted parent, do not add it. continue if exclude and is_inside_any(exclude, path): # Skip excluded paths. Excluded paths are processed by # _update_builder_with_changes. continue content_summary = self.work_tree.path_content_summary(path) kind = content_summary[0] # Note that when a filter of specific files is given, we must only # skip/record deleted files matching that filter. if not specific_files or is_inside_any(specific_files, path): if kind == 'missing': if not deleted_paths: # path won't have been split yet. path_segments = splitpath(path) deleted_dict = deleted_paths for segment in path_segments: deleted_dict = deleted_dict.setdefault(segment, {}) self.reporter.missing(path) self._next_progress_entry() deleted_ids.append(file_id) continue # TODO: have the builder do the nested commit just-in-time IF and # only if needed. if kind == 'tree-reference': # enforce repository nested tree policy. if (not self.work_tree.supports_tree_reference() or # repository does not support it either. not self.branch.repository._format.supports_tree_reference): kind = 'directory' content_summary = (kind, None, None, None) elif self.recursive == 'down': nested_revision_id = self._commit_nested_tree( file_id, path) content_summary = (kind, None, None, nested_revision_id) else: nested_revision_id = self.work_tree.get_reference_revision(file_id) content_summary = (kind, None, None, nested_revision_id) # Record an entry for this item # Note: I don't particularly want to have the existing_ie # parameter but the test suite currently (28-Jun-07) breaks # without it thanks to a unicode normalisation issue. :-( definitely_changed = kind != existing_ie.kind self._record_entry(path, file_id, specific_files, kind, name, parent_id, definitely_changed, existing_ie, report_changes, content_summary) # Unversion IDs that were found to be deleted self.deleted_ids = deleted_ids
def report(self, file_id, paths, versioned, renamed, modified, exe_change, kind): """Report one change to a file :param file_id: The file_id of the file :param path: The old and new paths as generated by Tree.iter_changes. :param versioned: may be 'added', 'removed', 'unchanged', or 'unversioned. :param renamed: may be True or False :param modified: may be 'created', 'deleted', 'kind changed', 'modified' or 'unchanged'. :param exe_change: True if the execute bit has changed :param kind: A pair of file kinds, as generated by Tree.iter_changes. None indicates no file present. """ if is_quiet(): return if paths[1] == '' and versioned == 'added' and self.suppress_root_add: return if self.view_files and not osutils.is_inside_any( self.view_files, paths[1]): return if versioned == 'unversioned': # skip ignored unversioned files if needed. if self.unversioned_filter is not None: if self.unversioned_filter(paths[1]): return # dont show a content change in the output. modified = 'unchanged' # we show both paths in the following situations: # the file versioning is unchanged AND # ( the path is different OR # the kind is different) if (versioned == 'unchanged' and (renamed or modified == 'kind changed')): if renamed: # on a rename, we show old and new old_path, path = paths else: # if it's not renamed, we're showing both for kind changes # so only show the new path old_path, path = paths[1], paths[1] # if the file is not missing in the source, we show its kind # when we show two paths. if kind[0] is not None: old_path += self.kind_marker(kind[0]) old_path += " => " elif versioned == 'removed': # not present in target old_path = "" path = paths[0] else: old_path = "" path = paths[1] if renamed: rename = "R" else: rename = self.versioned_map[versioned] # we show the old kind on the new path when the content is deleted. if modified == 'deleted': path += self.kind_marker(kind[0]) # otherwise we always show the current kind when there is one elif kind[1] is not None: path += self.kind_marker(kind[1]) if exe_change: exe = '*' else: exe = ' ' self.output("%s%s%s %s%s", rename, self.modified_map[modified], exe, old_path, path)
def iter_changes(self, include_unchanged=False, specific_files=None, pb=None, extra_trees=[], require_versioned=True, want_unversioned=False): """Generate an iterator of changes between trees. A tuple is returned: (file_id, (path_in_source, path_in_target), changed_content, versioned, parent, name, kind, executable) Changed_content is True if the file's content has changed. This includes changes to its kind, and to a symlink's target. versioned, parent, name, kind, executable are tuples of (from, to). If a file is missing in a tree, its kind is None. Iteration is done in parent-to-child order, relative to the target tree. There is no guarantee that all paths are in sorted order: the requirement to expand the search due to renames may result in children that should be found early being found late in the search, after lexically later results have been returned. :param require_versioned: Raise errors.PathsNotVersionedError if a path in the specific_files list is not versioned in one of source, target or extra_trees. :param want_unversioned: Should unversioned files be returned in the output. An unversioned file is defined as one with (False, False) for the versioned pair. """ result = [] lookup_trees = [self.source] if extra_trees: lookup_trees.extend(extra_trees) if specific_files == []: specific_file_ids = [] else: specific_file_ids = self.target.paths2ids(specific_files, lookup_trees, require_versioned=require_versioned) if want_unversioned: all_unversioned = sorted([(p.split('/'), p) for p in self.target.extras() if specific_files is None or osutils.is_inside_any(specific_files, p)]) all_unversioned = deque(all_unversioned) else: all_unversioned = deque() to_paths = {} from_entries_by_dir = list(self.source.inventory.iter_entries_by_dir( specific_file_ids=specific_file_ids)) from_data = dict((e.file_id, (p, e)) for p, e in from_entries_by_dir) to_entries_by_dir = list(self.target.inventory.iter_entries_by_dir( specific_file_ids=specific_file_ids)) num_entries = len(from_entries_by_dir) + len(to_entries_by_dir) entry_count = 0 # the unversioned path lookup only occurs on real trees - where there # can be extras. So the fake_entry is solely used to look up # executable it values when execute is not supported. fake_entry = InventoryFile('unused', 'unused', 'unused') for to_path, to_entry in to_entries_by_dir: while all_unversioned and all_unversioned[0][0] < to_path.split('/'): unversioned_path = all_unversioned.popleft() to_kind, to_executable, to_stat = \ self.target._comparison_data(fake_entry, unversioned_path[1]) yield (None, (None, unversioned_path[1]), True, (False, False), (None, None), (None, unversioned_path[0][-1]), (None, to_kind), (None, to_executable)) file_id = to_entry.file_id to_paths[file_id] = to_path entry_count += 1 changed_content = False from_path, from_entry = from_data.get(file_id, (None, None)) from_versioned = (from_entry is not None) if from_entry is not None: from_versioned = True from_name = from_entry.name from_parent = from_entry.parent_id from_kind, from_executable, from_stat = \ self.source._comparison_data(from_entry, from_path) entry_count += 1 else: from_versioned = False from_kind = None from_parent = None from_name = None from_executable = None versioned = (from_versioned, True) to_kind, to_executable, to_stat = \ self.target._comparison_data(to_entry, to_path) kind = (from_kind, to_kind) if kind[0] != kind[1]: changed_content = True elif from_kind == 'file': from_size = self.source._file_size(from_entry, from_stat) to_size = self.target._file_size(to_entry, to_stat) if from_size != to_size: changed_content = True elif (self.source.get_file_sha1(file_id, from_path, from_stat) != self.target.get_file_sha1(file_id, to_path, to_stat)): changed_content = True elif from_kind == 'symlink': if (self.source.get_symlink_target(file_id) != self.target.get_symlink_target(file_id)): changed_content = True elif from_kind == 'tree-reference': if (self.source.get_reference_revision(file_id, from_path) != self.target.get_reference_revision(file_id, to_path)): changed_content = True parent = (from_parent, to_entry.parent_id) name = (from_name, to_entry.name) executable = (from_executable, to_executable) if pb is not None: pb.update('comparing files', entry_count, num_entries) if (changed_content is not False or versioned[0] != versioned[1] or parent[0] != parent[1] or name[0] != name[1] or executable[0] != executable[1] or include_unchanged): yield (file_id, (from_path, to_path), changed_content, versioned, parent, name, kind, executable) while all_unversioned: # yield any trailing unversioned paths unversioned_path = all_unversioned.popleft() to_kind, to_executable, to_stat = \ self.target._comparison_data(fake_entry, unversioned_path[1]) yield (None, (None, unversioned_path[1]), True, (False, False), (None, None), (None, unversioned_path[0][-1]), (None, to_kind), (None, to_executable)) def get_to_path(to_entry): if to_entry.parent_id is None: to_path = '' # the root else: if to_entry.parent_id not in to_paths: # recurse up return get_to_path(self.target.inventory[to_entry.parent_id]) to_path = osutils.pathjoin(to_paths[to_entry.parent_id], to_entry.name) to_paths[to_entry.file_id] = to_path return to_path for path, from_entry in from_entries_by_dir: file_id = from_entry.file_id if file_id in to_paths: # already returned continue if not file_id in self.target.inventory: # common case - paths we have not emitted are not present in # target. to_path = None else: to_path = get_to_path(self.target.inventory[file_id]) entry_count += 1 if pb is not None: pb.update('comparing files', entry_count, num_entries) versioned = (True, False) parent = (from_entry.parent_id, None) name = (from_entry.name, None) from_kind, from_executable, stat_value = \ self.source._comparison_data(from_entry, path) kind = (from_kind, None) executable = (from_executable, None) changed_content = True # the parent's path is necessarily known at this point. yield(file_id, (path, to_path), changed_content, versioned, parent, name, kind, executable)
def _populate_from_inventory(self): """Populate the CommitBuilder by walking the working tree inventory.""" # Build the revision inventory. # # This starts by creating a new empty inventory. Depending on # which files are selected for commit, and what is present in the # current tree, the new inventory is populated. inventory entries # which are candidates for modification have their revision set to # None; inventory entries that are carried over untouched have their # revision set to their prior value. # # ESEPARATIONOFCONCERNS: this function is diffing and using the diff # results to create a new inventory at the same time, which results # in bugs like #46635. Any reason not to use/enhance Tree.changes_from? # ADHB 11-07-2006 specific_files = self.specific_files exclude = self.exclude report_changes = self.reporter.is_verbose() deleted_ids = [] # A tree of paths that have been deleted. E.g. if foo/bar has been # deleted, then we have {'foo':{'bar':{}}} deleted_paths = {} # XXX: Note that entries may have the wrong kind because the entry does # not reflect the status on disk. # NB: entries will include entries within the excluded ids/paths # because iter_entries_by_dir has no 'exclude' facility today. entries = self.work_tree.iter_entries_by_dir( specific_file_ids=self.specific_file_ids, yield_parents=True) for path, existing_ie in entries: file_id = existing_ie.file_id name = existing_ie.name parent_id = existing_ie.parent_id kind = existing_ie.kind # Skip files that have been deleted from the working tree. # The deleted path ids are also recorded so they can be explicitly # unversioned later. if deleted_paths: path_segments = splitpath(path) deleted_dict = deleted_paths for segment in path_segments: deleted_dict = deleted_dict.get(segment, None) if not deleted_dict: # We either took a path not present in the dict # (deleted_dict was None), or we've reached an empty # child dir in the dict, so are now a sub-path. break else: deleted_dict = None if deleted_dict is not None: # the path has a deleted parent, do not add it. continue if exclude and is_inside_any(exclude, path): # Skip excluded paths. Excluded paths are processed by # _update_builder_with_changes. continue content_summary = self.work_tree.path_content_summary(path) kind = content_summary[0] # Note that when a filter of specific files is given, we must only # skip/record deleted files matching that filter. if not specific_files or is_inside_any(specific_files, path): if kind == 'missing': if not deleted_paths: # path won't have been split yet. path_segments = splitpath(path) deleted_dict = deleted_paths for segment in path_segments: deleted_dict = deleted_dict.setdefault(segment, {}) self.reporter.missing(path) self._next_progress_entry() deleted_ids.append(file_id) continue # TODO: have the builder do the nested commit just-in-time IF and # only if needed. if kind == 'tree-reference': # enforce repository nested tree policy. if (not self.work_tree.supports_tree_reference() or # repository does not support it either. not self.branch.repository._format. supports_tree_reference): kind = 'directory' content_summary = (kind, None, None, None) elif self.recursive == 'down': nested_revision_id = self._commit_nested_tree( file_id, path) content_summary = (kind, None, None, nested_revision_id) else: nested_revision_id = self.work_tree.get_reference_revision( file_id) content_summary = (kind, None, None, nested_revision_id) # Record an entry for this item # Note: I don't particularly want to have the existing_ie # parameter but the test suite currently (28-Jun-07) breaks # without it thanks to a unicode normalisation issue. :-( definitely_changed = kind != existing_ie.kind self._record_entry(path, file_id, specific_files, kind, name, parent_id, definitely_changed, existing_ie, report_changes, content_summary) # Unversion IDs that were found to be deleted self.deleted_ids = deleted_ids