~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: Jelmer Vernooij
  • Date: 2009-04-02 15:28:11 UTC
  • mto: This revision was merged to the branch mainline in revision 4253.
  • Revision ID: jelmer@samba.org-20090402152811-0uxng6q5kseoug7q
Add new module with generic serializer information; keep XML-specific bits in 
xml_serializer.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2011 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,
 
62
    revision,
55
63
    trace,
56
64
    tree,
57
 
    ui,
58
65
    )
59
66
from bzrlib.branch import Branch
60
 
from bzrlib.cleanup import OperationWithCleanups
61
67
import bzrlib.config
62
68
from bzrlib.errors import (BzrError, PointlessCommit,
63
69
                           ConflictsInTree,
64
70
                           StrictCommitFailed
65
71
                           )
66
72
from bzrlib.osutils import (get_user_encoding,
67
 
                            is_inside_any,
 
73
                            kind_marker, isdir,isfile, is_inside_any,
 
74
                            is_inside_or_parent_of_any,
68
75
                            minimum_path_selection,
 
76
                            quotefn, sha_file, split_lines,
69
77
                            splitpath,
70
78
                            )
71
 
from bzrlib.trace import mutter, note, is_quiet
 
79
from bzrlib.testament import Testament
 
80
from bzrlib.trace import mutter, note, warning, is_quiet
72
81
from bzrlib.inventory import Inventory, InventoryEntry, make_entry
73
82
from bzrlib import symbol_versioning
 
83
from bzrlib.symbol_versioning import (deprecated_passed,
 
84
        deprecated_function,
 
85
        DEPRECATED_PARAMETER)
 
86
from bzrlib.workingtree import WorkingTree
74
87
from bzrlib.urlutils import unescape_for_display
 
88
import bzrlib.ui
75
89
 
76
90
 
77
91
class NullCommitReporter(object):
93
107
    def deleted(self, path):
94
108
        pass
95
109
 
 
110
    def escaped(self, escape_count, message):
 
111
        pass
 
112
 
96
113
    def missing(self, path):
97
114
        pass
98
115
 
131
148
 
132
149
    def completed(self, revno, rev_id):
133
150
        self._note('Committed revision %d.', revno)
134
 
        # self._note goes to the console too; so while we want to log the
135
 
        # rev_id, we can't trivially only log it. (See bug 526425). Long
136
 
        # term we should rearrange the reporting structure, but for now
137
 
        # we just mutter seperately. We mutter the revid and revno together
138
 
        # so that concurrent bzr invocations won't lead to confusion.
139
 
        mutter('Committed revid %s as revno %d.', rev_id, revno)
140
151
 
141
152
    def deleted(self, path):
142
153
        self._note('deleted %s', path)
143
154
 
 
155
    def escaped(self, escape_count, message):
 
156
        self._note("replaced %d control characters in message", escape_count)
 
157
 
144
158
    def missing(self, path):
145
159
        self._note('missing %s', path)
146
160
 
173
187
        self.reporter = reporter
174
188
        self.config = config
175
189
 
176
 
    @staticmethod
177
 
    def update_revprops(revprops, branch, authors=None, author=None,
178
 
                        local=False, possible_master_transports=None):
179
 
        if revprops is None:
180
 
            revprops = {}
181
 
        if possible_master_transports is None:
182
 
            possible_master_transports = []
183
 
        if not 'branch-nick' in revprops:
184
 
            revprops['branch-nick'] = branch._get_nick(
185
 
                local,
186
 
                possible_master_transports)
187
 
        if authors is not None:
188
 
            if author is not None:
189
 
                raise AssertionError('Specifying both author and authors '
190
 
                        'is not allowed. Specify just authors instead')
191
 
            if 'author' in revprops or 'authors' in revprops:
192
 
                # XXX: maybe we should just accept one of them?
193
 
                raise AssertionError('author property given twice')
194
 
            if authors:
195
 
                for individual in authors:
196
 
                    if '\n' in individual:
