~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: John Arbash Meinel
  • Author(s): Mark Hammond
  • Date: 2008-09-09 17:02:21 UTC
  • mto: This revision was merged to the branch mainline in revision 3697.
  • Revision ID: john@arbash-meinel.com-20080909170221-svim3jw2mrz0amp3
An updated transparent icon for bzr.

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
 
17
17
 
18
18
# The newly committed revision is going to have a shape corresponding
19
 
# to that of the working inventory.  Files that are not in the
 
19
# to that of the working tree.  Files that are not in the
20
20
# working tree and that were in the predecessor are reported as
21
21
# removed --- this can include files that were either removed from the
22
22
# inventory or deleted in the working tree.  If they were only
25
25
# We then consider the remaining entries, which will be in the new
26
26
# version.  Directory entries are simply copied across.  File entries
27
27
# must be checked to see if a new version of the file should be
28
 
# recorded.  For each parent revision inventory, we check to see what
 
28
# recorded.  For each parent revision tree, we check to see what
29
29
# version of the file was present.  If the file was present in at
30
30
# least one tree, and if it was the same version in all the trees,
31
31
# then we can just refer to that version.  Otherwise, a new version
59
59
from bzrlib import (
60
60
    debug,
61
61
    errors,
62
 
    inventory,
 
62
    revision,
63
63
    tree,
64
64
    )
65
65
from bzrlib.branch import Branch
71
71
from bzrlib.osutils import (kind_marker, isdir,isfile, is_inside_any,
72
72
                            is_inside_or_parent_of_any,
73
73
                            minimum_path_selection,
74
 
                            quotefn, sha_file, split_lines)
 
74
                            quotefn, sha_file, split_lines,
 
75
                            splitpath,
 
76
                            )
75
77
from bzrlib.testament import Testament
76
78
from bzrlib.trace import mutter, note, warning, is_quiet
77
79
from bzrlib.xml5 import serializer_v5
78
 
from bzrlib.inventory import Inventory, InventoryEntry
 
80
from bzrlib.inventory import InventoryEntry, make_entry
79
81
from bzrlib import symbol_versioning
80
82
from bzrlib.symbol_versioning import (deprecated_passed,
81
83
        deprecated_function,
89
91
    """I report on progress of a commit."""
90
92
 
91
93
    def started(self, revno, revid, location=None):
 
94
        if location is None:
 
95
            symbol_versioning.warn("As of bzr 1.0 you must pass a location "
 
96
                                   "to started.", DeprecationWarning,
 
97
                                   stacklevel=2)
92
98
        pass
93
99
 
94
100
    def snapshot_change(self, change, path):
131
137
 
132
138
    def started(self, revno, rev_id, location=None):
133
139
        if location is not None:
134
 
            location = ' to "' + unescape_for_display(location, 'utf-8') + '"'
 
140
            location = ' to: ' + unescape_for_display(location, 'utf-8')
135
141
        else:
 
142
            # When started was added, location was only made optional by
 
143
            # accident.  Matt Nordhoff 20071129
 
144
            symbol_versioning.warn("As of bzr 1.0 you must pass a location "
 
145
                                   "to started.", DeprecationWarning,
 
146
                                   stacklevel=2)
136
147
            location = ''
137
 
        self._note('Committing revision %d%s.', revno, location)
 
148
        self._note('Committing%s', location)
138
149
 
139
150
    def completed(self, revno, rev_id):
140
151
        self._note('Committed revision %d.', revno)
193
204
               reporter=None,
194
205
               config=None,
195
206
               message_callback=None,
196
 
               recursive='down'):
 
207
               recursive='down',
 
208
               exclude=None):
197
209
        """Commit working copy as a new revision.
198
210
 
199
211
        :param message: the commit message (it or message_callback is required)
221
233
        :param verbose: if True and the reporter is not None, report everything
222
234
        :param recursive: If set to 'down', commit in any subtrees that have
223
235
            pending changes of any sort during this commit.
 
236
        :param exclude: None or a list of relative paths to exclude from the
 
237
            commit. Pending changes to excluded files will be ignored by the
 
238
            commit. 
224
239
        """
225
240
        mutter('preparing to commit')
226
241
 
244
259
        self.bound_branch = None
245
260
        self.any_entries_changed = False
246
261
        self.any_entries_deleted = False
 
262
        if exclude is not None:
 
263
            self.exclude = sorted(
 
264
                minimum_path_selection(exclude))
 
265
        else:
 
266
            self.exclude = []
247
267
        self.local = local
248
268
        self.master_branch = None
249
269
        self.master_locked = False
269
289
 
270
290
        self.work_tree.lock_write()
271
291
        self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
292
        self.basis_revid = self.work_tree.last_revision()
272
293
        self.basis_tree = self.work_tree.basis_tree()
273
294
        self.basis_tree.lock_read()
274
295
        try:
317
338
            self.pb.show_count = True
318
339
            self.pb.show_bar = True
319
340
 
 
341
            self.basis_inv = self.basis_tree.inventory
 
342
            self._gather_parents()
320
343
            # After a merge, a selected file commit is not supported.
321
344
            # See 'bzr help merge' for an explanation as to why.
322
 
            self.basis_inv = self.basis_tree.inventory
323
 
            self._gather_parents()
324
345
            if len(self.parents) > 1 and self.specific_files:
325
346
                raise errors.CannotCommitSelectedFileMerge(self.specific_files)
 
347
            # Excludes are a form of selected file commit.
 
348
            if len(self.parents) > 1 and self.exclude:
 
349
                raise errors.CannotCommitSelectedFileMerge(self.exclude)
326
350
 
327
351
            # Collect the changes
328
352
            self._set_progress_stage("Collecting changes",
353
377
 
354
378
                # Prompt the user for a commit message if none provided
355
379
                message = message_callback(self)
356
 
                assert isinstance(message, unicode), type(message)
357
380
                self.message = message
358
381
                self._escape_commit_message()
359
382
 
369
392
            # Upload revision data to the master.
370
393
            # this will propagate merged revisions too if needed.
371
394
            if self.bound_branch:
372
 
                self._set_progress_stage("Uploading data to master branch")
373
 
                self.master_branch.repository.fetch(self.branch.repository,
374
 
                                                    revision_id=self.rev_id)
 
395
                if not self.master_branch.repository.has_same_location(
 
396
                        self.branch.repository):
 
397
                    self._set_progress_stage("Uploading data to master branch")
 
398
                    self.master_branch.repository.fetch(self.branch.repository,
 
399
                        revision_id=self.rev_id)
375
400
                # now the master has the revision data
376
401
                # 'commit' to the master first so a timeout here causes the
377
402
                # local branch to be out of date
383
408
 
384
409
            # Make the working tree up to date with the branch
385
410
            self._set_progress_stage("Updating the working tree")
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)
396
 
            self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
 
411
            self.work_tree.update_basis_by_delta(self.rev_id,
 
412
                 self._basis_delta)
397
413
            self.reporter.completed(new_revno, self.rev_id)
398
414
            self._process_post_hooks(old_revno, new_revno)
399
415
        finally:
414
430
            return
415
431
        # TODO: we could simplify this by using self._basis_delta.
416
432
 
417
 
        # The inital commit adds a root directory, but this in itself is not
418
 
        # a worthwhile commit.  
419
 
        if len(self.basis_inv) == 0 and len(self.builder.new_inventory) == 1:
 
433
        # The initial commit adds a root directory, but this in itself is not
 
434
        # a worthwhile commit.
 
435
        if (self.basis_revid == revision.NULL_REVISION and
 
436
            len(self.builder.new_inventory) == 1):
420
437
            raise PointlessCommit()
421
 
        # Shortcut, if the number of entries changes, then we obviously have
422
 
        # a change
423
 
        if len(self.builder.new_inventory) != len(self.basis_inv):
424
 
            return
425
438
        # If length == 1, then we only have the root entry. Which means
426
439
        # that there is no real difference (only the root could be different)
427
 
        if len(self.builder.new_inventory) != 1 and (self.any_entries_changed
428
 
            or self.any_entries_deleted):
 
440
        # unless deletes occured, in which case the length is irrelevant.
 
441
        if (self.any_entries_deleted or 
 
442
            (len(self.builder.new_inventory) != 1 and
 
443
             self.any_entries_changed)):
429
444
            return
430
445
        raise PointlessCommit()
431
446
 
645
660
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
646
661
        # ADHB 11-07-2006
647
662
 
648
 
        specific_files = self.specific_files
 
663
        exclude = self.exclude
 
664
        specific_files = self.specific_files or []
649
665
        mutter("Selecting files for commit with filter %s", specific_files)
650
666
 
651
667
        # Build the new inventory
652
 
        self._populate_from_inventory(specific_files)
 
668
        self._populate_from_inventory()
653
669
 
654
670
        # If specific files are selected, then all un-selected files must be
655
671
        # recorded in their previous state. For more details, see
656
672
        # https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
657
 
        if specific_files:
 
673
        if specific_files or exclude:
658
674
            for path, old_ie in self.basis_inv.iter_entries():
659
675
                if old_ie.file_id in self.builder.new_inventory:
660
676
                    # already added - skip.
661
677
                    continue
662
 
                if is_inside_any(specific_files, path):
663
 
                    # was inside the selected path, if not present it has been
664
 
                    # deleted so skip.
 
678
                if (is_inside_any(specific_files, path)
 
679
                    and not is_inside_any(exclude, path)):
 
680
                    # was inside the selected path, and not excluded - if not
 
681
                    # present it has been deleted so skip.
665
682
                    continue
 
