75
75
from .i18n import gettext
78
def filter_excluded(iter_changes, exclude):
79
"""Filter exclude filenames.
81
:param iter_changes: iter_changes function
82
:param exclude: List of paths to exclude
83
:return: iter_changes function
85
for change in iter_changes:
86
old_path = change[1][0]
87
new_path = change[1][1]
89
new_excluded = (new_path is not None and
90
is_inside_any(exclude, new_path))
92
old_excluded = (old_path is not None and
93
is_inside_any(exclude, old_path))
95
if old_excluded and new_excluded:
98
if old_excluded or new_excluded:
99
# TODO(jelmer): Perhaps raise an error here instead?
78
105
class NullCommitReporter(object):
79
106
"""I report on progress of a commit."""
636
648
old_revno, old_revid, new_revno, self.rev_id,
637
649
tree_delta, future_tree)
639
def _gather_parents(self):
640
"""Record the parents of a merge for merge detection."""
641
# TODO: Make sure that this list doesn't contain duplicate
642
# entries and the order is preserved when doing this.
643
if self.use_record_iter_changes:
645
self.basis_inv = self.basis_tree.root_inventory
646
self.parent_invs = [self.basis_inv]
647
for revision in self.parents[1:]:
648
if self.branch.repository.has_revision(revision):
649
mutter('commit parent revision {%s}', revision)
650
inventory = self.branch.repository.get_inventory(revision)
651
self.parent_invs.append(inventory)
653
mutter('commit parent ghost revision {%s}', revision)
655
651
def _update_builder_with_changes(self):
656
652
"""Update the commit builder with the data about what has changed.
658
exclude = self.exclude
659
654
specific_files = self.specific_files
660
655
mutter("Selecting files for commit with filter %r", specific_files)
662
657
self._check_strict()
663
if self.use_record_iter_changes:
664
iter_changes = self.work_tree.iter_changes(self.basis_tree,
665
specific_files=specific_files)
666
iter_changes = self._filter_iter_changes(iter_changes)
667
for file_id, path, fs_hash in self.builder.record_iter_changes(
668
self.work_tree, self.basis_revid, iter_changes):
669
self.work_tree._observed_sha1(file_id, path, fs_hash)
671
# Build the new inventory
672
self._populate_from_inventory()
673
self._record_unselected()
674
self._report_and_accumulate_deletes()
658
iter_changes = self.work_tree.iter_changes(self.basis_tree,
659
specific_files=specific_files)
661
iter_changes = filter_excluded(iter_changes, self.exclude)
662
iter_changes = self._filter_iter_changes(iter_changes)
663
for file_id, path, fs_hash in self.builder.record_iter_changes(
664
self.work_tree, self.basis_revid, iter_changes):
665
self.work_tree._observed_sha1(file_id, path, fs_hash)
676
667
def _filter_iter_changes(self, iter_changes):
677
668
"""Process iter_changes.
725
716
# Unversion IDs that were found to be deleted
726
717
self.deleted_ids = deleted_ids
728
def _record_unselected(self):
729
# If specific files are selected, then all un-selected files must be
730
# recorded in their previous state. For more details, see
731
# https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
732
if self.specific_files or self.exclude:
733
specific_files = self.specific_files or []
734
for path, old_ie in self.basis_inv.iter_entries():
735
if self.builder.new_inventory.has_id(old_ie.file_id):
736
# already added - skip.
738
if (is_inside_any(specific_files, path)
739
and not is_inside_any(self.exclude, path)):
740
# was inside the selected path, and not excluded - if not
741
# present it has been deleted so skip.
743
# From here down it was either not selected, or was excluded:
744
# We preserve the entry unaltered.
746
# Note: specific file commits after a merge are currently
747
# prohibited. This test is for sanity/safety in case it's
748
# required after that changes.
749
if len(self.parents) > 1:
751
self.builder.record_entry_contents(ie, self.parent_invs, path,
752
self.basis_tree, None)
754
def _report_and_accumulate_deletes(self):
755
if (isinstance(self.basis_inv, Inventory)
756
and isinstance(self.builder.new_inventory, Inventory)):
757
# the older Inventory classes provide a _byid dict, and building a
758
# set from the keys of this dict is substantially faster than even
759
# getting a set of ids from the inventory
761
# <lifeless> set(dict) is roughly the same speed as
762
# set(iter(dict)) and both are significantly slower than
764
deleted_ids = set(self.basis_inv._byid.keys()) - \
765
set(self.builder.new_inventory._byid.keys())
767
deleted_ids = set(self.basis_inv) - set(self.builder.new_inventory)
769
self.any_entries_deleted = True
770
deleted = sorted([(self.basis_tree.id2path(file_id), file_id)
771
for file_id in deleted_ids])
772
# XXX: this is not quite directory-order sorting
773
for path, file_id in deleted:
774
self.builder.record_delete(path, file_id)
775
self.reporter.deleted(path)
777
719
def _check_strict(self):
778
720
# XXX: when we use iter_changes this would likely be faster if
779
721
# iter_changes would check for us (even in the presence of
783
725
for unknown in self.work_tree.unknowns():
784
726
raise StrictCommitFailed()
786
def _populate_from_inventory(self):
787
"""Populate the CommitBuilder by walking the working tree inventory."""
788
# Build the revision inventory.
790
# This starts by creating a new empty inventory. Depending on
791
# which files are selected for commit, and what is present in the
792
# current tree, the new inventory is populated. inventory entries
793
# which are candidates for modification have their revision set to
794
# None; inventory entries that are carried over untouched have their
795
# revision set to their prior value.
797
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
798
# results to create a new inventory at the same time, which results
799
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
802
specific_files = self.specific_files
803
exclude = self.exclude
804
report_changes = self.reporter.is_verbose()
806
# A tree of paths that have been deleted. E.g. if foo/bar has been
807
# deleted, then we have {'foo':{'bar':{}}}
809
# XXX: Note that entries may have the wrong kind because the entry does
810
# not reflect the status on disk.
811
# NB: entries will include entries within the excluded ids/paths
812
# because iter_entries_by_dir has no 'exclude' facility today.
813
entries = self.work_tree.iter_entries_by_dir(
814
specific_file_ids=self.specific_file_ids, yield_parents=True)
815
for path, existing_ie in entries:
816
file_id = existing_ie.file_id
817
name = existing_ie.name
818
parent_id = existing_ie.parent_id
819
kind = existing_ie.kind
820
# Skip files that have been deleted from the working tree.
821
# The deleted path ids are also recorded so they can be explicitly
824
path_segments = splitpath(path)
825
deleted_dict = deleted_paths
826
for segment in path_segments:
827
deleted_dict = deleted_dict.get(segment, None)
829
# We either took a path not present in the dict
830
# (deleted_dict was None), or we've reached an empty
831
# child dir in the dict, so are now a sub-path.
835
if deleted_dict is not None:
836
# the path has a deleted parent, do not add it.
838
if exclude and is_inside_any(exclude, path):
839
# Skip excluded paths. Excluded paths are processed by
840
# _update_builder_with_changes.
842
content_summary = self.work_tree.path_content_summary(path)
843
kind = content_summary[0]
844
# Note that when a filter of specific files is given, we must only
845
# skip/record deleted files matching that filter.
846
if not specific_files or is_inside_any(specific_files, path):
847
if kind == 'missing':
848
if not deleted_paths:
849
# path won't have been split yet.
850
path_segments = splitpath(path)
851
deleted_dict = deleted_paths
852
for segment in path_segments:
853
deleted_dict = deleted_dict.setdefault(segment, {})
854
self.reporter.missing(path)
855
self._next_progress_entry()
856
deleted_ids.append(file_id)
858
# TODO: have the builder do the nested commit just-in-time IF and
860
if kind == 'tree-reference':
861
# enforce repository nested tree policy.
862
if (not self.work_tree.supports_tree_reference() or
863
# repository does not support it either.
864
not self.branch.repository._format.supports_tree_reference):
866
content_summary = (kind, None, None, None)
867
elif self.recursive == 'down':
868
nested_revision_id = self._commit_nested_tree(
870
content_summary = (kind, None, None, nested_revision_id)
872
nested_revision_id = self.work_tree.get_reference_revision(file_id)
873
content_summary = (kind, None, None, nested_revision_id)
875
# Record an entry for this item
876
# Note: I don't particularly want to have the existing_ie
877
# parameter but the test suite currently (28-Jun-07) breaks
878
# without it thanks to a unicode normalisation issue. :-(
879
definitely_changed = kind != existing_ie.kind
880
self._record_entry(path, file_id, specific_files, kind, name,
881
parent_id, definitely_changed, existing_ie, report_changes,
884
# Unversion IDs that were found to be deleted
885
self.deleted_ids = deleted_ids
887
728
def _commit_nested_tree(self, file_id, path):
888
729
"Commit a nested tree."
889
730
sub_tree = self.work_tree.get_nested_tree(file_id, path)
909
750
except errors.PointlessCommit:
910
751
return self.work_tree.get_reference_revision(file_id)
912
def _record_entry(self, path, file_id, specific_files, kind, name,
913
parent_id, definitely_changed, existing_ie, report_changes,
915
"Record the new inventory entry for a path if any."
916
# mutter('check %s {%s}', path, file_id)
917
# mutter('%s selected for commit', path)
918
if definitely_changed or existing_ie is None:
919
ie = make_entry(kind, name, parent_id, file_id)
921
ie = existing_ie.copy()
923
# For carried over entries we don't care about the fs hash - the repo
924
# isn't generating a sha, so we're not saving computation time.
925
_, _, fs_hash = self.builder.record_entry_contents(
926
ie, self.parent_invs, path, self.work_tree, content_summary)
928
self._report_change(ie, path)
930
self.work_tree._observed_sha1(ie.file_id, path, fs_hash)
933
def _report_change(self, ie, path):
934
"""Report a change to the user.
936
The change that has occurred is described relative to the basis
939
if (self.basis_inv.has_id(ie.file_id)):
940
basis_ie = self.basis_inv[ie.file_id]
943
change = ie.describe_change(basis_ie, ie)
944
if change in (InventoryEntry.RENAMED,
945
InventoryEntry.MODIFIED_AND_RENAMED):
946
old_path = self.basis_inv.id2path(ie.file_id)
947
self.reporter.renamed(change, old_path, path)
948
self._next_progress_entry()
950
if change == gettext('unchanged'):
952
self.reporter.snapshot_change(change, path)
953
self._next_progress_entry()
955
753
def _set_progress_stage(self, name, counter=False):
956
754
"""Set the progress stage and emit an update to the progress bar."""
957
755
self.pb_stage_name = name
975
773
text = gettext("%s - Stage") % (self.pb_stage_name, )
976
774
self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
978
def _set_specific_file_ids(self):
979
"""populate self.specific_file_ids if we will use it."""
980
if not self.use_record_iter_changes:
981
# If provided, ensure the specified files are versioned
982
if self.specific_files is not None:
983
# Note: This routine is being called because it raises
984
# PathNotVersionedError as a side effect of finding the IDs. We
985
# later use the ids we found as input to the working tree
986
# inventory iterator, so we only consider those ids rather than
987
# examining the whole tree again.
988
# XXX: Dont we have filter_unversioned to do this more
990
self.specific_file_ids = tree.find_ids_across_trees(
991
self.specific_files, [self.basis_tree, self.work_tree])
993
self.specific_file_ids = None