197
 
                        raise AssertionError('\\n is not a valid character '
198
 
                                'in an author identity')
199
 
                revprops['authors'] = '\n'.join(authors)
200
 
        if author is not None:
201
 
            symbol_versioning.warn('The parameter author was deprecated'
202
 
                   ' in version 1.13. Use authors instead',
203
 
                   DeprecationWarning)
204
 
            if 'author' in revprops or 'authors' in revprops:
205
 
                # XXX: maybe we should just accept one of them?
206
 
                raise AssertionError('author property given twice')
207
 
            if '\n' in author:
208
 
                raise AssertionError('\\n is not a valid character '
209
 
                        'in an author identity')
210
 
            revprops['authors'] = author
211
 
        return revprops
212
 
 
213
190
    def commit(self,
214
191
               message=None,
215
192
               timestamp=None,
228
205
               message_callback=None,
229
206
               recursive='down',
230
207
               exclude=None,
231
 
               possible_master_transports=None,
232
 
               lossy=False):
 
208
               possible_master_transports=None):
233
209
        """Commit working copy as a new revision.
234
210
 
235
211
        :param message: the commit message (it or message_callback is required)
236
 
        :param message_callback: A callback: message = message_callback(cmt_obj)
237
212
 
238
213
        :param timestamp: if not None, seconds-since-epoch for a
239
214
            postdated/predated commit.
240
215
 
241
 
        :param specific_files: If not None, commit only those files. An empty
242
 
            list means 'commit no files'.
 
216
        :param specific_files: If true, commit only those files.
243
217
 
244
218
        :param rev_id: If set, use this as the new revision id.
245
219
            Useful for test or import commands that need to tightly
262
236
        :param exclude: None or a list of relative paths to exclude from the
263
237
            commit. Pending changes to excluded files will be ignored by the
264
238
            commit.
265
 
        :param lossy: When committing to a foreign VCS, ignore any
266
 
            data that can not be natively represented.
267
239
        """
268
 
        operation = OperationWithCleanups(self._commit)
269
 
        self.revprops = revprops or {}
270
 
        # XXX: Can be set on __init__ or passed in - this is a bit ugly.
271
 
        self.config = config or self.config
272
 
        return operation.run(
273
 
               message=message,
274
 
               timestamp=timestamp,
275
 
               timezone=timezone,
276
 
               committer=committer,
277
 
               specific_files=specific_files,
278
 
               rev_id=rev_id,
279
 
               allow_pointless=allow_pointless,
280
 
               strict=strict,
281
 
               verbose=verbose,
282
 
               working_tree=working_tree,
283
 
               local=local,
284
 
               reporter=reporter,
285
 
               message_callback=message_callback,
286
 
               recursive=recursive,
287
 
               exclude=exclude,
288
 
               possible_master_transports=possible_master_transports,
289
 
               lossy=lossy)
290
 
 
291
 
    def _commit(self, operation, message, timestamp, timezone, committer,
292
 
            specific_files, rev_id, allow_pointless, strict, verbose,
293
 
            working_tree, local, reporter, message_callback, recursive,
294
 
            exclude, possible_master_transports, lossy):
295
240
        mutter('preparing to commit')
296
241
 
297
242
        if working_tree is None:
320
265
            self.exclude = []
321
266
        self.local = local
322
267
        self.master_branch = None
 
268
        self.master_locked = False
323
269
        self.recursive = recursive
324
270
        self.rev_id = None
325
 
        # self.specific_files is None to indicate no filter, or any iterable to
326
 
        # indicate a filter - [] means no files at all, as per iter_changes.
327
271
        if specific_files is not None:
328
272
            self.specific_files = sorted(
329
273
                minimum_path_selection(specific_files))
330
274
        else:
331
275
            self.specific_files = None
332
 
 
 
276
            
333
277
        self.allow_pointless = allow_pointless
 
278
        self.revprops = revprops
334
279
        self.message_callback = message_callback
335
280
        self.timestamp = timestamp
336
281
        self.timezone = timezone
