/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

Merge 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
235
263
        self.committer = committer
236
264
        self.strict = strict
237
265
        self.verbose = verbose
238
 
 
239
 
        if reporter is None and self.reporter is None:
240
 
            self.reporter = NullCommitReporter()
241
 
        elif reporter is not None:
242
 
            self.reporter = reporter
 
266
        # accumulates an inventory delta to the basis entry, so we can make
 
267
        # just the necessary updates to the workingtree's cached basis.
 
268
        self._basis_delta = []
243
269
 
244
270
        self.work_tree.lock_write()
245
271
        self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
256
282
            # Check that the working tree is up to date
257
283
            old_revno, new_revno = self._check_out_of_date_tree()
258
284
 
 
285
            # Complete configuration setup
 
286
            if reporter is not None:
 
287
                self.reporter = reporter
 
288
            elif self.reporter is None:
 
289
                self.reporter = self._select_reporter()
259
290
            if self.config is None:
260
291
                self.config = self.branch.get_config()
261
292
 
262
293
            # 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.
 
294
            if self.specific_files is not None:
 
295
                # Note: This routine is being called because it raises
 
296
                # PathNotVersionedError as a side effect of finding the IDs. We
 
297
                # later use the ids we found as input to the working tree
 
298
                # inventory iterator, so we only consider those ids rather than
 
299
                # examining the whole tree again.
267
300
                # XXX: Dont we have filter_unversioned to do this more
268
301
                # cheaply?
269
 
                tree.find_ids_across_trees(specific_files,
270
 
                                           [self.basis_tree, self.work_tree])
 
302
                self.specific_file_ids = tree.find_ids_across_trees(
 
303
                    specific_files, [self.basis_tree, self.work_tree])
271
304
 
272
305
            # Setup the progress bar. As the number of files that need to be
273
306
            # committed in unknown, progress is reported as stages.
290
323
            self._gather_parents()
291
324
            if len(self.parents) > 1 and self.specific_files:
292
325
                raise errors.CannotCommitSelectedFileMerge(self.specific_files)
293
 
            
 
326
 
294
327
            # Collect the changes
295
328
            self._set_progress_stage("Collecting changes",
296
329
                    entries_title="Directory")
298
331
                self.config, timestamp, timezone, committer, revprops, rev_id)
299
332
            
300
333
            try:
 
334
                # find the location being committed to
 
335
                if self.bound_branch:
 
336
                    master_location = self.master_branch.base
 
337
                else:
 
338
                    master_location = self.branch.base
 
339
 
 
340
                # report the start of the commit
 
341
                self.reporter.started(new_revno, self.rev_id, master_location)
 
342
 
301
343
                self._update_builder_with_changes()
 
344
                self._report_and_accumulate_deletes()
302
345
                self._check_pointless()
303
346
 
304
347
                # TODO: Now the new inventory is known, check for conflicts.
341
384
            # Make the working tree up to date with the branch
342
385
            self._set_progress_stage("Updating the working tree")
343
386
            rev_tree = self.builder.revision_tree()
 
387
            # XXX: This will need to be changed if we support doing a
 
388
            # selective commit while a merge is still pending - then we'd
 
389
            # still have multiple parents after the commit.
 
390
            #
 
391
            # XXX: update_basis_by_delta is slower at present because it works
 
392
            # on inventories, so this is not active until there's a native
 
393
            # dirstate implementation.
 
394
            ## self.work_tree.update_basis_by_delta(self.rev_id,
 
395
            ##      self._basis_delta)
344
396
            self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
345
397
            self.reporter.completed(new_revno, self.rev_id)
346
398
            self._process_post_hooks(old_revno, new_revno)
348
400
            self._cleanup()
349
401
        return self.rev_id
350
402
 
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
 
403
    def _select_reporter(self):
 
404
        """Select the CommitReporter to use."""
 
405
        if is_quiet():
 
406
            return NullCommitReporter()
 
407
        return ReportCommitToLog()
385
408
 
386
409
    def _check_pointless(self):
387
410
        if self.allow_pointless:
389
412
        # A merge with no effect on files
390
413
        if len(self.parents) > 1:
391
414
            return
392
 
        # work around the fact that a newly-initted tree does differ from its
393
 
        # basis
 
