~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: Matt Nordhoff
  • Date: 2009-04-04 02:50:01 UTC
  • mfrom: (4253 +trunk)
  • mto: This revision was merged to the branch mainline in revision 4256.
  • Revision ID: mnordhoff@mattnordhoff.com-20090404025001-z1403k0tatmc8l91
Merge bzr.dev, fixing conflicts.

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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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,
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
    def deleted(self, path):
108
109
        pass
109
110
 
110
111
    def escaped(self, escape_count, message):
130
131
        note(format, *args)
131
132
 
132
133
    def snapshot_change(self, change, path):
133
 
        if change == 'unchanged':
134
 
            return
135
 
        if change == 'added' and path == '':
 
134
        if path == '' and change in ('added', 'modified'):
136
135
            return
137
136
        self._note("%s %s", change, path)
138
137
 
151
150
    def completed(self, revno, rev_id):
152
151
        self._note('Committed revision %d.', revno)
153
152
 
154
 
    def deleted(self, file_id):
155
 
        self._note('deleted %s', file_id)
 
153
    def deleted(self, path):
 
154
        self._note('deleted %s', path)
156
155
 
157
156
    def escaped(self, escape_count, message):
158
157
        self._note("replaced %d control characters in message", escape_count)
237
236
            pending changes of any sort during this commit.
238
237
        :param exclude: None or a list of relative paths to exclude from the
239
238
            commit. Pending changes to excluded files will be ignored by the
240
 
            commit. 
 
239
            commit.
