/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

  • Committer: Vincent Ladeuil
  • Date: 2007-07-18 09:43:41 UTC
  • mto: (2778.5.1 vila)
  • mto: This revision was merged to the branch mainline in revision 2789.
  • Revision ID: v.ladeuil+lp@free.fr-20070718094341-edmgsog3el06yqow
Add performance analysis of missing.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007 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
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
 
# XXX: Can we do any better about making interrupted commits change
19
 
# nothing?  
20
 
 
21
 
# TODO: Separate 'prepare' phase where we find a list of potentially
22
 
# committed files.  We then can then pause the commit to prompt for a
23
 
# commit message, knowing the summary will be the same as what's
24
 
# actually used for the commit.  (But perhaps simpler to simply get
25
 
# the tree status, then use that for a selective commit?)
26
 
 
27
18
# The newly committed revision is going to have a shape corresponding
28
19
# to that of the working inventory.  Files that are not in the
29
20
# working tree and that were in the predecessor are reported as
55
46
# merges from, then it should still be reported as newly added
56
47
# relative to the basis revision.
57
48
 
58
 
# TODO: Do checks that the tree can be committed *before* running the 
59
 
# editor; this should include checks for a pointless commit and for 
60
 
# unknown or missing files.
61
 
 
62
 
# TODO: If commit fails, leave the message in a file somewhere.
63
 
 
 
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.
64
51
 
65
52
import os
66
53
import re
69
56
 
70
57
from cStringIO import StringIO
71
58
 
 
59
from bzrlib import (
 
60
    debug,
 
61
    errors,
 
62
    inventory,
 
63
    tree,
 
64
    )
 
65
from bzrlib.branch import Branch
72
66
import bzrlib.config
73
 
import bzrlib.errors as errors
74
67
from bzrlib.errors import (BzrError, PointlessCommit,
75
68
                           ConflictsInTree,
76
69
                           StrictCommitFailed
81
74
from bzrlib.testament import Testament
82
75
from bzrlib.trace import mutter, note, warning
83
76
from bzrlib.xml5 import serializer_v5
84
 
from bzrlib.inventory import Inventory, ROOT_ID, InventoryEntry
 
77
from bzrlib.inventory import Inventory, InventoryEntry
85
78
from bzrlib import symbol_versioning
86
79
from bzrlib.symbol_versioning import (deprecated_passed,
87
80
        deprecated_function,
88
81
        DEPRECATED_PARAMETER)
89
82
from bzrlib.workingtree import WorkingTree
 
83
import bzrlib.ui
90
84
 
91
85
 
92
86
class NullCommitReporter(object):
113
107
 
114
108
class ReportCommitToLog(NullCommitReporter):
115
109
 
116
 
    # this may be more useful if 'note' was replaced by an overridable
117
 
    # method on self, which would allow more trivial subclassing.
118
 
    # alternative, a callable could be passed in, allowing really trivial
119
 
    # reuse for some uis. RBC 20060511
 
110
    def _note(self, format, *args):
 
111
        """Output a message.
 
112
 
 
113
        Subclasses may choose to override this method.
 
114
        """
 
115
        note(format, *args)
120
116
 
121
117
    def snapshot_change(self, change, path):
122
118
        if change == 'unchanged':
123
119
            return
124
 
        note("%s %s", change, path)
 
120
        if change == 'added' and path == '':
 
121
            return
 
122
        self._note("%s %s", change, path)
125
123
 
126
124
    def completed(self, revno, rev_id):
127
 
        note('Committed revision %d.', revno)
 
125
        self._note('Committed revision %d.', revno)
128
126
    
129
127
    def deleted(self, file_id):
130
 
        note('deleted %s', file_id)
 
128
        self._note('deleted %s', file_id)
131
129
 
132
130
    def escaped(self, escape_count, message):
133
 
        note("replaced %d control characters in message", escape_count)
 
131
        self._note("replaced %d control characters in message", escape_count)
134
132
 
135
133
    def missing(self, path):
136
 
        note('missing %s', path)
 
134
        self._note('missing %s', path)
137
135
 
138
136
    def renamed(self, change, old_path, new_path):
139
 
        note('%s %s => %s', change, old_path, new_path)
 
137
        self._note('%s %s => %s', change, old_path, new_path)
140
138
 
141
139
 
142
140
class Commit(object):
158
156
            self.reporter = reporter
159
157
        else:
160
158
            self.reporter = NullCommitReporter()
161
 
        if config is not None:
162
 
            self.config = config
163
 
        else:
164
 
            self.config = None
 
159
        self.config = config
165
160
        
166
161
    def commit(self,
167
 
               branch=DEPRECATED_PARAMETER, message=None,
 
162
               message=None,
168
163
               timestamp=None,
169
164
               timezone=None,
170
165
               committer=None,
177
172
               working_tree=None,
178
173
               local=False,
179
174
               reporter=None,
180
 
               config=None):
 
175
               config=None,
 
176
               message_callback=None,
 
177
               recursive='down'):