339
284
        self.verbose = verbose
340
285
 
341
286
        self.work_tree.lock_write()
342
 
        operation.add_cleanup(self.work_tree.unlock)
343
287
        self.parents = self.work_tree.get_parent_ids()
344
288
        # We can use record_iter_changes IFF iter_changes is compatible with
345
289
        # the command line parameters, and the repository has fast delta
346
290
        # generation. See bug 347649.
347
291
        self.use_record_iter_changes = (
 
292
            not self.specific_files and
348
293
            not self.exclude and 
349
294
            not self.branch.repository._format.supports_tree_reference and
350
295
            (self.branch.repository._format.fast_deltas or
351
296
             len(self.parents) < 2))
352
 
        self.pb = ui.ui_factory.nested_progress_bar()
353
 
        operation.add_cleanup(self.pb.finished)
 
297
        self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
354
298
        self.basis_revid = self.work_tree.last_revision()
355
299
        self.basis_tree = self.work_tree.basis_tree()
356
300
        self.basis_tree.lock_read()
357
 
        operation.add_cleanup(self.basis_tree.unlock)
358
 
        # Cannot commit with conflicts present.
359
 
        if len(self.work_tree.conflicts()) > 0:
360
 
            raise ConflictsInTree
361
 
 
362
 
        # Setup the bound branch variables as needed.
363
 
        self._check_bound_branch(operation, possible_master_transports)
364
 
 
365
 
        # Check that the working tree is up to date
366
 
        old_revno, new_revno = self._check_out_of_date_tree()
367
 
 
368
 
        # Complete configuration setup
369
 
        if reporter is not None:
370
 
            self.reporter = reporter
371
 
        elif self.reporter is None:
372
 
            self.reporter = self._select_reporter()
373
 
        if self.config is None:
374
 
            self.config = self.branch.get_config()
375
 
 
376
 
        self._set_specific_file_ids()
377
 
 
378
 
        # Setup the progress bar. As the number of files that need to be
379
 
        # committed in unknown, progress is reported as stages.
380
 
        # We keep track of entries separately though and include that
381
 
        # information in the progress bar during the relevant stages.
382
 
        self.pb_stage_name = ""
383
 
        self.pb_stage_count = 0
384
 
        self.pb_stage_total = 5
385
 
        if self.bound_branch:
386
 
            # 2 extra stages: "Uploading data to master branch" and "Merging
387
 
            # tags to master branch"
388
 
            self.pb_stage_total += 2
389
 
        self.pb.show_pct = False
390
 
        self.pb.show_spinner = False
391
 
        self.pb.show_eta = False
392
 
        self.pb.show_count = True
393
 
        self.pb.show_bar = True
394
 
 
395
 
        self._gather_parents()
396
 
        # After a merge, a selected file commit is not supported.
397
 
        # See 'bzr help merge' for an explanation as to why.
398
 
        if len(self.parents) > 1 and self.specific_files is not None:
399
 
            raise errors.CannotCommitSelectedFileMerge(self.specific_files)
400
 
        # Excludes are a form of selected file commit.
401
 
        if len(self.parents) > 1 and self.exclude:
402
 
            raise errors.CannotCommitSelectedFileMerge(self.exclude)
403
 
 
404
 
        # Collect the changes
405
 
        self._set_progress_stage("Collecting changes", counter=True)
406
 
        self._lossy = lossy
407
 
        self.builder = self.branch.get_commit_builder(self.parents,
408
 
            self.config, timestamp, timezone, committer, self.revprops,
409
 
            rev_id, lossy=lossy)
410
 
        if not self.builder.supports_record_entry_contents and self.exclude:
411
 
            self.builder.abort()
412
 
            raise errors.ExcludesUnsupported(self.branch.repository)
413
 
 
414
301
        try:
415
 
            self.builder.will_record_deletes()
416
 
            # find the location being committed to
417
 
            if self.bound_branch:
418
 
                master_location = self.master_branch.base
419
 
            else:
420
 
                master_location = self.branch.base
421
 
 
422
 
            # report the start of the commit
423
 
            self.reporter.started(new_revno, self.rev_id, master_location)
424
 
 
425
 
            self._update_builder_with_changes()
426
 
            self._check_pointless()
427
 
 
428
 
            # TODO: Now the new inventory is known, check for conflicts.
429
 
            # ADHB 2006-08-08: If this is done, populate_new_inv should not add
430
 
            # weave lines, because nothing should be recorded until it is known
431
 
            # that commit will succeed.
432
 
            self._set_progress_stage("Saving data locally")
433
 
            self.builder.finish_inventory()
434
 
 
435
 
            # Prompt the user for a commit message if none provided
436
 
            message = message_callback(self)
437
 
            self.message = message
438
 
 
439
 
            # Add revision data to the local branch
440
 
            self.rev_id = self.builder.commit(self.message)
441
 
 
442
 
        except Exception, e:
443
 
            mutter("aborting commit write group because of exception:")
444
 
            trace.log_exception_quietly()
445
 
            self.builder.abort()
446
 
            raise
447
 
 
448
 
        self._process_pre_hooks(old_revno, new_revno)
449
 
 
450
 
        # Upload revision data to the master.
451
 
        # this will propagate merged revisions too if needed.
452
 
        if self.bound_branch:
453
 
            self._set_progress_stage("Uploading data to master branch")
454
 
            # 'commit' to the master first so a timeout here causes the
455
 
            # local branch to be out of date
456
 
            (new_revno, self.rev_id) = self.master_branch.import_last_revision_info_and_tags(
457
 
                self.branch, new_revno, self.rev_id, lossy=lossy)
458
 
            if lossy:
459
 
                self.branch.fetch(self.master_branch, self.rev_id)
460
 
 
461
 
        # and now do the commit locally.
462
 
        self.branch.set_last_revision_info(new_revno, self.rev_id)
463
 
 
464
 
        # Merge local tags to remote
465
 
        if self.bound_branch:
466
 
            self._set_progress_stage("Merging tags to master branch")
467
 
            tag_conflicts = self.branch.tags.merge_to(self.master_branch.tags)
468
 
            if tag_conflicts:
469
 
                warning_lines = ['    ' + name for name, _, _ in tag_conflicts]
470
 
                note("Conflicting tags in bound branch:\n" +
471
 
                    "\n".join(warning_lines))
472
 
 
473
 
        # Make the working tree be up to date with the branch. This
474
 
        # includes automatic changes scheduled to be made to the tree, such
475
 
        # as updating its basis and unversioning paths that were missing.
476
 
        self.work_tree.unversion(self.deleted_ids)
477
 
        self._set_progress_stage("Updating the working tree")
478
 
        self.work_tree.update_basis_by_delta(self.rev_id,
479
 
             self.builder.get_basis_delta())
480
 
        self.reporter.completed(new_revno, self.rev_id)
481
 
        self._process_post_hooks(old_revno, new_revno)
 
302
            # Cannot commit with conflicts present.
 
303
            if len(self.work_tree.conflicts()) > 0:
 
304
                raise ConflictsInTree
 
305
 
 
306
            # Setup the bound branch variables as needed.
 
307
            self._check_bound_branch(possible_master_transports)
 
308
 
 
309
            # Check that the working tree is up to date
 
310
            old_revno, new_revno = self._check_out_of_date_tree()
 
311
 
 
312
            # Complete configuration setup
 
313
            if reporter is not None:
 
314
                self.reporter = reporter
 
315
            elif self.reporter is None:
 
316
                self.reporter = self._select_reporter()
 
317
            if self.config is None:
 
318
                self.config = self.branch.get_config()
 
319
 
 
320
            self._set_specific_file_ids()
 
321
 
 
322
            # Setup the progress bar. As the number of files that need to be
 
323
            # committed in unknown, progress is reported as stages.
 
324
            # We keep track of entries separately though and include that
 
325
            # information in the progress bar during the relevant stages.
 
