~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: Ian Clatworthy
  • Date: 2008-04-01 04:19:06 UTC
  • mfrom: (3302.6.1 xma-mailmode)
  • mto: This revision was merged to the branch mainline in revision 3323.
  • Revision ID: ian.clatworthy@canonical.com-20080401041906-s7ekpfpo0tnyfkbz
Add mail-mode GNU Emacs mail package as a mail client option (Xavier Maillard)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007, 2008 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
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
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
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
18
# The newly committed revision is going to have a shape corresponding
60
60
    debug,
61
61
    errors,
62
62
    revision,
63
 
    trace,
64
63
    tree,
65
 
    xml_serializer,
66
64
    )
67
65
from bzrlib.branch import Branch
68
66
import bzrlib.config
70
68
                           ConflictsInTree,
71
69
                           StrictCommitFailed
72
70
                           )
73
 
from bzrlib.osutils import (get_user_encoding,
74
 
                            kind_marker, isdir,isfile, is_inside_any,
 
71
from bzrlib.osutils import (kind_marker, isdir,isfile, is_inside_any,
75
72
                            is_inside_or_parent_of_any,
76
73
                            minimum_path_selection,
77
74
                            quotefn, sha_file, split_lines,
79
76
                            )
80
77
from bzrlib.testament import Testament
81
78
from bzrlib.trace import mutter, note, warning, is_quiet
82
 
from bzrlib.inventory import Inventory, InventoryEntry, make_entry
 
79
from bzrlib.xml5 import serializer_v5
 
80
from bzrlib.inventory import InventoryEntry, make_entry
83
81
from bzrlib import symbol_versioning
84
82
from bzrlib.symbol_versioning import (deprecated_passed,
85
83
        deprecated_function,
105
103
    def completed(self, revno, rev_id):
106
104
        pass
107
105
 
108
 
    def deleted(self, path):
 
106
    def deleted(self, file_id):
109
107
        pass
110
108
 
111
109
    def escaped(self, escape_count, message):
131
129
        note(format, *args)
132
130
 
133
131
    def snapshot_change(self, change, path):
134
 
        if path == '' and change in ('added', 'modified'):
 
132
        if change == 'unchanged':
 
133
            return
 
134
        if change == 'added' and path == '':
135
135
            return
136
136
        self._note("%s %s", change, path)
137
137
 
150
150
    def completed(self, revno, rev_id):
151
151
        self._note('Committed revision %d.', revno)
152
152
 
153
 
    def deleted(self, path):
154
 
        self._note('deleted %s', path)
 
153
    def deleted(self, file_id):
 
154
        self._note('deleted %s', file_id)
155
155
 
156
156
    def escaped(self, escape_count, message):
157
157
        self._note("replaced %d control characters in message", escape_count)
204
204
               reporter=None,
205
205
               config=None,
206
206
               message_callback=None,
207
 
               recursive='down',
208
 
               exclude=None,
209
 
               possible_master_transports=None):
 
207
               recursive='down'):
210
208
        """Commit working copy as a new revision.
211
209
 
212
210
        :param message: the commit message (it or message_callback is required)
234
232
        :param verbose: if True and the reporter is not None, report everything
235
233
        :param recursive: If set to 'down', commit in any subtrees that have
236
234
            pending changes of any sort during this commit.
237
 
        :param exclude: None or a list of relative paths to exclude from the
238
 
            commit. Pending changes to excluded files will be ignored by the
239
 
            commit.
240
235
        """
241
236
        mutter('preparing to commit')
242
237
 
251
246
        if message_callback is None:
252
247
            if message is not None:
253
248
                if isinstance(message, str):
254
 
                    message = message.decode(get_user_encoding())
 
249
                    message = message.decode(bzrlib.user_encoding)
255
250
                message_callback = lambda x: message
256
251
            else:
257
252
                raise BzrError("The message or message_callback keyword"
258
253
                               " parameter is required for commit().")
259
254
 
260
255
        self.bound_branch = None
 
256
        self.any_entries_changed = False
261
257
        self.any_entries_deleted = False
262
 
        if exclude is not None:
263
 
            self.exclude = sorted(
264
 
                minimum_path_selection(exclude))
265
 
        else:
266
 
            self.exclude = []
267
258
        self.local = local
268
259
        self.master_branch = None
269
260
        self.master_locked = False
274
265
                minimum_path_selection(specific_files))
275
266
        else:
276
267
            self.specific_files = None
277
 
            
 
268
        self.specific_file_ids = None
278
269
        self.allow_pointless = allow_pointless
279
270
        self.revprops = revprops
280
271
        self.message_callback = message_callback
283
274
        self.committer = committer
284
275
        self.strict = strict
285
276
        self.verbose = verbose
 
277
        # accumulates an inventory delta to the basis entry, so we can make
 
278
        # just the necessary updates to the workingtree's cached basis.
 
279
        self._basis_delta = []
286
280
 
287
281
        self.work_tree.lock_write()
288
 
        self.parents = self.work_tree.get_parent_ids()
289
 
        # We can use record_iter_changes IFF iter_changes is compatible with
290
 
        # the command line parameters, and the repository has fast delta
291
 
        # generation. See bug 347649.
292
 
        self.use_record_iter_changes = (
293
 
            not self.specific_files and
294
 
            not self.exclude and 
295
 
            not self.branch.repository._format.supports_tree_reference and
296
 
            (self.branch.repository._format.fast_deltas or
297
 
             len(self.parents) < 2))
298
282
        self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
299
283
        self.basis_revid = self.work_tree.last_revision()
300
284
        self.basis_tree = self.work_tree.basis_tree()
305
289
                raise ConflictsInTree
306
290
 
307
291
            # Setup the bound branch variables as needed.
308
 
            self._check_bound_branch(possible_master_transports)
 
292
            self._check_bound_branch()
309
293
 
310
294
            # Check that the working tree is up to date
311
295
            old_revno, new_revno = self._check_out_of_date_tree()
318
302
            if self.config is None:
319
303
                self.config = self.branch.get_config()
320
304
 
321
 
            self._set_specific_file_ids()
 
305
            # If provided, ensure the specified files are versioned
 
306
            if self.specific_files is not None:
 
307
                # Note: This routine is being called because it raises
 
308
                # PathNotVersionedError as a side effect of finding the IDs. We
 
309
                # later use the ids we found as input to the working tree
 
310
                # inventory iterator, so we only consider those ids rather than
 
311
                # examining the whole tree again.
 
312
                # XXX: Dont we have filter_unversioned to do this more
 
313
                # cheaply?
 
314
                self.specific_file_ids = tree.find_ids_across_trees(
 
315
                    specific_files, [self.basis_tree, self.work_tree])
322
316
 
323
317
            # Setup the progress bar. As the number of files that need to be
324
318
            # committed in unknown, progress is reported as stages.
335
329
            self.pb.show_count = True
336
330
            self.pb.show_bar = True
337
331
 
338
 
            self._gather_parents()
339
332
            # After a merge, a selected file commit is not supported.
340
333
            # See 'bzr help merge' for an explanation as to why.
 
334
            self.basis_inv = self.basis_tree.inventory
 
335
            self._gather_parents()
341
336
            if len(self.parents) > 1 and self.specific_files:
342
337
                raise errors.CannotCommitSelectedFileMerge(self.specific_files)
343
 
            # Excludes are a form of selected file commit.
344
 
            if len(self.parents) > 1 and self.exclude:
345
 
                raise errors.CannotCommitSelectedFileMerge(self.exclude)
346
338
 
347
339
            # Collect the changes
348
 
            self._set_progress_stage("Collecting changes", counter=True)
 
340
            self._set_progress_stage("Collecting changes",
 
341
                    entries_title="Directory")
349
342
            self.builder = self.branch.get_commit_builder(self.parents,
350
343
                self.config, timestamp, timezone, committer, revprops, rev_id)
351
 
 
 
344
            
352
345
            try:
353
 
                self.builder.will_record_deletes()
354
346
                # find the location being committed to
355
347
                if self.bound_branch:
356
348
                    master_location = self.master_branch.base
361
353
                self.reporter.started(new_revno, self.rev_id, master_location)
362
354
 
363
355
                self._update_builder_with_changes()
 
356
                self._report_and_accumulate_deletes()
364
357
                self._check_pointless()
365
358
 
366
359
                # TODO: Now the new inventory is known, check for conflicts.
372
365
 
373
366
                # Prompt the user for a commit message if none provided
374
367
                message = message_callback(self)
 
368
                assert isinstance(message, unicode), type(message)
375
369
                self.message = message
376
370
                self._escape_commit_message()
377
371
 
378
372
                # Add revision data to the local branch
379
373
                self.rev_id = self.builder.commit(self.message)
