~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: Robert Collins
  • Date: 2009-04-24 00:45:11 UTC
  • mto: This revision was merged to the branch mainline in revision 4304.
  • Revision ID: robertc@robertcollins.net-20090424004511-8oszlwmvehlqwrla
Start building up a BzrDir.initialize_ex verb for the smart server.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 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
49
49
# TODO: Change the parameter 'rev_id' to 'revision_id' to be consistent with
50
50
# the rest of the code; add a deprecation of the old name.
51
51
 
 
52
import os
 
53
import re
 
54
import sys
 
55
import time
 
56
 
 
57
from cStringIO import StringIO
 
58
 
52
59
from bzrlib import (
53
60
    debug,
54
61
    errors,
55
62
    revision,
56
63
    trace,
57
64
    tree,
 
65
    xml_serializer,
58
66
    )
59
67
from bzrlib.branch import Branch
60
 
from bzrlib.cleanup import OperationWithCleanups
61
68
import bzrlib.config
62
69
from bzrlib.errors import (BzrError, PointlessCommit,
63
70
                           ConflictsInTree,
64
71
                           StrictCommitFailed
65
72
                           )
66
73
from bzrlib.osutils import (get_user_encoding,
67
 
                            is_inside_any,
 
74
                            kind_marker, isdir,isfile, is_inside_any,
 
75
                            is_inside_or_parent_of_any,
68
76
                            minimum_path_selection,
 
77
                            quotefn, sha_file, split_lines,
69
78
                            splitpath,
70
79
                            )
71
 
from bzrlib.trace import mutter, note, is_quiet
 
80
from bzrlib.testament import Testament
 
81
from bzrlib.trace import mutter, note, warning, is_quiet
72
82
from bzrlib.inventory import Inventory, InventoryEntry, make_entry
73
83
from bzrlib import symbol_versioning
 
84
from bzrlib.symbol_versioning import (deprecated_passed,
 
85
        deprecated_function,
 
86
        DEPRECATED_PARAMETER)
 
87
from bzrlib.workingtree import WorkingTree
74
88
from bzrlib.urlutils import unescape_for_display
75
89
import bzrlib.ui
76
90
 
94
108
    def deleted(self, path):
95
109
        pass
96
110
 
 
111
    def escaped(self, escape_count, message):
 
112
        pass
 
113
 
97
114
    def missing(self, path):
98
115
        pass
99
116
 
132
149
 
133
150
    def completed(self, revno, rev_id):
