~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

Merge from bzr.dev.

Show diffs side-by-side

added added

removed removed

Lines of Context:
68
68
                           ConflictsInTree,
69
69
                           StrictCommitFailed
70
70
                           )
71
 
from bzrlib.osutils import (kind_marker, isdir,isfile, is_inside_any, 
 
71
from bzrlib.osutils import (kind_marker, isdir,isfile, is_inside_any,
72
72
                            is_inside_or_parent_of_any,
 
73
                            minimum_path_selection,
73
74
                            quotefn, sha_file, split_lines)
74
75
from bzrlib.testament import Testament
75
 
from bzrlib.trace import mutter, note, warning
 
76
from bzrlib.trace import mutter, note, warning, is_quiet
76
77
from bzrlib.xml5 import serializer_v5
77
78
from bzrlib.inventory import Inventory, InventoryEntry
78
79
from bzrlib import symbol_versioning
80
81
        deprecated_function,
81
82
        DEPRECATED_PARAMETER)
82
83
from bzrlib.workingtree import WorkingTree
 
84
from bzrlib.urlutils import unescape_for_display
83
85
import bzrlib.ui
84
86
 
85
87
 
86
88
class NullCommitReporter(object):
87
89
    """I report on progress of a commit."""
88
90
 
 
91
    def started(self, revno, revid, location=None):
 
92
        pass
 
93
 
89
94
    def snapshot_change(self, change, path):
90
95
        pass
91
96
 
104
109
    def renamed(self, change, old_path, new_path):
105
110
        pass
106
111
 
 
112
    def is_verbose(self):
 
113
        return False
 
114
 
107
115
 
108
116
class ReportCommitToLog(NullCommitReporter):
109
117
 
121
129
            return
122
130
        self._note("%s %s", change, path)
123
131
 
 
132
    def started(self, revno, rev_id, location=None):
 
133
        if location is not None:
 
134
            location = ' to "' + unescape_for_display(location, 'utf-8') + '"'
 
135
        else:
 
136
            location = ''
 
137
        self._note('Committing revision %d%s.', revno, location)
 
138
 
124
139
    def completed(self, revno, rev_id):
125
140
        self._note('Committed revision %d.', revno)
126
 
    
 
141
 
127
142
    def deleted(self, file_id):
128
143
        self._note('deleted %s', file_id)
129
144
 
136
151
    def renamed(self, change, old_path, new_path):
137
152
        self._note('%s %s => %s', change, old_path, new_path)
138
153
 
 
154
    def is_verbose(self):
 
155
        return True
 
156
 
139
157
 
140
158
class Commit(object):
141
159
    """Task of committing a new revision.
152
170
    def __init__(self,
153
171
                 reporter=None,
154
172
                 config=None):
155
 
        if reporter is not None:
156
 
            self.reporter = reporter
157
 
        else:
158
 
            self.reporter = NullCommitReporter()
 
173
        """Create a Commit object.
 
174
 
 
175
        :param reporter: the default reporter to use or None to decide later
 
176
        """
 
177
        self.reporter = reporter
159
178
        self.config = config
160
 
        
 
179
 
161
180
    def commit(self,
162
181
               message=None,
163
182
               timestamp=None,
198
217
 
199
218
        :param revprops: Properties for new revision
200
219
        :param local: Perform a local only commit.
 
220
        :param reporter: the reporter to use or None for the default
 
221
        :param verbose: if True and the reporter is not None, report everything
201
222
        :param recursive: If set to 'down', commit in any subtrees that have
202
223
            pending changes of any sort during this commit.
203
224
        """
221
242
                               " parameter is required for commit().")
222
243
 
223
244
        self.bound_branch = None
 
245
        self.any_entries_changed = False
 
246
        self.any_entries_deleted = False
224
247
        self.local = local
225
248
        self.master_branch = None
226
249
        self.master_locked = False
 
250
        self.recursive = recursive
227
251
        self.rev_id = None
228
 
        self.specific_files = specific_files
 
252
        if specific_files is not None:
 
253
            self.specific_files = sorted(
 
254
                minimum_path_selection(specific_files))
 
255
        else:
 
256
            self.specific_files = None
 
257
        self.specific_file_ids = None
229
258
        self.allow_pointless = allow_pointless
230
 
        self.recursive = recursive
231
259
        self.revprops = revprops
232
260
        self.message_callback = message_callback
233
261
        self.timestamp = timestamp
236
264
        self.strict = strict
237
265
        self.verbose = verbose
238
266
 
239
 
        if reporter is None and self.reporter is None:
