~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: Martin Pool
  • Date: 2009-07-27 06:28:35 UTC
  • mto: This revision was merged to the branch mainline in revision 4587.
  • Revision ID: mbp@sourcefrog.net-20090727062835-o66p8it658tq1sma
Add CountedLock.get_physical_lock_status

Show diffs side-by-side

added added

removed removed

Lines of Context:
62
62
    revision,
63
63
    trace,
64
64
    tree,
 
65
    xml_serializer,
65
66
    )
66
67
from bzrlib.branch import Branch
67
68
import bzrlib.config
104
105
    def completed(self, revno, rev_id):
105
106
        pass
106
107
 
107
 
    def deleted(self, file_id):
108
 
        pass
109
 
 
110
 
    def escaped(self, escape_count, message):
 
108
    def deleted(self, path):
111
109
        pass
112
110
 
113
111
    def missing(self, path):
130
128
        note(format, *args)
131
129
 
132
130
    def snapshot_change(self, change, path):
133
 
        if change == 'unchanged':
134
 
            return
135
 
        if change == 'added' and path == '':
 
131
        if path == '' and change in ('added', 'modified'):
136
132
            return
137
133
        self._note("%s %s", change, path)
138
134
 
151
147
    def completed(self, revno, rev_id):
152
148
        self._note('Committed revision %d.', revno)
153
149
 
154
 
    def deleted(self, file_id):
155
 
        self._note('deleted %s', file_id)
156
 
 
157
 
    def escaped(self, escape_count, message):
158
 
        self._note("replaced %d control characters in message", escape_count)
 
150
    def deleted(self, path):
 
151
        self._note('deleted %s', path)
159
152
 
160
153
    def missing(self, path):
161
154
        self._note('missing %s', path)