134
151
        self._note('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)
141
152
 
142
153
    def deleted(self, path):
143
154
        self._note('deleted %s', path)
144
155
 
 
156
    def escaped(self, escape_count, message):
 
157
        self._note("replaced %d control characters in message", escape_count)
 
158
 
145
159
    def missing(self, path):
146
160
        self._note('missing %s', path)
147
161
 
196
210
        """Commit working copy as a new revision.
197
211
 
198
212
        :param message: the commit message (it or message_callback is required)
199
 
        :param message_callback: A callback: message = message_callback(cmt_obj)
200
213
 
201
214
        :param timestamp: if not None, seconds-since-epoch for a
202
215
            postdated/predated commit.
203
216
 
204
 
        :param specific_files: If not None, commit only those files. An empty
205
 
            list means 'commit no files'.
 
217
        :param specific_files: If true, commit only those files.
206
218
 
207
219
        :param rev_id: If set, use this as the new revision id.
208
220
            Useful for test or import commands that need to tightly
226
238
            commit. Pending changes to excluded files will be ignored by the
227
239
            commit.
228
240
        """
229
 
        operation = OperationWithCleanups(self._commit)
230
 
        self.revprops = revprops or {}
231
 
        # XXX: Can be set on __init__ or passed in - this is a bit ugly.
232
 
        self.config = config or self.config
233
 
        return operation.run(
234
 
               message=message,
235
 
               timestamp=timestamp,
236
 
               timezone=timezone,
237
 
               committer=committer,
238
 
               specific_files=specific_files,
239
 
               rev_id=rev_id,
240
 
               allow_pointless=allow_pointless,
241
 
               strict=strict,
242
 
               verbose=verbose,
243
 
               working_tree=working_tree,
244
 
               local=local,
245
 
               reporter=reporter,
246
 
               message_callback=message_callback,
247
 
               recursive=recursive,
248
 
               exclude=exclude,
249
 
               possible_master_transports=possible_master_transports)
250
 
 
251
 
    def _commit(self, operation, message, timestamp, timezone, committer,
252
 
            specific_files, rev_id, allow_pointless, strict, verbose,
253
 
            working_tree, local, reporter, message_callback, recursive,
254
 
            exclude, possible_master_transports):
255
241
        mutter('preparing to commit')
256
242
 
257
243
        if working_tree is None:
280
266
            self.exclude = []
281
267
        self.local = local
282
268
        self.master_branch = None
 
269
        self.master_locked = False
283
270
        self.recursive = recursive
284
271
        self.rev_id = None
285
 
        # self.specific_files is None to indicate no filter, or any iterable to
286
 
        # indicate a filter - [] means no files at all, as per iter_changes.
287
272
        if specific_files is not None:
288
273
            self.specific_files = sorted(
289
274
                minimum_path_selection(specific_files))
291
276
            self.specific_files = None
292
277
            
293
278
        self.allow_pointless = allow_pointless
 
279
        self.revprops = revprops
294
280
        self.message_callback = message_callback
295
281
        self.timestamp = timestamp
296
282
        self.timezone = timezone
299
285
        self.verbose = verbose
300
286
 
301
287
        self.work_tree.lock_write()
302
 
        operation.add_cleanup(self.work_tree.unlock)
303
288
        self.parents = self.work_tree.get_parent_ids()
304
289
        # We can use record_iter_changes IFF iter_changes is compatible with
305
290
        # the command line parameters, and the repository has fast delta
306
291
        # generation. See bug 347649.
307
292
        self.use_record_iter_changes = (
 
293
            not self.specific_files and
308
294
            not self.exclude and 
309
295
            not self.branch.repository._format.supports_tree_reference and
310
296
            (self.branch.repository._format.fast_deltas or
311
297
             len(self.parents) < 2))
312
298
        self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
313
 
        operation.add_cleanup(self.pb.finished)
314
299
        self.basis_revid = self.work_tree.last_revision()
315
300
        self.basis_tree = self.work_tree.basis_tree()
316
301
        self.basis_tree.lock_read()
317
 
        operation.add_cleanup(self.basis_tree.unlock)
318
 
        # Cannot commit with conflicts present.
319
 
        if len(self.work_tree.conflicts()) > 0:
320
 
            raise ConflictsInTree
321
 
 
322
 
        # Setup the bound branch variables as needed.
323
 
        self._check_bound_branch(operation, possible_master_transports)
324
 
 
325
 
        # Check that the working tree is up to date
326
 
        old_revno, new_revno = self._check_out_of_date_tree()
327
 
 
328
 
        # Complete configuration setup
329
 
        if reporter is not None:
330
 
            self.reporter = reporter
331
 
        elif self.reporter is None:
332
 
            self.reporter = self._select_reporter()
333
 
        if self.config is None:
334
 
            self.config = self.branch.get_config()
335
 
 
336
 
        self._set_specific_file_ids()
337
 
 
338
 
        # Setup the progress bar. As the number of files that need to be
339
 
        # committed in unknown, progress is reported as stages.
340
 
        # We keep track of entries separately though and include that
341
 
        # information in the progress bar during the relevant stages.
342
 
        self.pb_stage_name = ""
343
 
        self.pb_stage_count = 0
344
 
        self.pb_stage_total = 5
345
 
        if self.bound_branch:
346
 
            self.pb_stage_total += 1
347
 
        self.pb.show_pct = False
348
 
        self.pb.show_spinner = False
349
 
        self.pb.show_eta = False
350
 
        self.pb.show_count = True
351
 
        self.pb.show_bar = True
352
 
 
353
 
        self._gather_parents()
354
 
        # After a merge, a selected file commit is not supported.
355
 
        # See 'bzr help merge' for an explanation as to why.
356
 
        if len(self.parents) > 1 and self.specific_files is not None:
357
 
            raise errors.CannotCommitSelectedFileMerge(self.specific_files)
358
 
        # Excludes are a form of selected file commit.
359
 
        if len(self.parents) > 1 and self.exclude:
360
 
            raise errors.CannotCommitSelectedFileMerge(self.exclude)
361
 
 
362
 
        # Collect the changes
363
 
        self._set_progress_stage("Collecting changes", counter=True)
364
 
        self.builder = self.branch.get_commit_builder(self.parents,
365
 
            self.config, timestamp, timezone, committer, self.revprops, rev_id)
366
 
 
367
302
        try:
368
 
            self.builder.will_record_deletes()
369
 
            # find the location being committed to
370
 
            if self.bound_branch:
371
 
                master_location = self.master_branch.base
372
 
            else:
373
 
                master_location = self.branch.base
374
 
 
375
 
            # report the start of the commit
376
 
            self.reporter.started(new_revno, self.rev_id, master_location)
377
 
 
378
 
            self._update_builder_with_changes()
379
 
            self._check_pointless()
380
 
 
381
 
            # TODO: Now the new inventory is known, check for conflicts.
382
 
            # ADHB 2006-08-08: If this is done, populate_new_inv should not add
383
 
            # weave lines, because nothing should be recorded until it is known
384
 
            # that commit will succeed.
385
 
            self._set_progress_stage("Saving data locally")
386
 
            self.builder.finish_inventory()
387
 
 
388
 
            # Prompt the user for a commit message if none provided
389
 
            message = message_callback(self)
390
 
            self.message = message
391
 
 
392
 
            # Add revision data to the local branch
393
 
            self.rev_id = self.builder.commit(self.message)
394
 
 
395
 
        except Exception, e:
396
 
            mutter("aborting commit write group because of exception:")
397
 
            trace.log_exception_quietly()
398
 
            note("aborting commit write group: %r" % (e,))
399
 
            self.builder.abort()
400
 
            raise
401
 
 
402
 
        self._process_pre_hooks(old_revno, new_revno)
403
 
 
404
 
        # Upload revision data to the master.
405
 
        # this will propagate merged revisions too if needed.
406
 
        if self.bound_branch:
407
 
            self._set_progress_stage("Uploading data to master branch")
408
 
            # 'commit' to the master first so a timeout here causes the
409
 
            # local branch to be out of date
410
 
            self.master_branch.import_last_revision_info(
411
 
                self.branch.repository, new_revno, self.rev_id)
412
 
 
413
 
        # and now do the commit locally.
414
 
        self.branch.set_last_revision_info(new_revno, self.rev_id)
415
 
 
416
 
        # Make the working tree be up to date with the branch. This
417
 
        # includes automatic changes scheduled to be made to the tree, such
418
 
        # as updating its basis and unversioning paths that were missing.
419
 
        self.work_tree.unversion(self.deleted_ids)
420
 
        self._set_progress_stage("Updating the working tree")
421
 
        self.work_tree.update_basis_by_delta(self.rev_id,
422
 
             self.builder.get_basis_delta())
423
 
        self.reporter.completed(new_revno, self.rev_id)
424
 
        self._process_post_hooks(old_revno, new_revno)
 
303
            # Cannot commit with conflicts present.
 
304
            if len(self.work_tree.conflicts()) > 0:
 
305
                raise ConflictsInTree
 
306
 
 
307
            # Setup the bound branch variables as needed.
 
308
            self._check_bound_branch(possible_master_transports)
 
309
 
 
310
            # Check that the working tree is up to date
 
311
            old_revno, new_revno = self._check_out_of_date_tree()
 
312
 
 
313
            # Complete configuration setup
 
314
            if reporter is not None:
 
315
                self.reporter = reporter
 
316
            elif self.reporter is None:
 
317
                self.reporter = self._select_reporter()
 
318
            if self.config is None:
 
319
                self.config = self.branch.get_config()
 
320
 
 
321
            self._set_specific_file_ids()
 
322
 
 
323
            # Setup the progress bar. As the number of files that need to be
 
324
            # committed in unknown, progress is reported as stages.
 
325
            # We keep track of entries separately though and include that
 
326
            # information in the progress bar during the relevant stages.
 
327
            self.pb_stage_name = ""
 
328
            self.pb_stage_count = 0
 
329
            self.pb_stage_total = 5
 
330
            if self.bound_branch:
 
331
                self.pb_stage_total += 1
 
332
            self.pb.show_pct = False
 
333
            self.pb.show_spinner = False
 
334
            self.pb.show_eta = False
 
335
            self.pb.show_count = True
 
336
            self.pb.show_bar = True
 
337
 
 
338
            self._gather_parents()
 
339
            # After a merge, a selected file commit is not supported.
 
340
            # See 'bzr help merge' for an explanation as to why.
 
341
            if len(self.parents) > 1 and self.specific_files:
 
342
                raise errors.CannotCommitSelectedFileMerge(self.specific_files)
 
343
            # Excludes are a form of selected file commit.
 
344
            if len(self.parents) > 1 and self.exclude:
 
345
                raise errors.CannotCommitSelectedFileMerge(self.exclude)
 
346
 
 
347
            # Collect the changes
 
348
            self._set_progress_stage("Collecting changes", counter=True)
 
349
            self.builder = self.branch.get_commit_builder(self.parents,
 
350
                self.config, timestamp, timezone, committer, revprops, rev_id)
 
351
 
 
352
            try:
 
353
                self.builder.will_record_deletes()
 
354
                # find the location being committed to
 
355
                if self.bound_branch:
 
356
                    master_location = self.master_branch.base
 
357
                else:
 
358
                    master_location = self.branch.base
 
359
 
 
360
                # report the start of the commit
 
361
                self.reporter.started(new_revno, self.rev_id, master_location)
 
362
 
 
363
                self._update_builder_with_changes()
 
364
                self._check_pointless()
 
365
 
 
366
                # TODO: Now the new inventory is known, check for conflicts.
 
367
                # ADHB 2006-08-08: If this is done, populate_new_inv should not add
 
368
                # weave lines, because nothing should be recorded until it is known
 
369
                # that commit will succeed.
 
370
                self._set_progress_stage("Saving data locally")
 
371
                self.builder.finish_inventory()
 
372
 
 
373
                # Prompt the user for a commit message if none provided
 
374
                message = message_callback(self)
 
375
                self.message = message
 
376
                self._escape_commit_message()
 
377
 
 
378
                # Add revision data to the local branch
 
379
                self.rev_id = self.builder.commit(self.message)
 
380
 
 
381
            except Exception, e:
 
382
                mutter("aborting commit write group because of exception:")
 
383
                trace.log_exception_quietly()
 
384
                note("aborting commit write group: %r" % (e,))
 
385
                self.builder.abort()
 
386
                raise
 
387
 
 
388
            self._process_pre_hooks(old_revno, new_revno)
 
389
 
 
390
            # Upload revision data to the master.
 
391
            # this will propagate merged revisions too if needed.
 
392
            if self.bound_branch:
 
393
                self._set_progress_stage("Uploading data to master branch")
 
394
                # 'commit' to the master first so a timeout here causes the
 
395
                # local branch to be out of date
 
396
                self.master_branch.import_last_revision_info(
 
397
                    self.branch.repository, new_revno, self.rev_id)
 
398
 
 
399
            # and now do the commit locally.
 
400
            self.branch.set_last_revision_info(new_revno, self.rev_id)
 
401
 
 
402
            # Make the working tree up to date with the branch
 
403
            self._set_progress_stage("Updating the working tree")
 
404
            self.work_tree.update_basis_by_delta(self.rev_id,
 
405
                 self.builder.get_basis_delta())
 
406
            self.reporter.completed(new_revno, self.rev_id)
 
407
            self._process_post_hooks(old_revno, new_revno)
 
408
        finally:
 
409
            self._cleanup()
425
410
        return self.rev_id
426
411
 
427
412
    def _select_reporter(self):
449
434
            return
450
435
        raise PointlessCommit()
451
436
 
452
 
    def _check_bound_branch(self, operation, possible_master_transports=None):
 
437
    def _check_bound_branch(self, possible_master_transports=None):
453
438
        """Check to see if the local branch is bound.
454
439
 
455
440
        If it is bound, then most of the commit will actually be
490
475
        # so grab the lock
491
476
        self.bound_branch = self.branch
492
477
        self.master_branch.lock_write()
493
 
        operation.add_cleanup(self.master_branch.unlock)
 
478
        self.master_locked = True
494
479
 
495
480
    def _check_out_of_date_tree(self):
496
481
        """Check that the working tree is up to date.
581
566
                     old_revno, old_revid, new_revno, self.rev_id,
582
567
                     tree_delta, future_tree)
