~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2009-03-06 06:48:25 UTC
  • mfrom: (4070.8.6 debug-config)
  • Revision ID: pqm@pqm.ubuntu.com-20090306064825-kbpwggw21dygeix6
(mbp) debug_flags configuration option

Show diffs side-by-side

added added

removed removed

Lines of Context:
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
18
# The newly committed revision is going to have a shape corresponding
62
62
    revision,
63
63
    trace,
64
64
    tree,
65
 
    xml_serializer,
66
65
    )
67
66
from bzrlib.branch import Branch
68
67
import bzrlib.config
105
104
    def completed(self, revno, rev_id):
106
105
        pass
107
106
 
108
 
    def deleted(self, path):
 
107
    def deleted(self, file_id):
109
108
        pass
110
109
 
111
110
    def escaped(self, escape_count, message):
131
130
        note(format, *args)
132
131
 
133
132
    def snapshot_change(self, change, path):
134
 
        if path == '' and change in ('added', 'modified'):
 
133
        if change == 'unchanged':
 
134
            return
 
135
        if change == 'added' and path == '':
135
136
            return
136
137
        self._note("%s %s", change, path)
137
138
 
150
151
    def completed(self, revno, rev_id):
151
152
        self._note('Committed revision %d.', revno)
152
153
 
153
 
    def deleted(self, path):
154
 
        self._note('deleted %s', path)
 
154
    def deleted(self, file_id):
 
155
        self._note('deleted %s', file_id)
155
156
 
156
157
    def escaped(self, escape_count, message):
157
158
        self._note("replaced %d control characters in message", escape_count)
258
259
                               " parameter is required for commit().")
259
260
 
260
261
        self.bound_branch = None
 
262
        self.any_entries_changed = False
261
263
        self.any_entries_deleted = False
262
264
        if exclude is not None:
263
265
            self.exclude = sorted(
274
276
                minimum_path_selection(specific_files))
275
277
        else:
276
278
            self.specific_files = None
277
 
            
 
279
        self.specific_file_ids = None
278
280
        self.allow_pointless = allow_pointless
279
281
        self.revprops = revprops
280
282
        self.message_callback = message_callback
285
287
        self.verbose = verbose
286
288
 
287
289
        self.work_tree.lock_write()
288
 
        self.parents = self.work_tree.get_parent_ids()
289
 
        # We can use record_iter_changes IFF iter_changes is compatible with
290
 
        # the command line parameters, and the repository has fast delta
291
 
        # generation. See bug 347649.
292
 
        self.use_record_iter_changes = (
293
 
            not self.specific_files and
294
 
            not self.exclude and 
295
 
            not self.branch.repository._format.supports_tree_reference and
296
 
            (self.branch.repository._format.fast_deltas or
297
 
             len(self.parents) < 2))
298
290
        self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
299
291
        self.basis_revid = self.work_tree.last_revision()
300
292
        self.basis_tree = self.work_tree.basis_tree()
318
310
            if self.config is None:
319
311
                self.config = self.branch.get_config()
320
312
 
321
 
            self._set_specific_file_ids()
 
313
            # If provided, ensure the specified files are versioned
 
314
            if self.specific_files is not None:
 
315
                # Note: This routine is being called because it raises
 
316
                # PathNotVersionedError as a side effect of finding the IDs. We
 
317
                # later use the ids we found as input to the working tree
 
318
                # inventory iterator, so we only consider those ids rather than
 
319
                # examining the whole tree again.
 
320
                # XXX: Dont we have filter_unversioned to do this more
 
321
                # cheaply?
 
322
                self.specific_file_ids = tree.find_ids_across_trees(
 
323
                    specific_files, [self.basis_tree, self.work_tree])
322
324
 
323
325
            # Setup the progress bar. As the number of files that need to be
324
326
            # committed in unknown, progress is reported as stages.
335
337
            self.pb.show_count = True
336
338
            self.pb.show_bar = True
337
339
 
 
340
            self.basis_inv = self.basis_tree.inventory
338
341
            self._gather_parents()
339
342
            # After a merge, a selected file commit is not supported.
340
343
            # See 'bzr help merge' for an explanation as to why.
345
348
                raise errors.CannotCommitSelectedFileMerge(self.exclude)
346
349
 
347
350
            # Collect the changes
348
 
            self._set_progress_stage("Collecting changes", counter=True)
 
351
            self._set_progress_stage("Collecting changes",
 
352
                    entries_title="Directory")
349
353
            self.builder = self.branch.get_commit_builder(self.parents,
350
354
                self.config, timestamp, timezone, committer, revprops, rev_id)
351
355
 
361
365
                self.reporter.started(new_revno, self.rev_id, master_location)
