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