583
568
 
 
569
    def _cleanup(self):
 
570
        """Cleanup any open locks, progress bars etc."""
 
571
        cleanups = [self._cleanup_bound_branch,
 
572
                    self.basis_tree.unlock,
 
573
                    self.work_tree.unlock,
 
574
                    self.pb.finished]
 
575
        found_exception = None
 
576
        for cleanup in cleanups:
 
577
            try:
 
578
                cleanup()
 
579
            # we want every cleanup to run no matter what.
 
580
            # so we have a catchall here, but we will raise the
 
581
            # last encountered exception up the stack: and
 
582
            # typically this will be useful enough.
 
583
            except Exception, e:
 
584
                found_exception = e
 
585
        if found_exception is not None:
 
586
            # don't do a plan raise, because the last exception may have been
 
587
            # trashed, e is our sure-to-work exception even though it loses the
 
588
            # full traceback. XXX: RBC 20060421 perhaps we could check the
 
589
            # exc_info and if its the same one do a plain raise otherwise
 
590
            # 'raise e' as we do now.
 
591
            raise e
 
592
 
 
593
    def _cleanup_bound_branch(self):
 
594
        """Executed at the end of a try/finally to cleanup a bound branch.
 
595
 
 
596
        If the branch wasn't bound, this is a no-op.
 
597
        If it was, it resents self.branch to the local branch, instead
 
598
        of being the master.
 
599
        """
 