326
            self.pb_stage_name = ""
 
327
            self.pb_stage_count = 0
 
328
            self.pb_stage_total = 5
 
329
            if self.bound_branch:
 
330
                self.pb_stage_total += 1
 
331
            self.pb.show_pct = False
 
332
            self.pb.show_spinner = False
 
333
            self.pb.show_eta = False
 
334
            self.pb.show_count = True
 
335
            self.pb.show_bar = True
 
336
 
 
337
            self._gather_parents()
 
338
            # After a merge, a selected file commit is not supported.
 
339
            # See 'bzr help merge' for an explanation as to why.
 
340
            if len(self.parents) > 1 and self.specific_files:
 
341
                raise errors.CannotCommitSelectedFileMerge(self.specific_files)
 
342
            # Excludes are a form of selected file commit.
 
343
            if len(self.parents) > 1 and self.exclude:
 
344
                raise errors.CannotCommitSelectedFileMerge(self.exclude)
 
345
 
 
346
            # Collect the changes
 
347
            self._set_progress_stage("Collecting changes", counter=True)
 
348
            self.builder = self.branch.get_commit_builder(self.parents,
 
349
                self.config, timestamp, timezone, committer, revprops, rev_id)
 
350
 
 
351
            try:
 
352
                self.builder.will_record_deletes()
 
353
                # find the location being committed to
 
354
                if self.bound_branch:
 
355
                    master_location = self.master_branch.base
 
356
                else:
 
357
                    master_location = self.branch.base
 
358
 
 
359
                # report the start of the commit
 
360
                self.reporter.started(new_revno, self.rev_id, master_location)
 
361
 
 
362
                self._update_builder_with_changes()
 
363
                self._check_pointless()
 
364
 
 
365
                # TODO: Now the new inventory is known, check for conflicts.
 
366
                # ADHB 2006-08-08: If this is done, populate_new_inv should not add
 
367
                # weave lines, because nothing should be recorded until it is known
 
368
                # that commit will succeed.
 
369
                self._set_progress_stage("Saving data locally")
 
370
                self.builder.finish_inventory()
 
371
 
 
372
                # Prompt the user for a commit message if none provided
 
373
                message = message_callback(self)
 
374
                self.message = message
 
375
                self._escape_commit_message()
 
376
 
 
377
                # Add revision data to the local branch
 
378
                self.rev_id = self.builder.commit(self.message)
 
379
 
 
380
            except Exception, e:
 
381
                mutter("aborting commit write group because of exception:")
 
382
                trace.log_exception_quietly()
 
383
                note("aborting commit write group: %r" % (e,))
 
384
                self.builder.abort()
 
385
                raise
 
386
 
 
387
            self._process_pre_hooks(old_revno, new_revno)
 
388
 
 
389
            # Upload revision data to the master.
 
390
            # this will propagate merged revisions too if needed.
 
391
            if self.bound_branch:
 
392
                self._set_progress_stage("Uploading data to master branch")
 
393
                # 'commit' to the master first so a timeout here causes the
 
394
                # local branch to be out of date
 
395
                self.master_branch.import_last_revision_info(
 
396
                    self.branch.repository, new_revno, self.rev_id)
 
397
 
 
398
            # and now do the commit locally.
 
399
            self.branch.set_last_revision_info(new_revno, self.rev_id)
 
400
 
 
401
            # Make the working tree up to date with the branch
 
402
            self._set_progress_stage("Updating the working tree")
 
403
            self.work_tree.update_basis_by_delta(self.rev_id,
 
404
                 self.builder.get_basis_delta())
 
405
            self.reporter.completed(new_revno, self.rev_id)
 
406
            self._process_post_hooks(old_revno, new_revno)
 
407
        finally:
 
408
            self._cleanup()
482
409
        return self.rev_id
483
410
 
484
411
    def _select_reporter(self):
493
420
        # A merge with no effect on files
494
421
        if len(self.parents) > 1:
495
422
            return
 