241
240
        """
242
241
        mutter('preparing to commit')
243
242
 
259
258
                               " parameter is required for commit().")
260
259
 
261
260
        self.bound_branch = None
262
 
        self.any_entries_changed = False
263
261
        self.any_entries_deleted = False
264
262
        if exclude is not None:
265
263
            self.exclude = sorted(
276
274
                minimum_path_selection(specific_files))
277
275
        else:
278
276
            self.specific_files = None
279
 
        self.specific_file_ids = None
 
277
            
280
278
        self.allow_pointless = allow_pointless
281
279
        self.revprops = revprops
282
280
        self.message_callback = message_callback
287
285
        self.verbose = verbose
288
286
 
289
287
        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))
290
298
        self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
291
299
        self.basis_revid = self.work_tree.last_revision()
292
300
        self.basis_tree = self.work_tree.basis_tree()
310
318
            if self.config is None:
311
319
                self.config = self.branch.get_config()
312
320
 
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])
 
321
            self._set_specific_file_ids()
324
322
 
325
323
            # Setup the progress bar. As the number of files that need to be
326
324
            # committed in unknown, progress is reported as stages.
337
335
            self.pb.show_count = True
338
336
            self.pb.show_bar = True
339
337
 
340
 
            self.basis_inv = self.basis_tree.inventory
341
338
            self._gather_parents()
342
339
            # After a merge, a selected file commit is not supported.
343
340
            # See 'bzr help merge' for an explanation as to why.
348
345
                raise errors.CannotCommitSelectedFileMerge(self.exclude)
349
346
 
350
347
            # Collect the changes
351
 
            self._set_progress_stage("Collecting changes",
352
 
                    entries_title="Directory")
 
348
            self._set_progress_stage("Collecting changes", counter=True)
353
349
            self.builder = self.branch.get_commit_builder(self.parents,
354
350
                self.config, timestamp, timezone, committer, revprops, rev_id)
355
351
 
365
361
                self.reporter.started(new_revno, self.rev_id, master_location)
366
362
 
367
363
                self._update_builder_with_changes()
368
 
                self._report_and_accumulate_deletes()
369
364
                self._check_pointless()
370
365
 
371
366
                # TODO: Now the new inventory is known, check for conflicts.
395
390
            # Upload revision data to the master.
396
391
            # this will propagate merged revisions too if needed.
397
392
            if self.bound_branch:
398
 
                if not self.master_branch.repository.has_same_location(
399
 
                        self.branch.repository):
400
 
                    self._set_progress_stage("Uploading data to master branch")
401
 
                    self.master_branch.repository.fetch(self.branch.repository,
402
 
                        revision_id=self.rev_id)
403
 
                # now the master has the revision data
 
393
                self._set_progress_stage("Uploading data to master branch")
404
394
                # 'commit' to the master first so a timeout here causes the
405
395
                # local branch to be out of date
406
 
                self.master_branch.set_last_revision_info(new_revno,
407
 
                                                          self.rev_id)
 
396
                self.master_branch.import_last_revision_info(
 
397
                    self.branch.repository, new_revno, self.rev_id)
408
398
 
409
399
            # and now do the commit locally.
410
400
            self.branch.set_last_revision_info(new_revno, self.rev_id)
436
426
        # The initial commit adds a root directory, but this in itself is not
437
427
        # a worthwhile commit.
438
428
        if (self.basis_revid == revision.NULL_REVISION and
439
 
            len(self.builder.new_inventory) == 1):
 
429
            ((self.builder.new_inventory is not None and
 
430
             len(self.builder.new_inventory) == 1) or
 
431
            len(self.builder._basis_delta) == 1)):
440
432
            raise PointlessCommit()
441
 
        # If length == 1, then we only have the root entry. Which means
442
 
        # that there is no real difference (only the root could be different)
443
 
        # unless deletes occured, in which case the length is irrelevant.
444
 
        if (self.any_entries_deleted or 
445
 
            (len(self.builder.new_inventory) != 1 and
446
 
             self.any_entries_changed)):
 
433
        if self.builder.any_changes():
447
434
            return
448
435
        raise PointlessCommit()
449
436
 
476
463
        #       commits to the remote branch if they would fit.
477
464
        #       But for now, just require remote to be identical
478
465
        #       to local.
479
 
        
 
466
 
480
467
        # Make sure the local branch is identical to the master
481
468
        master_info = self.master_branch.last_revision_info()
482
469
        local_info = self.branch.last_revision_info()
539
526
    def _process_hooks(self, hook_name, old_revno, new_revno):
540
527
        if not Branch.hooks[hook_name]:
541
528
            return
542
 
        
 
529
 
543
530
        # new style commit hooks:
544
531
        if not self.bound_branch:
545
532
            hook_master = self.branch
554
541
            old_revid = self.parents[0]
555
542
        else:
556
543
            old_revid = bzrlib.revision.NULL_REVISION
557
 
        
 
544
 
558
545
        if hook_name == "pre_commit":
559
546
            future_tree = self.builder.revision_tree()
560
547
            tree_delta = future_tree.changes_from(self.basis_tree,
561
548
                                             include_root=True)
562
 
        
 
549
 
563
550
        for hook in Branch.hooks[hook_name]:
564
551
            # show the running hook in the progress bar. As hooks may
565
552
            # end up doing nothing (e.g. because they are not configured by
595
582
            # typically this will be useful enough.
596
583
            except Exception, e:
597
584
                found_exception = e
598
 
        if found_exception is not None: 
 
585
        if found_exception is not None:
599
586
            # don't do a plan raise, because the last exception may have been
600
587
            # trashed, e is our sure-to-work exception even though it loses the
601
588
            # full traceback. XXX: RBC 20060421 perhaps we could check the
602
 
            # exc_info and if its the same one do a plain raise otherwise 
 
589
            # exc_info and if its the same one do a plain raise otherwise
603
590
            # 'raise e' as we do now.
604
591
            raise e
605
592
 
621
608
        # serialiser not by commit. Then we can also add an unescaper
622
609
        # in the deserializer and start roundtripping revision messages
623
610
        # precisely. See repository_implementations/test_repository.py
624
 
        
625
 
        # Python strings can include characters that can't be
626
 
        # represented in well-formed XML; escape characters that
627
 
        # aren't listed in the XML specification
628
 
        # (http://www.w3.org/TR/REC-xml/#NT-Char).
629
 
        self.message, escape_count = re.subn(
630
 
            u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
631
 
            lambda match: match.group(0).encode('unicode_escape'),
 
611
        self.message, escape_count = xml_serializer.escape_invalid_chars(
632
612
            self.message)
633
613
        if escape_count:
634
614
            self.reporter.escaped(escape_count, self.message)
635
615
 
636
616
    def _gather_parents(self):
637
617
        """Record the parents of a merge for merge detection."""
638
 
        # TODO: Make sure that this list doesn't contain duplicate 
 
618
        # TODO: Make sure that this list doesn't contain duplicate
639
619
        # entries and the order is preserved when doing this.
640
 
        self.parents = self.work_tree.get_parent_ids()
 
620
        if self.use_record_iter_changes:
 
621
            return
 
622
        self.basis_inv = self.basis_tree.inventory
641
623
        self.parent_invs = [self.basis_inv]
642
624
        for revision in self.parents[1:]:
643
625
            if self.branch.repository.has_revision(revision):
650
632
    def _update_builder_with_changes(self):
651
633
        """Update the commit builder with the data about what has changed.