181
178
        """Commit working copy as a new revision.
182
179
 
183
 
        branch -- the deprecated branch to commit to. New callers should pass in 
184
 
                  working_tree instead
185
 
 
186
 
        message -- the commit message, a mandatory parameter
187
 
 
188
 
        timestamp -- if not None, seconds-since-epoch for a
189
 
             postdated/predated commit.
190
 
 
191
 
        specific_files -- If true, commit only those files.
192
 
 
193
 
        rev_id -- If set, use this as the new revision id.
 
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.
194
188
            Useful for test or import commands that need to tightly
195
189
            control what revisions are assigned.  If you duplicate
196
190
            a revision id that exists elsewhere it is your own fault.
197
191
            If null (default), a time/random revision id is generated.
198
192
 
199
 
        allow_pointless -- If true (default), commit even if nothing
 
193
        :param allow_pointless: If true (default), commit even if nothing
200
194
            has changed and no merges are recorded.
201
195
 
202
 
        strict -- If true, don't allow a commit if the working tree
 
196
        :param strict: If true, don't allow a commit if the working tree
203
197
            contains unknown files.
204
198
 
205
 
        revprops -- Properties for new revision
 
199
        :param revprops: Properties for new revision
206
200
        :param local: Perform a local only commit.
 
201
        :param recursive: If set to 'down', commit in any subtrees that have
 
202
            pending changes of any sort during this commit.
207
203
        """
208
204
        mutter('preparing to commit')
209
205
 
210
 
        if deprecated_passed(branch):
211
 
            symbol_versioning.warn("Commit.commit (branch, ...): The branch parameter is "
212
 
                 "deprecated as of bzr 0.8. Please use working_tree= instead.",
213
 
                 DeprecationWarning, stacklevel=2)
214
 
            self.branch = branch
215
 
            self.work_tree = self.branch.bzrdir.open_workingtree()
216
 
        elif working_tree is None:
217
 
            raise BzrError("One of branch and working_tree must be passed into commit().")
 
206
        if working_tree is None:
 
207
            raise BzrError("working_tree must be passed into commit().")
218
208
        else:
219
209
            self.work_tree = working_tree
220
210
            self.branch = self.work_tree.branch
221
 
        if message is None:
222
 
            raise BzrError("The message keyword parameter is required for commit().")
 
211
            if getattr(self.work_tree, 'requires_rich_root', lambda: False)():
 
212
                if not self.branch.repository.supports_rich_root():
 
213
                    raise errors.RootNotRich()
 
214
        if message_callback is None:
 
215
            if message is not None:
 
216
                if isinstance(message, str):
 
217
                    message = message.decode(bzrlib.user_encoding)
 
218
                message_callback = lambda x: message
 
219
            else:
 
220
                raise BzrError("The message or message_callback keyword"
 
221
                               " parameter is required for commit().")
223
222
 
224
223
        self.bound_branch = None
225
224
        self.local = local
228
227
        self.rev_id = None
229
228
        self.specific_files = specific_files
230
229
        self.allow_pointless = allow_pointless
 
230
        self.recursive = recursive
 
231
        self.revprops = revprops
 
232
        self.message_callback = message_callback
 
233
        self.timestamp = timestamp
 
234
        self.timezone = timezone
 
235
        self.committer = committer
 
236
        self.strict = strict
 
237
        self.verbose = verbose
231
238
 
232
239
        if reporter is None and self.reporter is None:
233
240
            self.reporter = NullCommitReporter()
236
243
 
237
244
        self.work_tree.lock_write()
