/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

Use global osutils, otherwise it creates a local var.

Which works, but causes us to run the import on every call.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2011 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
 
 
18
# The newly committed revision is going to have a shape corresponding
 
19
# to that of the working tree.  Files that are not in the
 
20
# working tree and that were in the predecessor are reported as
 
21
# removed --- this can include files that were either removed from the
 
22
# inventory or deleted in the working tree.  If they were only
 
23
# deleted from disk, they are removed from the working inventory.
 
24
 
 
25
# We then consider the remaining entries, which will be in the new
 
26
# version.  Directory entries are simply copied across.  File entries
 
27
# must be checked to see if a new version of the file should be
 
28
# recorded.  For each parent revision tree, we check to see what
 
29
# version of the file was present.  If the file was present in at
 
30
# least one tree, and if it was the same version in all the trees,
 
31
# then we can just refer to that version.  Otherwise, a new version
 
32
# representing the merger of the file versions must be added.
 
33
 
 
34
# TODO: Update hashcache before and after - or does the WorkingTree
 
35
# look after that?
 
36
 
 
37
# TODO: Rather than mashing together the ancestry and storing it back,
 
38
# perhaps the weave should have single method which does it all in one
 
39
# go, avoiding a lot of redundant work.
 
40
 
 
41
# TODO: Perhaps give a warning if one of the revisions marked as
 
42
# merged is already in the ancestry, and then don't record it as a
 
43
# distinct parent.
 
44
 
 
45
# TODO: If the file is newly merged but unchanged from the version it
 
46
# merges from, then it should still be reported as newly added
 
47
# relative to the basis revision.
 
48
 
 
49
# TODO: Change the parameter 'rev_id' to 'revision_id' to be consistent with
 
50
# the rest of the code; add a deprecation of the old name.
 
51
 
 
52
from bzrlib import (
 
53
    debug,
 
54
    errors,
 
55
    revision,
 
56
    trace,
 
57
    tree,
 
58
    ui,
 
59
    )
 
60
from bzrlib.branch import Branch
 
61
from bzrlib.cleanup import OperationWithCleanups
 
62
import bzrlib.config
 
63
from bzrlib.errors import (BzrError, PointlessCommit,
 
64
                           ConflictsInTree,
 
65
                           StrictCommitFailed
 
66
                           )
 
67
from bzrlib.osutils import (get_user_encoding,
 
68
                            is_inside_any,
 
69
                            minimum_path_selection,
 
70
                            splitpath,
 
71
                            )
 
72
from bzrlib.trace import mutter, note, is_quiet
 
73
from bzrlib.inventory import Inventory, InventoryEntry, make_entry
 
74
from bzrlib import symbol_versioning
 
75
from bzrlib.urlutils import unescape_for_display
 
76
 
 
77
 
 
78
class NullCommitReporter(object):
 
79
    """I report on progress of a commit."""
 
80
 
 
81
    def started(self, revno, revid, location=None):
 
82
        if location is None:
 
83
            symbol_versioning.warn("As of bzr 1.0 you must pass a location "
 
84
                                   "to started.", DeprecationWarning,
 
85
                                   stacklevel=2)
 
86
        pass
 
87
 
 
88
    def snapshot_change(self, change, path):
 
89
        pass
 
90
 
 
91
    def completed(self, revno, rev_id):
 
92
        pass
 
93
 
 
94
    def deleted(self, path):
 
95
        pass
 
96
 
 
97
    def missing(self, path):
 
98
        pass
 
99
 
 
100
    def renamed(self, change, old_path, new_path):
 
101
        pass
 
102
 
 
103
    def is_verbose(self):
 
104
        return False
 
105
 
 
106
 
 
107
class ReportCommitToLog(NullCommitReporter):
 
108
 
 
109
    def _note(self, format, *args):
 
110
        """Output a message.
 
111
 
 
112
        Subclasses may choose to override this method.
 
113
        """
 
114
        note(format, *args)
 
115
 
 
116
    def snapshot_change(self, change, path):
 
117
        if path == '' and change in ('added', 'modified'):
 
118
            return
 
119
        self._note("%s %s", change, path)
 
120
 
 
121
    def started(self, revno, rev_id, location=None):
 
122
        if location is not None:
 
123
            location = ' to: ' + unescape_for_display(location, 'utf-8')
 
124
        else:
 
125
            # When started was added, location was only made optional by
 
126
            # accident.  Matt Nordhoff 20071129
 
127
            symbol_versioning.warn("As of bzr 1.0 you must pass a location "
 
128
                                   "to started.", DeprecationWarning,
 
129
                                   stacklevel=2)
 
130
            location = ''
 
131
        self._note('Committing%s', location)
 
132
 
 
133
    def completed(self, revno, rev_id):
 
134
        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
 
 
142
    def deleted(self, path):
 
143
        self._note('deleted %s', path)
 
144
 
 
145
    def missing(self, path):
 
146
        self._note('missing %s', path)
 
147
 
 
148
    def renamed(self, change, old_path, new_path):
 
149
        self._note('%s %s => %s', change, old_path, new_path)
 
150
 
 
151
    def is_verbose(self):
 
152
        return True
 
153
 
 
154
 
 
155
class Commit(object):
 
