~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: Aaron Bentley
  • Date: 2007-07-25 19:32:22 UTC
  • mto: (1551.19.24 Aaron's mergeable stuff)
  • mto: This revision was merged to the branch mainline in revision 2664.
  • Revision ID: abentley@panoramicfeedback.com-20070725193222-lcq4z4980ffd4bf5
Stop using _merge_helper for merging

Show diffs side-by-side

added added

removed removed

Lines of Context:
57
57
from cStringIO import StringIO
58
58
 
59
59
from bzrlib import (
 
60
    debug,
60
61
    errors,
61
62
    inventory,
62
63
    tree,
109
110
    def _note(self, format, *args):
110
111
        """Output a message.
111
112
 
112
 
        Messages are output by writing directly to stderr instead of
113
 
        using bzrlib.trace.note(). The latter constantly updates the
114
 
        log file as we go causing an unnecessary performance hit.
115
 
 
116
 
        Subclasses may choose to override this method but need to be aware
117
 
        of its potential impact on performance.
 
113
        Subclasses may choose to override this method.
118
114
        """
119
 
        bzrlib.ui.ui_factory.clear_term()
120
 
        sys.stderr.write((format + "\n") % args)
 
115
        note(format, *args)
121
116
 
122
117
    def snapshot_change(self, change, path):
123
118
        if change == 'unchanged':
261
256
            # Check that the working tree is up to date
262
257
            old_revno,new_revno = self._check_out_of_date_tree()
263
258
 
264
 
            if strict:
265
 
                # raise an exception as soon as we find a single unknown.
266
 
                for unknown in self.work_tree.unknowns():
267
 
                    raise StrictCommitFailed()
268
 
                   
269
259
            if self.config is None:
270
260
                self.config = self.branch.get_config()
271
261
 
272
 
            self.work_inv = self.work_tree.inventory
273
 
            self.basis_inv = self.basis_tree.inventory
 
262
            # If provided, ensure the specified files are versioned
274
263
            if specific_files is not None:
275
 
                # Ensure specified files are versioned
276
 
                # (We don't actually need the ids here)
 
264
                # Note: We don't actually need the IDs here. This routine
 
265
                # is being called because it raises PathNotVerisonedError
 
266
                # as a side effect of finding the IDs.
277
267
                # XXX: Dont we have filter_unversioned to do this more
278
268
                # cheaply?
279
269
                tree.find_ids_across_trees(specific_files,
280
270
                                           [self.basis_tree, self.work_tree])
281
271
 
282
 
            # Setup the progress bar ...
283
 
            # one to finish, one for rev and inventory, and one for each
284
 
            # inventory entry, and the same for the new inventory.
285
 
            # note that this estimate is too long when we do a partial tree
286
 
            # commit which excludes some new files from being considered.
287
 
            # The estimate is corrected when we populate the new inv.
288
 
            self.pb_total = len(self.work_inv) + 5
289
 
            self.pb_count = 0
 
272
            # Setup the progress bar. As the number of files that need to be
 
273
            # committed in unknown, progress is reported as stages.
 
274
            # We keep track of entries separately though and include that
 
275
            # information in the progress bar during the relevant stages.
 
276
            self.pb_stage_name = ""
 
277
            self.pb_stage_count = 0
 
278
            self.pb_stage_total = 4
 
279
            if self.bound_branch:
 
280
                self.pb_stage_total += 1
 
281
            self.pb.show_pct = False
 
282
            self.pb.show_spinner = False
 
283
            self.pb.show_eta = False
 
284
            self.pb.show_count = True
 
285
            self.pb.show_bar = True
290
286
 
 
287
            # After a merge, a selected file commit is not supported.
 
288
            # See 'bzr help merge' for an explanation as to why.
 
289
            self.basis_inv = self.basis_tree.inventory
291
290
            self._gather_parents()
292
291
            if len(self.parents) > 1 and self.specific_files:
293
292
                raise errors.CannotCommitSelectedFileMerge(self.specific_files)
294
293
            
295
 
            # Build the new inventory
 
294
            # Collect the changes
 
295
            self._set_progress_stage("Collecting changes",
 
296
                    entries_title="Directory")
296
297
            self.builder = self.branch.get_commit_builder(self.parents,
297
298
                self.config, timestamp, timezone, committer, revprops, rev_id)
298
 
            self._remove_deleted()
299
 
            self._populate_new_inv()
300
 
            self._report_deletes()
 
299
            self._update_builder_with_changes()
301
300
            self._check_pointless()
302
 
            self._emit_progress_update()
303
301
 
304
 
            # TODO: Now the new inventory is known, check for conflicts and
305
 
            # prompt the user for a commit message.
 
302
            # TODO: Now the new inventory is known, check for conflicts.
306
303
            # ADHB 2006-08-08: If this is done, populate_new_inv should not add
307
304
            # weave lines, because nothing should be recorded until it is known
308
305
            # that commit will succeed.
 
306
            self._set_progress_stage("Saving data locally")
309
307
            self.builder.finish_inventory()
310
 
            self._emit_progress_update()
 
308
 
 
309
            # Prompt the user for a commit message if none provided
311
310
            message = message_callback(self)
312
311
            assert isinstance(message, unicode), type(message)
313
312
            self.message = message
315
314
 
316
315
            # Add revision data to the local branch
317
316
            self.rev_id = self.builder.commit(self.message)
318
 
            self._emit_progress_update()
319
317
            
320
 
            # upload revision data to the master.
 
318
            # Upload revision data to the master.
321
319
            # this will propagate merged revisions too if needed.
322
320
            if self.bound_branch:
 
321
                self._set_progress_stage("Uploading data to master branch")
323
322
                self.master_branch.repository.fetch(self.branch.repository,
324
323
                                                    revision_id=self.rev_id)
325
324
                # now the master has the revision data
332
331
            self.branch.set_last_revision_info(new_revno, self.rev_id)
333
332
 
334
333
            # Make the working tree up to date with the branch
 
334
            self._set_progress_stage("Updating the working tree")
335
335
            rev_tree = self.builder.revision_tree()
336
336
            self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
337
337
            self.reporter.completed(new_revno, self.rev_id)
338
 
 
339
 
            # Process the post commit hooks, if any
340
338
            self._process_hooks(old_revno, new_revno)
341
 
            self._emit_progress_update()
342
339
        finally:
343
340
            self._cleanup()
344
341
        return self.rev_id
466
463
 
467
464
    def _process_hooks(self, old_revno, new_revno):
468
465
        """Process any registered commit hooks."""
 
466
        # Process the post commit hooks, if any
 
467
        self._set_progress_stage("Running post commit hooks")
469
468
        # old style commit hooks - should be deprecated ? (obsoleted in
470
469
        # 0.15)
471
470
        if self.config.post_commit() is not None:
491
490
        else:
492
491
            old_revid = bzrlib.revision.NULL_REVISION
493
492
        for hook in Branch.hooks['post_commit']:
 
493
            # show the running hook in the progress bar. As hooks may
 
494
            # end up doing nothing (e.g. because they are not configured by
 
495
            # the user) this is still showing progress, not showing overall
 
496
            # actions - its up to each plugin to show a UI if it want's to
 
497
            # (such as 'Emailing diff to foo@example.com').
 
498
            self.pb_stage_name = "Running post commit hooks [%s]" % \
 
499
                Branch.hooks.get_hook_name(hook)
 
500
            self._emit_progress()
 
501
            if 'hooks' in debug.debug_flags:
 
502
                mutter("Invoking commit hook: %r", hook)
494
503
            hook(hook_local, hook_master, old_revno, old_revid, new_revno,
495
504
                self.rev_id)
496
505
 
562
571
            else:
563
572
                mutter('commit parent ghost revision {%s}', revision)
564
573
 
565
 
    def _remove_deleted(self):
566
 
        """Remove deleted files from the working inventories.
567
 
 
568
 
        This is done prior to taking the working inventory as the
569
 
        basis for the new committed inventory.
570
 
 
571
 
        This returns true if any files
572
 
        *that existed in the basis inventory* were deleted.
573
 
        Files that were added and deleted
574
 
        in the working copy don't matter.
575
 
        """
576
 
        specific = self.specific_files
577
 
        deleted_ids = []
578
 
        deleted_paths = set()
579
 
        for path, ie in self.work_inv.iter_entries():
580
 
            if is_inside_any(deleted_paths, path):
581
 
                # The tree will delete the required ids recursively.
582
 
                continue
583
 
            if specific and not is_inside_any(specific, path):
584
 
                continue
585
 
            if not self.work_tree.has_filename(path):
586
 
                deleted_paths.add(path)
587
 
                self.reporter.missing(path)
588
 
                deleted_ids.append(ie.file_id)
589
 
        self.work_tree.unversion(deleted_ids)
590
 
 
591
 
    def _populate_new_inv(self):
592
 
        """Build revision inventory.
593
 
 
594
 
        This creates a new empty inventory. Depending on
595
 
        which files are selected for commit, and what is present in the
596
 
        current tree, the new inventory is populated. inventory entries 
597
 
        which are candidates for modification have their revision set to
598
 
        None; inventory entries that are carried over untouched have their
599
 
        revision set to their prior value.
600
 
        """
 
574
    def _update_builder_with_changes(self):
 
575
        """Update the commit builder with the data about what has changed.
 
576
        """
 
577
        # Build the revision inventory.
 
578
        #
 
579
        # This starts by creating a new empty inventory. Depending on
 
580
        # which files are selected for commit, and what is present in the
 
581
        # current tree, the new inventory is populated. inventory entries 
 
582
        # which are candidates for modification have their revision set to
 
583
        # None; inventory entries that are carried over untouched have their
 
584
        # revision set to their prior value.
 
585
        #
601
586
        # ESEPARATIONOFCONCERNS: this function is diffing and using the diff
602
587
        # results to create a new inventory at the same time, which results
603
588
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
604
589
        # ADHB 11-07-2006
605
 
        mutter("Selecting files for commit with filter %s", self.specific_files)
606
 
        assert self.work_inv.root is not None
607
 
        entries = self.work_inv.iter_entries()
 
590
 
 
591
        specific_files = self.specific_files
 
592
        mutter("Selecting files for commit with filter %s", specific_files)
 
593
 
 
594
        # Check and warn about old CommitBuilders
608
595
        if not self.builder.record_root_entry:
609
596
            symbol_versioning.warn('CommitBuilders should support recording'
610
597
                ' the root entry as of bzr 0.10.', DeprecationWarning, 
611
598
                stacklevel=1)
612
599
            self.builder.new_inventory.add(self.basis_inv.root.copy())
 
600
 
 
601
        # Build the new inventory
 
602
        self._populate_from_inventory(specific_files)
 
603
 
 
604
        # If specific files are selected, then all un-selected files must be
 
605
        # recorded in their previous state. For more details, see
 
606
        # https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
 
607
        if specific_files:
 
608
            for path, new_ie in self.basis_inv.iter_entries():
 
609
                if new_ie.file_id in self.builder.new_inventory:
 
610
                    continue
 
611
                if is_inside_any(specific_files, path):
 
612
                    continue
 
613
                ie = new_ie.copy()
 
614
                ie.revision = None
 
615
                self.builder.record_entry_contents(ie, self.parent_invs, path,
 
616
                                                   self.basis_tree)
 
617
 
 
618
        # Report what was deleted. We could skip this when no deletes are
 
619
        # detected to gain a performance win, but it arguably serves as a
 
620
        # 'safety check' by informing the user whenever anything disappears.
 
621
        for path, ie in self.basis_inv.iter_entries():
 
622
            if ie.file_id not in self.builder.new_inventory:
 
623
                self.reporter.deleted(path)
 
624
 
 
625
    def _populate_from_inventory(self, specific_files):
 
626
        """Populate the CommitBuilder by walking the working tree inventory."""
 
627
        if self.strict:
 
628
            # raise an exception as soon as we find a single unknown.
 
629
            for unknown in self.work_tree.unknowns():
 
630
                raise StrictCommitFailed()
 
631
               
 
632
        deleted_ids = []
 
633
        deleted_paths = set()
 
634
        work_inv = self.work_tree.inventory
 
635
        assert work_inv.root is not None
 
636
        entries = work_inv.iter_entries()
 
637
        if not self.builder.record_root_entry:
613
638
            entries.next()
614
 
            self._emit_progress_update()
615
 
        for path, new_ie in entries:
616
 
            self._emit_progress_update()
617
 
            file_id = new_ie.file_id
 
639
        for path, existing_ie in entries:
 
640
            file_id = existing_ie.file_id
 
641
            name = existing_ie.name
 
642
            parent_id = existing_ie.parent_id
 
643
            kind = existing_ie.kind
 
644
            if kind == 'directory':
 
645
                self._next_progress_entry()
 
646
 
 
647
            # Skip files that have been deleted from the working tree.
 
648
            # The deleted files/directories are also recorded so they
 
649
            # can be explicitly unversioned later. Note that when a
 
650
            # filter of specific files is given, we must only skip/record
 
651
            # deleted files matching that filter.
 
652
            if is_inside_any(deleted_paths, path):
 
653
                continue
 
654
            if not specific_files or is_inside_any(specific_files, path):
 
655
                if not self.work_tree.has_filename(path):
 
656
                    deleted_paths.add(path)
 
657
                    self.reporter.missing(path)
 
658
                    deleted_ids.append(file_id)
 
659
                    continue
618
660
            try:
619
661
                kind = self.work_tree.kind(file_id)
 
662
                # TODO: specific_files filtering before nested tree processing
620
663
                if kind == 'tree-reference' and self.recursive == 'down':
621
 
                    # nested tree: commit in it
622
 
                    sub_tree = WorkingTree.open(self.work_tree.abspath(path))
623
 
                    # FIXME: be more comprehensive here:
624
 
                    # this works when both trees are in --trees repository,
625
 
                    # but when both are bound to a different repository,
626
 
                    # it fails; a better way of approaching this is to 
627
 
                    # finally implement the explicit-caches approach design
628
 
                    # a while back - RBC 20070306.
629
 
                    if (sub_tree.branch.repository.bzrdir.root_transport.base
630
 
                        ==
631
 
                        self.work_tree.branch.repository.bzrdir.root_transport.base):
632
 
                        sub_tree.branch.repository = \
633
 
                            self.work_tree.branch.repository
634
 
                    try:
635
 
                        sub_tree.commit(message=None, revprops=self.revprops,
636
 
                            recursive=self.recursive,
637
 
                            message_callback=self.message_callback,
638
 
                            timestamp=self.timestamp, timezone=self.timezone,
639
 
                            committer=self.committer,
640
 
                            allow_pointless=self.allow_pointless,
641
 
                            strict=self.strict, verbose=self.verbose,
642
 
                            local=self.local, reporter=self.reporter)
643
 
                    except errors.PointlessCommit:
644
 
                        pass
645
 
                if kind != new_ie.kind:
646
 
                    new_ie = inventory.make_entry(kind, new_ie.name,
647
 
                                                  new_ie.parent_id, file_id)
 
664
                    self._commit_nested_tree(file_id, path)
648
665
            except errors.NoSuchFile:
649
666
                pass
650
 
            # mutter('check %s {%s}', path, file_id)
651
 
            if (not self.specific_files or 
652
 
                is_inside_or_parent_of_any(self.specific_files, path)):
653
 
                    # mutter('%s selected for commit', path)
654
 
                    ie = new_ie.copy()
 
667
 
 
668
            # Record an entry for this item
 
669
            # Note: I don't particularly want to have the existing_ie
 
670
            # parameter but the test suite currently (28-Jun-07) breaks
 
671
            # without it thanks to a unicode normalisation issue. :-(
 
672
            definitely_changed = kind != existing_ie.kind 
 
673
            self._record_entry(path, file_id, specific_files, kind, name,
 
674
                parent_id, definitely_changed, existing_ie)
 
675
 
 
676
        # Unversion IDs that were found to be deleted
 
677
        self.work_tree.unversion(deleted_ids)
 
678
 
 
679
    def _commit_nested_tree(self, file_id, path):
 
680
        "Commit a nested tree."
 
681
        sub_tree = self.work_tree.get_nested_tree(file_id, path)
 
682
        # FIXME: be more comprehensive here:
 
683
        # this works when both trees are in --trees repository,
 
684
        # but when both are bound to a different repository,
 
685
        # it fails; a better way of approaching this is to 
 
686
        # finally implement the explicit-caches approach design
 
687
        # a while back - RBC 20070306.
 
688
        if (sub_tree.branch.repository.bzrdir.root_transport.base
 
689
            ==
 
690
            self.work_tree.branch.repository.bzrdir.root_transport.base):
 
691
            sub_tree.branch.repository = \
 
692
                self.work_tree.branch.repository
 
693
        try:
 
694
            sub_tree.commit(message=None, revprops=self.revprops,
 
695
                recursive=self.recursive,
 
696
                message_callback=self.message_callback,
 
697
                timestamp=self.timestamp, timezone=self.timezone,
 
698
                committer=self.committer,
 
699
                allow_pointless=self.allow_pointless,
 
700
                strict=self.strict, verbose=self.verbose,
 
701
                local=self.local, reporter=self.reporter)
 
702
        except errors.PointlessCommit:
 
703
            pass
 
704
 
 
705
    def _record_entry(self, path, file_id, specific_files, kind, name,
 
706
                      parent_id, definitely_changed, existing_ie=None):
 
707
        "Record the new inventory entry for a path if any."
 
708
        # mutter('check %s {%s}', path, file_id)
 
709
        if (not specific_files or 
 
710
            is_inside_or_parent_of_any(specific_files, path)):
 
711
                # mutter('%s selected for commit', path)
 
712
                if definitely_changed or existing_ie is None:
 
713
                    ie = inventory.make_entry(kind, name, parent_id, file_id)
 
714
                else:
 
715
                    ie = existing_ie.copy()
655
716
                    ie.revision = None
 
717
        else:
 
718
            # mutter('%s not selected for commit', path)
 
719
            if self.basis_inv.has_id(file_id):
 
720
                ie = self.basis_inv[file_id].copy()
656
721
            else:
657
 
                # mutter('%s not selected for commit', path)
658
 
                if self.basis_inv.has_id(file_id):
659
 
                    ie = self.basis_inv[file_id].copy()
660
 
                else:
661
 
                    # this entry is new and not being committed
662
 
                    continue
 
722
                # this entry is new and not being committed
 
723
                ie = None
 
724
        if ie is not None:
663
725
            self.builder.record_entry_contents(ie, self.parent_invs, 
664
726
                path, self.work_tree)
665
 
            # describe the nature of the change that has occurred relative to
666
 
            # the basis inventory.
667
 
            if (self.basis_inv.has_id(ie.file_id)):
668
 
                basis_ie = self.basis_inv[ie.file_id]
669
 
            else:
670
 
                basis_ie = None
671
 
            change = ie.describe_change(basis_ie, ie)
672
 
            if change in (InventoryEntry.RENAMED, 
673
 
                InventoryEntry.MODIFIED_AND_RENAMED):
674
 
                old_path = self.basis_inv.id2path(ie.file_id)
675
 
                self.reporter.renamed(change, old_path, path)
676
 
            else:
677
 
                self.reporter.snapshot_change(change, path)
678
 
 
679
 
        if not self.specific_files:
680
 
            return
681
 
 
682
 
        # ignore removals that don't match filespec
683
 
        for path, new_ie in self.basis_inv.iter_entries():
684
 
            if new_ie.file_id in self.work_inv:
685
 
                continue
686
 
            if is_inside_any(self.specific_files, path):
687
 
                continue
688
 
            ie = new_ie.copy()
689
 
            ie.revision = None
690
 
            self.builder.record_entry_contents(ie, self.parent_invs, path,
691
 
                                               self.basis_tree)
692
 
 
693
 
    def _emit_progress_update(self):
694
 
        """Emit an update to the progress bar."""
695
 
        self.pb.update("Committing", self.pb_count, self.pb_total)
696
 
        self.pb_count += 1
697
 
 
698
 
    def _report_deletes(self):
699
 
        for path, ie in self.basis_inv.iter_entries():
700
 
            if ie.file_id not in self.builder.new_inventory:
701
 
                self.reporter.deleted(path)
702
 
 
 
727
            self._report_change(ie, path)
 
728
        return ie
 
729
 
 
730
    def _report_change(self, ie, path):
 
731
        """Report a change to the user.
 
732
 
 
733
        The change that has occurred is described relative to the basis
 
734
        inventory.
 
735
        """
 
736
        if (self.basis_inv.has_id(ie.file_id)):
 
737
            basis_ie = self.basis_inv[ie.file_id]
 
738
        else:
 
739
            basis_ie = None
 
740
        change = ie.describe_change(basis_ie, ie)
 
741
        if change in (InventoryEntry.RENAMED, 
 
742
            InventoryEntry.MODIFIED_AND_RENAMED):
 
743
            old_path = self.basis_inv.id2path(ie.file_id)
 
744
            self.reporter.renamed(change, old_path, path)
 
745
        else:
 
746
            self.reporter.snapshot_change(change, path)
 
747
 
 
748
    def _set_progress_stage(self, name, entries_title=None):
 
749
        """Set the progress stage and emit an update to the progress bar."""
 
750
        self.pb_stage_name = name
 
751
        self.pb_stage_count += 1
 
752
        self.pb_entries_title = entries_title
 
753
        if entries_title is not None:
 
754
            self.pb_entries_count = 0
 
755
            self.pb_entries_total = '?'
 
756
        self._emit_progress()
 
757
 
 
758
    def _next_progress_entry(self):
 
759
        """Emit an update to the progress bar and increment the entry count."""
 
760
        self.pb_entries_count += 1
 
761
        self._emit_progress()
 
762
 
 
763
    def _emit_progress(self):
 
764
        if self.pb_entries_title:
 
765
            if self.pb_entries_total == '?':
 
766
                text = "%s [%s %d] - Stage" % (self.pb_stage_name,
 
767
                    self.pb_entries_title, self.pb_entries_count)
 
768
            else:
 
769
                text = "%s [%s %d/%s] - Stage" % (self.pb_stage_name,
 
770
                    self.pb_entries_title, self.pb_entries_count,
 
771
                    str(self.pb_entries_total))
 
772
        else:
 
773
            text = "%s - Stage" % (self.pb_stage_name)
 
774
        self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
703
775