415
        # TODO: we could simplify this by using self._basis_delta.
 
416
 
 
417
        # The inital commit adds a root directory, but this in itself is not
 
418
        # a worthwhile commit.  
394
419
        if len(self.basis_inv) == 0 and len(self.builder.new_inventory) == 1:
395
420
            raise PointlessCommit()
396
421
        # Shortcut, if the number of entries changes, then we obviously have
399
424
            return
400
425
        # If length == 1, then we only have the root entry. Which means
401
426
        # 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()):
 
427
        if len(self.builder.new_inventory) != 1 and (self.any_entries_changed
 
428
            or self.any_entries_deleted):
403
429
            return
404
430
        raise PointlessCommit()
405
431
 
622
648
        specific_files = self.specific_files
623
649
        mutter("Selecting files for commit with filter %s", specific_files)
624
650
 
625
 
        # Check and warn about old CommitBuilders
626
 
        if not self.builder.record_root_entry:
627
 
            symbol_versioning.warn('CommitBuilders should support recording'
628
 
                ' the root entry as of bzr 0.10.', DeprecationWarning, 
629
 
                stacklevel=1)
630
 
            self.builder.new_inventory.add(self.basis_inv.root.copy())
631
 
 
632
651
        # Build the new inventory
633
652
        self._populate_from_inventory(specific_files)
634
653
 
636
655
        # recorded in their previous state. For more details, see
637
656
        # https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
638
657
        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:
 
658
            for path, old_ie in self.basis_inv.iter_entries():
 
659
                if old_ie.file_id in self.builder.new_inventory:
 
660
                    # already added - skip.
641
661
                    continue
642
662
                if is_inside_any(specific_files, path):
 
663
                    # was inside the selected path, if not present it has been
 
664
                    # deleted so skip.
643
665
                    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)
 
666
                if old_ie.kind == 'directory':
 
667
                    self._next_progress_entry()
 
668
                # not in final inv yet, was not in the selected files, so is an
 
669
                # entry to be preserved unaltered.
 
670
                ie = old_ie.copy()
 
671
                # Note: specific file commits after a merge are currently
 
672
                # prohibited. This test is for sanity/safety in case it's
 
673
                # required after that changes.
 
674
                if len(self.parents) > 1:
 
675
                    ie.revision = None
 
676
                delta, version_recorded = self.builder.record_entry_contents(
 
677
                    ie, self.parent_invs, path, self.basis_tree, None)
 
678
                if version_recorded:
 
679
                    self.any_entries_changed = True
 
680
                if delta: self._basis_delta.append(delta)
648
681
 
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:
 
682
    def _report_and_accumulate_deletes(self):
 
683
        # XXX: Could the list of deleted paths and ids be instead taken from
 
684
        # _populate_from_inventory?
 
685
        deleted_ids = set(self.basis_inv._byid.keys()) - \
 
686
            set(self.builder.new_inventory._byid.keys())
 
687
        if deleted_ids:
 
688
            self.any_entries_deleted = True
 
689
            deleted = [(self.basis_inv.id2path(file_id), file_id)
 
690
                for file_id in deleted_ids]
 
691
            deleted.sort()
 
692
            # XXX: this is not quite directory-order sorting
 
693
            for path, file_id in deleted:
 
694
                self._basis_delta.append((path, None, file_id, None))
654
695
                self.reporter.deleted(path)
655
696
 
656
697
    def _populate_from_inventory(self, specific_files):
660
701
            for unknown in self.work_tree.unknowns():
661
702
                raise StrictCommitFailed()
662
703
               
 
704
        report_changes = self.reporter.is_verbose()
663
705
        deleted_ids = []
664
706
        deleted_paths = set()
665
707
        work_inv = self.work_tree.inventory
666
708
        assert work_inv.root is not None
667
 
        entries = work_inv.iter_entries()
668
 
        if not self.builder.record_root_entry:
669
 
            entries.next()
 
709
        # XXX: Note that entries may have the wrong kind.
 
710
        entries = work_inv.iter_entries_by_dir(
 
711
            specific_file_ids=self.specific_file_ids, yield_parents=True)
670
712
        for path, existing_ie in entries:
671
713
            file_id = existing_ie.file_id
672
714
            name = existing_ie.name