600
        if not self.bound_branch:
 
601
            return
 
602
        if self.master_locked:
 
603
            self.master_branch.unlock()
 
604
 
 
605
    def _escape_commit_message(self):
 
606
        """Replace xml-incompatible control characters."""
 
607
        # FIXME: RBC 20060419 this should be done by the revision
 
608
        # serialiser not by commit. Then we can also add an unescaper
 
609
        # in the deserializer and start roundtripping revision messages
 
610
        # precisely. See repository_implementations/test_repository.py
 
611
        self.message, escape_count = xml_serializer.escape_invalid_chars(
 
612
            self.message)
 
613
        if escape_count:
 
614
            self.reporter.escaped(escape_count, self.message)
 
615
 
584
616
    def _gather_parents(self):
585
617
        """Record the parents of a merge for merge detection."""
586
618
        # TODO: Make sure that this list doesn't contain duplicate
601
633
        """Update the commit builder with the data about what has changed.
602
634
        """
603
635
        exclude = self.exclude
604
 
        specific_files = self.specific_files
 
636
        specific_files = self.specific_files or []
605
637
        mutter("Selecting files for commit with filter %s", specific_files)
606
638
 
607
639
        self._check_strict()
608
640
        if self.use_record_iter_changes:
609
 
            iter_changes = self.work_tree.iter_changes(self.basis_tree,
610
 
                specific_files=specific_files)
 
