/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: Martin Pool
  • Date: 2007-08-20 05:53:39 UTC
  • mfrom: (2727 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2730.
  • Revision ID: mbp@sourcefrog.net-20070820055339-uzei7f7i7jo6tugg
merge trunk

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()
301
 
            self._check_pointless()
302
 
            self._emit_progress_update()
303
 
 
304
 
            # TODO: Now the new inventory is known, check for conflicts and
305
 
            # prompt the user for a commit message.
306
 
            # ADHB 2006-08-08: If this is done, populate_new_inv should not add
307
 
            # weave lines, because nothing should be recorded until it is known
308
 
            # that commit will succeed.
309
 
            self.builder.finish_inventory()
310
 
            self._emit_progress_update()
311
 
            message = message_callback(self)
312
 
            assert isinstance(message, unicode), type(message)
313
 
            self.message = message
314
 
            self._escape_commit_message()
315
 
 
316
 
            # Add revision data to the local branch
317
 
            self.rev_id = self.builder.commit(self.message)
318
 
            self._emit_progress_update()
319
 
            
320
 
            # upload revision data to the master.
 
299
            try:
 
300
                self._update_builder_with_changes()
 
301
                self._check_pointless()
 
302
 
 
303
                # TODO: Now the new inventory is known, check for conflicts.
 
304
                # ADHB 2006-08-08: If this is done, populate_new_inv should not add
 
305
                # weave lines, because nothing should be recorded until it is known
 
306
                # that commit will succeed.
 
307
                self._set_progress_stage("Saving data locally")
 
308
                self.builder.finish_inventory()
 
309
 
 
310
                # Prompt the user for a commit message if none provided
 
311
                message = message_callback(self)
 
312
                assert isinstance(message, unicode), type(message)
 
313
                self.message = message
 
314
                self._escape_commit_message()
 
315
 
 
316
                # Add revision data to the local branch
 
317
                self.rev_id = self.builder.commit(self.message)
 
318
            except:
 
319
                # perhaps this should be done by the CommitBuilder ?
 
320
                self.work_tree.branch.repository.abort_write_group()
 
321
                raise
 
322
 
 
323
            # Upload revision data to the master.
321
324
            # this will propagate merged revisions too if needed.
322
325
            if self.bound_branch:
 
326
                self._set_progress_stage("Uploading data to master branch")
323
327
                self.master_branch.repository.fetch(self.branch.repository,
324
328
                                                    revision_id=self.rev_id)
325
329
                # now the master has the revision data
332
336
            self.branch.set_last_revision_info(new_revno, self.rev_id)
333
337
 
334
338
            # Make the working tree up to date with the branch
 
339
            self._set_progress_stage("Updating the working tree")
335
340
            rev_tree = self.builder.revision_tree()
336
341
            self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
337
342
            self.reporter.completed(new_revno, self.rev_id)
338
 
 
339
 
            # Process the post commit hooks, if any
340
343
            self._process_hooks(old_revno, new_revno)
341
 
            self._emit_progress_update()
342
344
        finally:
343
345
            self._cleanup()
344
346
        return self.rev_id
466
468
 
467
469
    def _process_hooks(self, old_revno, new_revno):
468
470
        """Process any registered commit hooks."""
 
471
        # Process the post commit hooks, if any
 
472
        self._set_progress_stage("Running post commit hooks")
469
473
        # old style commit hooks - should be deprecated ? (obsoleted in
470
474
        # 0.15)
471
475
        if self.config.post_commit() is not None:
491
495
        else:
492
496
            old_revid = bzrlib.revision.NULL_REVISION
493
497
        for hook in Branch.hooks['post_commit']:
 
498
            # show the running hook in the progress bar. As hooks may
 
499
            # end up doing nothing (e.g. because they are not configured by
 
500
            # the user) this is still showing progress, not showing overall
 
501
            # actions - its up to each plugin to show a UI if it want's to
 
502
            # (such as 'Emailing diff to foo@example.com').
 
503
            self.pb_stage_name = "Running post commit hooks [%s]" % \
 
504
                Branch.hooks.get_hook_name(hook)
 
505
            self._emit_progress()
 
506
            if 'hooks' in debug.debug_flags:
 
507
                mutter("Invoking commit hook: %r", hook)
494
508
            hook(hook_local, hook_master, old_revno, old_revid, new_revno,
495
509
                self.rev_id)
496
510
 
562
576
            else:
563
577
                mutter('commit parent ghost revision {%s}', revision)
564
578
 
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
 
        """
 
579
    def _update_builder_with_changes(self):
 
580
        """Update the commit builder with the data about what has changed.
 
581
        """
 
582
        # Build the revision inventory.
 
583
        #
 
584
        # This starts by creating a new empty inventory. Depending on
 
585
        # which files are selected for commit, and what is present in the
 
586
        # current tree, the new inventory is populated. inventory entries 
 
587
        # which are candidates for modification have their revision set to
 
588
        # None; inventory entries that are carried over untouched have their
 
589
        # revision set to their prior value.
 
590
        #
601
591
        # ESEPARATIONOFCONCERNS: this function is diffing and using the diff
602
592
        # results to create a new inventory at the same time, which results
603
593
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
604
594
        # 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()
 
595
 
 
596
        specific_files = self.specific_files
 
597
        mutter("Selecting files for commit with filter %s", specific_files)
 
598
 
 
599
        # Check and warn about old CommitBuilders
608
600
        if not self.builder.record_root_entry:
609
601
            symbol_versioning.warn('CommitBuilders should support recording'
610
602
                ' the root entry as of bzr 0.10.', DeprecationWarning, 
611
603
                stacklevel=1)
612
604
            self.builder.new_inventory.add(self.basis_inv.root.copy())
 
605
 
 
606
        # Build the new inventory
 
607
        self._populate_from_inventory(specific_files)
 
608
 
 
609
        # If specific files are selected, then all un-selected files must be
 
610
        # recorded in their previous state. For more details, see
 
611
        # https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
 
612
        if specific_files:
 
613
            for path, new_ie in self.basis_inv.iter_entries():
 
614
                if new_ie.file_id in self.builder.new_inventory:
 
615
                    continue
 
616
                if is_inside_any(specific_files, path):
 
617
                    continue
 
618
                ie = new_ie.copy()
 
619
                ie.revision = None
 
620
                self.builder.record_entry_contents(ie, self.parent_invs, path,
 
621
                                                   self.basis_tree)
 
622
 
 
623
        # Report what was deleted. We could skip this when no deletes are
 
624
        # detected to gain a performance win, but it arguably serves as a
 
625
        # 'safety check' by informing the user whenever anything disappears.
 
626
        for path, ie in self.basis_inv.iter_entries():
 
627
            if ie.file_id not in self.builder.new_inventory:
 
628
                self.reporter.deleted(path)
 
629
 
 
630
    def _populate_from_inventory(self, specific_files):
 
631
        """Populate the CommitBuilder by walking the working tree inventory."""
 
632
        if self.strict:
 
633
            # raise an exception as soon as we find a single unknown.
 
634
            for unknown in self.work_tree.unknowns():
 
635
                raise StrictCommitFailed()
 
636
               
 
637
        deleted_ids = []
 
638
        deleted_paths = set()
 
639
        work_inv = self.work_tree.inventory
 
640
        assert work_inv.root is not None
 
641
        entries = work_inv.iter_entries()
 
642
        if not self.builder.record_root_entry:
613
643
            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
 
644
        for path, existing_ie in entries:
 
645
            file_id = existing_ie.file_id
 
646
            name = existing_ie.name
 
647
            parent_id = existing_ie.parent_id
 
648
            kind = existing_ie.kind
 
649
            if kind == 'directory':
 
650
                self._next_progress_entry()
 
651
 
 
652
            # Skip files that have been deleted from the working tree.
 
653
            # The deleted files/directories are also recorded so they
 
654
            # can be explicitly unversioned later. Note that when a
 
655
            # filter of specific files is given, we must only skip/record
 
656
            # deleted files matching that filter.
 
657
            if is_inside_any(deleted_paths, path):
 
658
                continue
 
659
            if not specific_files or is_inside_any(specific_files, path):
 
660
                if not self.work_tree.has_filename(path):
 
661
                    deleted_paths.add(path)
 
662
                    self.reporter.missing(path)
 
663
                    deleted_ids.append(file_id)
 
664
                    continue
618
665
            try:
619
666
                kind = self.work_tree.kind(file_id)
 
667
                # TODO: specific_files filtering before nested tree processing
620
668
                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)
 
669
                    self._commit_nested_tree(file_id, path)
648
670
            except errors.NoSuchFile:
649
671
                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()
 
672
 
 
673
            # Record an entry for this item
 
674
            # Note: I don't particularly want to have the existing_ie
 
675
            # parameter but the test suite currently (28-Jun-07) breaks
 
676
            # without it thanks to a unicode normalisation issue. :-(
 
677
            definitely_changed = kind != existing_ie.kind 
 
678
            self._record_entry(path, file_id, specific_files, kind, name,
 
679
                parent_id, definitely_changed, existing_ie)
 
680
 
 
681
        # Unversion IDs that were found to be deleted
 
682
        self.work_tree.unversion(deleted_ids)
 
683
 
 
684
    def _commit_nested_tree(self, file_id, path):
 
685
        "Commit a nested tree."
 
686
        sub_tree = self.work_tree.get_nested_tree(file_id, path)
 
687
        # FIXME: be more comprehensive here:
 
688
        # this works when both trees are in --trees repository,
 
689
        # but when both are bound to a different repository,
 
690
        # it fails; a better way of approaching this is to 
 
691
        # finally implement the explicit-caches approach design
 
692
        # a while back - RBC 20070306.
 
693
        if sub_tree.branch.repository.has_same_location(
 
694
            self.work_tree.branch.repository):
 
695
            sub_tree.branch.repository = \
 
696
                self.work_tree.branch.repository
 
697
        try:
 
698
            sub_tree.commit(message=None, revprops=self.revprops,
 
699
                recursive=self.recursive,
 
700
                message_callback=self.message_callback,
 
701
                timestamp=self.timestamp, timezone=self.timezone,
 
702
                committer=self.committer,
 
703
                allow_pointless=self.allow_pointless,
 
704
                strict=self.strict, verbose=self.verbose,
 
705
                local=self.local, reporter=self.reporter)
 
706
        except errors.PointlessCommit:
 
707
            pass
 
708
 
 
709
    def _record_entry(self, path, file_id, specific_files, kind, name,
 
710
                      parent_id, definitely_changed, existing_ie=None):
 
711
        "Record the new inventory entry for a path if any."
 
712
        # mutter('check %s {%s}', path, file_id)
 
713
        if (not specific_files or 
 
714
            is_inside_or_parent_of_any(specific_files, path)):
 
715
                # mutter('%s selected for commit', path)
 
716
                if definitely_changed or existing_ie is None:
 
717
                    ie = inventory.make_entry(kind, name, parent_id, file_id)
 
718
                else:
 
719
                    ie = existing_ie.copy()
655
720
                    ie.revision = None
 
721
        else:
 
722
            # mutter('%s not selected for commit', path)
 
723
            if self.basis_inv.has_id(file_id):
 
724
                ie = self.basis_inv[file_id].copy()
656
725
            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
 
726
                # this entry is new and not being committed
 
727
                ie = None
 
728
        if ie is not None:
663
729
            self.builder.record_entry_contents(ie, self.parent_invs, 
664
730
                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
 
 
 
731
            self._report_change(ie, path)
 
732
        return ie
 
733
 
 
734
    def _report_change(self, ie, path):
 
735
        """Report a change to the user.
 
736
 
 
737
        The change that has occurred is described relative to the basis
 
738
        inventory.
 
739
        """
 
740
        if (self.basis_inv.has_id(ie.file_id)):
 
741
            basis_ie = self.basis_inv[ie.file_id]
 
742
        else:
 
743
            basis_ie = None
 
744
        change = ie.describe_change(basis_ie, ie)
 
745
        if change in (InventoryEntry.RENAMED, 
 
746
            InventoryEntry.MODIFIED_AND_RENAMED):
 
747
            old_path = self.basis_inv.id2path(ie.file_id)
 
748
            self.reporter.renamed(change, old_path, path)
 
749
        else:
 
750
            self.reporter.snapshot_change(change, path)
 
751
 
 
752
    def _set_progress_stage(self, name, entries_title=None):
 
753
        """Set the progress stage and emit an update to the progress bar."""
 
754
        self.pb_stage_name = name
 
755
        self.pb_stage_count += 1
 
756
        self.pb_entries_title = entries_title
 
757
        if entries_title is not None:
 
758
            self.pb_entries_count = 0
 
759
            self.pb_entries_total = '?'
 
760
        self._emit_progress()
 
761
 
 
762
    def _next_progress_entry(self):
 
763
        """Emit an update to the progress bar and increment the entry count."""
 
764
        self.pb_entries_count += 1
 
765
        self._emit_progress()
 
766
 
 
767
    def _emit_progress(self):
 
768
        if self.pb_entries_title:
 
769
            if self.pb_entries_total == '?':
 
770
                text = "%s [%s %d] - Stage" % (self.pb_stage_name,
 
771
                    self.pb_entries_title, self.pb_entries_count)
 
772
            else:
 
773
                text = "%s [%s %d/%s] - Stage" % (self.pb_stage_name,
 
774
                    self.pb_entries_title, self.pb_entries_count,
 
775
                    str(self.pb_entries_total))
 
776
        else:
 
777
            text = "%s - Stage" % (self.pb_stage_name)
 
778
        self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
703
779