240
 
            self.reporter = NullCommitReporter()
241
 
        elif reporter is not None:
242
 
            self.reporter = reporter
243
 
 
244
267
        self.work_tree.lock_write()
245
268
        self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
246
269
        self.basis_tree = self.work_tree.basis_tree()
256
279
            # Check that the working tree is up to date
257
280
            old_revno, new_revno = self._check_out_of_date_tree()
258
281
 
 
282
            # Complete configuration setup
 
283
            if reporter is not None:
 
284
                self.reporter = reporter
 
285
            elif self.reporter is None:
 
286
                self.reporter = self._select_reporter()
259
287
            if self.config is None:
260
288
                self.config = self.branch.get_config()
261
289
 
262
290
            # If provided, ensure the specified files are versioned
263
 
            if specific_files is not None:
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.
 
291
            if self.specific_files is not None:
 
292
                # Note: This routine is being called because it raises
 
293
                # PathNotVersionedError as a side effect of finding the IDs. We
 
294
                # later use the ids we found as input to the working tree
 
295
                # inventory iterator, so we only consider those ids rather than
 
296
                # examining the whole tree again.
267
297
                # XXX: Dont we have filter_unversioned to do this more
268
298
                # cheaply?
269
 
                tree.find_ids_across_trees(specific_files,
270
 
                                           [self.basis_tree, self.work_tree])
 
299
                self.specific_file_ids = tree.find_ids_across_trees(
 
300
                    specific_files, [self.basis_tree, self.work_tree])
271
301
 
272
302
            # Setup the progress bar. As the number of files that need to be
273
303
            # committed in unknown, progress is reported as stages.
290
320
            self._gather_parents()
291
321
            if len(self.parents) > 1 and self.specific_files:
292
322
                raise errors.CannotCommitSelectedFileMerge(self.specific_files)
293
 
            
 
323
 
294
324
            # Collect the changes
295
325
            self._set_progress_stage("Collecting changes",
296
326
                    entries_title="Directory")
298
328
                self.config, timestamp, timezone, committer, revprops, rev_id)
299
329
            
300
330
            try:
 
331
                # find the location being committed to
 
332
                if self.bound_branch:
 
333
                    master_location = self.master_branch.base
 
334
                else:
 
335
                    master_location = self.branch.base
 
336
 
 
337
                # report the start of the commit
 
338
                self.reporter.started(new_revno, self.rev_id, master_location)
 
339
 
301
340
                self._update_builder_with_changes()
302
341
                self._check_pointless()
303
342
 
348
387
            self._cleanup()
349
388
        return self.rev_id
350
389
 
351
 
    def _any_real_changes(self):
352
 
        """Are there real changes between new_inventory and basis?
353
 
 
354
 
        For trees without rich roots, inv.root.revision changes every commit.
355
 
        But if that is the only change, we want to treat it as though there
356
 
        are *no* changes.
357
 
        """
358
 
        new_entries = self.builder.new_inventory.iter_entries()
359
 
        basis_entries = self.basis_inv.iter_entries()
360
 
        new_path, new_root_ie = new_entries.next()
361
 
        basis_path, basis_root_ie = basis_entries.next()
362
 
 
363
 
        # This is a copy of InventoryEntry.__eq__ only leaving out .revision
364
 
        def ie_equal_no_revision(this, other):
365
 
            return ((this.file_id == other.file_id)
366
 
                    and (this.name == other.name)
367
 
                    and (this.symlink_target == other.symlink_target)
368
 
                    and (this.text_sha1 == other.text_sha1)
369
 
                    and (this.text_size == other.text_size)
370
 
                    and (this.text_id == other.text_id)
371
 
                    and (this.parent_id == other.parent_id)
372
 
                    and (this.kind == other.kind)
373
 
                    and (this.executable == other.executable)
374
 
                    and (this.reference_revision == other.reference_revision)
375
 
                    )
376
 
        if not ie_equal_no_revision(new_root_ie, basis_root_ie):
377
 
            return True
378
 
 
379
 
        for new_ie, basis_ie in zip(new_entries, basis_entries):
380
 
            if new_ie != basis_ie:
381
 
                return True
382
 
 
383
 
        # No actual changes present
384
 
        return False
 
390
    def _select_reporter(self):
 
391
        """Select the CommitReporter to use."""
 
392
        if is_quiet():
 
393
            return NullCommitReporter()
 
394
        return ReportCommitToLog()
385
395
 
386
396
    def _check_pointless(self):
387
397
        if self.allow_pointless:
399
409
            return
400
410
        # If length == 1, then we only have the root entry. Which means
401
411
        # that there is no real difference (only the root could be different)
402
 
        if (len(self.builder.new_inventory) != 1 and self._any_real_changes()):
 
412
        if len(self.builder.new_inventory) != 1 and (self.any_entries_changed
 
413
            or self.any_entries_deleted):
403
414
            return
404
415
        raise PointlessCommit()
405
416
 
636
647
        # recorded in their previous state. For more details, see
637
648
        # https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
638
649
        if specific_files:
639
 
            for path, new_ie in self.basis_inv.iter_entries():
640
 
                if new_ie.file_id in self.builder.new_inventory:
 
650
            for path, old_ie in self.basis_inv.iter_entries():
 
651
                if old_ie.file_id in self.builder.new_inventory:
 
652
                    # already added - skip.
641
653
                    continue
642
654
                if is_inside_any(specific_files, path):
 
655
                    # was inside the selected path, if not present it has been
 
656
                    # deleted so skip.
643
657
                    continue
644
 
                ie = new_ie.copy()
645
 
                ie.revision = None
646
 
                self.builder.record_entry_contents(ie, self.parent_invs, path,
647
 
                                                   self.basis_tree)
 
658
                if old_ie.kind == 'directory':
 
659
                    self._next_progress_entry()
 
660
                # not in final inv yet, was not in the selected files, so is an
 
661
                # entry to be preserved unaltered.
 
662
                ie = old_ie.copy()
 
663
                # Note: specific file commits after a merge are currently
 
664
                # prohibited. This test is for sanity/safety in case it's
 
665
                # required after that changes.
 
666
                if len(self.parents) > 1:
 
667
                    ie.revision = None
 
668
                delta, version_recorded = self.builder.record_entry_contents(
 
669
                    ie, self.parent_invs, path, self.basis_tree, None)
 
670
                if version_recorded:
 
671
                    self.any_entries_changed = True
648
672
 
649
 
        # Report what was deleted. We could skip this when no deletes are
650
 
        # detected to gain a performance win, but it arguably serves as a
651
 
        # 'safety check' by informing the user whenever anything disappears.
652
 
        for path, ie in self.basis_inv.iter_entries():
653
 
            if ie.file_id not in self.builder.new_inventory:
654
 
                self.reporter.deleted(path)
 
673
        # note that deletes have occurred
 
674
        if set(self.basis_inv._byid.keys()) - set(self.builder.new_inventory._byid.keys()):
 
675
            self.any_entries_deleted = True
 
676
        # Report what was deleted.
 
677
        if self.any_entries_deleted and self.reporter.is_verbose():
 
678
            for path, ie in self.basis_inv.iter_entries():
 
679
                if ie.file_id not in self.builder.new_inventory:
 
680
                    self.reporter.deleted(path)
655
681
 
656
682
    def _populate_from_inventory(self, specific_files):
657
683
        """Populate the CommitBuilder by walking the working tree inventory."""
660
686
            for unknown in self.work_tree.unknowns():
661
687
                raise StrictCommitFailed()
662
688
               
 
689
        report_changes = self.reporter.is_verbose()
663
690
        deleted_ids = []
664
691
        deleted_paths = set()
665
692
        work_inv = self.work_tree.inventory
666
693
        assert work_inv.root is not None
667
 
        entries = work_inv.iter_entries()
 
694
        # XXX: Note that entries may have the wrong kind.
 
695
        entries = work_inv.iter_entries_by_dir(
 
696
            specific_file_ids=self.specific_file_ids, yield_parents=True)
668
697
        if not self.builder.record_root_entry:
669
698
            entries.next()
670
699
        for path, existing_ie in entries:
674
703
            kind = existing_ie.kind
675
704
            if kind == 'directory':
676
705
                self._next_progress_entry()
677
 
 
678
706
            # Skip files that have been deleted from the working tree.
679
707
            # The deleted files/directories are also recorded so they
680
708
            # can be explicitly unversioned later. Note that when a
682
710
            # deleted files matching that filter.
683
711
            if is_inside_any(deleted_paths, path):
684
712
                continue
 
713
            content_summary = self.work_tree.path_content_summary(path)
685
714
            if not specific_files or is_inside_any(specific_files, path):
686
 
                if not self.work_tree.has_filename(path):
 
715
                if content_summary[0] == 'missing':
687
716
                    deleted_paths.add(path)
688
717
                    self.reporter.missing(path)
689
718
                    deleted_ids.append(file_id)
690
719
                    continue