362
366
 
363
367
                self._update_builder_with_changes()
 
368
                self._report_and_accumulate_deletes()
364
369
                self._check_pointless()
365
370
 
366
371
                # TODO: Now the new inventory is known, check for conflicts.
426
431
        # The initial commit adds a root directory, but this in itself is not
427
432
        # a worthwhile commit.
428
433
        if (self.basis_revid == revision.NULL_REVISION and
429
 
            ((self.builder.new_inventory is not None and
430
 
             len(self.builder.new_inventory) == 1) or
431
 
            len(self.builder._basis_delta) == 1)):
 
434
            len(self.builder.new_inventory) == 1):
432
435
            raise PointlessCommit()
433
 
        if self.builder.any_changes():
 
436
        # If length == 1, then we only have the root entry. Which means
 
437
        # that there is no real difference (only the root could be different)
 
438
        # unless deletes occured, in which case the length is irrelevant.
 
439
        if (self.any_entries_deleted or
 
440
            (len(self.builder.new_inventory) != 1 and
 
441
             self.any_entries_changed)):
434
442
            return
435
443
        raise PointlessCommit()
436
444
 
608
616
        # serialiser not by commit. Then we can also add an unescaper
609
617
        # in the deserializer and start roundtripping revision messages
610
618
        # precisely. See repository_implementations/test_repository.py
611
 
        self.message, escape_count = xml_serializer.escape_invalid_chars(
 
619
 
 
620
        # Python strings can include characters that can't be
 
621
        # represented in well-formed XML; escape characters that
 
622
        # aren't listed in the XML specification
 
623
        # (http://www.w3.org/TR/REC-xml/#NT-Char).
 
624
        self.message, escape_count = re.subn(
 
625
            u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
 
626
            lambda match: match.group(0).encode('unicode_escape'),
612
627
            self.message)
613
628
        if escape_count:
614
629
            self.reporter.escaped(escape_count, self.message)
617
632
        """Record the parents of a merge for merge detection."""
618
633
        # TODO: Make sure that this list doesn't contain duplicate
619
634
        # entries and the order is preserved when doing this.
620
 
        if self.use_record_iter_changes:
621
 
            return
622
 
        self.basis_inv = self.basis_tree.inventory
 
635
        self.parents = self.work_tree.get_parent_ids()
623
636
        self.parent_invs = [self.basis_inv]
624
637
        for revision in self.parents[1:]:
625
638
            if self.branch.repository.has_revision(revision):
632
645
    def _update_builder_with_changes(self):
633
646
        """Update the commit builder with the data about what has changed.
634
647
        """
 
648
        # Build the revision inventory.
 
649
        #
 
650
        # This starts by creating a new empty inventory. Depending on
 
651
        # which files are selected for commit, and what is present in the
 
652
        # current tree, the new inventory is populated. inventory entries
 
653
        # which are candidates for modification have their revision set to
 
654
        # None; inventory entries that are carried over untouched have their
 
655
        # revision set to their prior value.
 
656
        #
 
657
        # ESEPARATIONOFCONCERNS: this function is diffing and using the diff
 
658
        # results to create a new inventory at the same time, which results
 
659
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
 
660
        # ADHB 11-07-2006
 
661
 
635
662
        exclude = self.exclude
636
663
        specific_files = self.specific_files or []
637
664
        mutter("Selecting files for commit with filter %s", specific_files)
638
665
 
639
 
        self._check_strict()
640
 
        if self.use_record_iter_changes:
641
 
            iter_changes = self.work_tree.iter_changes(self.basis_tree)
642
 
            iter_changes = self._filter_iter_changes(iter_changes)
643
 
            for file_id, path, fs_hash in self.builder.record_iter_changes(
644
 
                self.work_tree, self.basis_revid, iter_changes):
645
 
                self.work_tree._observed_sha1(file_id, path, fs_hash)
646
 
        else:
647
 
            # Build the new inventory
648
 
            self._populate_from_inventory()
649
 
            self._record_unselected()
650
 
            self._report_and_accumulate_deletes()
651
 
 
652
 
    def _filter_iter_changes(self, iter_changes):
653
 
        """Process iter_changes.
654
 
 
655
 
        This method reports on the changes in iter_changes to the user, and 
656
 
        converts 'missing' entries in the iter_changes iterator to 'deleted'
657
 
        entries. 'missing' entries have their
658
 
 
659
 
        :param iter_changes: An iter_changes to process.
660
 
        :return: A generator of changes.
661
 
        """
662
 
        reporter = self.reporter
663
 
        report_changes = reporter.is_verbose()
664
 
        deleted_ids = []
665
 
        for change in iter_changes:
666
 
            if report_changes:
667
 
                old_path = change[1][0]
668
 
                new_path = change[1][1]
669
 
                versioned = change[3][1]
670
 
            kind = change[6][1]
671
 
            versioned = change[3][1]
672
 
            if kind is None and versioned:
673
 
                # 'missing' path
674
 
                if report_changes:
675
 
                    reporter.missing(new_path)
676
 
                deleted_ids.append(change[0])
677
 
                # Reset the new path (None) and new versioned flag (False)
678
 
                change = (change[0], (change[1][0], None), change[2],
679
 
                    (change[3][0], False)) + change[4:]
680
 
            elif kind == 'tree-reference':
681
 
                if self.recursive == 'down':
682
 
                    self._commit_nested_tree(change[0], change[1][1])
683
 
            if change[3][0] or change[3][1]:
684
 
                yield change
685
 
                if report_changes:
686
 
                    if new_path is None:
687
 
                        reporter.deleted(old_path)
688
 
                    elif old_path is None:
689
 
                        reporter.snapshot_change('added', new_path)
690
 
                    elif old_path != new_path:
691
 
                        reporter.renamed('renamed', old_path, new_path)
692
 
                    else:
693
 
                        if (new_path or 
694
 
                            self.work_tree.branch.repository._format.rich_root_data):
695
 
                            # Don't report on changes to '' in non rich root
696
 
                            # repositories.
697
 
                            reporter.snapshot_change('modified', new_path)
698
 
            self._next_progress_entry()
699
 
        # Unversion IDs that were found to be deleted
700
 
        self.work_tree.unversion(deleted_ids)
701
 
 
702
 
    def _record_unselected(self):
 
666
        # Build the new inventory
 
667
        self._populate_from_inventory()
 
668
 
703
669
        # If specific files are selected, then all un-selected files must be
704
670
        # recorded in their previous state. For more details, see
705
671
        # https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
706
 
        if self.specific_files or self.exclude:
707
 
            specific_files = self.specific_files or []
 
672
        if specific_files or exclude:
708
673
            for path, old_ie in self.basis_inv.iter_entries():
709
674
                if old_ie.file_id in self.builder.new_inventory:
710
675
                    # already added - skip.
711
676
                    continue
712
677
                if (is_inside_any(specific_files, path)
713
 
                    and not is_inside_any(self.exclude, path)):
 
678
                    and not is_inside_any(exclude, path)):