156
    """Task of committing a new revision.
 
157
 
 
158
    This is a MethodObject: it accumulates state as the commit is
 
159
    prepared, and then it is discarded.  It doesn't represent
 
160
    historical revisions, just the act of recording a new one.
 
161
 
 
162
            missing_ids
 
163
            Modified to hold a list of files that have been deleted from
 
164
            the working directory; these should be removed from the
 
165
            working inventory.
 
166
    """
 
167
    def __init__(self,
 
168
                 reporter=None,
 
169
                 config=None):
 
170
        """Create a Commit object.
 
171
 
 
172
        :param reporter: the default reporter to use or None to decide later
 
173
        """
 
174
        self.reporter = reporter
 
175
        self.config = config
 
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
 
213
 
 
214
    def commit(self,
 
215
               message=None,
 
216
               timestamp=None,
 
217
               timezone=None,
 
218
               committer=None,
 
219
               specific_files=None,
 
220
               rev_id=None,
 
221
               allow_pointless=True,
 
222
               strict=False,
 
223
               verbose=False,
 
224
               revprops=None,
 
225
               working_tree=None,
 
226
               local=False,
 
227
               reporter=None,
 
228
               config=None,
 
229
               message_callback=None,
 
230
               recursive='down',
 
231
               exclude=None,
 
232
               possible_master_transports=None):
 
233
        """Commit working copy as a new revision.
 
234
 
 
235
        :param message: the commit message (it or message_callback is required)
 
236
        :param message_callback: A callback: message = message_callback(cmt_obj)
 
237
 
 
238
        :param timestamp: if not None, seconds-since-epoch for a
 
239
            postdated/predated commit.
 
240
 
 
241
        :param specific_files: If not None, commit only those files. An empty
 
242
            list means 'commit no files'.
 
243
 
 
244
        :param rev_id: If set, use this as the new revision id.
 
245
            Useful for test or import commands that need to tightly
 
246
            control what revisions are assigned.  If you duplicate
 
247
            a revision id that exists elsewhere it is your own fault.
 
248
            If null (default), a time/random revision id is generated.
 
249
 
 
250
        :param allow_pointless: If true (default), commit even if nothing
 
251
            has changed and no merges are recorded.
 
252
 
 
253
        :param strict: If true, don't allow a commit if the working tree
 
254
            contains unknown files.
 
255
 
 
256
        :param revprops: Properties for new revision
 
257
        :param local: Perform a local only commit.
 
258
        :param reporter: the reporter to use or None for the default
 
259
        :param verbose: if True and the reporter is not None, report everything
 
260
        :param recursive: If set to 'down', commit in any subtrees that have
 
261
            pending changes of any sort during this commit.
 
262
        :param exclude: None or a list of relative paths to exclude from the
 
263
            commit. Pending changes to excluded files will be ignored by the
 
264
            commit.
 
265
        """
 
266
        operation = OperationWithCleanups(self._commit)
 
267
        self.revprops = revprops or {}
 
268
        # XXX: Can be set on __init__ or passed in - this is a bit ugly.
 
269
        self.config = config or self.config
 
270
        return operation.run(
 
271
               message=message,
 
272
               timestamp=timestamp,
 
273
               timezone=timezone,
 
274
               committer=committer,
 
275
               specific_files=specific_files,
 
276
               rev_id=rev_id,
 
277
               allow_pointless=allow_pointless,
 
278
               strict=strict,
 
279
               verbose=verbose,
 
280
               working_tree=working_tree,
 
281
               local=local,
 
282
               reporter=reporter,
 
283
               message_callback=message_callback,
 
284
               recursive=recursive,
 
285
               exclude=exclude,
 
286
               possible_master_transports=possible_master_transports)
 
287
 
 
288
    def _commit(self, operation, message, timestamp, timezone, committer,
 
289
            specific_files, rev_id, allow_pointless, strict, verbose,
 
290
            working_tree, local, reporter, message_callback, recursive,
 
291
            exclude, possible_master_transports):
 
292
        mutter('preparing to commit')
 
293
 
 
294
        if working_tree is None:
 
295
            raise BzrError("working_tree must be passed into commit().")
 
296
        else:
 
297
            self.work_tree = working_tree
 
298
            self.branch = self.work_tree.branch
 
299
            if getattr(self.work_tree, 'requires_rich_root', lambda: False)():
 
300
                if not self.branch.repository.supports_rich_root():
 
301
                    raise errors.RootNotRich()
 
302
        if message_callback is None:
 
303
            if message is not None:
 
304
                if isinstance(message, str):
 
305
                    message = message.decode(get_user_encoding())
 
306
                message_callback = lambda x: message
 
307
            else:
 
308
                raise BzrError("The message or message_callback keyword"
 
309
                               " parameter is required for commit().")
 
310
 
 
311
        self.bound_branch = None
 
312
        self.any_entries_deleted = False
 
313
        if exclude is not None:
 
314
            self.exclude = sorted(
 
315
                minimum_path_selection(exclude))
 
316
        else:
 
317
            self.exclude = []
 
318
        self.local = local
 
319
        self.master_branch = None
 
320
        self.recursive = recursive
 
321
        self.rev_id = None
 
322
        # self.specific_files is None to indicate no filter, or any iterable to
 
323
        # indicate a filter - [] means no files at all, as per iter_changes.
 
