/breezy/trunk

To get this branch, use:
bzr branch https://code.breezy-vcs.org/breezy/trunk

« back to all changes in this revision

Viewing changes to breezy/commit.py

  • Committer: Jelmer Vernooij
  • Date: 2017-07-23 22:06:41 UTC
  • mfrom: (6738 trunk)
  • mto: This revision was merged to the branch mainline in revision 6739.
  • Revision ID: jelmer@jelmer.uk-20170723220641-69eczax9bmv8d6kk
Merge trunk, address review comments.

Show diffs side-by-side

added added

removed removed

Lines of Context:
75
75
from .i18n import gettext
76
76
 
77
77
 
 
78
def filter_excluded(iter_changes, exclude):
 
79
    """Filter exclude filenames.
 
80
 
 
81
    :param iter_changes: iter_changes function
 
82
    :param exclude: List of paths to exclude
 
83
    :return: iter_changes function
 
84
    """
 
85
    for change in iter_changes:
 
86
        old_path = change[1][0]
 
87
        new_path = change[1][1]
 
88
 
 
89
        new_excluded = (new_path is not None and
 
90
            is_inside_any(exclude, new_path))
 
91
 
 
92
        old_excluded = (old_path is not None and
 
93
            is_inside_any(exclude, old_path))
 
94
 
 
95
        if old_excluded and new_excluded:
 
96
            continue
 
97
 
 
98
        if old_excluded or new_excluded:
 
99
            # TODO(jelmer): Perhaps raise an error here instead?
 
100
            continue
 
101
 
 
102
        yield change
 
103
 
 
104
 
78
105
class NullCommitReporter(object):
79
106
    """I report on progress of a commit."""
80
107
 
317
344
        self.work_tree.lock_write()
318
345
        operation.add_cleanup(self.work_tree.unlock)
319
346
        self.parents = self.work_tree.get_parent_ids()
320
 
        # We can use record_iter_changes IFF iter_changes is compatible with
321
 
        # the command line parameters, and the repository has fast delta
322
 
        # generation. See bug 347649.
323
 
        self.use_record_iter_changes = (
324
 
            not self.exclude and 
325
 
            not self.branch.repository._format.supports_tree_reference and
326
 
            (self.branch.repository._format.fast_deltas or
327
 
             len(self.parents) < 2))
328
347
        self.pb = ui.ui_factory.nested_progress_bar()
329
348
        operation.add_cleanup(self.pb.finished)
330
349
        self.basis_revid = self.work_tree.last_revision()
349
368
        if self.config_stack is None:
350
369
            self.config_stack = self.work_tree.get_config_stack()
351
370
 
352
 
        self._set_specific_file_ids()
353
 
 
354
371
        # Setup the progress bar. As the number of files that need to be
355
372
        # committed in unknown, progress is reported as stages.
356
373
        # We keep track of entries separately though and include that
368
385
        self.pb.show_count = True
369
386
        self.pb.show_bar = True
370
387
 
371
 
        self._gather_parents()
372
388
        # After a merge, a selected file commit is not supported.
373
389
        # See 'bzr help merge' for an explanation as to why.
374
390
        if len(self.parents) > 1 and self.specific_files is not None:
383
399
        self.builder = self.branch.get_commit_builder(self.parents,
384
400
            self.config_stack, timestamp, timezone, committer, self.revprops,
385
401
            rev_id, lossy=lossy)
386
 
        if not self.builder.supports_record_entry_contents and self.exclude:
387
 
            self.builder.abort()
388
 
            raise errors.ExcludesUnsupported(self.branch.repository)
389
402
 
390
403
        if self.builder.updates_branch and self.bound_branch:
391
404
            self.builder.abort()
394
407
                "that update the branch")
395
408
 
396
409
        try:
397
 
            self.builder.will_record_deletes()
398
410
            # find the location being committed to
399
411
            if self.bound_branch:
400
412
                master_location = self.master_branch.base
636
648
                     old_revno, old_revid, new_revno, self.rev_id,
637
649
                     tree_delta, future_tree)
638
650
 
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:
644
 
            return
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)
652
 
            else:
653
 
                mutter('commit parent ghost revision {%s}', revision)
654
 
 
655
651
    def _update_builder_with_changes(self):
656
652
        """Update the commit builder with the data about what has changed.
657
653
        """
658
 
        exclude = self.exclude
659
654
        specific_files = self.specific_files
660
655
        mutter("Selecting files for commit with filter %r", specific_files)
661
656
 
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)
670
 
        else:
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)
 
660
        if self.exclude:
 
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)
675
666
 
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
727
718
 
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.
737
 
                    continue
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.
742
 
                    continue
