~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: Jelmer Vernooij
  • Date: 2012-01-24 13:14:06 UTC
  • mto: (6445.4.5 nested-trees-spec)
  • mto: This revision was merged to the branch mainline in revision 6518.
  • Revision ID: jelmer@samba.org-20120124131406-wedftkorbpv37bm0
Import nested tree doc from devnotes.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2005-2011 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
17
18
 
18
19
# The newly committed revision is going to have a shape corresponding
19
20
# to that of the working tree.  Files that are not in the
52
53
from bzrlib import (
53
54
    debug,
54
55
    errors,
55
 
    revision,
56
56
    trace,
57
57
    tree,
 
58
    ui,
58
59
    )
59
60
from bzrlib.branch import Branch
60
61
from bzrlib.cleanup import OperationWithCleanups
72
73
from bzrlib.inventory import Inventory, InventoryEntry, make_entry
73
74
from bzrlib import symbol_versioning
74
75
from bzrlib.urlutils import unescape_for_display
75
 
import bzrlib.ui
76
 
 
 
76
from bzrlib.i18n import gettext
77
77
 
78
78
class NullCommitReporter(object):
79
79
    """I report on progress of a commit."""
114
114
        note(format, *args)
115
115
 
116
116
    def snapshot_change(self, change, path):
117
 
        if path == '' and change in ('added', 'modified'):
 
117
        if path == '' and change in (gettext('added'), gettext('modified')):
118
118
            return
119
119
        self._note("%s %s", change, path)
120
120
 
128
128
                                   "to started.", DeprecationWarning,
129
129
                                   stacklevel=2)
130
130
            location = ''
131
 
        self._note('Committing%s', location)
 
131
        self._note(gettext('Committing%s'), location)
132
132
 
133
133
    def completed(self, revno, rev_id):
134
 
        self._note('Committed revision %d.', revno)
 
134
        self._note(gettext('Committed revision %d.'), revno)
135
135
        # self._note goes to the console too; so while we want to log the
136
136
        # rev_id, we can't trivially only log it. (See bug 526425). Long
137
137
        # term we should rearrange the reporting structure, but for now
140
140
        mutter('Committed revid %s as revno %d.', rev_id, revno)
141
141
 
142
142
    def deleted(self, path):
143
 
        self._note('deleted %s', path)
 
143
        self._note(gettext('deleted %s'), path)
144
144
 
145
145
    def missing(self, path):
146
 
        self._note('missing %s', path)
 
146
        self._note(gettext('missing %s'), path)
147
147
 
148
148
    def renamed(self, change, old_path, new_path):
149
149
        self._note('%s %s => %s', change, old_path, new_path)
166
166
    """
167
167
    def __init__(self,
168
168
                 reporter=None,
169
 
                 config=None):
 
169
                 config_stack=None):
170
170
        """Create a Commit object.
171
171
 
172
172
        :param reporter: the default reporter to use or None to decide later
173
173
        """
174
174
        self.reporter = reporter
175
 
        self.config = config
 
175
        self.config_stack = config_stack
176
176
 
177
177
    @staticmethod
178
178
    def update_revprops(revprops, branch, authors=None, author=None,
229
229
               message_callback=None,
230
230
               recursive='down',
231
231
               exclude=None,
232
 
               possible_master_transports=None):
 
232
               possible_master_transports=None,
 
233
               lossy=False):
233
234
        """Commit working copy as a new revision.
234
235
 
235
236
        :param message: the commit message (it or message_callback is required)
262
263
        :param exclude: None or a list of relative paths to exclude from the
263
264
            commit. Pending changes to excluded files will be ignored by the
264
265
            commit.
 
266
        :param lossy: When committing to a foreign VCS, ignore any
 
267
            data that can not be natively represented.