324
        if specific_files is not None:
 
325
            self.specific_files = sorted(
 
326
                minimum_path_selection(specific_files))
 
327
        else:
 
328
            self.specific_files = None
 
329
 
 
330
        self.allow_pointless = allow_pointless
 
331
        self.message_callback = message_callback
 
332
        self.timestamp = timestamp
 
333
        self.timezone = timezone
 
334
        self.committer = committer
 
335
        self.strict = strict
 
336
        self.verbose = verbose
 
337
 
 
338
        self.work_tree.lock_write()
 
339
        operation.add_cleanup(self.work_tree.unlock)
 
340
        self.parents = self.work_tree.get_parent_ids()
 
341
        # We can use record_iter_changes IFF iter_changes is compatible with
 
342
        # the command line parameters, and the repository has fast delta
 
343
        # generation. See bug 347649.
 
344
        self.use_record_iter_changes = (
 
345
            not self.exclude and 
 
346
            not self.branch.repository._format.supports_tree_reference and
 
347
            (self.branch.repository._format.fast_deltas or
 
348
             len(self.parents) < 2))
 
349
        self.pb = ui.ui_factory.nested_progress_bar()
 
350
        operation.add_cleanup(self.pb.finished)
 
351
        self.basis_revid = self.work_tree.last_revision()
 
352
        self.basis_tree = self.work_tree.basis_tree()
 
353
        self.basis_tree.lock_read()
 
354
        operation.add_cleanup(self.basis_tree.unlock)
 
355
        # Cannot commit with conflicts present.
 
356
        if len(self.work_tree.conflicts()) > 0:
 
357
            raise ConflictsInTree
 
358
 
 
359
        # Setup the bound branch variables as needed.
 
360
        self._check_bound_branch(operation, possible_master_transports)
 
361
 
 
362
        # Check that the working tree is up to date
 
363
        old_revno, new_revno = self._check_out_of_date_tree()
 
364
 
 
365
        # Complete configuration setup
 
366
        if reporter is not None:
 
367
            self.reporter = reporter
 
368
        elif self.reporter is None:
 
369
            self.reporter = self._select_reporter()
 
370
        if self.config is None:
 
371
            self.config = self.branch.get_config()
 
372
 
 
373
        self._set_specific_file_ids()
 
374
 
 
375
        # Setup the progress bar. As the number of files that need to be
 
376
        # committed in unknown, progress is reported as stages.
 
377
        # We keep track of entries separately though and include that
 
378
        # information in the progress bar during the relevant stages.
 
379
        self.pb_stage_name = ""
 
380
        self.pb_stage_count = 0
 
381
        self.pb_stage_total = 5
 
382
        if self.bound_branch:
 
383
            # 2 extra stages: "Uploading data to master branch" and "Merging
 
384
            # tags to master branch"
 
385
            self.pb_stage_total += 2
 
386
        self.pb.show_pct = False
 
387
        self.pb.show_spinner = False
 
388
        self.pb.show_eta = False
 
389
        self.pb.show_count = True
 
390
        self.pb.show_bar = True
 
391
 
 
392
        self._gather_parents()
 
393
        # After a merge, a selected file commit is not supported.
 
394
        # See 'bzr help merge' for an explanation as to why.
 
395
        if len(self.parents) > 1 and self.specific_files is not None:
 
396
            raise errors.CannotCommitSelectedFileMerge(self.specific_files)
 
397
        # Excludes are a form of selected file commit.
 
398
        if len(self.parents) > 1 and self.exclude:
 
399
            raise errors.CannotCommitSelectedFileMerge(self.exclude)
 
400
 
 
401
        # Collect the changes
 
402
        self._set_progress_stage("Collecting changes", counter=True)
 
403
        self.builder = self.branch.get_commit_builder(self.parents,
 
404
            self.config, timestamp, timezone, committer, self.revprops, rev_id)
 
405
        if not self.builder.supports_record_entry_contents and self.exclude:
 
406
            self.builder.abort()
 
407
            raise errors.ExcludesUnsupported(self.branch.repository)
 
408
 
 
409
        try:
 
410
            self.builder.will_record_deletes()
 
411
            # find the location being committed to
 
412
            if self.bound_branch:
 
413
                master_location = self.master_branch.base
 
414
            else:
 
415
                master_location = self.branch.base
 
416
 
 
417
            # report the start of the commit
 
418
            self.reporter.started(new_revno, self.rev_id, master_location)
 
419
 
 
420
            self._update_builder_with_changes()
 
421
            self._check_pointless()
 
422
 
 
423
            # TODO: Now the new inventory is known, check for conflicts.
 
424
            # ADHB 2006-08-08: If this is done, populate_new_inv should not add
 
425
            # weave lines, because nothing should be recorded until it is known
 
426
            # that commit will succeed.
 
427
            self._set_progress_stage("Saving data locally")
 
428
            self.builder.finish_inventory()
 
429
 
 
430
            # Prompt the user for a commit message if none provided
 
431
            message = message_callback(self)
 
432
            self.message = message
 
433
 
 
434
            # Add revision data to the local branch
 
435
            self.rev_id = self.builder.commit(self.message)
 
436
 
 
437
        except Exception, e:
 
438
            mutter("aborting commit write group because of exception:")
 