714
679
                    # was inside the selected path, and not excluded - if not
715
680
                    # present it has been deleted so skip.
716
681
                    continue
717
682
                # From here down it was either not selected, or was excluded:
 
683
                if old_ie.kind == 'directory':
 
684
                    self._next_progress_entry()
718
685
                # We preserve the entry unaltered.
719
686
                ie = old_ie.copy()
720
687
                # Note: specific file commits after a merge are currently
722
689
                # required after that changes.
723
690
                if len(self.parents) > 1:
724
691
                    ie.revision = None
725
 
                self.builder.record_entry_contents(ie, self.parent_invs, path,
726
 
                    self.basis_tree, None)
 
692
                _, version_recorded, _ = self.builder.record_entry_contents(
 
693
                    ie, self.parent_invs, path, self.basis_tree, None)
 
694
                if version_recorded:
 
695
                    self.any_entries_changed = True
727
696
 
728
697
    def _report_and_accumulate_deletes(self):
 
698
        # XXX: Could the list of deleted paths and ids be instead taken from
 
699
        # _populate_from_inventory?
729
700
        if (isinstance(self.basis_inv, Inventory)
730
701
            and isinstance(self.builder.new_inventory, Inventory)):
731
702
            # the older Inventory classes provide a _byid dict, and building a
749
720
                self.builder.record_delete(path, file_id)
750
721
                self.reporter.deleted(path)
751
722
 
752
 
    def _check_strict(self):
753
 
        # XXX: when we use iter_changes this would likely be faster if
754
 
        # iter_changes would check for us (even in the presence of
755
 
        # selected_files).
 
723
    def _populate_from_inventory(self):
 
724
        """Populate the CommitBuilder by walking the working tree inventory."""
756
725
        if self.strict:
757
726
            # raise an exception as soon as we find a single unknown.
758
727
            for unknown in self.work_tree.unknowns():
759
728
                raise StrictCommitFailed()
760
729
 
761
 
    def _populate_from_inventory(self):
762
 
        """Populate the CommitBuilder by walking the working tree inventory."""
763
 
        # Build the revision inventory.
764
 
        #
765
 
        # This starts by creating a new empty inventory. Depending on
766
 
        # which files are selected for commit, and what is present in the
767
 
        # current tree, the new inventory is populated. inventory entries