211
204
        """Commit working copy as a new revision.
212
205
 
213
206
        :param message: the commit message (it or message_callback is required)
 
207
        :param message_callback: A callback: message = message_callback(cmt_obj)
214
208
 
215
209
        :param timestamp: if not None, seconds-since-epoch for a
216
210
            postdated/predated commit.
275
269
                minimum_path_selection(specific_files))
276
270
        else:
277
271
            self.specific_files = None
278
 
        self.specific_file_ids = None
 
272
            
279
273
        self.allow_pointless = allow_pointless
280
274
        self.revprops = revprops
281
275
        self.message_callback = message_callback
286
280
        self.verbose = verbose
287
281
 
288
282
        self.work_tree.lock_write()
 
283
        self.parents = self.work_tree.get_parent_ids()
 
284
        # We can use record_iter_changes IFF iter_changes is compatible with
 
285
        # the command line parameters, and the repository has fast delta
 
286
        # generation. See bug 347649.
 
287
        self.use_record_iter_changes = (
 
288
            not self.specific_files and
 
289
            not self.exclude and 
 
290
            not self.branch.repository._format.supports_tree_reference and
 
291
            (self.branch.repository._format.fast_deltas or
 
292
             len(self.parents) < 2))
289
293
        self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
290
294
        self.basis_revid = self.work_tree.last_revision()
291
295
        self.basis_tree = self.work_tree.basis_tree()
309
313
            if self.config is None:
310
314
                self.config = self.branch.get_config()
311
315
 
312
 
            # If provided, ensure the specified files are versioned
313
 
            if self.specific_files is not None:
314
 
                # Note: This routine is being called because it raises
315
 
                # PathNotVersionedError as a side effect of finding the IDs. We
316
 
                # later use the ids we found as input to the working tree
317
 
                # inventory iterator, so we only consider those ids rather than
318
 
                # examining the whole tree again.
319
 
                # XXX: Dont we have filter_unversioned to do this more
320
 
                # cheaply?
321
 
                self.specific_file_ids = tree.find_ids_across_trees(
322
 
                    specific_files, [self.basis_tree, self.work_tree])
 
316
            self._set_specific_file_ids()
323
317
 
324
318
            # Setup the progress bar. As the number of files that need to be
325
319
            # committed in unknown, progress is reported as stages.
336
330
            self.pb.show_count = True
337
331
            self.pb.show_bar = True
338
332
 
339
 
            self.basis_inv = self.basis_tree.inventory
340
333
            self._gather_parents()
341
334
            # After a merge, a selected file commit is not supported.
342
335
            # See 'bzr help merge' for an explanation as to why.
347
340
                raise errors.CannotCommitSelectedFileMerge(self.exclude)
348
341
 
349
342
            # Collect the changes
350
 
            self._set_progress_stage("Collecting changes",
351
 
                    entries_title="Directory")
 
343
            self._set_progress_stage("Collecting changes", counter=True)
352
344
            self.builder = self.branch.get_commit_builder(self.parents,
353
345
                self.config, timestamp, timezone, committer, revprops, rev_id)
354
346
 
364
356
                self.reporter.started(new_revno, self.rev_id, master_location)
365
357
 
366
358
                self._update_builder_with_changes()
367
 
                self._report_and_accumulate_deletes()
368
359
                self._check_pointless()
369
360
 
370
361
                # TODO: Now the new inventory is known, check for conflicts.
377
368
                # Prompt the user for a commit message if none provided
378
369
                message = message_callback(self)
379
370
                self.message = message
380
 
                self._escape_commit_message()
381
371
 
382
372
                # Add revision data to the local branch
383
373
                self.rev_id = self.builder.commit(self.message)
403
393
            # and now do the commit locally.
404
394
            self.branch.set_last_revision_info(new_revno, self.rev_id)
405
395
 
406
 
            # Make the working tree up to date with the branch
 
396
            # Make the working tree be up to date with the branch. This
 
397
            # includes automatic changes scheduled to be made to the tree, such
 
398
            # as updating its basis and unversioning paths that were missing.
 
399
            self.work_tree.unversion(self.deleted_ids)
407
400
            self._set_progress_stage("Updating the working tree")
408
401
            self.work_tree.update_basis_by_delta(self.rev_id,
409
402
                 self.builder.get_basis_delta())
430
423
        # The initial commit adds a root directory, but this in itself is not
431
424
        # a worthwhile commit.
432
425
        if (self.basis_revid == revision.NULL_REVISION and
433
 
            len(self.builder.new_inventory) == 1):
 
426
            ((self.builder.new_inventory is not None and
 
427
             len(self.builder.new_inventory) == 1) or
 
428
            len(self.builder._basis_delta) == 1)):
434
429
            raise PointlessCommit()
435
 
        # If length == 1, then we only have the root entry. Which means
436
 
        # that there is no real difference (only the root could be different)
437
 
        # unless deletes occured, in which case the length is irrelevant.
438
 
        if (self.any_entries_deleted or
439
 
            (len(self.builder.new_inventory) != 1 and
440
 
             self.builder.any_changes())):
 
430
        if self.builder.any_changes():
441
431
            return
442
432
        raise PointlessCommit()
443
433
 
609
599
        if self.master_locked:
610
600
            self.master_branch.unlock()
611
601
 
612
 
    def _escape_commit_message(self):
613
 
        """Replace xml-incompatible control characters."""
614
 
        # FIXME: RBC 20060419 this should be done by the revision
615
 
        # serialiser not by commit. Then we can also add an unescaper
616
 
        # in the deserializer and start roundtripping revision messages
617
 
        # precisely. See repository_implementations/test_repository.py
618
 
 
619
 
        # Python strings can include characters that can't be
620
 
        # represented in well-formed XML; escape characters that
621
 
        # aren't listed in the XML specification
622
 
        # (http://www.w3.org/TR/REC-xml/#NT-Char).
623
 
        self.message, escape_count = re.subn(
624
 
            u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
625
 
            lambda match: match.group(0).encode('unicode_escape'),
626
 
            self.message)
627
 
        if escape_count:
628
 
            self.reporter.escaped(escape_count, self.message)
629
 
 
630
602
    def _gather_parents(self):
631
603
        """Record the parents of a merge for merge detection."""
632
604
        # TODO: Make sure that this list doesn't contain duplicate
633
605
        # entries and the order is preserved when doing this.
634
 
        self.parents = self.work_tree.get_parent_ids()
 
606
        if self.use_record_iter_changes:
 
607
            return
 
608
        self.basis_inv = self.basis_tree.inventory
635
609
        self.parent_invs = [self.basis_inv]
636
610
        for revision in self.parents[1:]:
637
611
            if self.branch.repository.has_revision(revision):
644
618
    def _update_builder_with_changes(self):
645
619
        """Update the commit builder with the data about what has changed.