439
            trace.log_exception_quietly()
 
440
            note("aborting commit write group: %r" % (e,))
 
441
            self.builder.abort()
 
442
            raise
 
443
 
 
444
        self._process_pre_hooks(old_revno, new_revno)
 
445
 
 
446
        # Upload revision data to the master.
 
447
        # this will propagate merged revisions too if needed.
 
448
        if self.bound_branch:
 
449
            self._set_progress_stage("Uploading data to master branch")
 
450
            # 'commit' to the master first so a timeout here causes the
 
451
            # local branch to be out of date
 
452
            self.master_branch.import_last_revision_info_and_tags(
 
453
                self.branch, new_revno, self.rev_id)
 
454
 
 
455
        # and now do the commit locally.
 
456
        self.branch.set_last_revision_info(new_revno, self.rev_id)
 
457
 
 
458
        # Merge local tags to remote
 
459
        if self.bound_branch:
 
460
            self._set_progress_stage("Merging tags to master branch")
 
461
            tag_conflicts = self.branch.tags.merge_to(self.master_branch.tags)
 
462
            if tag_conflicts:
 
463
                warning_lines = ['    ' + name for name, _, _ in tag_conflicts]
 
464
                note("Conflicting tags in bound branch:\n" +
 
465
                    "\n".join(warning_lines))
 
466
 
 
467
        # Make the working tree be up to date with the branch. This
 
468
        # includes automatic changes scheduled to be made to the tree, such
 
469
        # as updating its basis and unversioning paths that were missing.
 
470
        self.work_tree.unversion(self.deleted_ids)
 
471
        self._set_progress_stage("Updating the working tree")
 
472
        self.work_tree.update_basis_by_delta(self.rev_id,
 
473
             self.builder.get_basis_delta())
 
474
        self.reporter.completed(new_revno, self.rev_id)
 
475
        self._process_post_hooks(old_revno, new_revno)
 
476
        return self.rev_id
 
477
 
 
478
    def _select_reporter(self):
 
479
        """Select the CommitReporter to use."""
 
480
        if is_quiet():
 
481
            return NullCommitReporter()
 
482
        return ReportCommitToLog()
 
483
 
 
484
    def _check_pointless(self):
 
485
        if self.allow_pointless:
 
486
            return
 
487
        # A merge with no effect on files
 
488
        if len(self.parents) > 1:
 
489
            return
 
490
        # TODO: we could simplify this by using self.builder.basis_delta.
 
491
 
 
492
        # The initial commit adds a root directory, but this in itself is not
 
493
        # a worthwhile commit.
 
494
        if (self.basis_revid == revision.NULL_REVISION and
 
495
            ((self.builder.new_inventory is not None and
 
496
             len(self.builder.new_inventory) == 1) or
 
497
            len(self.builder._basis_delta) == 1)):
 
498
            raise PointlessCommit()
 
499
        if self.builder.any_changes():
 
500
            return
 
501
        raise PointlessCommit()
 
502
 
 
503
    def _check_bound_branch(self, operation, possible_master_transports=None):
 
504
        """Check to see if the local branch is bound.
 
505
 
 
506
        If it is bound, then most of the commit will actually be
 
507
        done using the remote branch as the target branch.
 
508
        Only at the end will the local branch be updated.
 
509
        """
 
510
        if self.local and not self.branch.get_bound_location():
 
511
            raise errors.LocalRequiresBoundBranch()
 
512
 
 
513
        if not self.local:
 
514
            self.master_branch = self.branch.get_master_branch(
 
515
                possible_master_transports)
 
516
 
 
517
        if not self.master_branch:
 
518
            # make this branch the reference branch for out of date checks.
 
519
            self.master_branch = self.branch
 
520
            return
 
521
 
 
522
        # If the master branch is bound, we must fail
 
523
        master_bound_location = self.master_branch.get_bound_location()
 
524
        if master_bound_location:
 
525
            raise errors.CommitToDoubleBoundBranch(self.branch,
 
526
                    self.master_branch, master_bound_location)
 
527
 
 
528
        # TODO: jam 20051230 We could automatically push local
 
529
        #       commits to the remote branch if they would fit.
 
530
        #       But for now, just require remote to be identical
 
531
        #       to local.
 
532
 
 
533
        # Make sure the local branch is identical to the master
 
534
        master_info = self.master_branch.last_revision_info()
 
535
        local_info = self.branch.last_revision_info()
 
536
        if local_info != master_info:
 
537
            raise errors.BoundBranchOutOfDate(self.branch,
 
538
                    self.master_branch)
 
539
 
 
540
        # Now things are ready to change the master branch
 
541
        # so grab the lock
 
542
        self.bound_branch = self.branch
 
543
        self.master_branch.lock_write()
 
544
        operation.add_cleanup(self.master_branch.unlock)
 
545
 
 
546
    def _check_out_of_date_tree(self):
 
547
        """Check that the working tree is up to date.
 
548
 
 
549
        :return: old_revision_number,new_revision_number tuple
 
550
        """
 
551
        try:
 
552
            first_tree_parent = self.work_tree.get_parent_ids()[0]
 
553
        except IndexError:
 
554
            # if there are no parents, treat our parent as 'None'
 
555
            # this is so that we still consider the master branch
 