641
            iter_changes = self.work_tree.iter_changes(self.basis_tree)
611
642
            iter_changes = self._filter_iter_changes(iter_changes)
612
643
            for file_id, path, fs_hash in self.builder.record_iter_changes(
613
644
                self.work_tree, self.basis_revid, iter_changes):
666
697
                            reporter.snapshot_change('modified', new_path)
667
698
            self._next_progress_entry()
668
699
        # Unversion IDs that were found to be deleted
669
 
        self.deleted_ids = deleted_ids
 
700
        self.work_tree.unversion(deleted_ids)
670
701
 
671
702
    def _record_unselected(self):
672
703
        # If specific files are selected, then all un-selected files must be
785
816
                # _update_builder_with_changes.
786
817
                continue
787
818
            content_summary = self.work_tree.path_content_summary(path)
788
 
            kind = content_summary[0]
789
819
            # Note that when a filter of specific files is given, we must only
790
820
            # skip/record deleted files matching that filter.
791
821
            if not specific_files or is_inside_any(specific_files, path):
792
 
                if kind == 'missing':
 
822
                if content_summary[0] == 'missing':
793
823
                    if not deleted_paths:
794
824
                        # path won't have been split yet.
795
825
                        path_segments = splitpath(path)
802
832
                    continue
803
833
            # TODO: have the builder do the nested commit just-in-time IF and
804
834
            # only if needed.
805
 
            if kind == 'tree-reference':
 
835
            if content_summary[0] == 'tree-reference':
806
836
                # enforce repository nested tree policy.
807
837
                if (not self.work_tree.supports_tree_reference() or
808
838
                    # repository does not support it either.
809
839
                    not self.branch.repository._format.supports_tree_reference):
810
 
                    kind = 'directory'
811
 
                    content_summary = (kind, None, None, None)
812
 
                elif self.recursive == 'down':
 
840
                    content_summary = ('directory',) + content_summary[1:]
 
841
            kind = content_summary[0]
 
842
            # TODO: specific_files filtering before nested tree processing
 
843
            if kind == 'tree-reference':
 
844
                if self.recursive == 'down':
813
845
                    nested_revision_id = self._commit_nested_tree(
814
846
                        file_id, path)
815
 
                    content_summary = (kind, None, None, nested_revision_id)
 
847
                    content_summary = content_summary[:3] + (
 
848
                        nested_revision_id,)
816
849
                else:
817
 
                    nested_revision_id = self.work_tree.get_reference_revision(file_id)
818
 
                    content_summary = (kind, None, None, nested_revision_id)
 
850
                    content_summary = content_summary[:3] + (
 
851
                        self.work_tree.get_reference_revision(file_id),)
819
852
 
820
853
            # Record an entry for this item
821
854
            # Note: I don't particularly want to have the existing_ie
827
860
                content_summary)
828
861
 
829
862
        # Unversion IDs that were found to be deleted
830
 
        self.deleted_ids = deleted_ids
 
863
        self.work_tree.unversion(deleted_ids)
831
864
 
832
865
    def _commit_nested_tree(self, file_id, path):
833
866
        "Commit a nested tree."