238
245
        self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
246
        self.basis_tree = self.work_tree.basis_tree()
 
247
        self.basis_tree.lock_read()
239
248
        try:
240
249
            # Cannot commit with conflicts present.
241
 
            if len(self.work_tree.conflicts())>0:
 
250
            if len(self.work_tree.conflicts()) > 0:
242
251
                raise ConflictsInTree
243
252
 
244
 
            # setup the bound branch variables as needed.
 
253
            # Setup the bound branch variables as needed.
245
254
            self._check_bound_branch()
246
255
 
247
 
            # check for out of date working trees
248
 
            try:
249
 
                first_tree_parent = self.work_tree.get_parent_ids()[0]
250
 
            except IndexError:
251
 
                # if there are no parents, treat our parent as 'None'
252
 
                # this is so that we still consier the master branch
253
 
                # - in a checkout scenario the tree may have no
254
 
                # parents but the branch may do.
255
 
                first_tree_parent = None
256
 
            master_last = self.master_branch.last_revision()
257
 
            if (master_last is not None and
258
 
                master_last != first_tree_parent):
259
 
                raise errors.OutOfDateTree(self.work_tree)
260
 
    
 
256
            # Check that the working tree is up to date
 
257
            old_revno,new_revno = self._check_out_of_date_tree()
 
258
 
261
259
            if strict:
262
260
                # raise an exception as soon as we find a single unknown.
263
261
                for unknown in self.work_tree.unknowns():
265
263
                   
266
264
            if self.config is None:
267
265
                self.config = self.branch.get_config()
268
 
      
269
 
            if isinstance(message, str):
270
 
                message = message.decode(bzrlib.user_encoding)
271
 
            assert isinstance(message, unicode), type(message)
272
 
            self.message = message
273
 
            self._escape_commit_message()
274
 
 
275
 
            self.work_inv = self.work_tree.inventory
276
 
            self.basis_tree = self.work_tree.basis_tree()
 
266
 
 
267
            # If provided, ensure the specified files are versioned
 
268
            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.
 
272
                # XXX: Dont we have filter_unversioned to do this more
 
273
                # cheaply?
 
