~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: v.ladeuil+lp at free
  • Date: 2007-05-15 17:40:32 UTC
  • mto: (2485.8.44 bzr.connection.sharing)
  • mto: This revision was merged to the branch mainline in revision 2646.
  • Revision ID: v.ladeuil+lp@free.fr-20070515174032-qzdkangpv29l9e7g
Add a test that check that init connect only once. It fails.

* __init__.py:
(test_suite): Register the new test class.

* test_init.py: 
(InstrumentedTransport): A transport that can track connections.
(TransportHooks): Transport specific hooks.
(TestInit): Iniit command behavior tests.

* ftp.py:
(FtpTransport.__init__): Mark place that need fixing regarding
transport connection sharing

* builtins.py:
(cmd_init.run): Mark places that need fixing regarding transport
connection sharing.

Show diffs side-by-side

added added

removed removed

Lines of Context:
57
57
from cStringIO import StringIO
58
58
 
59
59
from bzrlib import (
60
 
    debug,
61
60
    errors,
62
61
    inventory,
63
62
    tree,
107
106
 
108
107
class ReportCommitToLog(NullCommitReporter):
109
108
 
110
 
    def _note(self, format, *args):
111
 
        """Output a message.
112
 
 
113
 
        Subclasses may choose to override this method.
114
 
        """
115
 
        note(format, *args)
 
109
    # this may be more useful if 'note' was replaced by an overridable
 
110
    # method on self, which would allow more trivial subclassing.
 
111
    # alternative, a callable could be passed in, allowing really trivial
 
112
    # reuse for some uis. RBC 20060511
116
113
 
117
114
    def snapshot_change(self, change, path):
118
115
        if change == 'unchanged':
119
116
            return
120
117
        if change == 'added' and path == '':
121
118
            return
122
 
        self._note("%s %s", change, path)
 
119
        note("%s %s", change, path)
123
120
 
124
121
    def completed(self, revno, rev_id):
125
 
        self._note('Committed revision %d.', revno)
 
122
        note('Committed revision %d.', revno)
126
123
    
127
124
    def deleted(self, file_id):
128
 
        self._note('deleted %s', file_id)
 
125
        note('deleted %s', file_id)
129
126
 
130
127
    def escaped(self, escape_count, message):
131
 
        self._note("replaced %d control characters in message", escape_count)
 
128
        note("replaced %d control characters in message", escape_count)
132
129
 
133
130
    def missing(self, path):
134
 
        self._note('missing %s', path)
 
131
        note('missing %s', path)
135
132
 
136
133
    def renamed(self, change, old_path, new_path):
137
 
        self._note('%s %s => %s', change, old_path, new_path)
 
134
        note('%s %s => %s', change, old_path, new_path)
138
135
 
139
136
 
140
137
class Commit(object):
156
153
            self.reporter = reporter
157
154
        else:
158
155
            self.reporter = NullCommitReporter()
159
 
        self.config = config
 
156
        if config is not None:
 
157
            self.config = config
 
158
        else:
 
159
            self.config = None
160
160
        
161
161
    def commit(self,
162
162
               message=None,
177
177
               recursive='down'):
178
178
        """Commit working copy as a new revision.
179
179
 
180
 
        :param message: the commit message (it or message_callback is required)
181
 
 
182
 
        :param timestamp: if not None, seconds-since-epoch for a
183
 
            postdated/predated commit.
184
 
 
185
 
        :param specific_files: If true, commit only those files.
186
 
 
187
 
        :param rev_id: If set, use this as the new revision id.
 
180
        message -- the commit message (it or message_callback is required)
 
181
 
 
182
        timestamp -- if not None, seconds-since-epoch for a
 
183
             postdated/predated commit.
 
184
 
 
185
        specific_files -- If true, commit only those files.
 
186
 
 
187
        rev_id -- If set, use this as the new revision id.
188
188
            Useful for test or import commands that need to tightly
189
189
            control what revisions are assigned.  If you duplicate
190
190
            a revision id that exists elsewhere it is your own fault.
191
191
            If null (default), a time/random revision id is generated.
192
192
 
193
 
        :param allow_pointless: If true (default), commit even if nothing
 
193
        allow_pointless -- If true (default), commit even if nothing
194
194
            has changed and no merges are recorded.
195
195
 
196
 
        :param strict: If true, don't allow a commit if the working tree
 
196
        strict -- If true, don't allow a commit if the working tree
197
197
            contains unknown files.
198
198
 
199
 
        :param revprops: Properties for new revision
 
199
        revprops -- Properties for new revision
200
200
        :param local: Perform a local only commit.
201
201
        :param recursive: If set to 'down', commit in any subtrees that have
202
202
            pending changes of any sort during this commit.
233
233
        self.timestamp = timestamp
234
234
        self.timezone = timezone
235
235
        self.committer = committer
 
236
        self.specific_files = specific_files
236
237
        self.strict = strict
237
238
        self.verbose = verbose
 
239
        self.local = local
238
240
 
239
241
        if reporter is None and self.reporter is None:
240
242
            self.reporter = NullCommitReporter()
247
249
        self.basis_tree.lock_read()
248
250
        try:
249
251
            # Cannot commit with conflicts present.
250
 
            if len(self.work_tree.conflicts()) > 0:
 
252
            if len(self.work_tree.conflicts())>0:
251
253
                raise ConflictsInTree
252
254
 
253
 
            # Setup the bound branch variables as needed.
 
255
            # setup the bound branch variables as needed.
254
256
            self._check_bound_branch()
255
257
 
256
 
            # Check that the working tree is up to date
257
 
            old_revno,new_revno = self._check_out_of_date_tree()
258
 
 
 
258
            # check for out of date working trees
 
259
            try:
 
260
                first_tree_parent = self.work_tree.get_parent_ids()[0]
 
261
            except IndexError:
 
262
                # if there are no parents, treat our parent as 'None'
 
263
                # this is so that we still consier the master branch
 
264
                # - in a checkout scenario the tree may have no
 
265
                # parents but the branch may do.
 
266
                first_tree_parent = bzrlib.revision.NULL_REVISION
 
267
            old_revno, master_last = self.master_branch.last_revision_info()
 
268
            if master_last != first_tree_parent:
 
269
                if master_last != bzrlib.revision.NULL_REVISION:
 
270
                    raise errors.OutOfDateTree(self.work_tree)
 
271
            if self.branch.repository.has_revision(first_tree_parent):
 
272
                new_revno = old_revno + 1
 
273
            else:
 
274
                # ghost parents never appear in revision history.
 
275
                new_revno = 1
259
276
            if strict:
260
277
                # raise an exception as soon as we find a single unknown.
261
278
                for unknown in self.work_tree.unknowns():
264
281
            if self.config is None:
265
282
                self.config = self.branch.get_config()
266
283
 
267
 
            # If provided, ensure the specified files are versioned
 
284
            self.work_inv = self.work_tree.inventory
 
285
            self.basis_inv = self.basis_tree.inventory
268
286
            if specific_files is not None:
269
 
                # Note: We don't actually need the IDs here. This routine
270
 
                # is being called because it raises PathNotVerisonedError
271
 
                # as a side effect of finding the IDs.
 
287
                # Ensure specified files are versioned
 
288
                # (We don't actually need the ids here)
272
289
                # XXX: Dont we have filter_unversioned to do this more
273
290
                # cheaply?
274
291
                tree.find_ids_across_trees(specific_files,
275
292
                                           [self.basis_tree, self.work_tree])
276
 
 
277
 
            # Setup the progress bar. As the number of files that need to be
278
 
            # committed in unknown, progress is reported as stages.
279
 
            # We keep track of entries separately though and include that
280
 
            # information in the progress bar during the relevant stages.
281
 
            self.pb_stage_name = ""
282
 
            self.pb_stage_count = 0
283
 
            self.pb_stage_total = 4
284
 
            if self.bound_branch:
285
 
                self.pb_stage_total += 1
286
 
            self.pb.show_pct = False
287
 
            self.pb.show_spinner = False
288
 
            self.pb.show_eta = False
289
 
            self.pb.show_count = True
290
 
            self.pb.show_bar = True
291
 
 
292
 
            # After a merge, a selected file commit is not supported.
293
 
            # See 'bzr help merge' for an explanation as to why.
294
 
            self.basis_inv = self.basis_tree.inventory
 
293
            # one to finish, one for rev and inventory, and one for each
 
294
            # inventory entry, and the same for the new inventory.
 
295
            # note that this estimate is too long when we do a partial tree
 
296
            # commit which excludes some new files from being considered.
 
297
            # The estimate is corrected when we populate the new inv.
 
298
            self.pb_total = len(self.work_inv) + 5
 
299
            self.pb_count = 0
 
300
 
295
301
            self._gather_parents()
296
302
            if len(self.parents) > 1 and self.specific_files:
297
 
                raise errors.CannotCommitSelectedFileMerge(self.specific_files)
 
303
                raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
 
304
                        self.specific_files)
298
305
            
299
 
            # Collect the changes
300
 
            self._emit_progress_set_stage("Collecting changes", show_entries=True)
301
306
            self.builder = self.branch.get_commit_builder(self.parents,
302
307
                self.config, timestamp, timezone, committer, revprops, rev_id)
303
 
            self._update_builder_with_changes()
 
308
            
 
309
            self._remove_deleted()
 
310
            self._populate_new_inv()
 
311
            self._report_deletes()
 
312
 
304
313
            self._check_pointless()
305
314
 
306
 
            # TODO: Now the new inventory is known, check for conflicts.
 
315
            self._emit_progress_update()
 
316
            # TODO: Now the new inventory is known, check for conflicts and
 
317
            # prompt the user for a commit message.
307
318
            # ADHB 2006-08-08: If this is done, populate_new_inv should not add
308
319
            # weave lines, because nothing should be recorded until it is known
309
320
            # that commit will succeed.
310
 
            self._emit_progress_set_stage("Saving data locally")
311
321
            self.builder.finish_inventory()
312
 
 
313
 
            # Prompt the user for a commit message if none provided
 
322
            self._emit_progress_update()
314
323
            message = message_callback(self)
315
324
            assert isinstance(message, unicode), type(message)
316
325
            self.message = message
317
326
            self._escape_commit_message()
318
327
 
319
 
            # Add revision data to the local branch
320
328
            self.rev_id = self.builder.commit(self.message)
 
329
            self._emit_progress_update()
 
330
            # revision data is in the local branch now.
321
331
            
322
 
            # Upload revision data to the master.
 
332
            # upload revision data to the master.
323
333
            # this will propagate merged revisions too if needed.
324
334
            if self.bound_branch:
325
 
                self._emit_progress_set_stage("Uploading data to master branch")
326
335
                self.master_branch.repository.fetch(self.branch.repository,
327
336
                                                    revision_id=self.rev_id)
328
337
                # now the master has the revision data
329
 
                # 'commit' to the master first so a timeout here causes the
330
 
                # local branch to be out of date
 
338
                # 'commit' to the master first so a timeout here causes the local
 
339
                # branch to be out of date
331
340
                self.master_branch.set_last_revision_info(new_revno,
332
341
                                                          self.rev_id)
333
342
 
334
343
            # and now do the commit locally.
335
344
            self.branch.set_last_revision_info(new_revno, self.rev_id)
336
345
 
337
 
            # Make the working tree up to date with the branch
338
 
            self._emit_progress_set_stage("Updating the working tree")
339
346
            rev_tree = self.builder.revision_tree()
340
347
            self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
 
348
            # now the work tree is up to date with the branch
 
349
            
341
350
            self.reporter.completed(new_revno, self.rev_id)
342
 
            self._process_hooks(old_revno, new_revno)
 
351
            # old style commit hooks - should be deprecated ? (obsoleted in
 
352
            # 0.15)
 
353
            if self.config.post_commit() is not None:
 
354
                hooks = self.config.post_commit().split(' ')
 
355
                # this would be nicer with twisted.python.reflect.namedAny
 
356
                for hook in hooks:
 
357
                    result = eval(hook + '(branch, rev_id)',
 
358
                                  {'branch':self.branch,
 
359
                                   'bzrlib':bzrlib,
 
360
                                   'rev_id':self.rev_id})
 
361
            # new style commit hooks:
 
362
            if not self.bound_branch:
 
363
                hook_master = self.branch
 
364
                hook_local = None
 
365
            else:
 
366
                hook_master = self.master_branch
 
367
                hook_local = self.branch
 
368
            # With bound branches, when the master is behind the local branch,
 
369
            # the 'old_revno' and old_revid values here are incorrect.
 
370
            # XXX: FIXME ^. RBC 20060206
 
371
            if self.parents:
 
372
                old_revid = self.parents[0]
 
373
            else:
 
374
                old_revid = bzrlib.revision.NULL_REVISION
 
375
            for hook in Branch.hooks['post_commit']:
 
376
                hook(hook_local, hook_master, old_revno, old_revid, new_revno,
 
377
                    self.rev_id)
 
378
            self._emit_progress_update()
343
379
        finally:
344
380
            self._cleanup()
345
381
        return self.rev_id
441
477
        self.master_branch.lock_write()
442
478
        self.master_locked = True
443
479
 
444
 
    def _check_out_of_date_tree(self):
445
 
        """Check that the working tree is up to date.
446
 
 
447
 
        :return: old_revision_number,new_revision_number tuple
448
 
        """
449
 
        try:
450
 
            first_tree_parent = self.work_tree.get_parent_ids()[0]
451
 
        except IndexError:
452
 
            # if there are no parents, treat our parent as 'None'
453
 
            # this is so that we still consider the master branch
454
 
            # - in a checkout scenario the tree may have no
455
 
            # parents but the branch may do.
456
 
            first_tree_parent = bzrlib.revision.NULL_REVISION
457
 
        old_revno, master_last = self.master_branch.last_revision_info()
458
 
        if master_last != first_tree_parent:
459
 
            if master_last != bzrlib.revision.NULL_REVISION:
460
 
                raise errors.OutOfDateTree(self.work_tree)
461
 
        if self.branch.repository.has_revision(first_tree_parent):
462
 
            new_revno = old_revno + 1
463
 
        else:
464
 
            # ghost parents never appear in revision history.
465
 
            new_revno = 1
466
 
        return old_revno,new_revno
467
 
 
468
 
    def _process_hooks(self, old_revno, new_revno):
469
 
        """Process any registered commit hooks."""
470
 
        # Process the post commit hooks, if any
471
 
        self._emit_progress_set_stage("Running post commit hooks")
472
 
        # old style commit hooks - should be deprecated ? (obsoleted in
473
 
        # 0.15)
474
 
        if self.config.post_commit() is not None:
475
 
            hooks = self.config.post_commit().split(' ')
476
 
            # this would be nicer with twisted.python.reflect.namedAny
477
 
            for hook in hooks:
478
 
                result = eval(hook + '(branch, rev_id)',
479
 
                              {'branch':self.branch,
480
 
                               'bzrlib':bzrlib,
481
 
                               'rev_id':self.rev_id})
482
 
        # new style commit hooks:
483
 
        if not self.bound_branch:
484
 
            hook_master = self.branch
485
 
            hook_local = None
486
 
        else:
487
 
            hook_master = self.master_branch
488
 
            hook_local = self.branch
489
 
        # With bound branches, when the master is behind the local branch,
490
 
        # the 'old_revno' and old_revid values here are incorrect.
491
 
        # XXX: FIXME ^. RBC 20060206
492
 
        if self.parents:
493
 
            old_revid = self.parents[0]
494
 
        else:
495
 
            old_revid = bzrlib.revision.NULL_REVISION
496
 
        for hook in Branch.hooks['post_commit']:
497
 
            # show the running hook in the progress bar. As hooks may
498
 
            # end up doing nothing (e.g. because they are not configured by
499
 
            # the user) this is still showing progress, not showing overall
500
 
            # actions - its up to each plugin to show a UI if it want's to
501
 
            # (such as 'Emailing diff to foo@example.com').
502
 
            self.pb_stage_name = "Running post commit hooks [%s]" % \
503
 
                Branch.hooks.get_hook_name(hook)
504
 
            self._emit_progress()
505
 
            if 'hooks' in debug.debug_flags:
506
 
                mutter("Invoking commit hook: %r", hook)
507
 
            hook(hook_local, hook_master, old_revno, old_revid, new_revno,
508
 
                self.rev_id)
509
 
 
510
480
    def _cleanup(self):
511
481
        """Cleanup any open locks, progress bars etc."""
512
482
        cleanups = [self._cleanup_bound_branch,
575
545
            else:
576
546
                mutter('commit parent ghost revision {%s}', revision)
577
547
 
578
 
    def _update_builder_with_changes(self):
579
 
        """Update the commit builder with the data about what has changed.
580
 
        """
581
 
        # Build the revision inventory.
582
 
        #
583
 
        # This starts by creating a new empty inventory. Depending on
584
 
        # which files are selected for commit, and what is present in the
585
 
        # current tree, the new inventory is populated. inventory entries 
586
 
        # which are candidates for modification have their revision set to
587
 
        # None; inventory entries that are carried over untouched have their
588
 
        # revision set to their prior value.
589
 
        #
 
548
    def _remove_deleted(self):
 
549
        """Remove deleted files from the working inventories.
 
550
 
 
551
        This is done prior to taking the working inventory as the
 
552
        basis for the new committed inventory.
 
553
 
 
554
        This returns true if any files
 
555
        *that existed in the basis inventory* were deleted.
 
556
        Files that were added and deleted
 
557
        in the working copy don't matter.
 
558
        """
 
559
        specific = self.specific_files
 
560
        deleted_ids = []
 
561
        deleted_paths = set()
 
562
        for path, ie in self.work_inv.iter_entries():
 
563
            if is_inside_any(deleted_paths, path):
 
564
                # The tree will delete the required ids recursively.
 
565
                continue
 
566
            if specific and not is_inside_any(specific, path):
 
567
                continue
 
568
            if not self.work_tree.has_filename(path):
 
569
                deleted_paths.add(path)
 
570
                self.reporter.missing(path)
 
571
                deleted_ids.append(ie.file_id)
 
572
        self.work_tree.unversion(deleted_ids)
 
573
 
 
574
    def _populate_new_inv(self):
 
575
        """Build revision inventory.
 
576
 
 
577
        This creates a new empty inventory. Depending on
 
578
        which files are selected for commit, and what is present in the
 
579
        current tree, the new inventory is populated. inventory entries 
 
580
        which are candidates for modification have their revision set to
 
581
        None; inventory entries that are carried over untouched have their
 
582
        revision set to their prior value.
 
583
        """
590
584
        # ESEPARATIONOFCONCERNS: this function is diffing and using the diff
591
585
        # results to create a new inventory at the same time, which results
592
586
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
593
587
        # ADHB 11-07-2006
594
 
 
595
 
        specific_files = self.specific_files
596
 
        mutter("Selecting files for commit with filter %s", specific_files)
597
 
        work_inv = self.work_tree.inventory
598
 
        assert work_inv.root is not None
599
 
        self.pb_entries_total = len(work_inv)
600
 
 
601
 
        # Check and warn about old CommitBuilders
602
 
        entries = work_inv.iter_entries()
 
588
        mutter("Selecting files for commit with filter %s", self.specific_files)
 
589
        assert self.work_inv.root is not None
 
590
        entries = self.work_inv.iter_entries()
603
591
        if not self.builder.record_root_entry:
604
592
            symbol_versioning.warn('CommitBuilders should support recording'
605
593
                ' the root entry as of bzr 0.10.', DeprecationWarning, 
606
594
                stacklevel=1)
607
595
            self.builder.new_inventory.add(self.basis_inv.root.copy())
608
596
            entries.next()
609
 
 
610
 
        deleted_ids = []
611
 
        deleted_paths = set()
 
597
            self._emit_progress_update()
612
598
        for path, new_ie in entries:
613
 
            self._emit_progress_next_entry()
 
599
            self._emit_progress_update()
614
600
            file_id = new_ie.file_id
615
 
 
616
 
            # Skip files that have been deleted from the working tree.
617
 
            # The deleted files/directories are also recorded so they
618
 
            # can be explicitly unversioned later. Note that when a
619
 
            # filter of specific files is given, we must only skip/record
620
 
            # deleted files matching that filter.
621
 
            if is_inside_any(deleted_paths, path):
622
 
                continue
623
 
            if not specific_files or is_inside_any(specific_files, path):
624
 
                if not self.work_tree.has_filename(path):
625
 
                    deleted_paths.add(path)
626
 
                    self.reporter.missing(path)
627
 
                    deleted_ids.append(file_id)
628
 
                    continue
629
601
            try:
630
602
                kind = self.work_tree.kind(file_id)
631
603
                if kind == 'tree-reference' and self.recursive == 'down':
659
631
            except errors.NoSuchFile:
660
632
                pass
661
633
            # mutter('check %s {%s}', path, file_id)
662
 
            if (not specific_files or 
663
 
                is_inside_or_parent_of_any(specific_files, path)):
 
634
            if (not self.specific_files or 
 
635
                is_inside_or_parent_of_any(self.specific_files, path)):
664
636
                    # mutter('%s selected for commit', path)
665
637
                    ie = new_ie.copy()
666
638
                    ie.revision = None
687
659
            else:
688
660
                self.reporter.snapshot_change(change, path)
689
661
 
690
 
        # Unversion IDs that were found to be deleted
691
 
        self.work_tree.unversion(deleted_ids)
692
 
 
693
 
        # If specific files/directories were nominated, it is possible
694
 
        # that some data from outside those needs to be preserved from
695
 
        # the basis tree. For example, if a file x is moved from out of
696
 
        # directory foo into directory bar and the user requests
697
 
        # ``commit foo``, then information about bar/x must also be
698
 
        # recorded.
699
 
        if specific_files:
700
 
            for path, new_ie in self.basis_inv.iter_entries():
701
 
                if new_ie.file_id in work_inv:
702
 
                    continue
703
 
                if is_inside_any(specific_files, path):
704
 
                    continue
705
 
                ie = new_ie.copy()
706
 
                ie.revision = None
707
 
                self.builder.record_entry_contents(ie, self.parent_invs, path,
708
 
                                                   self.basis_tree)
709
 
 
710
 
        # Report what was deleted. We could skip this when no deletes are
711
 
        # detected to gain a performance win, but it arguably serves as a
712
 
        # 'safety check' by informing the user whenever anything disappears.
 
662
        if not self.specific_files:
 
663
            return
 
664
 
 
665
        # ignore removals that don't match filespec
 
666
        for path, new_ie in self.basis_inv.iter_entries():
 
667
            if new_ie.file_id in self.work_inv:
 
668
                continue
 
669
            if is_inside_any(self.specific_files, path):
 
670
                continue
 
671
            ie = new_ie.copy()
 
672
            ie.revision = None
 
673
            self.builder.record_entry_contents(ie, self.parent_invs, path,
 
674
                                               self.basis_tree)
 
675
 
 
676
    def _emit_progress_update(self):
 
677
        """Emit an update to the progress bar."""
 
678
        self.pb.update("Committing", self.pb_count, self.pb_total)
 
679
        self.pb_count += 1
 
680
 
 
681
    def _report_deletes(self):
713
682
        for path, ie in self.basis_inv.iter_entries():
714
683
            if ie.file_id not in self.builder.new_inventory:
715
684
                self.reporter.deleted(path)
716
685
 
717
 
    def _emit_progress_set_stage(self, name, show_entries=False):
718
 
        """Set the progress stage and emit an update to the progress bar."""
719
 
        self.pb_stage_name = name
720
 
        self.pb_stage_count += 1
721
 
        self.pb_entries_show = show_entries
722
 
        if show_entries:
723
 
            self.pb_entries_count = 0
724
 
            self.pb_entries_total = '?'
725
 
        self._emit_progress()
726
 
 
727
 
    def _emit_progress_next_entry(self):
728
 
        """Emit an update to the progress bar and increment the file count."""
729
 
        self.pb_entries_count += 1
730
 
        self._emit_progress()
731
 
 
732
 
    def _emit_progress(self):
733
 
        if self.pb_entries_show:
734
 
            text = "%s [Entry %d/%s] - Stage" % (self.pb_stage_name,
735
 
                self.pb_entries_count,str(self.pb_entries_total))
736
 
        else:
737
 
            text = "%s - Stage" % (self.pb_stage_name)
738
 
        self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
739
686