646
620
        """
647
 
        # Build the revision inventory.
648
 
        #
649
 
        # This starts by creating a new empty inventory. Depending on
650
 
        # which files are selected for commit, and what is present in the
651
 
        # current tree, the new inventory is populated. inventory entries
652
 
        # which are candidates for modification have their revision set to
653
 
        # None; inventory entries that are carried over untouched have their
654
 
        # revision set to their prior value.
655
 
        #
656
 
        # ESEPARATIONOFCONCERNS: this function is diffing and using the diff
657
 
        # results to create a new inventory at the same time, which results
658
 
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
659
 
        # ADHB 11-07-2006
660
 
 
661
621
        exclude = self.exclude
662
622
        specific_files = self.specific_files or []
663
623
        mutter("Selecting files for commit with filter %s", specific_files)
664
624
 
665
 
        # Build the new inventory
666
 
        self._populate_from_inventory()
667
 
 
 
625
        self._check_strict()
 
626
        if self.use_record_iter_changes:
 
627
            iter_changes = self.work_tree.iter_changes(self.basis_tree)
 
628
            iter_changes = self._filter_iter_changes(iter_changes)
 
629
            for file_id, path, fs_hash in self.builder.record_iter_changes(
 
630
                self.work_tree, self.basis_revid, iter_changes):
 
631
                self.work_tree._observed_sha1(file_id, path, fs_hash)
 
632
        else:
 
633
            # Build the new inventory
 
634
            self._populate_from_inventory()
 
635
            self._record_unselected()
 
636
            self._report_and_accumulate_deletes()
 
637
 
 
638
    def _filter_iter_changes(self, iter_changes):
 
639
        """Process iter_changes.
 
640
 
 
641
        This method reports on the changes in iter_changes to the user, and 
 
642
        converts 'missing' entries in the iter_changes iterator to 'deleted'
 
643
        entries. 'missing' entries have their
 
644
 
 
645
        :param iter_changes: An iter_changes to process.
 
646
        :return: A generator of changes.
 