380
374
 
381
 
            except Exception, e:
382
 
                mutter("aborting commit write group because of exception:")
383
 
                trace.log_exception_quietly()
384
 
                note("aborting commit write group: %r" % (e,))
 
375
            except:
385
376
                self.builder.abort()
386
377
                raise
387
378
 
390
381
            # Upload revision data to the master.
391
382
            # this will propagate merged revisions too if needed.
392
383
            if self.bound_branch:
393
 
                self._set_progress_stage("Uploading data to master branch")
 
384
                if not self.master_branch.repository.has_same_location(
 
385
                        self.branch.repository):
 
386
                    self._set_progress_stage("Uploading data to master branch")
 
387
                    self.master_branch.repository.fetch(self.branch.repository,
 
388
                        revision_id=self.rev_id)
 
389
                # now the master has the revision data
394
390
                # 'commit' to the master first so a timeout here causes the
395
391
                # local branch to be out of date
396
 
                self.master_branch.import_last_revision_info(
397
 
                    self.branch.repository, new_revno, self.rev_id)
 
392
                self.master_branch.set_last_revision_info(new_revno,
 
393
                                                          self.rev_id)
398
394
 
399
395
            # and now do the commit locally.
400
396
            self.branch.set_last_revision_info(new_revno, self.rev_id)
402
398
            # Make the working tree up to date with the branch
403
399
            self._set_progress_stage("Updating the working tree")
404
400
            self.work_tree.update_basis_by_delta(self.rev_id,
405
 
                 self.builder.get_basis_delta())
 
401
                 self._basis_delta)
406
402
            self.reporter.completed(new_revno, self.rev_id)
407
403
            self._process_post_hooks(old_revno, new_revno)
408
404
        finally:
421
417
        # A merge with no effect on files
422
418
        if len(self.parents) > 1:
423
419
            return
424
 
        # TODO: we could simplify this by using self.builder.basis_delta.
 
420
        # TODO: we could simplify this by using self._basis_delta.
425
421
 
426
422
        # The initial commit adds a root directory, but this in itself is not
427
423
        # a worthwhile commit.
428
424
        if (self.basis_revid == revision.NULL_REVISION and
429
 
            ((self.builder.new_inventory is not None and
430
 
             len(self.builder.new_inventory) == 1) or
431
 
            len(self.builder._basis_delta) == 1)):
 
425
            len(self.builder.new_inventory) == 1):
432
426
            raise PointlessCommit()
433
 
        if self.builder.any_changes():
 
427
        # If length == 1, then we only have the root entry. Which means
 
428
        # that there is no real difference (only the root could be different)
 
429
        # unless deletes occured, in which case the length is irrelevant.
 
430
        if (self.any_entries_deleted or 
 
431
            (len(self.builder.new_inventory) != 1 and
 
432
             self.any_entries_changed)):
434
433
            return
435
434
        raise PointlessCommit()
436
435
 
437
 
    def _check_bound_branch(self, possible_master_transports=None):
 
436
    def _check_bound_branch(self):
