~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: Vincent Ladeuil
  • Date: 2010-03-10 09:33:04 UTC
  • mto: (5082.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 5083.
  • Revision ID: v.ladeuil+lp@free.fr-20100310093304-4245t4tazd4sxoav
Cleanup test from overly cautious checks.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2011 Canonical Ltd
 
1
# Copyright (C) 2005-2010 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
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
 
from __future__ import absolute_import
18
17
 
19
18
# The newly committed revision is going to have a shape corresponding
20
19
# to that of the working tree.  Files that are not in the
50
49
# TODO: Change the parameter 'rev_id' to 'revision_id' to be consistent with
51
50
# the rest of the code; add a deprecation of the old name.
52
51
 
 
52
import os
 
53
import re
 
54
import sys
 
55
import time
 
56
 
 
57
from cStringIO import StringIO
 
58
 
53
59
from bzrlib import (
54
60
    debug,
55
61
    errors,
 
62
    revision,
56
63
    trace,
57
64
    tree,
58
 
    ui,
 
65
    xml_serializer,
59
66
    )
60
67
from bzrlib.branch import Branch
61
68
from bzrlib.cleanup import OperationWithCleanups
65
72
                           StrictCommitFailed
66
73
                           )
67
74
from bzrlib.osutils import (get_user_encoding,
68
 
                            is_inside_any,
 
75
                            kind_marker, isdir,isfile, is_inside_any,
 
76
                            is_inside_or_parent_of_any,
69
77
                            minimum_path_selection,
 
78
                            quotefn, sha_file, split_lines,
70
79
                            splitpath,
71
80
                            )
72
 
from bzrlib.trace import mutter, note, is_quiet
 
81
from bzrlib.testament import Testament
 
82
from bzrlib.trace import mutter, note, warning, is_quiet
73
83
from bzrlib.inventory import Inventory, InventoryEntry, make_entry
74
84
from bzrlib import symbol_versioning
 
85
from bzrlib.symbol_versioning import (deprecated_passed,
 
86
        deprecated_function,
 
87
        DEPRECATED_PARAMETER)
 
88
from bzrlib.workingtree import WorkingTree
75
89
from bzrlib.urlutils import unescape_for_display
76
 
from bzrlib.i18n import gettext
 
90
import bzrlib.ui
 
91
 
77
92
 
78
93
class NullCommitReporter(object):
79
94
    """I report on progress of a commit."""
114
129
        note(format, *args)
115
130
 
116
131
    def snapshot_change(self, change, path):
117
 
        if path == '' and change in (gettext('added'), gettext('modified')):
 
132
        if path == '' and change in ('added', 'modified'):
118
133
            return
119
134
        self._note("%s %s", change, path)
120
135
 
128
143
                                   "to started.", DeprecationWarning,
129
144
                                   stacklevel=2)
130
145
            location = ''
131
 
        self._note(gettext('Committing%s'), location)
 
146
        self._note('Committing%s', location)
132
147
 
133
148
    def completed(self, revno, rev_id):
134
 
        self._note(gettext('Committed revision %d.'), revno)
135
 
        # self._note goes to the console too; so while we want to log the
136
 
        # rev_id, we can't trivially only log it. (See bug 526425). Long
137
 
        # term we should rearrange the reporting structure, but for now
138
 
        # we just mutter seperately. We mutter the revid and revno together
139
 
        # so that concurrent bzr invocations won't lead to confusion.
140
 
        mutter('Committed revid %s as revno %d.', rev_id, revno)
 
149
        self._note('Committed revision %d.', revno)
141
150
 
142
151
    def deleted(self, path):
143
 
        self._note(gettext('deleted %s'), path)
 
152
        self._note('deleted %s', path)
144
153
 
145
154
    def missing(self, path):
146
 
        self._note(gettext('missing %s'), path)
 
155
        self._note('missing %s', path)
147
156
 
148
157
    def renamed(self, change, old_path, new_path):
149
158
        self._note('%s %s => %s', change, old_path, new_path)
166
175
    """
167
176
    def __init__(self,
168
177
                 reporter=None,
169
 
                 config_stack=None):
 
178
                 config=None):
170
179
        """Create a Commit object.