743
 
                # From here down it was either not selected, or was excluded:
744
 
                # We preserve the entry unaltered.
745
 
                ie = old_ie.copy()
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:
750
 
                    ie.revision = None
751
 
                self.builder.record_entry_contents(ie, self.parent_invs, path,
752
 
                    self.basis_tree, None)
753
 
 
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
760
 
            #
761
 
            # <lifeless> set(dict) is roughly the same speed as
762
 
            # set(iter(dict)) and both are significantly slower than
763
 
            # set(dict.keys())
764
 
            deleted_ids = set(self.basis_inv._byid.keys()) - \
765
 
               set(self.builder.new_inventory._byid.keys())
766
 
        else:
767
 
            deleted_ids = set(self.basis_inv) - set(self.builder.new_inventory)
768
 
        if deleted_ids:
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)
776
 
 
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()
785
727
 
786
 
    def _populate_from_inventory(self):
787
 
        """Populate the CommitBuilder by walking the working tree inventory."""
788
 
        # Build the revision inventory.
789
 
        #
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.
796
 
        #
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?
800
 
        # ADHB 11-07-2006
801
 
 
802
 
        specific_files = self.specific_files
803
 
        exclude = self.exclude
804
 
        report_changes = self.reporter.is_verbose()
805
 
        deleted_ids = []
806
 
        # A tree of paths that have been deleted. E.g. if foo/bar has been
807
 
        # deleted, then we have {'foo':{'bar':{}}}
808
 
        deleted_paths = {}
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
822
 
            # unversioned later.
823
 
            if deleted_paths:
824
 
                path_segments = splitpath(path)
825
 
                deleted_dict = deleted_paths
826
 
                for segment in path_segments:
827
 
                    deleted_dict = deleted_dict.get(segment, None)
828
 
                    if not deleted_dict:
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.
832
 
                        break
833
 
                else:
834
 
                    deleted_dict = None
835
 
                if deleted_dict is not None:
836
 
                    # the path has a deleted parent, do not add it.
837
 
                    continue
838
 
            if exclude and is_inside_any(exclude, path):
839
 
                # Skip excluded paths. Excluded paths are processed by
840
 
                # _update_builder_with_changes.
841
 
                continue
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)
857
 
                    continue
858
 
            # TODO: have the builder do the nested commit just-in-time IF and
859
 
            # only if needed.
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):
865
 
                    kind = 'directory'
866
 
                    content_summary = (kind, None, None, None)
867
 
                elif self.recursive == 'down':
868
 
                    nested_revision_id = self._commit_nested_tree(
869
 
                        file_id, path)
870
 
                    content_summary = (kind, None, None, nested_revision_id)
871
 
                else:
872
 
                    nested_revision_id = self.work_tree.get_reference_revision(file_id)
873
 
                    content_summary = (kind, None, None, nested_revision_id)
874
 
 
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,
882
 
                content_summary)
883
 
 
884
 
        # Unversion IDs that were found to be deleted
885
 
        self.deleted_ids = deleted_ids
886
 
 
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)
911
752
 
912
 
    def _record_entry(self, path, file_id, specific_files, kind, name,
913
 
        parent_id, definitely_changed, existing_ie, report_changes,
914
 
        content_summary):
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)
920
 
        else:
921
 
            ie = existing_ie.copy()
922
 
            ie.revision = None
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)
927
 
        if report_changes:
928
 
            self._report_change(ie, path)
929
 
        if fs_hash:
930
 
            self.work_tree._observed_sha1(ie.file_id, path, fs_hash)
931
 
        return ie
932
 
 
933
 
    def _report_change(self, ie, path):
934
 
        """Report a change to the user.
935
 
 
936
 
        The change that has occurred is described relative to the basis
937
 
        inventory.
938
 
        """
939
 
        if (self.basis_inv.has_id(ie.file_id)):
940
 
            basis_ie = self.basis_inv[ie.file_id]
941
 
        else:
942
 
            basis_ie = None
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()
949
 
        else:
950
 
            if change == gettext('unchanged'):
951
 
                return
952
 
            self.reporter.snapshot_change(change, path)
953
 
            self._next_progress_entry()
954
 
 
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
974
772
        else:
975
773
            text = gettext("%s - Stage") % (self.pb_stage_name, )
976
774
        self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
977
 
 
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
989
 
                # cheaply?
990
 
                self.specific_file_ids = tree.find_ids_across_trees(
991
 
                    self.specific_files, [self.basis_tree, self.work_tree])
992
 
            else:
993
 
                self.specific_file_ids = None