683
                # From here down it was either not selected, or was excluded:
666
684
                if old_ie.kind == 'directory':
667
685
                    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.
 
686
                # We preserve the entry unaltered.
670
687
                ie = old_ie.copy()
671
688
                # Note: specific file commits after a merge are currently
672
689
                # prohibited. This test is for sanity/safety in case it's
686
703
            set(self.builder.new_inventory._byid.keys())
687
704
        if deleted_ids:
688
705
            self.any_entries_deleted = True
689
 
            deleted = [(self.basis_inv.id2path(file_id), file_id)
 
706
            deleted = [(self.basis_tree.id2path(file_id), file_id)
690
707
                for file_id in deleted_ids]
691
708
            deleted.sort()
692
709
            # XXX: this is not quite directory-order sorting
694
711
                self._basis_delta.append((path, None, file_id, None))
695
712
                self.reporter.deleted(path)
696
713
 
697
 
    def _populate_from_inventory(self, specific_files):
 
714
    def _populate_from_inventory(self):
698
715
        """Populate the CommitBuilder by walking the working tree inventory."""
699
716
        if self.strict:
700
717
            # raise an exception as soon as we find a single unknown.
701
718
            for unknown in self.work_tree.unknowns():
702
719
                raise StrictCommitFailed()
703
 
               
 
720
        
 
721
        specific_files = self.specific_files
 
722
        exclude = self.exclude
704
723
        report_changes = self.reporter.is_verbose()
705
724
        deleted_ids = []
706
 
        deleted_paths = set()
 
725
        # A tree of paths that have been deleted. E.g. if foo/bar has been
 
726
        # deleted, then we have {'foo':{'bar':{}}}
 
727
        deleted_paths = {}
 
728
        # XXX: Note that entries may have the wrong kind because the entry does
 
729
        # not reflect the status on disk.
707
730
        work_inv = self.work_tree.inventory
708
 
        assert work_inv.root is not None
709
 
        # XXX: Note that entries may have the wrong kind.
 
731
        # NB: entries will include entries within the excluded ids/paths
 
732
        # because iter_entries_by_dir has no 'exclude' facility today.
710
733
        entries = work_inv.iter_entries_by_dir(
711
734
            specific_file_ids=self.specific_file_ids, yield_parents=True)
712
735
        for path, existing_ie in entries:
717
740
            if kind == 'directory':
718
741
                self._next_progress_entry()
719
742
            # Skip files that have been deleted from the working tree.
720
 
            # The deleted files/directories are also recorded so they
721
 
            # can be explicitly unversioned later. Note that when a
722
 
            # filter of specific files is given, we must only skip/record
723
 
            # deleted files matching that filter.
724
 
            if is_inside_any(deleted_paths, path):
 
743
            # The deleted path ids are also recorded so they can be explicitly
 
744
            # unversioned later.
 
745
            if deleted_paths:
 
746
                path_segments = splitpath(path)
 
747
                deleted_dict = deleted_paths
 
748
                for segment in path_segments:
 
749
                    deleted_dict = deleted_dict.get(segment, None)
 
750
                    if not deleted_dict:
 
751
                        # We either took a path not present in the dict
 
752
                        # (deleted_dict was None), or we've reached an empty
 
753
                        # child dir in the dict, so are now a sub-path.
 
754
                        break
 
755
                else:
 
756
                    deleted_dict = None
 
757
                if deleted_dict is not None:
 
758
                    # the path has a deleted parent, do not add it.
 
759
                    continue
 
760
            if exclude and is_inside_any(exclude, path):
 
761
                # Skip excluded paths. Excluded paths are processed by
 
762
                # _update_builder_with_changes.
725
763
                continue
726
764
            content_summary = self.work_tree.path_content_summary(path)
 
765
            # Note that when a filter of specific files is given, we must only
 
766
            # skip/record deleted files matching that filter.
727
767
            if not specific_files or is_inside_any(specific_files, path):
728
768
                if content_summary[0] == 'missing':
729
 
                    deleted_paths.add(path)
 
769
                    if not deleted_paths:
 
770
                        # path won't have been split yet.
 
771
                        path_segments = splitpath(path)
 
772
                    deleted_dict = deleted_paths
 
773
                    for segment in path_segments:
 
774
                        deleted_dict = deleted_dict.setdefault(segment, {})
730
775
                    self.reporter.missing(path)
731
776
                    deleted_ids.append(file_id)
732
777
                    continue
794
839
        # mutter('check %s {%s}', path, file_id)
795
840
        # mutter('%s selected for commit', path)
796
841
        if definitely_changed or existing_ie is None:
797
 
            ie = inventory.make_entry(kind, name, parent_id, file_id)
 
842
            ie = make_entry(kind, name, parent_id, file_id)
798
843
        else:
799
844
            ie = existing_ie.copy()
800
845
            ie.revision = None