~jelmer/ubuntu/maverick/bzr/2.2.5

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

ImportĀ upstreamĀ versionĀ 1.13~rc1

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007, 2008 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
60
60
    debug,
61
61
    errors,
62
62
    revision,
 
63
    trace,
63
64
    tree,
64
65
    )
65
66
from bzrlib.branch import Branch
68
69
                           ConflictsInTree,
69
70
                           StrictCommitFailed
70
71
                           )
71
 
from bzrlib.osutils import (kind_marker, isdir,isfile, is_inside_any,
 
72
from bzrlib.osutils import (get_user_encoding,
 
73
                            kind_marker, isdir,isfile, is_inside_any,
72
74
                            is_inside_or_parent_of_any,
73
75
                            minimum_path_selection,
74
76
                            quotefn, sha_file, split_lines,
76
78
                            )
77
79
from bzrlib.testament import Testament
78
80
from bzrlib.trace import mutter, note, warning, is_quiet
79
 
from bzrlib.xml5 import serializer_v5
80
 
from bzrlib.inventory import InventoryEntry, make_entry
 
81
from bzrlib.inventory import Inventory, InventoryEntry, make_entry
81
82
from bzrlib import symbol_versioning
82
83
from bzrlib.symbol_versioning import (deprecated_passed,
83
84
        deprecated_function,
204
205
               reporter=None,
205
206
               config=None,
206
207
               message_callback=None,
207
 
               recursive='down'):
 
208
               recursive='down',
 
209
               exclude=None,
 
210
               possible_master_transports=None):
208
211
        """Commit working copy as a new revision.
209
212
 
210
213
        :param message: the commit message (it or message_callback is required)
232
235
        :param verbose: if True and the reporter is not None, report everything
233
236
        :param recursive: If set to 'down', commit in any subtrees that have
234
237
            pending changes of any sort during this commit.
 
238
        :param exclude: None or a list of relative paths to exclude from the
 
239
            commit. Pending changes to excluded files will be ignored by the
 
240
            commit.
235
241
        """
236
242
        mutter('preparing to commit')
237
243
 
246
252
        if message_callback is None:
247
253
            if message is not None:
248
254
                if isinstance(message, str):
249
 
                    message = message.decode(bzrlib.user_encoding)
 
255
                    message = message.decode(get_user_encoding())
250
256
                message_callback = lambda x: message
251
257
            else:
252
258
                raise BzrError("The message or message_callback keyword"
255
261
        self.bound_branch = None
256
262
        self.any_entries_changed = False
257
263
        self.any_entries_deleted = False
 
264
        if exclude is not None:
 
265
            self.exclude = sorted(
 
266
                minimum_path_selection(exclude))
 
267
        else:
 
268
            self.exclude = []
258
269
        self.local = local
259
270
        self.master_branch = None
260
271
        self.master_locked = False
274
285
        self.committer = committer
275
286
        self.strict = strict
276
287
        self.verbose = verbose
277
 
        # accumulates an inventory delta to the basis entry, so we can make
278
 
        # just the necessary updates to the workingtree's cached basis.
279
 
        self._basis_delta = []
280
288
 
281
289
        self.work_tree.lock_write()
282
290
        self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
289
297
                raise ConflictsInTree
290
298
 
291
299
            # Setup the bound branch variables as needed.
292
 
            self._check_bound_branch()
 
300
            self._check_bound_branch(possible_master_transports)
293
301
 
294
302
            # Check that the working tree is up to date
295
303
            old_revno, new_revno = self._check_out_of_date_tree()
329
337
            self.pb.show_count = True
330
338
            self.pb.show_bar = True
331
339
 
 
340
            self.basis_inv = self.basis_tree.inventory
 
341
            self._gather_parents()
332
342
            # After a merge, a selected file commit is not supported.
333
343
            # See 'bzr help merge' for an explanation as to why.
334
 
            self.basis_inv = self.basis_tree.inventory
335
 
            self._gather_parents()
336
344
            if len(self.parents) > 1 and self.specific_files:
337
345
                raise errors.CannotCommitSelectedFileMerge(self.specific_files)
 
346
            # Excludes are a form of selected file commit.
 
347
            if len(self.parents) > 1 and self.exclude:
 
348
                raise errors.CannotCommitSelectedFileMerge(self.exclude)
338
349
 
339
350
            # Collect the changes
340
351
            self._set_progress_stage("Collecting changes",
341
352
                    entries_title="Directory")
342
353
            self.builder = self.branch.get_commit_builder(self.parents,
343
354
                self.config, timestamp, timezone, committer, revprops, rev_id)
344
 
            
 
355
 
345
356
            try:
 
357
                self.builder.will_record_deletes()
346
358
                # find the location being committed to
347
359
                if self.bound_branch:
348
360
                    master_location = self.master_branch.base
371
383
                # Add revision data to the local branch
372
384
                self.rev_id = self.builder.commit(self.message)
373
385
 
374
 
            except:
 
386
            except Exception, e:
 
387
                mutter("aborting commit write group because of exception:")
 
388
                trace.log_exception_quietly()
 
389
                note("aborting commit write group: %r" % (e,))
375
390
                self.builder.abort()
376
391
                raise
377
392
 
380
395
            # Upload revision data to the master.
381
396
            # this will propagate merged revisions too if needed.
382
397
            if self.bound_branch:
383
 
                if not self.master_branch.repository.has_same_location(
384
 
                        self.branch.repository):
385
 
                    self._set_progress_stage("Uploading data to master branch")
386
 
                    self.master_branch.repository.fetch(self.branch.repository,
387
 
                        revision_id=self.rev_id)
388
 
                # now the master has the revision data
 
398
                self._set_progress_stage("Uploading data to master branch")
389
399
                # 'commit' to the master first so a timeout here causes the
390
400
                # local branch to be out of date
391
 
                self.master_branch.set_last_revision_info(new_revno,
392
 
                                                          self.rev_id)
 
401
                self.master_branch.import_last_revision_info(
 
402
                    self.branch.repository, new_revno, self.rev_id)
393
403
 
394
404
            # and now do the commit locally.
395
405
            self.branch.set_last_revision_info(new_revno, self.rev_id)
397
407
            # Make the working tree up to date with the branch
398
408
            self._set_progress_stage("Updating the working tree")
399
409
            self.work_tree.update_basis_by_delta(self.rev_id,
400
 
                 self._basis_delta)
 
410
                 self.builder.get_basis_delta())