768
 
        # which are candidates for modification have their revision set to
769
 
        # None; inventory entries that are carried over untouched have their
770
 
        # revision set to their prior value.
771
 
        #
772
 
        # ESEPARATIONOFCONCERNS: this function is diffing and using the diff
773
 
        # results to create a new inventory at the same time, which results
774
 
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
775
 
        # ADHB 11-07-2006
776
 
 
777
730
        specific_files = self.specific_files
778
731
        exclude = self.exclude
779
732
        report_changes = self.reporter.is_verbose()
793
746
            name = existing_ie.name
794
747
            parent_id = existing_ie.parent_id
795
748
            kind = existing_ie.kind
 
749
            if kind == 'directory':
 
750
                self._next_progress_entry()
796
751
            # Skip files that have been deleted from the working tree.
797
752
            # The deleted path ids are also recorded so they can be explicitly
798
753
            # unversioned later.
827
782
                    for segment in path_segments:
828
783
                        deleted_dict = deleted_dict.setdefault(segment, {})
829
784
                    self.reporter.missing(path)
830
 
                    self._next_progress_entry()
831
785
                    deleted_ids.append(file_id)
832
786
                    continue
833
787
            # TODO: have the builder do the nested commit just-in-time IF and
900
854
            ie.revision = None
901
855
        # For carried over entries we don't care about the fs hash - the repo
902
856
        # isn't generating a sha, so we're not saving computation time.
903
 
        _, _, fs_hash = self.builder.record_entry_contents(
 
857
        _, version_recorded, fs_hash = self.builder.record_entry_contents(
904
858
            ie, self.parent_invs, path, self.work_tree, content_summary)
 
859
        if version_recorded:
 
860
            self.any_entries_changed = True
905
861
        if report_changes:
906
862
            self._report_change(ie, path)
907
863
        if fs_hash:
923
879
            InventoryEntry.MODIFIED_AND_RENAMED):
924
880
            old_path = self.basis_inv.id2path(ie.file_id)
925
881
            self.reporter.renamed(change, old_path, path)
926
 
            self._next_progress_entry()
927
882
        else:
928
 
            if change == 'unchanged':
929
 
                return
930
883
            self.reporter.snapshot_change(change, path)
931
 
            self._next_progress_entry()
932
884
 
933
 
    def _set_progress_stage(self, name, counter=False):
 
885
    def _set_progress_stage(self, name, entries_title=None):
934
886
        """Set the progress stage and emit an update to the progress bar."""
935
887
        self.pb_stage_name = name
936
888
        self.pb_stage_count += 1
937
 
        if counter:
 
889
        self.pb_entries_title = entries_title
 
890
        if entries_title is not None:
938
891
            self.pb_entries_count = 0
939
 
        else:
940
 
            self.pb_entries_count = None
 
892
            self.pb_entries_total = '?'
941
893
        self._emit_progress()
942
894
 
943
895
    def _next_progress_entry(self):
946
898
        self._emit_progress()
947
899
 
948
900
    def _emit_progress(self):
949
 
        if self.pb_entries_count is not None:
950
 
            text = "%s [%d] - Stage" % (self.pb_stage_name,
951
 
                self.pb_entries_count)
 
901
        if self.pb_entries_title:
 
902
            if self.pb_entries_total == '?':
 
903
                text = "%s [%s %d] - Stage" % (self.pb_stage_name,
 
904
                    self.pb_entries_title, self.pb_entries_count)
 
905
            else:
 
906
                text = "%s [%s %d/%s] - Stage" % (self.pb_stage_name,
 
907
                    self.pb_entries_title, self.pb_entries_count,
 
908
                    str(self.pb_entries_total))
952
909
        else:
953
 
            text = "%s - Stage" % (self.pb_stage_name, )
 
910
            text = "%s - Stage" % (self.pb_stage_name)
954
911
        self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
955
912
 
956
 
    def _set_specific_file_ids(self):
957
 
        """populate self.specific_file_ids if we will use it."""
958
 
        if not self.use_record_iter_changes:
959
 
            # If provided, ensure the specified files are versioned
960
 
            if self.specific_files is not None:
961
 
                # Note: This routine is being called because it raises
962
 
                # PathNotVersionedError as a side effect of finding the IDs. We
963
 
                # later use the ids we found as input to the working tree
964
 
                # inventory iterator, so we only consider those ids rather than
965
 
                # examining the whole tree again.
966
 
                # XXX: Dont we have filter_unversioned to do this more
967
 
                # cheaply?
968
 
                self.specific_file_ids = tree.find_ids_across_trees(
969
 
                    self.specific_files, [self.basis_tree, self.work_tree])
970
 
            else:
971
 
                self.specific_file_ids = None