423
        # TODO: we could simplify this by using self.builder.basis_delta.
 
424
 
 
425
        # The initial commit adds a root directory, but this in itself is not
 
426
        # a worthwhile commit.
 
427
        if (self.basis_revid == revision.NULL_REVISION and
 
428
            ((self.builder.new_inventory is not None and
 
429
             len(self.builder.new_inventory) == 1) or
 
430
            len(self.builder._basis_delta) == 1)):
 
431
            raise PointlessCommit()
496
432
        if self.builder.any_changes():
497
433
            return
498
434
        raise PointlessCommit()
499
435
 
500
 
    def _check_bound_branch(self, operation, possible_master_transports=None):
 
436
    def _check_bound_branch(self, possible_master_transports=None):
501
437
        """Check to see if the local branch is bound.
502
438
 
503
439
        If it is bound, then most of the commit will actually be
538
474
        # so grab the lock
539
475
        self.bound_branch = self.branch
540
476
        self.master_branch.lock_write()
541
 
        operation.add_cleanup(self.master_branch.unlock)
 
477
        self.master_locked = True
542
478
 
543
479
    def _check_out_of_date_tree(self):
544
480
        """Check that the working tree is up to date.
629
565
                     old_revno, old_revid, new_revno, self.rev_id,
630
566
                     tree_delta, future_tree)
631
567
 
 
568
    def _cleanup(self):
 
569
        """Cleanup any open locks, progress bars etc."""
 
570
        cleanups = [self._cleanup_bound_branch,
 
571
                    self.basis_tree.unlock,
 
572
                    self.work_tree.unlock,
 
573
                    self.pb.finished]
 
574
        found_exception = None
 
575
        for cleanup in cleanups:
 
576
            try:
 
577
                cleanup()
 
578
            # we want every cleanup to run no matter what.
 
579
            # so we have a catchall here, but we will raise the
 
580
            # last encountered exception up the stack: and
 
581
            # typically this will be useful enough.
 
582
            except Exception, e:
 
583
                found_exception = e
 
584
        if found_exception is not None:
 
585
            # don't do a plan raise, because the last exception may have been
 
586
            # trashed, e is our sure-to-work exception even though it loses the
 
587
            # full traceback. XXX: RBC 20060421 perhaps we could check the
 
588
            # exc_info and if its the same one do a plain raise otherwise
 
589
            # 'raise e' as we do now.
 
590
            raise e
 
591
 
 
592
    def _cleanup_bound_branch(self):
 
593
        """Executed at the end of a try/finally to cleanup a bound branch.
 
594
 
 
595
        If the branch wasn't bound, this is a no-op.
 
596
        If it was, it resents self.branch to the local branch, instead
 
597
        of being the master.
 
598
        """
 
599
        if not self.bound_branch:
 
600
            return
 
601
        if self.master_locked:
 
602
            self.master_branch.unlock()
 
603
 
 
604
    def _escape_commit_message(self):
 
605
        """Replace xml-incompatible control characters."""
 
606
        # FIXME: RBC 20060419 this should be done by the revision
 
607
        # serialiser not by commit. Then we can also add an unescaper
 
608
        # in the deserializer and start roundtripping revision messages
 
609
        # precisely. See repository_implementations/test_repository.py
 
610
 
 
611
        # Python strings can include characters that can't be
 
612
        # represented in well-formed XML; escape characters that
 
613
        # aren't listed in the XML specification
 
614
        # (http://www.w3.org/TR/REC-xml/#NT-Char).
 
615
        self.message, escape_count = re.subn(
 
616
            u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
 
617
            lambda match: match.group(0).encode('unicode_escape'),
 
618
            self.message)
 
619
        if escape_count:
 
620
            self.reporter.escaped(escape_count, self.message)
 
621
 
632
622
    def _gather_parents(self):
633
623
        """Record the parents of a merge for merge detection."""
634
624
        # TODO: Make sure that this list doesn't contain duplicate
649
639
        """Update the commit builder with the data about what has changed.
650
640
        """