556
            # - in a checkout scenario the tree may have no
 
557
            # parents but the branch may do.
 
558
            first_tree_parent = bzrlib.revision.NULL_REVISION
 
559
        old_revno, master_last = self.master_branch.last_revision_info()
 
560
        if master_last != first_tree_parent:
 
561
            if master_last != bzrlib.revision.NULL_REVISION:
 
562
                raise errors.OutOfDateTree(self.work_tree)
 
563
        if self.branch.repository.has_revision(first_tree_parent):
 
564
            new_revno = old_revno + 1
 
565
        else:
 
566
            # ghost parents never appear in revision history.
 
567
            new_revno = 1
 
568
        return old_revno,new_revno
 
569
 
 
570
    def _process_pre_hooks(self, old_revno, new_revno):
 
571
        """Process any registered pre commit hooks."""
 
572
        self._set_progress_stage("Running pre_commit hooks")
 
573
        self._process_hooks("pre_commit", old_revno, new_revno)
 
574
 
 
575
    def _process_post_hooks(self, old_revno, new_revno):
 
576
        """Process any registered post commit hooks."""
 
577
        # Process the post commit hooks, if any
 
578
        self._set_progress_stage("Running post_commit hooks")
 
579
        # old style commit hooks - should be deprecated ? (obsoleted in
 
580
        # 0.15)
 
581
        if self.config.post_commit() is not None:
 
582
            hooks = self.config.post_commit().split(' ')
 
583
            # this would be nicer with twisted.python.reflect.namedAny
 
584
            for hook in hooks:
 
585
                result = eval(hook + '(branch, rev_id)',
 
586
                              {'branch':self.branch,
 
587
                               'bzrlib':bzrlib,
 
588
                               'rev_id':self.rev_id})
 
589
        # process new style post commit hooks
 
590
        self._process_hooks("post_commit", old_revno, new_revno)
 
591
 
 
592
    def _process_hooks(self, hook_name, old_revno, new_revno):
 
593
        if not Branch.hooks[hook_name]:
 
594
            return
 
595
 
 
596
        # new style commit hooks:
 
597
        if not self.bound_branch:
 
598
            hook_master = self.branch
 
599
            hook_local = None
 
600
        else:
 
601
            hook_master = self.master_branch
 
602
            hook_local = self.branch
 
603
        # With bound branches, when the master is behind the local branch,
 
604
        # the 'old_revno' and old_revid values here are incorrect.
 
605
        # XXX: FIXME ^. RBC 20060206
 
606
        if self.parents:
 
607
            old_revid = self.parents[0]
 
608
        else:
 
609
            old_revid = bzrlib.revision.NULL_REVISION
 
610
 
 
611
        if hook_name == "pre_commit":
 
612
            future_tree = self.builder.revision_tree()
 
613
            tree_delta = future_tree.changes_from(self.basis_tree,
 
614
                                             include_root=True)
 
615
 
 
616
        for hook in Branch.hooks[hook_name]:
 
617
            # show the running hook in the progress bar. As hooks may
 
618
            # end up doing nothing (e.g. because they are not configured by
 
619
            # the user) this is still showing progress, not showing overall
 
620
            # actions - its up to each plugin to show a UI if it want's to
 
621
            # (such as 'Emailing diff to foo@example.com').
 
622
            self.pb_stage_name = "Running %s hooks [%s]" % \
 
623
                (hook_name, Branch.hooks.get_hook_name(hook))
 
624
            self._emit_progress()
 
625
            if 'hooks' in debug.debug_flags:
 
626
                mutter("Invoking commit hook: %r", hook)
 
627
            if hook_name == "post_commit":
 
628
                hook(hook_local, hook_master, old_revno, old_revid, new_revno,
 
629
                     self.rev_id)
 
630
            elif hook_name == "pre_commit":
 
631
                hook(hook_local, hook_master,
 
632
                     old_revno, old_revid, new_revno, self.rev_id,
 
633
                     tree_delta, future_tree)
 
634
 
 
635
    def _gather_parents(self):
 
636
        """Record the parents of a merge for merge detection."""
 
637
        # TODO: Make sure that this list doesn't contain duplicate
 
638
        # entries and the order is preserved when doing this.
 
639
        if self.use_record_iter_changes:
 
640
            return
 
641
        self.basis_inv = self.basis_tree.inventory
 
642
        self.parent_invs = [self.basis_inv]
 
643
        for revision in self.parents[1:]:
 
644
            if self.branch.repository.has_revision(revision):
 
645
                mutter('commit parent revision {%s}', revision)
 
646
                inventory = self.branch.repository.get_inventory(revision)
 
647
                self.parent_invs.append(inventory)
 
648
            else:
 
649
                mutter('commit parent ghost revision {%s}', revision)
 
650
 
 
651
    def _update_builder_with_changes(self):
 
652
        """Update the commit builder with the data about what has changed.
 
653
        """
 
654
        exclude = self.exclude
 
655
        specific_files = self.specific_files
 
656
        mutter("Selecting files for commit with filter %s", specific_files)
 
657
 
 
658
        self._check_strict()
 
659
        if self.use_record_iter_changes:
 
660
            iter_changes = self.work_tree.iter_changes(self.basis_tree,
 
661
                specific_files=specific_files)
 