438
437
        """Check to see if the local branch is bound.
439
438
 
440
439
        If it is bound, then most of the commit will actually be
445
444
            raise errors.LocalRequiresBoundBranch()
446
445
 
447
446
        if not self.local:
448
 
            self.master_branch = self.branch.get_master_branch(
449
 
                possible_master_transports)
 
447
            self.master_branch = self.branch.get_master_branch()
450
448
 
451
449
        if not self.master_branch:
452
450
            # make this branch the reference branch for out of date checks.
463
461
        #       commits to the remote branch if they would fit.
464
462
        #       But for now, just require remote to be identical
465
463
        #       to local.
466
 
 
 
464
        
467
465
        # Make sure the local branch is identical to the master
468
466
        master_info = self.master_branch.last_revision_info()
469
467
        local_info = self.branch.last_revision_info()
526
524
    def _process_hooks(self, hook_name, old_revno, new_revno):
527
525
        if not Branch.hooks[hook_name]:
528
526
            return
529
 
 
 
527
        
530
528
        # new style commit hooks:
531
529
        if not self.bound_branch:
532
530
            hook_master = self.branch
541
539
            old_revid = self.parents[0]
542
540
        else:
543
541
            old_revid = bzrlib.revision.NULL_REVISION
544
 
 
 
542
        
545
543
        if hook_name == "pre_commit":
546
544
            future_tree = self.builder.revision_tree()
547
545
            tree_delta = future_tree.changes_from(self.basis_tree,
548
546
                                             include_root=True)
549
 
 
 
547
        
550
548
        for hook in Branch.hooks[hook_name]:
551
549
            # show the running hook in the progress bar. As hooks may
552
550
            # end up doing nothing (e.g. because they are not configured by
582
580
            # typically this will be useful enough.
583
581
            except Exception, e:
584
582
                found_exception = e
585
 
        if found_exception is not None:
 
583
        if found_exception is not None: 
586
584
            # don't do a plan raise, because the last exception may have been
587
585
            # trashed, e is our sure-to-work exception even though it loses the
588
586
            # full traceback. XXX: RBC 20060421 perhaps we could check the
589
 
            # exc_info and if its the same one do a plain raise otherwise
 
587
            # exc_info and if its the same one do a plain raise otherwise 
590
588
            # 'raise e' as we do now.
591
589
            raise e
592
590
 
608
606
        # serialiser not by commit. Then we can also add an unescaper
609
607
        # in the deserializer and start roundtripping revision messages
610
608
        # precisely. See repository_implementations/test_repository.py
611
 
        self.message, escape_count = xml_serializer.escape_invalid_chars(
 
609
        
 
610
        # Python strings can include characters that can't be
 
611
        # represented in well-formed XML; escape characters that
 
612
        # aren't listed in the XML specification
 
613
        # (http://www.w3.org/TR/REC-xml/#NT-Char).
 
614
        self.message, escape_count = re.subn(
 
615
            u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
 
616
            lambda match: match.group(0).encode('unicode_escape'),
612
617
            self.message)
613
618
        if escape_count:
614
619
            self.reporter.escaped(escape_count, self.message)
615
620
 
616
621
    def _gather_parents(self):
617
622
        """Record the parents of a merge for merge detection."""
618
 
        # TODO: Make sure that this list doesn't contain duplicate
 
623
        # TODO: Make sure that this list doesn't contain duplicate 
619
624
        # entries and the order is preserved when doing this.
620
 
        if self.use_record_iter_changes:
621
 
            return
622
 
        self.basis_inv = self.basis_tree.inventory
 
625
        self.parents = self.work_tree.get_parent_ids()
623
626
        self.parent_invs = [self.basis_inv]
624
627
        for revision in self.parents[1:]:
625
628
            if self.branch.repository.has_revision(revision):
632
635
    def _update_builder_with_changes(self):
633
636
        """Update the commit builder with the data about what has changed.
634
637
        """
635
 
        exclude = self.exclude
636
 
        specific_files = self.specific_files or []
 
638
        # Build the revision inventory.
 
639
        #
 
640
        # This starts by creating a new empty inventory. Depending on
 
641
        # which files are selected for commit, and what is present in the
 
642
        # current tree, the new inventory is populated. inventory entries 
 
643
        # which are candidates for modification have their revision set to
 
644
        # None; inventory entries that are carried over untouched have their
 
645
        # revision set to their prior value.
 
646
        #
 
647
        # ESEPARATIONOFCONCERNS: this function is diffing and using the diff
 
648
        # results to create a new inventory at the same time, which results
 
649
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
 
650
        # ADHB 11-07-2006
 
651
 
 
652
        specific_files = self.specific_files
637
653
        mutter("Selecting files for commit with filter %s", specific_files)
638
654
 
639
 
        self._check_strict()
640
 
        if self.use_record_iter_changes:
641
 
            iter_changes = self.work_tree.iter_changes(self.basis_tree)
642
 
            iter_changes = self._filter_iter_changes(iter_changes)
643
 
            for file_id, path, fs_hash in self.builder.record_iter_changes(
644
 
                self.work_tree, self.basis_revid, iter_changes):
645
 
                self.work_tree._observed_sha1(file_id, path, fs_hash)
646
 
        else:
647
 
            # Build the new inventory
648
 
            self._populate_from_inventory()
649
 
            self._record_unselected()
650
 
            self._report_and_accumulate_deletes()
651
 
 
652
 
    def _filter_iter_changes(self, iter_changes):
653
 
        """Process iter_changes.
654
 
 
655
 
        This method reports on the changes in iter_changes to the user, and 
656
 
        converts 'missing' entries in the iter_changes iterator to 'deleted'
657
 
        entries. 'missing' entries have their
658
 
 
659
 
        :param iter_changes: An iter_changes to process.