401
411
            self.reporter.completed(new_revno, self.rev_id)
402
412
            self._process_post_hooks(old_revno, new_revno)
403
413
        finally:
416
426
        # A merge with no effect on files
417
427
        if len(self.parents) > 1:
418
428
            return
419
 
        # TODO: we could simplify this by using self._basis_delta.
 
429
        # TODO: we could simplify this by using self.builder.basis_delta.
420
430
 
421
431
        # The initial commit adds a root directory, but this in itself is not
422
432
        # a worthwhile commit.
426
436
        # If length == 1, then we only have the root entry. Which means
427
437
        # that there is no real difference (only the root could be different)
428
438
        # unless deletes occured, in which case the length is irrelevant.
429
 
        if (self.any_entries_deleted or 
 
439
        if (self.any_entries_deleted or
430
440
            (len(self.builder.new_inventory) != 1 and
431
441
             self.any_entries_changed)):
432
442
            return
433
443
        raise PointlessCommit()
434
444
 
435
 
    def _check_bound_branch(self):
 
445
    def _check_bound_branch(self, possible_master_transports=None):
436
446
        """Check to see if the local branch is bound.
437
447
 
438
448
        If it is bound, then most of the commit will actually be
443
453
            raise errors.LocalRequiresBoundBranch()
444
454
 
445
455
        if not self.local:
446
 
            self.master_branch = self.branch.get_master_branch()
 
456
            self.master_branch = self.branch.get_master_branch(
 
457
                possible_master_transports)
447
458
 
448
459
        if not self.master_branch:
449
460
            # make this branch the reference branch for out of date checks.
460
471
        #       commits to the remote branch if they would fit.
461
472
        #       But for now, just require remote to be identical
462
473
        #       to local.
463
 
        
 
474
 
464
475
        # Make sure the local branch is identical to the master
465
476
        master_info = self.master_branch.last_revision_info()
466
477
        local_info = self.branch.last_revision_info()
523
534
    def _process_hooks(self, hook_name, old_revno, new_revno):
524
535
        if not Branch.hooks[hook_name]:
525
536
            return
526
 
        
 
537
 
527
538
        # new style commit hooks:
528
539
        if not self.bound_branch:
529
540
            hook_master = self.branch
538
549
            old_revid = self.parents[0]
539
550
        else:
540
551
            old_revid = bzrlib.revision.NULL_REVISION
541
 
        
 
552
 
542
553
        if hook_name == "pre_commit":
543
554
            future_tree = self.builder.revision_tree()
544
555
            tree_delta = future_tree.changes_from(self.basis_tree,
545
556
                                             include_root=True)
546
 
        
 
557
 
547
558
        for hook in Branch.hooks[hook_name]:
548
559
            # show the running hook in the progress bar. As hooks may
549
560
            # end up doing nothing (e.g. because they are not configured by
579
590
            # typically this will be useful enough.
580
591
            except Exception, e:
581
592
                found_exception = e
582
 
        if found_exception is not None: 
 
593
        if found_exception is not None:
583
594
            # don't do a plan raise, because the last exception may have been
584
595
            # trashed, e is our sure-to-work exception even though it loses the
585
596
            # full traceback. XXX: RBC 20060421 perhaps we could check the
586
 
            # exc_info and if its the same one do a plain raise otherwise 
 
597
            # exc_info and if its the same one do a plain raise otherwise
587
598
            # 'raise e' as we do now.
588
599
            raise e
589
600
 
605
616
        # serialiser not by commit. Then we can also add an unescaper
606
617
        # in the deserializer and start roundtripping revision messages
607
618
        # precisely. See repository_implementations/test_repository.py
608
 
        
 
619
 
609
620
        # Python strings can include characters that can't be
610
621
        # represented in well-formed XML; escape characters that
611
622
        # aren't listed in the XML specification
619
630
 
620
631
    def _gather_parents(self):
621
632
        """Record the parents of a merge for merge detection."""
622
 
        # TODO: Make sure that this list doesn't contain duplicate 
 
633
        # TODO: Make sure that this list doesn't contain duplicate
623
634
        # entries and the order is preserved when doing this.
624
635
        self.parents = self.work_tree.get_parent_ids()
625
636
        self.parent_invs = [self.basis_inv]
638
649
        #
639
650
        # This starts by creating a new empty inventory. Depending on
640
651
        # which files are selected for commit, and what is present in the
641
 
        # current tree, the new inventory is populated. inventory entries 
 
652
        # current tree, the new inventory is populated. inventory entries
642
653
        # which are candidates for modification have their revision set to
643
654
        # None; inventory entries that are carried over untouched have their
644
655
        # revision set to their prior value.
648
659
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
649
660
        # ADHB 11-07-2006
650
661
 
651
 
        specific_files = self.specific_files
 
662
        exclude = self.exclude
 
663
        specific_files = self.specific_files or []
652
664
        mutter("Selecting files for commit with filter %s", specific_files)
653
665
 
654
666
        # Build the new inventory
655
 
        self._populate_from_inventory(specific_files)
 
667
        self._populate_from_inventory()
656
668
 
657
669
        # If specific files are selected, then all un-selected files must be
658
670
        # recorded in their previous state. For more details, see
659
671
        # https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
660
 
        if specific_files:
 
672
        if specific_files or exclude:
661
673
            for path, old_ie in self.basis_inv.iter_entries():
662
674
                if old_ie.file_id in self.builder.new_inventory:
663
675
                    # already added - skip.
664
676
                    continue
665
 
                if is_inside_any(specific_files, path):
666
 
                    # was inside the selected path, if not present it has been
667
 
                    # deleted so skip.
 
677
                if (is_inside_any(specific_files, path)
 
678
                    and not is_inside_any(exclude, path)):
 
679
                    # was inside the selected path, and not excluded - if not
 
680
                    # present it has been deleted so skip.
668
681
                    continue
 
682
                # From here down it was either not selected, or was excluded:
669
683
                if old_ie.kind == 'directory':
670
684
                    self._next_progress_entry()
671
 
                # not in final inv yet, was not in the selected files, so is an
672
 
                # entry to be preserved unaltered.
 
685
                # We preserve the entry unaltered.
673
686
                ie = old_ie.copy()
674
687
                # Note: specific file commits after a merge are currently
675
688
                # prohibited. This test is for sanity/safety in case it's
676
689
                # required after that changes.
677
690
                if len(self.parents) > 1:
678
691
                    ie.revision = None
679
 
                delta, version_recorded = self.builder.record_entry_contents(
 
692
                _, version_recorded, _ = self.builder.record_entry_contents(
680
693
                    ie, self.parent_invs, path, self.basis_tree, None)
681
694
                if version_recorded:
682
695
                    self.any_entries_changed = True
683
 
                if delta: self._basis_delta.append(delta)
684
696
 
685
697
    def _report_and_accumulate_deletes(self):
686
698
        # XXX: Could the list of deleted paths and ids be instead taken from
687
699
        # _populate_from_inventory?
688
 
        deleted_ids = set(self.basis_inv._byid.keys()) - \
689
 
            set(self.builder.new_inventory._byid.keys())
 
700
        if (isinstance(self.basis_inv, Inventory)
 
701
            and isinstance(self.builder.new_inventory, Inventory)):
 
702
            # the older Inventory classes provide a _byid dict, and building a
 
703
            # set from the keys of this dict is substantially faster than even
 
704
            # getting a set of ids from the inventory
 
705
            #
 
706
            # <lifeless> set(dict) is roughly the same speed as
 
707
            # set(iter(dict)) and both are significantly slower than
 
708
            # set(dict.keys())
 
709
            deleted_ids = set(self.basis_inv._byid.keys()) - \
 
710
               set(self.builder.new_inventory._byid.keys())
 
711
        else:
 
712
            deleted_ids = set(self.basis_inv) - set(self.builder.new_inventory)
690
713
        if deleted_ids:
691
714
            self.any_entries_deleted = True
692
715
            deleted = [(self.basis_tree.id2path(file_id), file_id)
694
717
            deleted.sort()
695
718
            # XXX: this is not quite directory-order sorting
696
719
            for path, file_id in deleted:
697
 
                self._basis_delta.append((path, None, file_id, None))
 
720
                self.builder.record_delete(path, file_id)
698
721
                self.reporter.deleted(path)
699
722
 
700
 
    def _populate_from_inventory(self, specific_files):
 
723
    def _populate_from_inventory(self):
701
724
        """Populate the CommitBuilder by walking the working tree inventory."""
702
725
        if self.strict:
703
726
            # raise an exception as soon as we find a single unknown.
704
727
            for unknown in self.work_tree.unknowns():
705
728
                raise StrictCommitFailed()
706
 
               
 
729
 
 
730
        specific_files = self.specific_files
 
731
        exclude = self.exclude
707
732
        report_changes = self.reporter.is_verbose()
708
733
        deleted_ids = []
709
734
        # A tree of paths that have been deleted. E.g. if foo/bar has been
712
737
        # XXX: Note that entries may have the wrong kind because the entry does
713
738
        # not reflect the status on disk.
714
739
        work_inv = self.work_tree.inventory
 
740
        # NB: entries will include entries within the excluded ids/paths
 
741
        # because iter_entries_by_dir has no 'exclude' facility today.
715
742
        entries = work_inv.iter_entries_by_dir(
716
743
            specific_file_ids=self.specific_file_ids, yield_parents=True)
717
744
        for path, existing_ie in entries:
739
766
                if deleted_dict is not None:
740
767
                    # the path has a deleted parent, do not add it.
741
768
                    continue
 
769
            if exclude and is_inside_any(exclude, path):
 
770
                # Skip excluded paths. Excluded paths are processed by
 
771
                # _update_builder_with_changes.
 
772
                continue
742
773
            content_summary = self.work_tree.path_content_summary(path)
743
774
            # Note that when a filter of specific files is given, we must only
744
775
            # skip/record deleted files matching that filter.
791
822
        # FIXME: be more comprehensive here:
792
823
        # this works when both trees are in --trees repository,
793
824
        # but when both are bound to a different repository,
794
 
        # it fails; a better way of approaching this is to 
 
825
        # it fails; a better way of approaching this is to
795
826
        # finally implement the explicit-caches approach design
796
827
        # a while back - RBC 20070306.
797
828
        if sub_tree.branch.repository.has_same_location(
821
852
        else:
822
853
            ie = existing_ie.copy()
823
854
            ie.revision = None
824
 
        delta, version_recorded = self.builder.record_entry_contents(ie,
825
 
            self.parent_invs, path, self.work_tree, content_summary)
826
 
        if delta:
827
 
            self._basis_delta.append(delta)
 
855
        # For carried over entries we don't care about the fs hash - the repo
 
856
        # isn't generating a sha, so we're not saving computation time.
 
857
        _, version_recorded, fs_hash = self.builder.record_entry_contents(
 
858
            ie, self.parent_invs, path, self.work_tree, content_summary)
828
859
        if version_recorded:
829
860
            self.any_entries_changed = True
830
861
        if report_changes:
831
862
            self._report_change(ie, path)
 
863
        if fs_hash:
 
864
            self.work_tree._observed_sha1(ie.file_id, path, fs_hash)
832
865
        return ie
833
866
 
834
867
    def _report_change(self, ie, path):
842
875
        else:
843
876
            basis_ie = None
844
877
        change = ie.describe_change(basis_ie, ie)
845
 
        if change in (InventoryEntry.RENAMED, 
 
878
        if change in (InventoryEntry.RENAMED,
846
879
            InventoryEntry.MODIFIED_AND_RENAMED):
847
880
            old_path = self.basis_inv.id2path(ie.file_id)
848
881
            self.reporter.renamed(change, old_path, path)