662
            iter_changes = self._filter_iter_changes(iter_changes)
 
663
            for file_id, path, fs_hash in self.builder.record_iter_changes(
 
664
                self.work_tree, self.basis_revid, iter_changes):
 
665
                self.work_tree._observed_sha1(file_id, path, fs_hash)
 
666
        else:
 
667
            # Build the new inventory
 
668
            self._populate_from_inventory()
 
669
            self._record_unselected()
 
670
            self._report_and_accumulate_deletes()
 
671
 
 
672
    def _filter_iter_changes(self, iter_changes):
 
673
        """Process iter_changes.
 
674
 
 
675
        This method reports on the changes in iter_changes to the user, and 
 
676
        converts 'missing' entries in the iter_changes iterator to 'deleted'
 
677
        entries. 'missing' entries have their
 
678
 
 
679
        :param iter_changes: An iter_changes to process.
 
680
        :return: A generator of changes.
 
681
        """
 
682
        reporter = self.reporter
 
683
        report_changes = reporter.is_verbose()
 
684
        deleted_ids = []
 
685
        for change in iter_changes:
 
686
            if report_changes:
 
687
                old_path = change[1][0]
 
688
                new_path = change[1][1]
 
689
                versioned = change[3][1]
 
690
            kind = change[6][1]
 
691
            versioned = change[3][1]
 
692
            if kind is None and versioned:
 
693
                # 'missing' path
 
694
                if report_changes:
 
695
                    reporter.missing(new_path)
 
696
                deleted_ids.append(change[0])
 
697
                # Reset the new path (None) and new versioned flag (False)
 
698
                change = (change[0], (change[1][0], None), change[2],
 
699
                    (change[3][0], False)) + change[4:]
 
700
            elif kind == 'tree-reference':
 
701
                if self.recursive == 'down':
 
702
                    self._commit_nested_tree(change[0], change[1][1])
 
703
            if change[3][0] or change[3][1]:
 
704
                yield change
 
705
                if report_changes:
 
706
                    if new_path is None:
 
707
                        reporter.deleted(old_path)
 
708
                    elif old_path is None:
 
709
                        reporter.snapshot_change('added', new_path)
 
710
                    elif old_path != new_path:
 
711
                        reporter.renamed('renamed', old_path, new_path)
 
712
                    else:
 
713
                        if (new_path or 
 
714
                            self.work_tree.branch.repository._format.rich_root_data):
 
715
                            # Don't report on changes to '' in non rich root
 
716
                            # repositories.
 
717
                            reporter.snapshot_change('modified', new_path)
 
718
            self._next_progress_entry()
 
719
        # Unversion IDs that were found to be deleted
 
720
        self.deleted_ids = deleted_ids
 
721
 
 
722
    def _record_unselected(self):
 
723
        # If specific files are selected, then all un-selected files must be
 
724
        # recorded in their previous state. For more details, see
 
725
        # https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
 
726
        if self.specific_files or self.exclude:
 
727
            specific_files = self.specific_files or []
 
728
            for path, old_ie in self.basis_inv.iter_entries():
 
729
                if old_ie.file_id in self.builder.new_inventory:
 
730
                    # already added - skip.
 
731
                    continue
 
732
                if (is_inside_any(specific_files, path)
 
733
                    and not is_inside_any(self.exclude, path)):
 
734
                    # was inside the selected path, and not excluded - if not
 
735
                    # present it has been deleted so skip.
 
736
                    continue
 
737
                # From here down it was either not selected, or was excluded:
 
738
                # We preserve the entry unaltered.
 
739
                ie = old_ie.copy()
 
740
                # Note: specific file commits after a merge are currently
 
741
                # prohibited. This test is for sanity/safety in case it's
 
742
                # required after that changes.
 
743
                if len(self.parents) > 1:
 
744
                    ie.revision = None
 
745
                self.builder.record_entry_contents(ie, self.parent_invs, path,
 
746
                    self.basis_tree, None)
 
747
 
 
748
    def _report_and_accumulate_deletes(self):
 
749
        if (isinstance(self.basis_inv, Inventory)
 
750
            and isinstance(self.builder.new_inventory, Inventory)):
 
751
            # the older Inventory classes provide a _byid dict, and building a
 
752
            # set from the keys of this dict is substantially faster than even
 
753
            # getting a set of ids from the inventory
 
754
            #
 
755
            # <lifeless> set(dict) is roughly the same speed as
 
756
            # set(iter(dict)) and both are significantly slower than
 
757
            # set(dict.keys())
 
758
            deleted_ids = set(self.basis_inv._byid.keys()) - \
 
759
               set(self.builder.new_inventory._byid.keys())
 
760
        else:
 
761
            deleted_ids = set(self.basis_inv) - set(self.builder.new_inventory)
 
762
        if deleted_ids:
 
763
            self.any_entries_deleted = True
 
764
            deleted = [(self.basis_tree.id2path(file_id), file_id)
 
765
                for file_id in deleted_ids]
 
766
            deleted.sort()
 
767
            # XXX: this is not quite directory-order sorting
 
768
            for path, file_id in deleted:
 
769
                self.builder.record_delete(path, file_id)
 
770
                self.reporter.deleted(path)
 
771
 
 
772
    def _check_strict(self):
 