651
641
        exclude = self.exclude
652
 
        specific_files = self.specific_files
 
642
        specific_files = self.specific_files or []
653
643
        mutter("Selecting files for commit with filter %s", specific_files)
654
644
 
655
645
        self._check_strict()
656
646
        if self.use_record_iter_changes:
657
 
            iter_changes = self.work_tree.iter_changes(self.basis_tree,
658
 
                specific_files=specific_files)
 
647
            iter_changes = self.work_tree.iter_changes(self.basis_tree)
659
648
            iter_changes = self._filter_iter_changes(iter_changes)
660
649
            for file_id, path, fs_hash in self.builder.record_iter_changes(
661
650
                self.work_tree, self.basis_revid, iter_changes):
714
703
                            reporter.snapshot_change('modified', new_path)
715
704
            self._next_progress_entry()
716
705
        # Unversion IDs that were found to be deleted
717
 
        self.deleted_ids = deleted_ids
 
706
        self.work_tree.unversion(deleted_ids)
718
707
 
719
708
    def _record_unselected(self):
720
709
        # If specific files are selected, then all un-selected files must be
723
712
        if self.specific_files or self.exclude:
724
713
            specific_files = self.specific_files or []
725
714
            for path, old_ie in self.basis_inv.iter_entries():
726
 
                if self.builder.new_inventory.has_id(old_ie.file_id):
 
715
                if old_ie.file_id in self.builder.new_inventory:
727
716
                    # already added - skip.
728
717
                    continue
729
718
                if (is_inside_any(specific_files, path)
833
822
                # _update_builder_with_changes.
834
823
                continue
835
824
            content_summary = self.work_tree.path_content_summary(path)
836
 
            kind = content_summary[0]
837
825
            # Note that when a filter of specific files is given, we must only
838
826
            # skip/record deleted files matching that filter.
839
827
            if not specific_files or is_inside_any(specific_files, path):
840
 
                if kind == 'missing':
 
828
                if content_summary[0] == 'missing':
841
829
                    if not deleted_paths:
842
830
                        # path won't have been split yet.
843
831
                        path_segments = splitpath(path)
850
838
                    continue
851
839
            # TODO: have the builder do the nested commit just-in-time IF and
852
840
            # only if needed.
853
 
            if kind == 'tree-reference':
 
841
            if content_summary[0] == 'tree-reference':
854
842
                # enforce repository nested tree policy.
855
843
                if (not self.work_tree.supports_tree_reference() or
856
844
                    # repository does not support it either.
857
845
                    not self.branch.repository._format.supports_tree_reference):
858
 
                    kind = 'directory'
859
 
                    content_summary = (kind, None, None, None)
860
 
                elif self.recursive == 'down':
 
846
                    content_summary = ('directory',) + content_summary[1:]
 
847
            kind = content_summary[0]
 
848
            # TODO: specific_files filtering before nested tree processing
 
849
            if kind == 'tree-reference':
 
850
                if self.recursive == 'down':
861
851
                    nested_revision_id = self._commit_nested_tree(
862
852
                        file_id, path)
863
 
                    content_summary = (kind, None, None, nested_revision_id)
 
853
                    content_summary = content_summary[:3] + (
 
854
                        nested_revision_id,)
864
855
                else:
865
 
                    nested_revision_id = self.work_tree.get_reference_revision(file_id)
866
 
                    content_summary = (kind, None, None, nested_revision_id)
 
856
                    content_summary = content_summary[:3] + (
 
857
                        self.work_tree.get_reference_revision(file_id),)
867
858
 
868
859
            # Record an entry for this item
869
860
            # Note: I don't particularly want to have the existing_ie
875
866
                content_summary)
876
867
 
877
868
        # Unversion IDs that were found to be deleted
878
 
        self.deleted_ids = deleted_ids
 
869
        self.work_tree.unversion(deleted_ids)
879
870
 
880
871
    def _commit_nested_tree(self, file_id, path):
881
872
        "Commit a nested tree."