265
268
        """
266
269
        operation = OperationWithCleanups(self._commit)
267
270
        self.revprops = revprops or {}
268
271
        # XXX: Can be set on __init__ or passed in - this is a bit ugly.
269
 
        self.config = config or self.config
 
272
        self.config_stack = config or self.config_stack
270
273
        return operation.run(
271
274
               message=message,
272
275
               timestamp=timestamp,
283
286
               message_callback=message_callback,
284
287
               recursive=recursive,
285
288
               exclude=exclude,
286
 
               possible_master_transports=possible_master_transports)
 
289
               possible_master_transports=possible_master_transports,
 
290
               lossy=lossy)
287
291
 
288
292
    def _commit(self, operation, message, timestamp, timezone, committer,
289
293
            specific_files, rev_id, allow_pointless, strict, verbose,
290
294
            working_tree, local, reporter, message_callback, recursive,
291
 
            exclude, possible_master_transports):
 
295
            exclude, possible_master_transports, lossy):
292
296
        mutter('preparing to commit')
293
297
 
294
298
        if working_tree is None:
326
330
                minimum_path_selection(specific_files))
327
331
        else:
328
332
            self.specific_files = None
329
 
            
 
333
 
330
334
        self.allow_pointless = allow_pointless
331
335
        self.message_callback = message_callback
332
336
        self.timestamp = timestamp
346
350
            not self.branch.repository._format.supports_tree_reference and
347
351
            (self.branch.repository._format.fast_deltas or
348
352
             len(self.parents) < 2))
349
 
        self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
353
        self.pb = ui.ui_factory.nested_progress_bar()
350
354
        operation.add_cleanup(self.pb.finished)
351
355
        self.basis_revid = self.work_tree.last_revision()
352
356
        self.basis_tree = self.work_tree.basis_tree()
360
364
        self._check_bound_branch(operation, possible_master_transports)
361
365
 
362
366
        # Check that the working tree is up to date
363
 
        old_revno, new_revno = self._check_out_of_date_tree()
 
367
        old_revno, old_revid, new_revno = self._check_out_of_date_tree()
364
368
 
365
369
        # Complete configuration setup
366
370
        if reporter is not None:
367
371
            self.reporter = reporter
368
372
        elif self.reporter is None:
369
373
            self.reporter = self._select_reporter()
370
 
        if self.config is None:
371
 
            self.config = self.branch.get_config()
 
374
        if self.config_stack is None:
 
375
            self.config_stack = self.branch.get_config_stack()
372
376
 
373
377
        self._set_specific_file_ids()
374
378
 
380
384
        self.pb_stage_count = 0
381
385
        self.pb_stage_total = 5
382
386
        if self.bound_branch:
383
 
            self.pb_stage_total += 1
 
387
            # 2 extra stages: "Uploading data to master branch" and "Merging
 
388
            # tags to master branch"
 
389
            self.pb_stage_total += 2
384
390
        self.pb.show_pct = False
385
391
        self.pb.show_spinner = False
386
392
        self.pb.show_eta = False
398
404
 
399
405
        # Collect the changes
400
406
        self._set_progress_stage("Collecting changes", counter=True)
 
407
        self._lossy = lossy
401
408
        self.builder = self.branch.get_commit_builder(self.parents,
402
 
            self.config, timestamp, timezone, committer, self.revprops, rev_id)
 
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")
403
420
 
404
421
        try:
405
422
            self.builder.will_record_deletes()
432
449
        except Exception, e:
433
450
            mutter("aborting commit write group because of exception:")
434
451
            trace.log_exception_quietly()
435
 
            note("aborting commit write group: %r" % (e,))
436
452
            self.builder.abort()
437
453
            raise
438
454
 
439
 
        self._process_pre_hooks(old_revno, new_revno)
440
 
 
441
 
        # Upload revision data to the master.
442
 
        # this will propagate merged revisions too if needed.
443
 
        if self.bound_branch:
444
 
            self._set_progress_stage("Uploading data to master branch")
445
 
            # 'commit' to the master first so a timeout here causes the
446
 
            # local branch to be out of date
447
 
            self.master_branch.import_last_revision_info(
448
 
                self.branch.repository, new_revno, self.rev_id)
449
 
 
450
 
        # and now do the commit locally.
451
 
        self.branch.set_last_revision_info(new_revno, self.rev_id)
 
455
        self._update_branches(old_revno, old_revid, new_revno)
452
456
 
453
457
        # Make the working tree be up to date with the branch. This
454
458
        # includes automatic changes scheduled to be made to the tree, such
461
465
        self._process_post_hooks(old_revno, new_revno)
462
466
        return self.rev_id
463
467
 
 
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
 
464
514
    def _select_reporter(self):
465
515
        """Select the CommitReporter to use."""
466
516
        if is_quiet():
473
523
        # A merge with no effect on files
474
524
        if len(self.parents) > 1:
475
525
            return
476
 
        # TODO: we could simplify this by using self.builder.basis_delta.
477
 
 
478
 
        # The initial commit adds a root directory, but this in itself is not
479
 
        # a worthwhile commit.
480
 
        if (self.basis_revid == revision.NULL_REVISION and
481
 
            ((self.builder.new_inventory is not None and
482
 
             len(self.builder.new_inventory) == 1) or
483
 
            len(self.builder._basis_delta) == 1)):
484
 
            raise PointlessCommit()
485
526
        if self.builder.any_changes():
486
527
            return
487
528
        raise PointlessCommit()
532
573
    def _check_out_of_date_tree(self):
533
574
        """Check that the working tree is up to date.