773
        # XXX: when we use iter_changes this would likely be faster if
 
774
        # iter_changes would check for us (even in the presence of
 
775
        # selected_files).
 
776
        if self.strict:
 
777
            # raise an exception as soon as we find a single unknown.
 
778
            for unknown in self.work_tree.unknowns():
 
779
                raise StrictCommitFailed()
 
780
 
 
781
    def _populate_from_inventory(self):
 
782
        """Populate the CommitBuilder by walking the working tree inventory."""
 
783
        # Build the revision inventory.
 
784
        #
 
785
        # This starts by creating a new empty inventory. Depending on
 
786
        # which files are selected for commit, and what is present in the
 
787
        # current tree, the new inventory is populated. inventory entries
 
788
        # which are candidates for modification have their revision set to
 
789
        # None; inventory entries that are carried over untouched have their
 
790
        # revision set to their prior value.
 
791
        #
 
792
        # ESEPARATIONOFCONCERNS: this function is diffing and using the diff
 
793
        # results to create a new inventory at the same time, which results
 
794
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
 
795
        # ADHB 11-07-2006
 
796
 
 
797
        specific_files = self.specific_files
 
798
        exclude = self.exclude
 
799
        report_changes = self.reporter.is_verbose()
 
800
        deleted_ids = []
 
801
        # A tree of paths that have been deleted. E.g. if foo/bar has been
 
802
        # deleted, then we have {'foo':{'bar':{}}}
 
803
        deleted_paths = {}
 
804
        # XXX: Note that entries may have the wrong kind because the entry does
 
805
        # not reflect the status on disk.
 
806
        work_inv = self.work_tree.inventory
 
807
        # NB: entries will include entries within the excluded ids/paths
 
808
        # because iter_entries_by_dir has no 'exclude' facility today.
 
809
        entries = work_inv.iter_entries_by_dir(
 
810
            specific_file_ids=self.specific_file_ids, yield_parents=True)
 
811
        for path, existing_ie in entries:
 
812
            file_id = existing_ie.file_id
 
813
            name = existing_ie.name
 
814
            parent_id = existing_ie.parent_id
 
815
            kind = existing_ie.kind
 
816
            # Skip files that have been deleted from the working tree.
 
817
            # The deleted path ids are also recorded so they can be explicitly
 
818
            # unversioned later.
 
819
            if deleted_paths:
 
820
                path_segments = splitpath(path)
 
821
                deleted_dict = deleted_paths
 
822
                for segment in path_segments:
 
823
                    deleted_dict = deleted_dict.get(segment, None)
 
824
                    if not deleted_dict:
 
825
                        # We either took a path not present in the dict
 
826
                        # (deleted_dict was None), or we've reached an empty
 
827
                        # child dir in the dict, so are now a sub-path.
 
828
                        break
 
829
                else:
 
830
                    deleted_dict = None
 
831
                if deleted_dict is not None:
 
832
                    # the path has a deleted parent, do not add it.
 
833
                    continue
 
834
            if exclude and is_inside_any(exclude, path):
 
835
                # Skip excluded paths. Excluded paths are processed by
 
836
                # _update_builder_with_changes.
 
837
                continue
 
838
            content_summary = self.work_tree.path_content_summary(path)
 
839
            kind = content_summary[0]
 
840
            # Note that when a filter of specific files is given, we must only
 
841
            # skip/record deleted files matching that filter.
 
842
            if not specific_files or is_inside_any(specific_files, path):
 
843
                if kind == 'missing':
 
844
                    if not deleted_paths:
 
845
                        # path won't have been split yet.
 
846
                        path_segments = splitpath(path)
 
847
                    deleted_dict = deleted_paths
 
848
                    for segment in path_segments:
 
849
                        deleted_dict = deleted_dict.setdefault(segment, {})
 
850
                    self.reporter.missing(path)
 
851
                    self._next_progress_entry()
 
852
                    deleted_ids.append(file_id)
 
853
                    continue
 
854
            # TODO: have the builder do the nested commit just-in-time IF and
 
855
            # only if needed.
 
856
            if kind == 'tree-reference':
 
857
                # enforce repository nested tree policy.
 
858
                if (not self.work_tree.supports_tree_reference() or
 
859
                    # repository does not support it either.
 
860
                    not self.branch.repository._format.supports_tree_reference):
 
861
                    kind = 'directory'
 
862
                    content_summary = (kind, None, None, None)
 
863
                elif self.recursive == 'down':
 
864
                    nested_revision_id = self._commit_nested_tree(
 
865
                        file_id, path)
 
866
                    content_summary = (kind, None, None, nested_revision_id)
 
867
                else:
 
868
                    nested_revision_id = self.work_tree.get_reference_revision(file_id)
 
869
                    content_summary = (kind, None, None, nested_revision_id)
 
870
 
 
871
            # Record an entry for this item
 
872
            # Note: I don't particularly want to have the existing_ie
 
873
            # parameter but the test suite currently (28-Jun-07) breaks
 
874
            # without it thanks to a unicode normalisation issue. :-(
 
875
            definitely_changed = kind != existing_ie.kind
 
876
            self._record_entry(path, file_id, specific_files, kind, name,
 
877
                parent_id, definitely_changed, existing_ie, report_changes,
 
878
                content_summary)
 