660
 
        :return: A generator of changes.
661
 
        """
662
 
        reporter = self.reporter
663
 
        report_changes = reporter.is_verbose()
664
 
        deleted_ids = []
665
 
        for change in iter_changes:
666
 
            if report_changes:
667
 
                old_path = change[1][0]
668
 
                new_path = change[1][1]
669
 
                versioned = change[3][1]
670
 
            kind = change[6][1]
671
 
            versioned = change[3][1]
672
 
            if kind is None and versioned:
673
 
                # 'missing' path
674
 
                if report_changes:
675
 
                    reporter.missing(new_path)
676
 
                deleted_ids.append(change[0])
677
 
                # Reset the new path (None) and new versioned flag (False)
678
 
                change = (change[0], (change[1][0], None), change[2],
679
 
                    (change[3][0], False)) + change[4:]
680
 
            elif kind == 'tree-reference':
681
 
                if self.recursive == 'down':
682
 
                    self._commit_nested_tree(change[0], change[1][1])
683
 
            if change[3][0] or change[3][1]:
684
 
                yield change
685
 
                if report_changes:
686
 
                    if new_path is None:
687
 
                        reporter.deleted(old_path)
688
 
                    elif old_path is None:
689
 
                        reporter.snapshot_change('added', new_path)
690
 
                    elif old_path != new_path:
691
 
                        reporter.renamed('renamed', old_path, new_path)
692
 
                    else:
693
 
                        if (new_path or 
694
 
                            self.work_tree.branch.repository._format.rich_root_data):
695
 
                            # Don't report on changes to '' in non rich root
696
 
                            # repositories.
697
 
                            reporter.snapshot_change('modified', new_path)
698
 
            self._next_progress_entry()
699
 
        # Unversion IDs that were found to be deleted
700
 
        self.work_tree.unversion(deleted_ids)
701
 
 
702
 
    def _record_unselected(self):
 
655
        # Build the new inventory
 
656
        self._populate_from_inventory(specific_files)
 
657
 
703
658
        # If specific files are selected, then all un-selected files must be
704
659
        # recorded in their previous state. For more details, see
705
660
        # https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
706
 
        if self.specific_files or self.exclude:
707
 
            specific_files = self.specific_files or []
 
661
        if specific_files:
708
662
            for path, old_ie in self.basis_inv.iter_entries():
709
663
                if old_ie.file_id in self.builder.new_inventory:
710
664
                    # already added - skip.
711
665
                    continue
712
 
                if (is_inside_any(specific_files, path)
713
 
                    and not is_inside_any(self.exclude, path)):
714
 
                    # was inside the selected path, and not excluded - if not
715
 
                    # present it has been deleted so skip.
 
666
                if is_inside_any(specific_files, path):
 
667
                    # was inside the selected path, if not present it has been
 
668
                    # deleted so skip.
716
669
                    continue
717
 
                # From here down it was either not selected, or was excluded:
718
 
                # We preserve the entry unaltered.
 
670
                if old_ie.kind == 'directory':
 
671
                    self._next_progress_entry()
 
672
                # not in final inv yet, was not in the selected files, so is an
 
673
                # entry to be preserved unaltered.
719
674
                ie = old_ie.copy()
720
675
                # Note: specific file commits after a merge are currently
721
676
                # prohibited. This test is for sanity/safety in case it's
722
677
                # required after that changes.
723
678
                if len(self.parents) > 1:
724
679
                    ie.revision = None
725
 
                self.builder.record_entry_contents(ie, self.parent_invs, path,
726
 
                    self.basis_tree, None)
 
680
                delta, version_recorded = self.builder.record_entry_contents(
 
681
                    ie, self.parent_invs, path, self.basis_tree, None)
 
682
                if version_recorded:
 
683
                    self.any_entries_changed = True
 
684
                if delta: self._basis_delta.append(delta)
727
685
 
728
686
    def _report_and_accumulate_deletes(self):
729
 
        if (isinstance(self.basis_inv, Inventory)
730
 
            and isinstance(self.builder.new_inventory, Inventory)):
731
 
            # the older Inventory classes provide a _byid dict, and building a
732
 
            # set from the keys of this dict is substantially faster than even
733
 
            # getting a set of ids from the inventory
734
 
            #
735
 
            # <lifeless> set(dict) is roughly the same speed as
736
 
            # set(iter(dict)) and both are significantly slower than
737
 
            # set(dict.keys())
738
 
            deleted_ids = set(self.basis_inv._byid.keys()) - \
739
 
               set(self.builder.new_inventory._byid.keys())
740
 
        else:
741
 
            deleted_ids = set(self.basis_inv) - set(self.builder.new_inventory)
 
687
        # XXX: Could the list of deleted paths and ids be instead taken from
 
688
        # _populate_from_inventory?
 
689
        deleted_ids = set(self.basis_inv._byid.keys()) - \
 
690
            set(self.builder.new_inventory._byid.keys())
742
691
        if deleted_ids:
743
692
            self.any_entries_deleted = True
744
693
            deleted = [(self.basis_tree.id2path(file_id), file_id)
746
695
            deleted.sort()
747
696
            # XXX: this is not quite directory-order sorting
748
697
            for path, file_id in deleted:
749
 
                self.builder.record_delete(path, file_id)
 
698
                self._basis_delta.append((path, None, file_id, None))
750
699
                self.reporter.deleted(path)
751
700
 
752
 
    def _check_strict(self):
753
 
        # XXX: when we use iter_changes this would likely be faster if
754
 
        # iter_changes would check for us (even in the presence of
755
 
        # selected_files).
 
701
    def _populate_from_inventory(self, specific_files):
 
702
        """Populate the CommitBuilder by walking the working tree inventory."""
756
703
        if self.strict:
757
704
            # raise an exception as soon as we find a single unknown.
758
705
            for unknown in self.work_tree.unknowns():
759
706
                raise StrictCommitFailed()
760
 
 
761
 
    def _populate_from_inventory(self):
762
 
        """Populate the CommitBuilder by walking the working tree inventory."""
763
 
        # Build the revision inventory.
764
 
        #
765
 
        # This starts by creating a new empty inventory. Depending on
766
 
        # which files are selected for commit, and what is present in the
767
 
        # current tree, the new inventory is populated. inventory entries
768
 
        # which are candidates for modification have their revision set to
769
 
        # None; inventory entries that are carried over untouched have their
770
 
        # revision set to their prior value.
771
 
        #
772
 
        # ESEPARATIONOFCONCERNS: this function is diffing and using the diff
773
 
        # results to create a new inventory at the same time, which results
774
 
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
775
 
        # ADHB 11-07-2006
776
 
 
777
 
        specific_files = self.specific_files
778
 
        exclude = self.exclude
 
707
               
779
708
        report_changes = self.reporter.is_verbose()
780
709
        deleted_ids = []
781
710
        # A tree of paths that have been deleted. E.g. if foo/bar has been
784
713
        # XXX: Note that entries may have the wrong kind because the entry does
785
714
        # not reflect the status on disk.
786
715
        work_inv = self.work_tree.inventory
787
 
        # NB: entries will include entries within the excluded ids/paths
788
 
        # because iter_entries_by_dir has no 'exclude' facility today.
789
716
        entries = work_inv.iter_entries_by_dir(
790
717
            specific_file_ids=self.specific_file_ids, yield_parents=True)
791
718
        for path, existing_ie in entries:
793
720
            name = existing_ie.name
794
721
            parent_id = existing_ie.parent_id
795
722
            kind = existing_ie.kind
 
723
            if kind == 'directory':
 
724
                self._next_progress_entry()
796
725
            # Skip files that have been deleted from the working tree.
797
726
            # The deleted path ids are also recorded so they can be explicitly
798
727
            # unversioned later.
811
740
                if deleted_dict is not None:
812
741
                    # the path has a deleted parent, do not add it.
813
742
                    continue
814
 
            if exclude and is_inside_any(exclude, path):
815
 
                # Skip excluded paths. Excluded paths are processed by
816
 
                # _update_builder_with_changes.
817
 
                continue
818
743
            content_summary = self.work_tree.path_content_summary(path)
819
744
            # Note that when a filter of specific files is given, we must only
820
745
            # skip/record deleted files matching that filter.
827
752
                    for segment in path_segments:
828
753
                        deleted_dict = deleted_dict.setdefault(segment, {})
829
754
                    self.reporter.missing(path)
830
 
                    self._next_progress_entry()
831
755
                    deleted_ids.append(file_id)
832
756
                    continue
833
757
            # TODO: have the builder do the nested commit just-in-time IF and
868
792
        # FIXME: be more comprehensive here:
869
793
        # this works when both trees are in --trees repository,
870
794
        # but when both are bound to a different repository,
871
 
        # it fails; a better way of approaching this is to
 
795
        # it fails; a better way of approaching this is to 
872
796
        # finally implement the explicit-caches approach design
873
797
        # a while back - RBC 20070306.
874
798
        if sub_tree.branch.repository.has_same_location(
898
822
        else:
899
823
            ie = existing_ie.copy()
900
824
            ie.revision = None
901
 
        # For carried over entries we don't care about the fs hash - the repo
902
 
        # isn't generating a sha, so we're not saving computation time.
903
 
        _, _, fs_hash = self.builder.record_entry_contents(
904
 
            ie, self.parent_invs, path, self.work_tree, content_summary)
 
825
        delta, version_recorded = self.builder.record_entry_contents(ie,
 
826
            self.parent_invs, path, self.work_tree, content_summary)
 
827
        if delta:
 
828
            self._basis_delta.append(delta)
 
829
        if version_recorded:
 
830
            self.any_entries_changed = True
905
831
        if report_changes:
906
832
            self._report_change(ie, path)
907
 
        if fs_hash:
908
 
            self.work_tree._observed_sha1(ie.file_id, path, fs_hash)
909
833
        return ie
910
834
 
911
835
    def _report_change(self, ie, path):
919
843
        else:
920
844
            basis_ie = None
921
845
        change = ie.describe_change(basis_ie, ie)
922
 
        if change in (InventoryEntry.RENAMED,
 
846
        if change in (InventoryEntry.RENAMED, 
923
847
            InventoryEntry.MODIFIED_AND_RENAMED):
924
848
            old_path = self.basis_inv.id2path(ie.file_id)
925
849
            self.reporter.renamed(change, old_path, path)
926
 
            self._next_progress_entry()
927
850
        else:
928
 
            if change == 'unchanged':
929
 
                return
930
851
            self.reporter.snapshot_change(change, path)
931
 
            self._next_progress_entry()
932
852
 
933
 
    def _set_progress_stage(self, name, counter=False):
 
853
    def _set_progress_stage(self, name, entries_title=None):
934
854
        """Set the progress stage and emit an update to the progress bar."""
935
855
        self.pb_stage_name = name
936
856
        self.pb_stage_count += 1
937
 
        if counter:
 
857
        self.pb_entries_title = entries_title
 
858
        if entries_title is not None:
938
859
            self.pb_entries_count = 0
939
 
        else:
940
 
            self.pb_entries_count = None
 
860
            self.pb_entries_total = '?'
941
861
        self._emit_progress()
942
862
 
943
863
    def _next_progress_entry(self):
946
866
        self._emit_progress()
947
867
 
948
868
    def _emit_progress(self):
949
 
        if self.pb_entries_count is not None:
950
 
            text = "%s [%d] - Stage" % (self.pb_stage_name,
951
 
                self.pb_entries_count)
 
869
        if self.pb_entries_title:
 
870
            if self.pb_entries_total == '?':
 
871
                text = "%s [%s %d] - Stage" % (self.pb_stage_name,
 
872
                    self.pb_entries_title, self.pb_entries_count)
 
873
            else:
 
874
                text = "%s [%s %d/%s] - Stage" % (self.pb_stage_name,
 
875
                    self.pb_entries_title, self.pb_entries_count,
 
876
                    str(self.pb_entries_total))
952
877
        else:
953
 
            text = "%s - Stage" % (self.pb_stage_name, )
 
878
            text = "%s - Stage" % (self.pb_stage_name)
954
879
        self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
955
880
 
956
 
    def _set_specific_file_ids(self):
957
 
        """populate self.specific_file_ids if we will use it."""
958
 
        if not self.use_record_iter_changes:
959
 
            # If provided, ensure the specified files are versioned
960
 
            if self.specific_files is not None:
961
 
                # Note: This routine is being called because it raises
962
 
                # PathNotVersionedError as a side effect of finding the IDs. We
963
 
                # later use the ids we found as input to the working tree
964
 
                # inventory iterator, so we only consider those ids rather than
965
 
                # examining the whole tree again.
966
 
                # XXX: Dont we have filter_unversioned to do this more
967
 
                # cheaply?
968
 
                self.specific_file_ids = tree.find_ids_across_trees(
969
 
                    self.specific_files, [self.basis_tree, self.work_tree])
970
 
            else:
971
 
                self.specific_file_ids = None