534
575
 
535
 
        :return: old_revision_number,new_revision_number tuple
 
576
        :return: old_revision_number, old_revision_id, new_revision_number
 
577
            tuple
536
578
        """
537
579
        try:
538
580
            first_tree_parent = self.work_tree.get_parent_ids()[0]
551
593
        else:
552
594
            # ghost parents never appear in revision history.
553
595
            new_revno = 1
554
 
        return old_revno,new_revno
 
596
        return old_revno, master_last, new_revno
555
597
 
556
598
    def _process_pre_hooks(self, old_revno, new_revno):
557
599
        """Process any registered pre commit hooks."""
563
605
        # Process the post commit hooks, if any
564
606
        self._set_progress_stage("Running post_commit hooks")
565
607
        # old style commit hooks - should be deprecated ? (obsoleted in
566
 
        # 0.15)
567
 
        if self.config.post_commit() is not None:
568
 
            hooks = self.config.post_commit().split(' ')
 
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(' ')
569
612
            # this would be nicer with twisted.python.reflect.namedAny
570
613
            for hook in hooks:
571
614
                result = eval(hook + '(branch, rev_id)',
683
726
                # Reset the new path (None) and new versioned flag (False)
684
727
                change = (change[0], (change[1][0], None), change[2],
685
728
                    (change[3][0], False)) + change[4:]
 
729
                new_path = change[1][1]
 
730
                versioned = False
686
731
            elif kind == 'tree-reference':
687
732
                if self.recursive == 'down':
688
733
                    self._commit_nested_tree(change[0], change[1][1])
692
737
                    if new_path is None:
693
738
                        reporter.deleted(old_path)
694
739
                    elif old_path is None:
695
 
                        reporter.snapshot_change('added', new_path)
 
740
                        reporter.snapshot_change(gettext('added'), new_path)
696
741
                    elif old_path != new_path:
697
 
                        reporter.renamed('renamed', old_path, new_path)
 
742
                        reporter.renamed(gettext('renamed'), old_path, new_path)
698
743
                    else:
699
744
                        if (new_path or 
700
745
                            self.work_tree.branch.repository._format.rich_root_data):
701
746
                            # Don't report on changes to '' in non rich root
702
747
                            # repositories.
703
 
                            reporter.snapshot_change('modified', new_path)
 
748
                            reporter.snapshot_change(gettext('modified'), new_path)
704
749
            self._next_progress_entry()
705
750
        # Unversion IDs that were found to be deleted
706
751
        self.deleted_ids = deleted_ids
712
757
        if self.specific_files or self.exclude:
713
758
            specific_files = self.specific_files or []
714
759
            for path, old_ie in self.basis_inv.iter_entries():
715
 
                if old_ie.file_id in self.builder.new_inventory:
 
760
                if self.builder.new_inventory.has_id(old_ie.file_id):
716
761
                    # already added - skip.
717
762
                    continue
718
763
                if (is_inside_any(specific_files, path)
929
974
            self.reporter.renamed(change, old_path, path)
930
975
            self._next_progress_entry()
931
976
        else:
932
 
            if change == 'unchanged':
 
977
            if change == gettext('unchanged'):
933
978
                return
934
979
            self.reporter.snapshot_change(change, path)
935
980
            self._next_progress_entry()
951
996
 
952
997
    def _emit_progress(self):
953
998
        if self.pb_entries_count is not None:
954
 
            text = "%s [%d] - Stage" % (self.pb_stage_name,
 
999
            text = gettext("{0} [{1}] - Stage").format(self.pb_stage_name,
955
1000
                self.pb_entries_count)
956
1001
        else:
957
 
            text = "%s - Stage" % (self.pb_stage_name, )
 
1002
            text = gettext("%s - Stage") % (self.pb_stage_name, )
958
1003
        self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
959
1004
 
960
1005
    def _set_specific_file_ids(self):