691
 
            try:
692
 
                kind = self.work_tree.kind(file_id)
693
 
                # TODO: specific_files filtering before nested tree processing
694
 
                if kind == 'tree-reference' and self.recursive == 'down':
695
 
                    self._commit_nested_tree(file_id, path)
696
 
            except errors.NoSuchFile:
697
 
                pass
 
720
            # TODO: have the builder do the nested commit just-in-time IF and
 
721
            # only if needed.
 
722
            if content_summary[0] == 'tree-reference':
 
723
                # enforce repository nested tree policy.
 
724
                if (not self.work_tree.supports_tree_reference() or
 
725
                    # repository does not support it either.
 
726
                    not self.branch.repository._format.supports_tree_reference):
 
727
                    content_summary = ('directory',) + content_summary[1:]
 
728
            kind = content_summary[0]
 
729
            # TODO: specific_files filtering before nested tree processing
 
730
            if kind == 'tree-reference':
 
731
                if self.recursive == 'down':
 
732
                    nested_revision_id = self._commit_nested_tree(
 
733
                        file_id, path)
 
734
                    content_summary = content_summary[:3] + (
 
735
                        nested_revision_id,)
 
736
                else:
 
737
                    content_summary = content_summary[:3] + (
 
738
                        self.work_tree.get_reference_revision(file_id),)
698
739
 
699
740
            # Record an entry for this item
700
741
            # Note: I don't particularly want to have the existing_ie
701
742
            # parameter but the test suite currently (28-Jun-07) breaks
702
743
            # without it thanks to a unicode normalisation issue. :-(
703
 
            definitely_changed = kind != existing_ie.kind 
 
744
            definitely_changed = kind != existing_ie.kind
704
745
            self._record_entry(path, file_id, specific_files, kind, name,
705
 
                parent_id, definitely_changed, existing_ie)
 
746
                parent_id, definitely_changed, existing_ie, report_changes,
 
747
                content_summary)
706
748
 
707
749
        # Unversion IDs that were found to be deleted
708
750
        self.work_tree.unversion(deleted_ids)
721
763
            sub_tree.branch.repository = \
722
764
                self.work_tree.branch.repository
723
765
        try:
724
 
            sub_tree.commit(message=None, revprops=self.revprops,
 
766
            return sub_tree.commit(message=None, revprops=self.revprops,
725
767
                recursive=self.recursive,
726
768
                message_callback=self.message_callback,
727
769
                timestamp=self.timestamp, timezone=self.timezone,
730
772
                strict=self.strict, verbose=self.verbose,
731
773
                local=self.local, reporter=self.reporter)
732
774
        except errors.PointlessCommit:
733
 
            pass
 
775
            return self.work_tree.get_reference_revision(file_id)
734
776
 
735
777
    def _record_entry(self, path, file_id, specific_files, kind, name,
736
 
                      parent_id, definitely_changed, existing_ie=None):
 
778
        parent_id, definitely_changed, existing_ie, report_changes,
 
779
        content_summary):
737
780
        "Record the new inventory entry for a path if any."
738
781
        # mutter('check %s {%s}', path, file_id)
739
 
        if (not specific_files or 
740
 
            is_inside_or_parent_of_any(specific_files, path)):
741
 
                # mutter('%s selected for commit', path)
742
 
                if definitely_changed or existing_ie is None:
743
 
                    ie = inventory.make_entry(kind, name, parent_id, file_id)
744
 
                else:
745
 
                    ie = existing_ie.copy()
746
 
                    ie.revision = None
 
782
        # mutter('%s selected for commit', path)
 
783
        if definitely_changed or existing_ie is None:
 
784
            ie = inventory.make_entry(kind, name, parent_id, file_id)
747
785
        else:
748
 
            # mutter('%s not selected for commit', path)
749
 
            if self.basis_inv.has_id(file_id):
750
 
                ie = self.basis_inv[file_id].copy()
751
 
            else:
752
 
                # this entry is new and not being committed
753
 
                ie = None
754
 
        if ie is not None:
755
 
            self.builder.record_entry_contents(ie, self.parent_invs, 
756
 
                path, self.work_tree)
 
786
            ie = existing_ie.copy()
 
787
            ie.revision = None
 
788
        delta, version_recorded = self.builder.record_entry_contents(ie,
 
789
            self.parent_invs, path, self.work_tree, content_summary)
 
790
        if version_recorded:
 
791
            self.any_entries_changed = True
 
792
        if report_changes:
757
793
            self._report_change(ie, path)
758
794
        return ie
759
795