652
634
        """
653
 
        # Build the revision inventory.
654
 
        #
655
 
        # This starts by creating a new empty inventory. Depending on
656
 
        # which files are selected for commit, and what is present in the
657
 
        # current tree, the new inventory is populated. inventory entries 
658
 
        # which are candidates for modification have their revision set to
659
 
        # None; inventory entries that are carried over untouched have their
660
 
        # revision set to their prior value.
661
 
        #
662
 
        # ESEPARATIONOFCONCERNS: this function is diffing and using the diff
663
 
        # results to create a new inventory at the same time, which results
664
 
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
665
 
        # ADHB 11-07-2006
666
 
 
667
635
        exclude = self.exclude
668
636
        specific_files = self.specific_files or []
669
637
        mutter("Selecting files for commit with filter %s", specific_files)
670
638
 
671
 
        # Build the new inventory
672
 
        self._populate_from_inventory()
673
 
 
 
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):
674
703
        # If specific files are selected, then all un-selected files must be
675
704
        # recorded in their previous state. For more details, see
676
705
        # https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
677
 
        if specific_files or exclude:
 
706
        if self.specific_files or self.exclude:
 
707
            specific_files = self.specific_files or []
678
708
            for path, old_ie in self.basis_inv.iter_entries():
679
709
                if old_ie.file_id in self.builder.new_inventory:
680
710
                    # already added - skip.
681
711
                    continue
682
712
                if (is_inside_any(specific_files, path)
683
 
                    and not is_inside_any(exclude, path)):
 
713
                    and not is_inside_any(self.exclude, path)):
684
714
                    # was inside the selected path, and not excluded - if not
685
715
                    # present it has been deleted so skip.
686
716
                    continue
687
717
                # From here down it was either not selected, or was excluded:
688
 
                if old_ie.kind == 'directory':
689
 
                    self._next_progress_entry()
690
718
                # We preserve the entry unaltered.
691
719
                ie = old_ie.copy()
692
720
                # Note: specific file commits after a merge are currently
694
722
                # required after that changes.
695
723
                if len(self.parents) > 1:
696
724
                    ie.revision = None
697
 
                _, version_recorded, _ = self.builder.record_entry_contents(
698
 
                    ie, self.parent_invs, path, self.basis_tree, None)
699
 
                if version_recorded:
700
 
                    self.any_entries_changed = True
 
725
                self.builder.record_entry_contents(ie, self.parent_invs, path,
 
726
                    self.basis_tree, None)
701
727
 
702
728
    def _report_and_accumulate_deletes(self):
703
 
        # XXX: Could the list of deleted paths and ids be instead taken from
704
 
        # _populate_from_inventory?
705
729
        if (isinstance(self.basis_inv, Inventory)
706
730
            and isinstance(self.builder.new_inventory, Inventory)):
707
731
            # the older Inventory classes provide a _byid dict, and building a
725
749
                self.builder.record_delete(path, file_id)
726
750
                self.reporter.deleted(path)
727
751
 
728
 
    def _populate_from_inventory(self):
729
 
        """Populate the CommitBuilder by walking the working tree inventory."""
 
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).
730
756
        if self.strict:
731
757
            # raise an exception as soon as we find a single unknown.
732
758
            for unknown in self.work_tree.unknowns():
733
759
                raise StrictCommitFailed()
734
 
        
 
760
 
 
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
 
735
777
        specific_files = self.specific_files
736
778
        exclude = self.exclude
737
779
        report_changes = self.reporter.is_verbose()
751
793
            name = existing_ie.name
752
794
            parent_id = existing_ie.parent_id
753
795
            kind = existing_ie.kind
754
 
            if kind == 'directory':
755
 
                self._next_progress_entry()
756
796
            # Skip files that have been deleted from the working tree.
757
797
            # The deleted path ids are also recorded so they can be explicitly
758
798
            # unversioned later.
787
827
                    for segment in path_segments:
788
828
                        deleted_dict = deleted_dict.setdefault(segment, {})
789
829
                    self.reporter.missing(path)
 
830
                    self._next_progress_entry()
790
831
                    deleted_ids.append(file_id)
791
832
                    continue
792
833
            # TODO: have the builder do the nested commit just-in-time IF and
827
868
        # FIXME: be more comprehensive here:
828
869
        # this works when both trees are in --trees repository,
829
870
        # but when both are bound to a different repository,
830
 
        # it fails; a better way of approaching this is to 
 
871
        # it fails; a better way of approaching this is to
831
872
        # finally implement the explicit-caches approach design
832
873
        # a while back - RBC 20070306.
833
874
        if sub_tree.branch.repository.has_same_location(
859
900
            ie.revision = None
860
901
        # For carried over entries we don't care about the fs hash - the repo
861
902
        # isn't generating a sha, so we're not saving computation time.
862
 
        _, version_recorded, fs_hash = self.builder.record_entry_contents(
 
903
        _, _, fs_hash = self.builder.record_entry_contents(
863
904
            ie, self.parent_invs, path, self.work_tree, content_summary)
864
 
        if version_recorded:
865
 
            self.any_entries_changed = True
866
905
        if report_changes:
867
906
            self._report_change(ie, path)
868
907
        if fs_hash:
880
919
        else:
881
920
            basis_ie = None
882
921
        change = ie.describe_change(basis_ie, ie)
883
 
        if change in (InventoryEntry.RENAMED, 
 
922
        if change in (InventoryEntry.RENAMED,
884
923
            InventoryEntry.MODIFIED_AND_RENAMED):
885
924
            old_path = self.basis_inv.id2path(ie.file_id)
886
925
            self.reporter.renamed(change, old_path, path)
 
926
            self._next_progress_entry()
887
927
        else:
 
928
            if change == 'unchanged':
 
929
                return
888
930
            self.reporter.snapshot_change(change, path)
 
931
            self._next_progress_entry()
889
932
 
890
 
    def _set_progress_stage(self, name, entries_title=None):
 
933
    def _set_progress_stage(self, name, counter=False):
891
934
        """Set the progress stage and emit an update to the progress bar."""
892
935
        self.pb_stage_name = name
893
936
        self.pb_stage_count += 1
894
 
        self.pb_entries_title = entries_title
895
 
        if entries_title is not None:
 
937
        if counter:
896
938
            self.pb_entries_count = 0
897
 
            self.pb_entries_total = '?'
 
939
        else:
 
940
            self.pb_entries_count = None
898
941
        self._emit_progress()
899
942
 
900
943
    def _next_progress_entry(self):
903
946
        self._emit_progress()
904
947
 
905
948
    def _emit_progress(self):
906
 
        if self.pb_entries_title:
907
 
            if self.pb_entries_total == '?':
908
 
                text = "%s [%s %d] - Stage" % (self.pb_stage_name,
909
 
                    self.pb_entries_title, self.pb_entries_count)
910
 
            else:
911
 
                text = "%s [%s %d/%s] - Stage" % (self.pb_stage_name,
912
 
                    self.pb_entries_title, self.pb_entries_count,
913
 
                    str(self.pb_entries_total))
 
949
        if self.pb_entries_count is not None:
 
950
            text = "%s [%d] - Stage" % (self.pb_stage_name,
 
951
                self.pb_entries_count)
914
952
        else:
915
 
            text = "%s - Stage" % (self.pb_stage_name)
 
953
            text = "%s - Stage" % (self.pb_stage_name, )
916
954
        self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
917
955
 
 
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