171
180
 
172
181
        :param reporter: the default reporter to use or None to decide later
173
182
        """
174
183
        self.reporter = reporter
175
 
        self.config_stack = config_stack
176
 
 
177
 
    @staticmethod
178
 
    def update_revprops(revprops, branch, authors=None, author=None,
179
 
                        local=False, possible_master_transports=None):
180
 
        if revprops is None:
181
 
            revprops = {}
182
 
        if possible_master_transports is None:
183
 
            possible_master_transports = []
184
 
        if not 'branch-nick' in revprops:
185
 
            revprops['branch-nick'] = branch._get_nick(
186
 
                local,
187
 
                possible_master_transports)
188
 
        if authors is not None:
189
 
            if author is not None:
190
 
                raise AssertionError('Specifying both author and authors '
191
 
                        'is not allowed. Specify just authors instead')
192
 
            if 'author' in revprops or 'authors' in revprops:
193
 
                # XXX: maybe we should just accept one of them?
194
 
                raise AssertionError('author property given twice')
195
 
            if authors:
196
 
                for individual in authors:
197
 
                    if '\n' in individual:
198
 
                        raise AssertionError('\\n is not a valid character '
199
 
                                'in an author identity')
200
 
                revprops['authors'] = '\n'.join(authors)
201
 
        if author is not None:
202
 
            symbol_versioning.warn('The parameter author was deprecated'
203
 
                   ' in version 1.13. Use authors instead',
204
 
                   DeprecationWarning)
205
 
            if 'author' in revprops or 'authors' in revprops:
206
 
                # XXX: maybe we should just accept one of them?
207
 
                raise AssertionError('author property given twice')
208
 
            if '\n' in author:
209
 
                raise AssertionError('\\n is not a valid character '
210
 
                        'in an author identity')
211
 
            revprops['authors'] = author
212
 
        return revprops
 
184
        self.config = config
213
185
 
214
186
    def commit(self,
215
187
               message=None,
229
201
               message_callback=None,
230
202
               recursive='down',
231
203
               exclude=None,
232
 
               possible_master_transports=None,
233
 
               lossy=False):
 
204
               possible_master_transports=None):
234
205
        """Commit working copy as a new revision.
235
206
 
236
207
        :param message: the commit message (it or message_callback is required)
263
234
        :param exclude: None or a list of relative paths to exclude from the
264
235
            commit. Pending changes to excluded files will be ignored by the
265
236
            commit.
266
 
        :param lossy: When committing to a foreign VCS, ignore any
267
 
            data that can not be natively represented.