879
 
 
880
        # Unversion IDs that were found to be deleted
 
881
        self.deleted_ids = deleted_ids
 
882
 
 
883
    def _commit_nested_tree(self, file_id, path):
 
884
        "Commit a nested tree."
 
885
        sub_tree = self.work_tree.get_nested_tree(file_id, path)
 
886
        # FIXME: be more comprehensive here:
 
887
        # this works when both trees are in --trees repository,
 
888
        # but when both are bound to a different repository,
 
889
        # it fails; a better way of approaching this is to
 
890
        # finally implement the explicit-caches approach design
 
891
        # a while back - RBC 20070306.
 
892
        if sub_tree.branch.repository.has_same_location(
 
893
            self.work_tree.branch.repository):
 
894
            sub_tree.branch.repository = \
 
895
                self.work_tree.branch.repository
 
896
        try:
 
897
            return sub_tree.commit(message=None, revprops=self.revprops,
 
898
                recursive=self.recursive,
 
899
                message_callback=self.message_callback,
 
900
                timestamp=self.timestamp, timezone=self.timezone,
 
901
                committer=self.committer,
 
902
                allow_pointless=self.allow_pointless,
 
903
                strict=self.strict, verbose=self.verbose,
 
904
                local=self.local, reporter=self.reporter)
 
905
        except errors.PointlessCommit:
 
906
            return self.work_tree.get_reference_revision(file_id)
 
907
 
 
908
    def _record_entry(self, path, file_id, specific_files, kind, name,
 
909
        parent_id, definitely_changed, existing_ie, report_changes,
 
910
        content_summary):
 
911
        "Record the new inventory entry for a path if any."
 
912
        # mutter('check %s {%s}', path, file_id)
 
913
        # mutter('%s selected for commit', path)
 
914
        if definitely_changed or existing_ie is None:
 
915
            ie = make_entry(kind, name, parent_id, file_id)
 
916
        else:
 
917
            ie = existing_ie.copy()
 
918
            ie.revision = None
 
919
        # For carried over entries we don't care about the fs hash - the repo
 
920
        # isn't generating a sha, so we're not saving computation time.
 
921
        _, _, fs_hash = self.builder.record_entry_contents(
 
922
            ie, self.parent_invs, path, self.work_tree, content_summary)
 
923
        if report_changes:
 
924
            self._report_change(ie, path)
 
925
        if fs_hash:
 
926
            self.work_tree._observed_sha1(ie.file_id, path, fs_hash)
 
927
        return ie
 
928
 
 
929
    def _report_change(self, ie, path):
 
930
        """Report a change to the user.
 
931
 
 
932
        The change that has occurred is described relative to the basis
 
933
        inventory.
 
934
        """
 
935
        if (self.basis_inv.has_id(ie.file_id)):
 
936
            basis_ie = self.basis_inv[ie.file_id]
 
937
        else:
 
938
            basis_ie = None
 
939
        change = ie.describe_change(basis_ie, ie)
 
940
        if change in (InventoryEntry.RENAMED,
 
941
            InventoryEntry.MODIFIED_AND_RENAMED):
 
942
            old_path = self.basis_inv.id2path(ie.file_id)
 
943
            self.reporter.renamed(change, old_path, path)
 
944
            self._next_progress_entry()
 
945
        else:
 
946
            if change == 'unchanged':
 
947
                return
 
948
            self.reporter.snapshot_change(change, path)
 
949
            self._next_progress_entry()
 
950
 
 
951
    def _set_progress_stage(self, name, counter=False):
 
952
        """Set the progress stage and emit an update to the progress bar."""
 
953
        self.pb_stage_name = name
 
954
        self.pb_stage_count += 1
 
955
        if counter:
 
956
            self.pb_entries_count = 0
 
957
        else:
 
958
            self.pb_entries_count = None
 
959
        self._emit_progress()
 
960
 
 
961
    def _next_progress_entry(self):
 
962
        """Emit an update to the progress bar and increment the entry count."""
 
963
        self.pb_entries_count += 1
 
964
        self._emit_progress()
 
965
 
 
966
    def _emit_progress(self):
 
967
        if self.pb_entries_count is not None:
 
968
            text = "%s [%d] - Stage" % (self.pb_stage_name,
 
969
                self.pb_entries_count)
 
970
        else:
 
971
            text = "%s - Stage" % (self.pb_stage_name, )
 
972
        self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
 
973
 
 
974
    def _set_specific_file_ids(self):
 
975
        """populate self.specific_file_ids if we will use it."""
 
976
        if not self.use_record_iter_changes:
 
977
            # If provided, ensure the specified files are versioned
 
978
            if self.specific_files is not None:
 
979
                # Note: This routine is being called because it raises
 
980
                # PathNotVersionedError as a side effect of finding the IDs. We
 
981
                # later use the ids we found as input to the working tree
 
982
                # inventory iterator, so we only consider those ids rather than
 
983
                # examining the whole tree again.
 
984
                # XXX: Dont we have filter_unversioned to do this more
 
985
                # cheaply?
 
986
                self.specific_file_ids = tree.find_ids_across_trees(
 
987
                    self.specific_files, [self.basis_tree, self.work_tree])
 
988
            else:
 
989
                self.specific_file_ids = None