274
                tree.find_ids_across_trees(specific_files,
 
275
                                           [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.
277
294
            self.basis_inv = self.basis_tree.inventory
278
 
            # one to finish, one for rev and inventory, and one for each
279
 
            # inventory entry, and the same for the new inventory.
280
 
            # note that this estimate is too long when we do a partial tree
281
 
            # commit which excludes some new files from being considered.
282
 
            # The estimate is corrected when we populate the new inv.
283
 
            self.pb_total = len(self.work_inv) + 5
284
 
            self.pb_count = 0
285
 
 
286
295
            self._gather_parents()
287
296
            if len(self.parents) > 1 and self.specific_files:
288
 
                raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
289
 
                        self.specific_files)
 
297
                raise errors.CannotCommitSelectedFileMerge(self.specific_files)
290
298
            
291
 
            self.builder = self.branch.get_commit_builder(self.parents, 
 
299
            # Collect the changes
 
300
            self._emit_progress_set_stage("Collecting changes", show_entries=True)
 
301
            self.builder = self.branch.get_commit_builder(self.parents,
292
302
                self.config, timestamp, timezone, committer, revprops, rev_id)
293
 
            
294
 
            self._remove_deleted()
295
 
            self._populate_new_inv()
296
 
            self._report_deletes()
297
 
 
 
303
            self._update_builder_with_changes()
298
304
            self._check_pointless()
299
305
 
300
 
            self._emit_progress_update()
301
 
            # TODO: Now the new inventory is known, check for conflicts and
302
 
            # prompt the user for a commit message.
 
306
            # TODO: Now the new inventory is known, check for conflicts.
303
307
            # ADHB 2006-08-08: If this is done, populate_new_inv should not add
304
308
            # weave lines, because nothing should be recorded until it is known
305
309
            # that commit will succeed.
 
310
            self._emit_progress_set_stage("Saving data locally")
306
311
            self.builder.finish_inventory()
307
 
            self._emit_progress_update()
 
312
 
 
313
            # Prompt the user for a commit message if none provided
 
314
            message = message_callback(self)
 
315
            assert isinstance(message, unicode), type(message)
 
316
            self.message = message
 
317
            self._escape_commit_message()
 
318
 
 
319
            # Add revision data to the local branch
308
320
            self.rev_id = self.builder.commit(self.message)
309
 
            self._emit_progress_update()
310
 
            # revision data is in the local branch now.
311
321
            
312
 
            # upload revision data to the master.
 
322
            # Upload revision data to the master.
313
323
            # this will propagate merged revisions too if needed.
314
324
            if self.bound_branch:
 
325
                self._emit_progress_set_stage("Uploading data to master branch")
315
326
                self.master_branch.repository.fetch(self.branch.repository,
316
327
                                                    revision_id=self.rev_id)
317
328
                # now the master has the revision data
318
 
                # 'commit' to the master first so a timeout here causes the local
319
 
                # branch to be out of date
320
 
                self.master_branch.append_revision(self.rev_id)
 
329
                # 'commit' to the master first so a timeout here causes the
 
330
                # local branch to be out of date
 
331
                self.master_branch.set_last_revision_info(new_revno,
 
332
                                                          self.rev_id)
321
333
 
322
334
            # and now do the commit locally.
323
 
            self.branch.append_revision(self.rev_id)
 
335
            self.branch.set_last_revision_info(new_revno, self.rev_id)
324
336
 
325
 
            # if the builder gave us the revisiontree it created back, we
326
 
            # could use it straight away here.
327
 
            # TODO: implement this.
328
 
            self.work_tree.set_parent_trees([(self.rev_id,
329
 
                self.branch.repository.revision_tree(self.rev_id))])
330
 
            # now the work tree is up to date with the branch
331
 
            
332
 
            self.reporter.completed(self.branch.revno(), self.rev_id)
333
 
            if self.config.post_commit() is not None:
334
 
                hooks = self.config.post_commit().split(' ')
335
 
                # this would be nicer with twisted.python.reflect.namedAny
336
 
                for hook in hooks:
337
 
                    result = eval(hook + '(branch, rev_id)',
338
 
                                  {'branch':self.branch,
339
 
                                   'bzrlib':bzrlib,
340
 
                                   'rev_id':self.rev_id})
341
 
            self._emit_progress_update()
 
337
            # Make the working tree up to date with the branch
 
338
            self._emit_progress_set_stage("Updating the working tree")
 
339
            rev_tree = self.builder.revision_tree()
 
340
            self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
 
341
            self.reporter.completed(new_revno, self.rev_id)
 
342
            self._process_hooks(old_revno, new_revno)
342
343
        finally:
343
344
            self._cleanup()
344
345
        return self.rev_id
345
346
 
 
347
    def _any_real_changes(self):
 
348
        """Are there real changes between new_inventory and basis?
 
349
 
 
350
        For trees without rich roots, inv.root.revision changes every commit.
 
351
        But if that is the only change, we want to treat it as though there
 
352
        are *no* changes.
 
353
        """
 
354
        new_entries = self.builder.new_inventory.iter_entries()
 
355
        basis_entries = self.basis_inv.iter_entries()
 
356
        new_path, new_root_ie = new_entries.next()
 
357
        basis_path, basis_root_ie = basis_entries.next()
 
358
 
 
359
        # This is a copy of InventoryEntry.__eq__ only leaving out .revision
 
360
        def ie_equal_no_revision(this, other):
 
361
            return ((this.file_id == other.file_id)
 
362
                    and (this.name == other.name)
 
363
                    and (this.symlink_target == other.symlink_target)
 
364
                    and (this.text_sha1 == other.text_sha1)
 
365
                    and (this.text_size == other.text_size)
 
366
                    and (this.text_id == other.text_id)
 
367
                    and (this.parent_id == other.parent_id)
 
368
                    and (this.kind == other.kind)
 
369
                    and (this.executable == other.executable)
 
370
                    and (this.reference_revision == other.reference_revision)
 
371
                    )
 
372
        if not ie_equal_no_revision(new_root_ie, basis_root_ie):
 
373
            return True
 
374
 
 
375
        for new_ie, basis_ie in zip(new_entries, basis_entries):
 
376
            if new_ie != basis_ie:
 
377
                return True
 
378
 
 
379
        # No actual changes present
 
380
        return False
 
381
 
346
382
    def _check_pointless(self):
347
383
        if self.allow_pointless:
348
384
            return
351
387
            return
352
388
        # work around the fact that a newly-initted tree does differ from its
353
389
        # basis
 
390
        if len(self.basis_inv) == 0 and len(self.builder.new_inventory) == 1:
 
391
            raise PointlessCommit()
 
392
        # Shortcut, if the number of entries changes, then we obviously have
 
393
        # a change
354
394
        if len(self.builder.new_inventory) != len(self.basis_inv):
355
395
            return
356
 
        if (len(self.builder.new_inventory) != 1 and
357
 
            self.builder.new_inventory != self.basis_inv):
 
396
        # If length == 1, then we only have the root entry. Which means
 
397
        # that there is no real difference (only the root could be different)
 
398
        if (len(self.builder.new_inventory) != 1 and self._any_real_changes()):
358
399
            return
359
400
        raise PointlessCommit()
360
401
 
388
429
        #       to local.
389
430
        
390
431
        # Make sure the local branch is identical to the master
391
 
        master_rh = self.master_branch.revision_history()
392
 
        local_rh = self.branch.revision_history()
393
 
        if local_rh != master_rh:
 
432
        master_info = self.master_branch.last_revision_info()
 
433
        local_info = self.branch.last_revision_info()
 
434
        if local_info != master_info:
394
435
            raise errors.BoundBranchOutOfDate(self.branch,
395
436
                    self.master_branch)
396
437
 
400
441
        self.master_branch.lock_write()
401
442
        self.master_locked = True
402
443
 
 
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
 
403
510
    def _cleanup(self):
404
511
        """Cleanup any open locks, progress bars etc."""
405
512
        cleanups = [self._cleanup_bound_branch,
 
513
                    self.basis_tree.unlock,
406
514
                    self.work_tree.unlock,
407
515
                    self.pb.finished]
408
516
        found_exception = None
458
566
        # TODO: Make sure that this list doesn't contain duplicate 
459
567
        # entries and the order is preserved when doing this.
460
568
        self.parents = self.work_tree.get_parent_ids()
461
 
        self.parent_invs = []
462
 
        for revision in self.parents:
 
569
        self.parent_invs = [self.basis_inv]
 
570
        for revision in self.parents[1:]:
463
571
            if self.branch.repository.has_revision(revision):
464
572
                mutter('commit parent revision {%s}', revision)
465
573
                inventory = self.branch.repository.get_inventory(revision)
467
575
            else:
468
576
                mutter('commit parent ghost revision {%s}', revision)
469
577
 
470
 
    def _remove_deleted(self):
471
 
        """Remove deleted files from the working inventories.
472
 
 
473
 
        This is done prior to taking the working inventory as the
474
 
        basis for the new committed inventory.
475
 
 
476
 
        This returns true if any files
477
 
        *that existed in the basis inventory* were deleted.
478
 
        Files that were added and deleted
479
 
        in the working copy don't matter.
480
 
        """
481
 
        specific = self.specific_files
482
 
        deleted_ids = []
483
 
        deleted_paths = set()
484
 
        for path, ie in self.work_inv.iter_entries():
485
 
            if is_inside_any(deleted_paths, path):
486
 
                # The tree will delete the required ids recursively.
487
 
                continue
488
 
            if specific and not is_inside_any(specific, path):
489
 
                continue
490
 
            if not self.work_tree.has_filename(path):
491
 
                deleted_paths.add(path)
492
 
                self.reporter.missing(path)
493
 
                deleted_ids.append(ie.file_id)
494
 
        self.work_tree.unversion(deleted_ids)
495
 
 
496
 
    def _populate_new_inv(self):
497
 
        """Build revision inventory.
498
 
 
499
 
        This creates a new empty inventory. Depending on
500
 
        which files are selected for commit, and what is present in the
501
 
        current tree, the new inventory is populated. inventory entries 
502
 
        which are candidates for modification have their revision set to
503
 
        None; inventory entries that are carried over untouched have their
504
 
        revision set to their prior value.
505
 
        """
 
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
        #
506
590
        # ESEPARATIONOFCONCERNS: this function is diffing and using the diff
507
591
        # results to create a new inventory at the same time, which results
508
592
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
509
593
        # ADHB 11-07-2006
510
 
        mutter("Selecting files for commit with filter %s", self.specific_files)
511
 
        entries = self.work_inv.iter_entries()
 
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()
512
603
        if not self.builder.record_root_entry:
513
604
            symbol_versioning.warn('CommitBuilders should support recording'
514
605
                ' the root entry as of bzr 0.10.', DeprecationWarning, 
515
606
                stacklevel=1)
516
607
            self.builder.new_inventory.add(self.basis_inv.root.copy())
517
608
            entries.next()
518
 
            self._emit_progress_update()
 
609
 
 
610
        deleted_ids = []
 
611
        deleted_paths = set()
519
612
        for path, new_ie in entries:
520
 
            self._emit_progress_update()
 
613
            self._emit_progress_next_entry()
521
614
            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
            try:
 
630
                kind = self.work_tree.kind(file_id)
 
631
                if kind == 'tree-reference' and self.recursive == 'down':
 
632
                    # nested tree: commit in it
 
633
                    sub_tree = WorkingTree.open(self.work_tree.abspath(path))
 
634
                    # FIXME: be more comprehensive here:
 
635
                    # this works when both trees are in --trees repository,
 
636
                    # but when both are bound to a different repository,
 
637
                    # it fails; a better way of approaching this is to 
 
638
                    # finally implement the explicit-caches approach design
 
639
                    # a while back - RBC 20070306.
 
640
                    if (sub_tree.branch.repository.bzrdir.root_transport.base
 
641
                        ==
 
642
                        self.work_tree.branch.repository.bzrdir.root_transport.base):
 
643
                        sub_tree.branch.repository = \
 
644
                            self.work_tree.branch.repository
 
645
                    try:
 
646
                        sub_tree.commit(message=None, revprops=self.revprops,
 
647
                            recursive=self.recursive,
 
648
                            message_callback=self.message_callback,
 
649
                            timestamp=self.timestamp, timezone=self.timezone,
 
650
                            committer=self.committer,
 
651
                            allow_pointless=self.allow_pointless,
 
652
                            strict=self.strict, verbose=self.verbose,
 
653
                            local=self.local, reporter=self.reporter)
 
654
                    except errors.PointlessCommit:
 
655
                        pass
 
656
                if kind != new_ie.kind:
 
657
                    new_ie = inventory.make_entry(kind, new_ie.name,
 
658
                                                  new_ie.parent_id, file_id)
 
659
            except errors.NoSuchFile:
 
660
                pass
522
661
            # mutter('check %s {%s}', path, file_id)
523
 
            if (not self.specific_files or 
524
 
                is_inside_or_parent_of_any(self.specific_files, path)):
 
662
            if (not specific_files or 
 
663
                is_inside_or_parent_of_any(specific_files, path)):
525
664
                    # mutter('%s selected for commit', path)
526
665
                    ie = new_ie.copy()
527
666
                    ie.revision = None
532
671
                else:
533
672
                    # this entry is new and not being committed
534
673
                    continue
535
 
 
536
674
            self.builder.record_entry_contents(ie, self.parent_invs, 
537
675
                path, self.work_tree)
538
676
            # describe the nature of the change that has occurred relative to
549
687
            else:
550
688
                self.reporter.snapshot_change(change, path)
551
689
 
552
 
        if not self.specific_files:
553
 
            return
554
 
 
555
 
        # ignore removals that don't match filespec
556
 
        for path, new_ie in self.basis_inv.iter_entries():
557
 
            if new_ie.file_id in self.work_inv:
558
 
                continue
559
 
            if is_inside_any(self.specific_files, path):
560
 
                continue
561
 
            ie = new_ie.copy()
562
 
            ie.revision = None
563
 
            self.builder.record_entry_contents(ie, self.parent_invs, path,
564
 
                                               self.basis_tree)
565
 
 
566
 
    def _emit_progress_update(self):
567
 
        """Emit an update to the progress bar."""
568
 
        self.pb.update("Committing", self.pb_count, self.pb_total)
569
 
        self.pb_count += 1
570
 
 
571
 
    def _report_deletes(self):
 
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.
572
713
        for path, ie in self.basis_inv.iter_entries():
573
714
            if ie.file_id not in self.builder.new_inventory:
574
715
                self.reporter.deleted(path)
575
716
 
 
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)
576
739