268
237
        """
269
238
        operation = OperationWithCleanups(self._commit)
270
 
        self.revprops = revprops or {}
271
 
        # XXX: Can be set on __init__ or passed in - this is a bit ugly.
272
 
        self.config_stack = config or self.config_stack
273
239
        return operation.run(
274
240
               message=message,
275
241
               timestamp=timestamp,
280
246
               allow_pointless=allow_pointless,
281
247
               strict=strict,
282
248
               verbose=verbose,
 
249
               revprops=revprops,
283
250
               working_tree=working_tree,
284
251
               local=local,
285
252
               reporter=reporter,
 
253
               config=config,
286
254
               message_callback=message_callback,
287
255
               recursive=recursive,
288
256
               exclude=exclude,
289
 
               possible_master_transports=possible_master_transports,
290
 
               lossy=lossy)
 
257
               possible_master_transports=possible_master_transports)
291
258
 
292
259
    def _commit(self, operation, message, timestamp, timezone, committer,
293
 
            specific_files, rev_id, allow_pointless, strict, verbose,
294
 
            working_tree, local, reporter, message_callback, recursive,
295
 
            exclude, possible_master_transports, lossy):
 
260
            specific_files, rev_id, allow_pointless, strict, verbose, revprops,
 
261
            working_tree, local, reporter, config, message_callback, recursive,
 
262
            exclude, possible_master_transports):
296
263
        mutter('preparing to commit')
297
264
 
298
265
        if working_tree is None:
330
297
                minimum_path_selection(specific_files))
331
298
        else:
332
299
            self.specific_files = None
333
 
 
 
300
            
334
301
        self.allow_pointless = allow_pointless
 
302
        self.revprops = revprops
335
303
        self.message_callback = message_callback
336
304
        self.timestamp = timestamp
337
305
        self.timezone = timezone
350
318
            not self.branch.repository._format.supports_tree_reference and
351
319
            (self.branch.repository._format.fast_deltas or
352
320
             len(self.parents) < 2))
353
 
        self.pb = ui.ui_factory.nested_progress_bar()
 
321
        self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
354
322
        operation.add_cleanup(self.pb.finished)
355
323
        self.basis_revid = self.work_tree.last_revision()
356
324
        self.basis_tree = self.work_tree.basis_tree()
364
332
        self._check_bound_branch(operation, possible_master_transports)
365
333
 
366
334
        # Check that the working tree is up to date
367
 
        old_revno, old_revid, new_revno = self._check_out_of_date_tree()
 
335
        old_revno, new_revno = self._check_out_of_date_tree()
368
336
 
369
337
        # Complete configuration setup
370
338
        if reporter is not None:
371
339
            self.reporter = reporter
372
340
        elif self.reporter is None:
373
341
            self.reporter = self._select_reporter()
374
 
        if self.config_stack is None:
375
 
            self.config_stack = self.work_tree.get_config_stack()
 
342
        if self.config is None:
 
343
            self.config = self.branch.get_config()
376
344
 
377
345
        self._set_specific_file_ids()
378
346
 
384
352
        self.pb_stage_count = 0
385
353
        self.pb_stage_total = 5
386
354
        if self.bound_branch:
387
 
            # 2 extra stages: "Uploading data to master branch" and "Merging
388
 
            # tags to master branch"
389
 
            self.pb_stage_total += 2
 
355
            self.pb_stage_total += 1
390
356
        self.pb.show_pct = False
391
357
        self.pb.show_spinner = False
392
358
        self.pb.show_eta = False
404
370
 
405
371
        # Collect the changes
406
372
        self._set_progress_stage("Collecting changes", counter=True)
407
 
        self._lossy = lossy
408
373
        self.builder = self.branch.get_commit_builder(self.parents,
409
 
            self.config_stack, timestamp, timezone, committer, self.revprops,
410
 
            rev_id, lossy=lossy)
411
 
        if not self.builder.supports_record_entry_contents and self.exclude:
412
 
            self.builder.abort()
413
 
            raise errors.ExcludesUnsupported(self.branch.repository)
414
 
 
415
 
        if self.builder.updates_branch and self.bound_branch:
416
 
            self.builder.abort()
417
 
            raise AssertionError(
418
 
                "bound branches not supported for commit builders "
419
 
                "that update the branch")
 
374
            self.config, timestamp, timezone, committer, revprops, rev_id)
420
375
 
421
376
        try:
422
377
            self.builder.will_record_deletes()
449
404
        except Exception, e:
450
405
            mutter("aborting commit write group because of exception:")
451
406
            trace.log_exception_quietly()
 
407
            note("aborting commit write group: %r" % (e,))
452
408
            self.builder.abort()
453
409
            raise
454
410
 
455
 
        self._update_branches(old_revno, old_revid, new_revno)
 
411
        self._process_pre_hooks(old_revno, new_revno)
 
412
 
 
413
        # Upload revision data to the master.
 
414
        # this will propagate merged revisions too if needed.
 
415
        if self.bound_branch:
 
416
            self._set_progress_stage("Uploading data to master branch")
 
417
            # 'commit' to the master first so a timeout here causes the
 
418
            # local branch to be out of date
 
419
            self.master_branch.import_last_revision_info(
 
420
                self.branch.repository, new_revno, self.rev_id)
 
421
 
 
422
        # and now do the commit locally.
 
423
        self.branch.set_last_revision_info(new_revno, self.rev_id)
456
424
 
457
425
        # Make the working tree be up to date with the branch. This
458
426
        # includes automatic changes scheduled to be made to the tree, such
465
433
        self._process_post_hooks(old_revno, new_revno)
466
434
        return self.rev_id
467
435
 
468
 
    def _update_branches(self, old_revno, old_revid, new_revno):
469
 
        """Update the master and local branch to the new revision.