674
716
            kind = existing_ie.kind
675
717
            if kind == 'directory':
676
718
                self._next_progress_entry()
677
 
 
678
719
            # Skip files that have been deleted from the working tree.
679
720
            # The deleted files/directories are also recorded so they
680
721
            # can be explicitly unversioned later. Note that when a
682
723
            # deleted files matching that filter.
683
724
            if is_inside_any(deleted_paths, path):
684
725
                continue
 
726
            content_summary = self.work_tree.path_content_summary(path)
685
727
            if not specific_files or is_inside_any(specific_files, path):
686
 
                if not self.work_tree.has_filename(path):
 
728
                if content_summary[0] == 'missing':
687
729
                    deleted_paths.add(path)
688
730
                    self.reporter.missing(path)
689
731
                    deleted_ids.append(file_id)
690
732
                    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
 
733
            # TODO: have the builder do the nested commit just-in-time IF and
 
734
            # only if needed.
 
735
            if content_summary[0] == 'tree-reference':
 
736
                # enforce repository nested tree policy.
 
737
                if (not self.work_tree.supports_tree_reference() or
 
738
                    # repository does not support it either.
 
739
                    not self.branch.repository._format.supports_tree_reference):
 
740
                    content_summary = ('directory',) + content_summary[1:]
 
741
            kind = content_summary[0]
 
742
            # TODO: specific_files filtering before nested tree processing
 
743
            if kind == 'tree-reference':
 
744
                if self.recursive == 'down':
 
745
                    nested_revision_id = self._commit_nested_tree(
 
746
                        file_id, path)
 
747
                    content_summary = content_summary[:3] + (
 
748
                        nested_revision_id,)
 
749
                else:
 
750
                    content_summary = content_summary[:3] + (
 
751
                        self.work_tree.get_reference_revision(file_id),)
698
752
 
699
753
            # Record an entry for this item
700
754
            # Note: I don't particularly want to have the existing_ie
701
755
            # parameter but the test suite currently (28-Jun-07) breaks
702
756
            # without it thanks to a unicode normalisation issue. :-(
703
 
            definitely_changed = kind != existing_ie.kind 
 
757
            definitely_changed = kind != existing_ie.kind
704
758
            self._record_entry(path, file_id, specific_files, kind, name,
705
 
                parent_id, definitely_changed, existing_ie)
 
759
                parent_id, definitely_changed, existing_ie, report_changes,
 
760
                content_summary)
706
761
 
707
762
        # Unversion IDs that were found to be deleted
708
763
        self.work_tree.unversion(deleted_ids)
721
776
            sub_tree.branch.repository = \
722
777
                self.work_tree.branch.repository
723
778
        try:
724
 
            sub_tree.commit(message=None, revprops=self.revprops,
 
779
            return sub_tree.commit(message=None, revprops=self.revprops,
725
780
                recursive=self.recursive,
726
781
                message_callback=self.message_callback,
727
782
                timestamp=self.timestamp, timezone=self.timezone,
730
785
                strict=self.strict, verbose=self.verbose,
731
786
                local=self.local, reporter=self.reporter)
732
787
        except errors.PointlessCommit:
733
 
            pass
 
788
            return self.work_tree.get_reference_revision(file_id)
734
789
 
735
790
    def _record_entry(self, path, file_id, specific_files, kind, name,
736
 
                      parent_id, definitely_changed, existing_ie=None):
 
791
        parent_id, definitely_changed, existing_ie, report_changes,
 
792
        content_summary):
737
793
        "Record the new inventory entry for a path if any."
738
794
        # 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
 
795
        # mutter('%s selected for commit', path)
 
796
        if definitely_changed or existing_ie is None:
 
797
            ie = inventory.make_entry(kind, name, parent_id, file_id)
747
798
        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)
 
799
            ie = existing_ie.copy()
 
800
            ie.revision = None
 
801
        delta, version_recorded = self.builder.record_entry_contents(ie,
 
802
            self.parent_invs, path, self.work_tree, content_summary)
 
803
        if delta:
 
804
            self._basis_delta.append(delta)
 
805
        if version_recorded:
 
806
            self.any_entries_changed = True
 
807
        if report_changes:
757
808
            self._report_change(ie, path)
758
809
        return ie
759
810