654
648
old_revno, old_revid, new_revno, self.rev_id,
655
649
tree_delta, future_tree)
657
def _gather_parents(self):
658
"""Record the parents of a merge for merge detection."""
659
# TODO: Make sure that this list doesn't contain duplicate
660
# entries and the order is preserved when doing this.
661
if self.use_record_iter_changes:
663
self.basis_inv = self.basis_tree.root_inventory
664
self.parent_invs = [self.basis_inv]
665
for revision in self.parents[1:]:
666
if self.branch.repository.has_revision(revision):
667
mutter('commit parent revision {%s}', revision)
668
inventory = self.branch.repository.get_inventory(revision)
669
self.parent_invs.append(inventory)
671
mutter('commit parent ghost revision {%s}', revision)
673
651
def _update_builder_with_changes(self):
674
652
"""Update the commit builder with the data about what has changed.
676
exclude = self.exclude
677
654
specific_files = self.specific_files
678
655
mutter("Selecting files for commit with filter %r", specific_files)
680
657
self._check_strict()
681
if self.use_record_iter_changes:
682
iter_changes = self.work_tree.iter_changes(self.basis_tree,
683
specific_files=specific_files)
685
iter_changes = filter_excluded(iter_changes, self.exclude)
686
iter_changes = self._filter_iter_changes(iter_changes)
687
for file_id, path, fs_hash in self.builder.record_iter_changes(
688
self.work_tree, self.basis_revid, iter_changes):
689
self.work_tree._observed_sha1(file_id, path, fs_hash)
691
# Build the new inventory
692
self._populate_from_inventory()
693
self._record_unselected()
694
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)
696
667
def _filter_iter_changes(self, iter_changes):
697
668
"""Process iter_changes.
745
716
# Unversion IDs that were found to be deleted
746
717
self.deleted_ids = deleted_ids
748
def _record_unselected(self):
749
# If specific files are selected, then all un-selected files must be
750
# recorded in their previous state. For more details, see
751
# https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
752
if self.specific_files or self.exclude:
753
specific_files = self.specific_files or []
754
for path, old_ie in self.basis_inv.iter_entries():
755
if self.builder.new_inventory.has_id(old_ie.file_id):
756
# already added - skip.
758
if (is_inside_any(specific_files, path)
759
and not is_inside_any(self.exclude, path)):
760
# was inside the selected path, and not excluded - if not
761
# present it has been deleted so skip.
763
# From here down it was either not selected, or was excluded:
764
# We preserve the entry unaltered.
766
# Note: specific file commits after a merge are currently
767
# prohibited. This test is for sanity/safety in case it's
768
# required after that changes.
769
if len(self.parents) > 1:
771
self.builder.record_entry_contents(ie, self.parent_invs, path,
772
self.basis_tree, None)
774
def _report_and_accumulate_deletes(self):
775
if (isinstance(self.basis_inv, Inventory)
776
and isinstance(self.builder.new_inventory, Inventory)):
777
# the older Inventory classes provide a _byid dict, and building a
778
# set from the keys of this dict is substantially faster than even
779
# getting a set of ids from the inventory
781
# <lifeless> set(dict) is roughly the same speed as
782
# set(iter(dict)) and both are significantly slower than
784
deleted_ids = set(self.basis_inv._byid.keys()) - \
785
set(self.builder.new_inventory._byid.keys())
787
deleted_ids = set(self.basis_inv) - set(self.builder.new_inventory)
789
self.any_entries_deleted = True
790
deleted = sorted([(self.basis_tree.id2path(file_id), file_id)
791
for file_id in deleted_ids])
792
# XXX: this is not quite directory-order sorting
793
for path, file_id in deleted:
794
self.builder.record_delete(path, file_id)
795
self.reporter.deleted(path)
797
719
def _check_strict(self):
798
720
# XXX: when we use iter_changes this would likely be faster if
799
721
# iter_changes would check for us (even in the presence of
803
725
for unknown in self.work_tree.unknowns():
804
726
raise StrictCommitFailed()
806
def _populate_from_inventory(self):
807
"""Populate the CommitBuilder by walking the working tree inventory."""
808
# Build the revision inventory.
810
# This starts by creating a new empty inventory. Depending on
811
# which files are selected for commit, and what is present in the
812
# current tree, the new inventory is populated. inventory entries
813
# which are candidates for modification have their revision set to
814
# None; inventory entries that are carried over untouched have their
815
# revision set to their prior value.
817
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
818
# results to create a new inventory at the same time, which results
819
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
822
specific_files = self.specific_files
823
exclude = self.exclude
824
report_changes = self.reporter.is_verbose()
826
# A tree of paths that have been deleted. E.g. if foo/bar has been
827
# deleted, then we have {'foo':{'bar':{}}}
829
# XXX: Note that entries may have the wrong kind because the entry does
830
# not reflect the status on disk.
831
# NB: entries will include entries within the excluded ids/paths
832
# because iter_entries_by_dir has no 'exclude' facility today.
833
entries = self.work_tree.iter_entries_by_dir(
834
specific_file_ids=self.specific_file_ids, yield_parents=True)
835
for path, existing_ie in entries:
836
file_id = existing_ie.file_id
837
name = existing_ie.name
838
parent_id = existing_ie.parent_id
839
kind = existing_ie.kind
840
# Skip files that have been deleted from the working tree.
841
# The deleted path ids are also recorded so they can be explicitly
844
path_segments = splitpath(path)
845
deleted_dict = deleted_paths
846
for segment in path_segments:
847
deleted_dict = deleted_dict.get(segment, None)
849
# We either took a path not present in the dict
850
# (deleted_dict was None), or we've reached an empty
851
# child dir in the dict, so are now a sub-path.
855
if deleted_dict is not None:
856
# the path has a deleted parent, do not add it.
858
if exclude and is_inside_any(exclude, path):
859
# Skip excluded paths. Excluded paths are processed by
860
# _update_builder_with_changes.
862
content_summary = self.work_tree.path_content_summary(path)
863
kind = content_summary[0]
864
# Note that when a filter of specific files is given, we must only
865
# skip/record deleted files matching that filter.
866
if not specific_files or is_inside_any(specific_files, path):
867
if kind == 'missing':
868
if not deleted_paths:
869
# path won't have been split yet.
870
path_segments = splitpath(path)
871
deleted_dict = deleted_paths
872
for segment in path_segments:
873
deleted_dict = deleted_dict.setdefault(segment, {})
874
self.reporter.missing(path)
875
self._next_progress_entry()
876
deleted_ids.append(file_id)
878
# TODO: have the builder do the nested commit just-in-time IF and
880
if kind == 'tree-reference':
881
# enforce repository nested tree policy.
882
if (not self.work_tree.supports_tree_reference() or
883
# repository does not support it either.
884
not self.branch.repository._format.supports_tree_reference):
886
content_summary = (kind, None, None, None)
887
elif self.recursive == 'down':
888
nested_revision_id = self._commit_nested_tree(
890
content_summary = (kind, None, None, nested_revision_id)
892
nested_revision_id = self.work_tree.get_reference_revision(file_id)
893
content_summary = (kind, None, None, nested_revision_id)
895
# Record an entry for this item
896
# Note: I don't particularly want to have the existing_ie
897
# parameter but the test suite currently (28-Jun-07) breaks
898
# without it thanks to a unicode normalisation issue. :-(
899
definitely_changed = kind != existing_ie.kind
900
self._record_entry(path, file_id, specific_files, kind, name,
901
parent_id, definitely_changed, existing_ie, report_changes,
904
# Unversion IDs that were found to be deleted
905
self.deleted_ids = deleted_ids
907
728
def _commit_nested_tree(self, file_id, path):
908
729
"Commit a nested tree."
909
730
sub_tree = self.work_tree.get_nested_tree(file_id, path)
929
750
except errors.PointlessCommit:
930
751
return self.work_tree.get_reference_revision(file_id)
932
def _record_entry(self, path, file_id, specific_files, kind, name,
933
parent_id, definitely_changed, existing_ie, report_changes,
935
"Record the new inventory entry for a path if any."
936
# mutter('check %s {%s}', path, file_id)
937
# mutter('%s selected for commit', path)
938
if definitely_changed or existing_ie is None:
939
ie = make_entry(kind, name, parent_id, file_id)
941
ie = existing_ie.copy()
943
# For carried over entries we don't care about the fs hash - the repo
944
# isn't generating a sha, so we're not saving computation time.
945
_, _, fs_hash = self.builder.record_entry_contents(
946
ie, self.parent_invs, path, self.work_tree, content_summary)
948
self._report_change(ie, path)
950
self.work_tree._observed_sha1(ie.file_id, path, fs_hash)
953
def _report_change(self, ie, path):
954
"""Report a change to the user.
956
The change that has occurred is described relative to the basis
959
if (self.basis_inv.has_id(ie.file_id)):
960
basis_ie = self.basis_inv[ie.file_id]
963
change = ie.describe_change(basis_ie, ie)
964
if change in (InventoryEntry.RENAMED,
965
InventoryEntry.MODIFIED_AND_RENAMED):
966
old_path = self.basis_inv.id2path(ie.file_id)
967
self.reporter.renamed(change, old_path, path)
968
self._next_progress_entry()
970
if change == gettext('unchanged'):
972
self.reporter.snapshot_change(change, path)
973
self._next_progress_entry()
975
753
def _set_progress_stage(self, name, counter=False):
976
754
"""Set the progress stage and emit an update to the progress bar."""
977
755
self.pb_stage_name = name
995
773
text = gettext("%s - Stage") % (self.pb_stage_name, )
996
774
self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
998
def _set_specific_file_ids(self):
999
"""populate self.specific_file_ids if we will use it."""
1000
if not self.use_record_iter_changes:
1001
# If provided, ensure the specified files are versioned
1002
if self.specific_files is not None:
1003
# Note: This routine is being called because it raises
1004
# PathNotVersionedError as a side effect of finding the IDs. We
1005
# later use the ids we found as input to the working tree
1006
# inventory iterator, so we only consider those ids rather than
1007
# examining the whole tree again.
1008
# XXX: Dont we have filter_unversioned to do this more
1010
self.specific_file_ids = tree.find_ids_across_trees(
1011
self.specific_files, [self.basis_tree, self.work_tree])
1013
self.specific_file_ids = None