470
 
 
471
 
        This will try to make sure that the master branch is updated
472
 
        before the local branch.
473
 
 
474
 
        :param old_revno: Revision number of master branch before the
475
 
            commit
476
 
        :param old_revid: Tip of master branch before the commit
477
 
        :param new_revno: Revision number of the new commit
478
 
        """
479
 
        if not self.builder.updates_branch:
480
 
            self._process_pre_hooks(old_revno, new_revno)
481
 
 
482
 
            # Upload revision data to the master.
483
 
            # this will propagate merged revisions too if needed.
484
 
            if self.bound_branch:
485
 
                self._set_progress_stage("Uploading data to master branch")
486
 
                # 'commit' to the master first so a timeout here causes the
487
 
                # local branch to be out of date
488
 
                (new_revno, self.rev_id) = self.master_branch.import_last_revision_info_and_tags(
489
 
                    self.branch, new_revno, self.rev_id, lossy=self._lossy)
490
 
                if self._lossy:
491
 
                    self.branch.fetch(self.master_branch, self.rev_id)
492
 
 
493
 
            # and now do the commit locally.
494
 
            self.branch.set_last_revision_info(new_revno, self.rev_id)
495
 
        else:
496
 
            try:
497
 
                self._process_pre_hooks(old_revno, new_revno)
498
 
            except:
499
 
                # The commit builder will already have updated the branch,
500
 
                # revert it.
501
 
                self.branch.set_last_revision_info(old_revno, old_revid)
502
 
                raise
503
 
 
504
 
        # Merge local tags to remote
505
 
        if self.bound_branch:
506
 
            self._set_progress_stage("Merging tags to master branch")
507
 
            tag_updates, tag_conflicts = self.branch.tags.merge_to(
508
 
                self.master_branch.tags)
509
 
            if tag_conflicts:
510
 
                warning_lines = ['    ' + name for name, _, _ in tag_conflicts]
511
 
                note( gettext("Conflicting tags in bound branch:\n{0}".format(
512
 
                    "\n".join(warning_lines))) )
513
 
 
514
436
    def _select_reporter(self):
515
437
        """Select the CommitReporter to use."""
516
438
        if is_quiet():
523
445
        # A merge with no effect on files
524
446
        if len(self.parents) > 1:
525
447
            return
 
448
        # TODO: we could simplify this by using self.builder.basis_delta.
 
449
 
 
450
        # The initial commit adds a root directory, but this in itself is not
 
451
        # a worthwhile commit.
 
452
        if (self.basis_revid == revision.NULL_REVISION and
 
453
            ((self.builder.new_inventory is not None and
 
454
             len(self.builder.new_inventory) == 1) or
 
455
            len(self.builder._basis_delta) == 1)):
 
456
            raise PointlessCommit()
526
457
        if self.builder.any_changes():
527
458
            return
528
459
        raise PointlessCommit()
573
504
    def _check_out_of_date_tree(self):
574
505
        """Check that the working tree is up to date.
575
506
 
576
 
        :return: old_revision_number, old_revision_id, new_revision_number
577
 
            tuple
 
507
        :return: old_revision_number,new_revision_number tuple