647
        """
 
648
        reporter = self.reporter
 
649
        report_changes = reporter.is_verbose()
 
650
        deleted_ids = []
 
651
        for change in iter_changes:
 
652
            if report_changes:
 
653
                old_path = change[1][0]
 
654
                new_path = change[1][1]
 
655
                versioned = change[3][1]
 
656
            kind = change[6][1]
 
657
            versioned = change[3][1]
 
658
            if kind is None and versioned:
 
659
                # 'missing' path
 
660
                if report_changes:
 
661
                    reporter.missing(new_path)
 
662
                deleted_ids.append(change[0])
 
663
                # Reset the new path (None) and new versioned flag (False)
 
664
                change = (change[0], (change[1][0], None), change[2],
 
665
                    (change[3][0], False)) + change[4:]
 
666
            elif kind == 'tree-reference':
 
667
                if self.recursive == 'down':
 
668
                    self._commit_nested_tree(change[0], change[1][1])
 
669
            if change[3][0] or change[3][1]:
 
670
                yield change
 
671
                if report_changes:
 
672
                    if new_path is None:
 
673
                        reporter.deleted(old_path)
 
674
                    elif old_path is None:
 
675
                        reporter.snapshot_change('added', new_path)
 
676
                    elif old_path != new_path:
 
677
                        reporter.renamed('renamed', old_path, new_path)
 
678
                    else:
 
679
                        if (new_path or 
 
680
                            self.work_tree.branch.repository._format.rich_root_data):
 
681
                            # Don't report on changes to '' in non rich root
 
682
                            # repositories.
 
683
                            reporter.snapshot_change('modified', new_path)
 
684
            self._next_progress_entry()
 
685
        # Unversion IDs that were found to be deleted
 
686
        self.deleted_ids = deleted_ids
 
687
 
 
688
    def _record_unselected(self):
668
689
        # If specific files are selected, then all un-selected files must be
669
690
        # recorded in their previous state. For more details, see
670
691
        # https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
671
 
        if specific_files or exclude:
 
692
        if self.specific_files or self.exclude:
 
693
            specific_files = self.specific_files or []
672
694
            for path, old_ie in self.basis_inv.iter_entries():
673
695
                if old_ie.file_id in self.builder.new_inventory:
674
696
                    # already added - skip.
675
697
                    continue
676
698
                if (is_inside_any(specific_files, path)
677
 
                    and not is_inside_any(exclude, path)):
 
699
                    and not is_inside_any(self.exclude, path)):
678
700
                    # was inside the selected path, and not excluded - if not
679
701
                    # present it has been deleted so skip.
680
702
                    continue
681
703
                # From here down it was either not selected, or was excluded:
682
 
                if old_ie.kind == 'directory':
683
 
                    self._next_progress_entry()
684
704
                # We preserve the entry unaltered.
685
705
                ie = old_ie.copy()
686
706
                # Note: specific file commits after a merge are currently
692
712
                    self.basis_tree, None)
693
713
 
694
714
    def _report_and_accumulate_deletes(self):
695
 
        # XXX: Could the list of deleted paths and ids be instead taken from
696
 
        # _populate_from_inventory?
697
715
        if (isinstance(self.basis_inv, Inventory)
698
716
            and isinstance(self.builder.new_inventory, Inventory)):
699
717
            # the older Inventory classes provide a _byid dict, and building a
717
735
                self.builder.record_delete(path, file_id)
718
736
                self.reporter.deleted(path)
719
737
 
720
 
    def _populate_from_inventory(self):
721
 
        """Populate the CommitBuilder by walking the working tree inventory."""
 
738
    def _check_strict(self):
 
739
        # XXX: when we use iter_changes this would likely be faster if
 
740
        # iter_changes would check for us (even in the presence of
 
741
        # selected_files).
722
742
        if self.strict:
723
743
            # raise an exception as soon as we find a single unknown.
724
744
            for unknown in self.work_tree.unknowns():
725
745
                raise StrictCommitFailed()
726
746
 
 
747
    def _populate_from_inventory(self):
 
748
        """Populate the CommitBuilder by walking the working tree inventory."""
 
749
        # Build the revision inventory.
 
750
        #
 
751
        # This starts by creating a new empty inventory. Depending on
 
752
        # which files are selected for commit, and what is present in the
 
753
        # current tree, the new inventory is populated. inventory entries
 
754
        # which are candidates for modification have their revision set to
 
755
        # None; inventory entries that are carried over untouched have their
 
756
        # revision set to their prior value.
 
757
        #
 
758
        # ESEPARATIONOFCONCERNS: this function is diffing and using the diff
 
759
        # results to create a new inventory at the same time, which results
 
760
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
 
761
        # ADHB 11-07-2006
 
762
 
727
763
        specific_files = self.specific_files
728
764
        exclude = self.exclude
729
765
        report_changes = self.reporter.is_verbose()
743
779
            name = existing_ie.name
744
780
            parent_id = existing_ie.parent_id
745
781
            kind = existing_ie.kind
746
 
            if kind == 'directory':
747
 
                self._next_progress_entry()
748
782
            # Skip files that have been deleted from the working tree.
749
783
            # The deleted path ids are also recorded so they can be explicitly
750
784
            # unversioned later.
779
813
                    for segment in path_segments:
780
814
                        deleted_dict = deleted_dict.setdefault(segment, {})
781
815
                    self.reporter.missing(path)
 
816
                    self._next_progress_entry()
782
817
                    deleted_ids.append(file_id)
783
818
                    continue
784
819
            # TODO: have the builder do the nested commit just-in-time IF and
811
846
                content_summary)
812
847
 
813
848
        # Unversion IDs that were found to be deleted
814
 
        self.work_tree.unversion(deleted_ids)
 
849
        self.deleted_ids = deleted_ids
815
850
 
816
851
    def _commit_nested_tree(self, file_id, path):
817
852
        "Commit a nested tree."
874
909
            InventoryEntry.MODIFIED_AND_RENAMED):
875
910
            old_path = self.basis_inv.id2path(ie.file_id)
876
911
            self.reporter.renamed(change, old_path, path)
 
912
            self._next_progress_entry()
877
913
        else:
 
914
            if change == 'unchanged':
 
915
                return
878
916
            self.reporter.snapshot_change(change, path)
 
917
            self._next_progress_entry()
879
918
 
880
 
    def _set_progress_stage(self, name, entries_title=None):
 
919
    def _set_progress_stage(self, name, counter=False):
881
920
        """Set the progress stage and emit an update to the progress bar."""
882
921
        self.pb_stage_name = name
883
922
        self.pb_stage_count += 1
884
 
        self.pb_entries_title = entries_title
885
 
        if entries_title is not None:
 
923
        if counter:
886
924
            self.pb_entries_count = 0
887
 
            self.pb_entries_total = '?'
 
925
        else:
 
926
            self.pb_entries_count = None
888
927
        self._emit_progress()
889
928
 
890
929
    def _next_progress_entry(self):
893
932
        self._emit_progress()
894
933
 
895
934
    def _emit_progress(self):
896
 
        if self.pb_entries_title:
897
 
            if self.pb_entries_total == '?':
898
 
                text = "%s [%s %d] - Stage" % (self.pb_stage_name,
899
 
                    self.pb_entries_title, self.pb_entries_count)
900
 
            else:
901
 
                text = "%s [%s %d/%s] - Stage" % (self.pb_stage_name,
902
 
                    self.pb_entries_title, self.pb_entries_count,
903
 
                    str(self.pb_entries_total))
 
935
        if self.pb_entries_count is not None:
 
936
            text = "%s [%d] - Stage" % (self.pb_stage_name,
 
937
                self.pb_entries_count)
904
938
        else:
905
 
            text = "%s - Stage" % (self.pb_stage_name)
 
939
            text = "%s - Stage" % (self.pb_stage_name, )
906
940
        self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
907
941
 
 
942
    def _set_specific_file_ids(self):
 
943
        """populate self.specific_file_ids if we will use it."""
 
944
        if not self.use_record_iter_changes:
 
945
            # If provided, ensure the specified files are versioned
 
946
            if self.specific_files is not None:
 
947
                # Note: This routine is being called because it raises
 
948
                # PathNotVersionedError as a side effect of finding the IDs. We
 
949
                # later use the ids we found as input to the working tree
 
950
                # inventory iterator, so we only consider those ids rather than
 
951
                # examining the whole tree again.
 
952
                # XXX: Dont we have filter_unversioned to do this more
 
953
                # cheaply?
 
954
                self.specific_file_ids = tree.find_ids_across_trees(
 
955
                    self.specific_files, [self.basis_tree, self.work_tree])
 
956
            else:
 
957
                self.specific_file_ids = None