578
508
        """
579
509
        try:
580
510
            first_tree_parent = self.work_tree.get_parent_ids()[0]
593
523
        else:
594
524
            # ghost parents never appear in revision history.
595
525
            new_revno = 1
596
 
        return old_revno, master_last, new_revno
 
526
        return old_revno,new_revno
597
527
 
598
528
    def _process_pre_hooks(self, old_revno, new_revno):
599
529
        """Process any registered pre commit hooks."""
605
535
        # Process the post commit hooks, if any
606
536
        self._set_progress_stage("Running post_commit hooks")
607
537
        # old style commit hooks - should be deprecated ? (obsoleted in
608
 
        # 0.15^H^H^H^H 2.5.0)
609
 
        post_commit = self.config_stack.get('post_commit')
610
 
        if post_commit is not None:
611
 
            hooks = post_commit.split(' ')
 
538
        # 0.15)
 
539
        if self.config.post_commit() is not None:
 
540
            hooks = self.config.post_commit().split(' ')
612
541
            # this would be nicer with twisted.python.reflect.namedAny
613
542
            for hook in hooks:
614
543
                result = eval(hook + '(branch, rev_id)',
667
596
        # entries and the order is preserved when doing this.
668
597
        if self.use_record_iter_changes:
669
598
            return
670
 
        self.basis_inv = self.basis_tree.root_inventory
 
599
        self.basis_inv = self.basis_tree.inventory
671
600
        self.parent_invs = [self.basis_inv]
672
601
        for revision in self.parents[1:]:
673
602
            if self.branch.repository.has_revision(revision):
726
655
                # Reset the new path (None) and new versioned flag (False)
727
656
                change = (change[0], (change[1][0], None), change[2],
728
657
                    (change[3][0], False)) + change[4:]
729
 
                new_path = change[1][1]
730
 
                versioned = False
731
658
            elif kind == 'tree-reference':
732
659
                if self.recursive == 'down':
733
660
                    self._commit_nested_tree(change[0], change[1][1])
737
664
                    if new_path is None:
738
665
                        reporter.deleted(old_path)
739
666
                    elif old_path is None:
740
 
                        reporter.snapshot_change(gettext('added'), new_path)
 
667
                        reporter.snapshot_change('added', new_path)
741
668
                    elif old_path != new_path:
742
 
                        reporter.renamed(gettext('renamed'), old_path, new_path)
 
669
                        reporter.renamed('renamed', old_path, new_path)
743
670
                    else:
744
671
                        if (new_path or 
745
672
                            self.work_tree.branch.repository._format.rich_root_data):
746
673
                            # Don't report on changes to '' in non rich root
747
674
                            # repositories.
748
 
                            reporter.snapshot_change(gettext('modified'), new_path)
 
675
                            reporter.snapshot_change('modified', new_path)
749
676
            self._next_progress_entry()
750
677
        # Unversion IDs that were found to be deleted
751
678
        self.deleted_ids = deleted_ids
757
684
        if self.specific_files or self.exclude:
758
685
            specific_files = self.specific_files or []
759
686
            for path, old_ie in self.basis_inv.iter_entries():
760
 
                if self.builder.new_inventory.has_id(old_ie.file_id):
 
687
                if old_ie.file_id in self.builder.new_inventory:
761
688
                    # already added - skip.
762
689
                    continue
763
690
                if (is_inside_any(specific_files, path)
834
761
        deleted_paths = {}
835
762
        # XXX: Note that entries may have the wrong kind because the entry does
836
763
        # not reflect the status on disk.
 
764
        work_inv = self.work_tree.inventory
837
765
        # NB: entries will include entries within the excluded ids/paths
838
766
        # because iter_entries_by_dir has no 'exclude' facility today.
839
 
        entries = self.work_tree.iter_entries_by_dir(
 
767
        entries = work_inv.iter_entries_by_dir(
840
768
            specific_file_ids=self.specific_file_ids, yield_parents=True)
841
769
        for path, existing_ie in entries:
842
770
            file_id = existing_ie.file_id
973
901
            self.reporter.renamed(change, old_path, path)
974
902
            self._next_progress_entry()
975
903
        else:
976
 
            if change == gettext('unchanged'):
 
904
            if change == 'unchanged':
977
905
                return
978
906
            self.reporter.snapshot_change(change, path)
979
907
            self._next_progress_entry()
995
923
 
996
924
    def _emit_progress(self):
997
925
        if self.pb_entries_count is not None:
998
 
            text = gettext("{0} [{1}] - Stage").format(self.pb_stage_name,
 
926
            text = "%s [%d] - Stage" % (self.pb_stage_name,
999
927
                self.pb_entries_count)
1000
928
        else:
1001
 
            text = gettext("%s - Stage") % (self.pb_stage_name, )
 
929
            text = "%s - Stage" % (self.pb_stage_name, )
1002
930
        self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
1003
931
 
1004
932
    def _set_specific_file_ids(self):