/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: Ian Clatworthy
  • Date: 2008-04-17 08:21:14 UTC
  • mto: (4171.1.1 ianc-integration)
  • mto: This revision was merged to the branch mainline in revision 4173.
  • Revision ID: ian.clatworthy@canonical.com-20080417082114-76imbiarcbj16ov3
first cut at working tree content filtering

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):
 
107
        pass
 
108
 
 
109
    def escaped(self, escape_count, message):
109
110
        pass
110
111
 
111
112
    def missing(self, path):
128
129
        note(format, *args)
129
130
 
130
131
    def snapshot_change(self, change, path):
131
 
        if path == '' and change in ('added', 'modified'):
 
132
        if change == 'unchanged':
 
133
            return
 
134
        if change == 'added' and path == '':
132
135
            return
133
136
        self._note("%s %s", change, path)
134
137
 
147
150
    def completed(self, revno, rev_id):
148
151
        self._note('Committed revision %d.', revno)
149
152
 
150
 
    def deleted(self, path):
151
 
        self._note('deleted %s', path)
 
153
    def deleted(self, file_id):
 
154
        self._note('deleted %s', file_id)
 
155
 
 
156
    def escaped(self, escape_count, message):
 
157
        self._note("replaced %d control characters in message", escape_count)
152
158
 
153
159
    def missing(self, path):
154
160
        self._note('missing %s', path)
198
204
               reporter=None,
199
205
               config=None,
200
206
               message_callback=None,
201
 
               recursive='down',
202
 
               exclude=None,
203
 
               possible_master_transports=None):
 
207
               recursive='down'):
204
208
        """Commit working copy as a new revision.
205
209
 
206
210
        :param message: the commit message (it or message_callback is required)
207
 
        :param message_callback: A callback: message = message_callback(cmt_obj)
208
211
 
209
212
        :param timestamp: if not None, seconds-since-epoch for a
210
213
            postdated/predated commit.
211
214
 
212
 
        :param specific_files: If not None, commit only those files. An empty
213
 
            list means 'commit no files'.
 
215
        :param specific_files: If true, commit only those files.
214
216
 
215
217
        :param rev_id: If set, use this as the new revision id.
216
218
            Useful for test or import commands that need to tightly
230
232
        :param verbose: if True and the reporter is not None, report everything
231
233
        :param recursive: If set to 'down', commit in any subtrees that have
232
234
            pending changes of any sort during this commit.
233
 
        :param exclude: None or a list of relative paths to exclude from the
234
 
            commit. Pending changes to excluded files will be ignored by the
235
 
            commit.
236
235
        """
237
236
        mutter('preparing to commit')
238
237
 
247
246
        if message_callback is None:
248
247
            if message is not None:
249
248
                if isinstance(message, str):
250
 
                    message = message.decode(get_user_encoding())
 
249
                    message = message.decode(bzrlib.user_encoding)
251
250
                message_callback = lambda x: message
252
251
            else:
253
252
                raise BzrError("The message or message_callback keyword"
254
253
                               " parameter is required for commit().")
255
254
 
256
255
        self.bound_branch = None
 
256
        self.any_entries_changed = False
257
257
        self.any_entries_deleted = False
258
 
        if exclude is not None:
259
 
            self.exclude = sorted(
260
 
                minimum_path_selection(exclude))
261
 
        else:
262
 
            self.exclude = []
263
258
        self.local = local
264
259
        self.master_branch = None
265
260
        self.master_locked = False
266
261
        self.recursive = recursive
267
262
        self.rev_id = None
268
 
        # self.specific_files is None to indicate no filter, or any iterable to
269
 
        # indicate a filter - [] means no files at all, as per iter_changes.
270
263
        if specific_files is not None:
271
264
            self.specific_files = sorted(
272
265
                minimum_path_selection(specific_files))
273
266
        else:
274
267
            self.specific_files = None
275
 
            
 
268
        self.specific_file_ids = None
276
269
        self.allow_pointless = allow_pointless
277
270
        self.revprops = revprops
278
271
        self.message_callback = message_callback
281
274
        self.committer = committer
282
275
        self.strict = strict
283
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 = []
284
280
 
285
281
        self.work_tree.lock_write()
286
 
        self.parents = self.work_tree.get_parent_ids()
287
 
        # We can use record_iter_changes IFF iter_changes is compatible with
288
 
        # the command line parameters, and the repository has fast delta
289
 
        # generation. See bug 347649.
290
 
        self.use_record_iter_changes = (
291
 
            not self.exclude and 
292
 
            not self.branch.repository._format.supports_tree_reference and
293
 
            (self.branch.repository._format.fast_deltas or
294
 
             len(self.parents) < 2))
295
282
        self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
296
283
        self.basis_revid = self.work_tree.last_revision()
297
284
        self.basis_tree = self.work_tree.basis_tree()
302
289
                raise ConflictsInTree
303
290
 
304
291
            # Setup the bound branch variables as needed.
305
 
            self._check_bound_branch(possible_master_transports)
 
292
            self._check_bound_branch()
306
293
 
307
294
            # Check that the working tree is up to date
308
295
            old_revno, new_revno = self._check_out_of_date_tree()
315
302
            if self.config is None:
316
303
                self.config = self.branch.get_config()
317
304
 
318
 
            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])
319
316
 
320
317
            # Setup the progress bar. As the number of files that need to be
321
318
            # committed in unknown, progress is reported as stages.
332
329
            self.pb.show_count = True
333
330
            self.pb.show_bar = True
334
331
 
335
 
            self._gather_parents()
336
332
            # After a merge, a selected file commit is not supported.
337
333
            # See 'bzr help merge' for an explanation as to why.
338
 
            if len(self.parents) > 1 and self.specific_files is not None:
 
334
            self.basis_inv = self.basis_tree.inventory
 
335
            self._gather_parents()
 
336
            if len(self.parents) > 1 and self.specific_files:
339
337
                raise errors.CannotCommitSelectedFileMerge(self.specific_files)
340
 
            # Excludes are a form of selected file commit.
341
 
            if len(self.parents) > 1 and self.exclude:
342
 
                raise errors.CannotCommitSelectedFileMerge(self.exclude)
343
338
 
344
339
            # Collect the changes
345
 
            self._set_progress_stage("Collecting changes", counter=True)
 
340
            self._set_progress_stage("Collecting changes",
 
341
                    entries_title="Directory")
346
342
            self.builder = self.branch.get_commit_builder(self.parents,
347
343
                self.config, timestamp, timezone, committer, revprops, rev_id)
348
 
 
 
344
            
349
345
            try:
350
 
                self.builder.will_record_deletes()
351
346
                # find the location being committed to
352
347
                if self.bound_branch:
353
348
                    master_location = self.master_branch.base
358
353
                self.reporter.started(new_revno, self.rev_id, master_location)
359
354
 
360
355
                self._update_builder_with_changes()
 
356
                self._report_and_accumulate_deletes()
361
357
                self._check_pointless()
362
358
 
363
359
                # TODO: Now the new inventory is known, check for conflicts.
369
365
 
370
366
                # Prompt the user for a commit message if none provided
371
367
                message = message_callback(self)
 
368
                assert isinstance(message, unicode), type(message)
372
369
                self.message = message
 
370
                self._escape_commit_message()
373
371
 
374
372
                # Add revision data to the local branch
375
373
                self.rev_id = self.builder.commit(self.message)
376
374
 
377
 
            except Exception, e:
378
 
                mutter("aborting commit write group because of exception:")
379
 
                trace.log_exception_quietly()
380
 
                note("aborting commit write group: %r" % (e,))
 
375
            except:
381
376
                self.builder.abort()
382
377
                raise
383
378
 
386
381
            # Upload revision data to the master.
387
382
            # this will propagate merged revisions too if needed.
388
383
            if self.bound_branch:
389
 
                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
390
390
                # 'commit' to the master first so a timeout here causes the
391
391
                # local branch to be out of date
392
 
                self.master_branch.import_last_revision_info(
393
 
                    self.branch.repository, new_revno, self.rev_id)
 
392
                self.master_branch.set_last_revision_info(new_revno,
 
393
                                                          self.rev_id)
394
394
 
395
395
            # and now do the commit locally.
396
396
            self.branch.set_last_revision_info(new_revno, self.rev_id)
397
397
 
398
 
            # Make the working tree be up to date with the branch. This
399
 
            # includes automatic changes scheduled to be made to the tree, such
400
 
            # as updating its basis and unversioning paths that were missing.
401
 
            self.work_tree.unversion(self.deleted_ids)
 
398
            # Make the working tree up to date with the branch
402
399
            self._set_progress_stage("Updating the working tree")
403
400
            self.work_tree.update_basis_by_delta(self.rev_id,
404
 
                 self.builder.get_basis_delta())
 
401
                 self._basis_delta)
405
402
            self.reporter.completed(new_revno, self.rev_id)
406
403
            self._process_post_hooks(old_revno, new_revno)
407
404
        finally:
420
417
        # A merge with no effect on files
421
418
        if len(self.parents) > 1:
422
419
            return
423
 
        # TODO: we could simplify this by using self.builder.basis_delta.
 
420
        # TODO: we could simplify this by using self._basis_delta.
424
421
 
425
422
        # The initial commit adds a root directory, but this in itself is not
426
423
        # a worthwhile commit.
427
424
        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)):
 
425
            len(self.builder.new_inventory) == 1):
431
426
            raise PointlessCommit()
432
 
        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)):
433
433
            return
434
434
        raise PointlessCommit()
435
435
 
436
 
    def _check_bound_branch(self, possible_master_transports=None):
 
436
    def _check_bound_branch(self):
437
437
        """Check to see if the local branch is bound.
438
438
 
439
439
        If it is bound, then most of the commit will actually be
444
444
            raise errors.LocalRequiresBoundBranch()
445
445
 
446
446
        if not self.local:
447
 
            self.master_branch = self.branch.get_master_branch(
448
 
                possible_master_transports)
 
447
            self.master_branch = self.branch.get_master_branch()
449
448
 
450
449
        if not self.master_branch:
451
450
            # make this branch the reference branch for out of date checks.
462
461
        #       commits to the remote branch if they would fit.
463
462
        #       But for now, just require remote to be identical
464
463
        #       to local.
465
 
 
 
464
        
466
465
        # Make sure the local branch is identical to the master
467
466
        master_info = self.master_branch.last_revision_info()
468
467
        local_info = self.branch.last_revision_info()
525
524
    def _process_hooks(self, hook_name, old_revno, new_revno):
526
525
        if not Branch.hooks[hook_name]:
527
526
            return
528
 
 
 
527
        
529
528
        # new style commit hooks:
530
529
        if not self.bound_branch:
531
530
            hook_master = self.branch
540
539
            old_revid = self.parents[0]
541
540
        else:
542
541
            old_revid = bzrlib.revision.NULL_REVISION
543
 
 
 
542
        
544
543
        if hook_name == "pre_commit":
545
544
            future_tree = self.builder.revision_tree()
546
545
            tree_delta = future_tree.changes_from(self.basis_tree,
547
546
                                             include_root=True)
548
 
 
 
547
        
549
548
        for hook in Branch.hooks[hook_name]:
550
549
            # show the running hook in the progress bar. As hooks may
551
550
            # end up doing nothing (e.g. because they are not configured by
581
580
            # typically this will be useful enough.
582
581
            except Exception, e:
583
582
                found_exception = e
584
 
        if found_exception is not None:
 
583
        if found_exception is not None: 
585
584
            # don't do a plan raise, because the last exception may have been
586
585
            # trashed, e is our sure-to-work exception even though it loses the
587
586
            # full traceback. XXX: RBC 20060421 perhaps we could check the
588
 
            # 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 
589
588
            # 'raise e' as we do now.
590
589
            raise e
591
590
 
601
600
        if self.master_locked:
602
601
            self.master_branch.unlock()
603
602
 
 
603
    def _escape_commit_message(self):
 
604
        """Replace xml-incompatible control characters."""
 
605
        # FIXME: RBC 20060419 this should be done by the revision
 
606
        # serialiser not by commit. Then we can also add an unescaper
 
607
        # in the deserializer and start roundtripping revision messages
 
608
        # precisely. See repository_implementations/test_repository.py
 
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'),
 
617
            self.message)
 
618
        if escape_count:
 
619
            self.reporter.escaped(escape_count, self.message)
 
620
 
604
621
    def _gather_parents(self):
605
622
        """Record the parents of a merge for merge detection."""
606
 
        # TODO: Make sure that this list doesn't contain duplicate
 
623
        # TODO: Make sure that this list doesn't contain duplicate 
607
624
        # entries and the order is preserved when doing this.
608
 
        if self.use_record_iter_changes:
609
 
            return
610
 
        self.basis_inv = self.basis_tree.inventory
 
625
        self.parents = self.work_tree.get_parent_ids()
611
626
        self.parent_invs = [self.basis_inv]
612
627
        for revision in self.parents[1:]:
613
628
            if self.branch.repository.has_revision(revision):
620
635
    def _update_builder_with_changes(self):
621
636
        """Update the commit builder with the data about what has changed.
622
637
        """
623
 
        exclude = self.exclude
 
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
 
624
652
        specific_files = self.specific_files
625
653
        mutter("Selecting files for commit with filter %s", specific_files)
626
654
 
627
 
        self._check_strict()
628
 
        if self.use_record_iter_changes:
629
 
            iter_changes = self.work_tree.iter_changes(self.basis_tree,
630
 
                specific_files=specific_files)
631
 
            iter_changes = self._filter_iter_changes(iter_changes)
632
 
            for file_id, path, fs_hash in self.builder.record_iter_changes(
633
 
                self.work_tree, self.basis_revid, iter_changes):
634
 
                self.work_tree._observed_sha1(file_id, path, fs_hash)
635
 
        else:
636
 
            # Build the new inventory
637
 
            self._populate_from_inventory()
638
 
            self._record_unselected()
639
 
            self._report_and_accumulate_deletes()
640
 
 
641
 
    def _filter_iter_changes(self, iter_changes):
642
 
        """Process iter_changes.
643
 
 
644
 
        This method reports on the changes in iter_changes to the user, and 
645
 
        converts 'missing' entries in the iter_changes iterator to 'deleted'
646
 
        entries. 'missing' entries have their
647
 
 
648
 
        :param iter_changes: An iter_changes to process.
649
 
        :return: A generator of changes.
650
 
        """
651
 
        reporter = self.reporter
652
 
        report_changes = reporter.is_verbose()
653
 
        deleted_ids = []
654
 
        for change in iter_changes:
655
 
            if report_changes:
656
 
                old_path = change[1][0]
657
 
                new_path = change[1][1]
658
 
                versioned = change[3][1]
659
 
            kind = change[6][1]
660
 
            versioned = change[3][1]
661
 
            if kind is None and versioned:
662
 
                # 'missing' path
663
 
                if report_changes:
664
 
                    reporter.missing(new_path)
665
 
                deleted_ids.append(change[0])
666
 
                # Reset the new path (None) and new versioned flag (False)
667
 
                change = (change[0], (change[1][0], None), change[2],
668
 
                    (change[3][0], False)) + change[4:]
669
 
            elif kind == 'tree-reference':
670
 
                if self.recursive == 'down':
671
 
                    self._commit_nested_tree(change[0], change[1][1])
672
 
            if change[3][0] or change[3][1]:
673
 
                yield change
674
 
                if report_changes:
675
 
                    if new_path is None:
676
 
                        reporter.deleted(old_path)
677
 
                    elif old_path is None:
678
 
                        reporter.snapshot_change('added', new_path)
679
 
                    elif old_path != new_path:
680
 
                        reporter.renamed('renamed', old_path, new_path)
681
 
                    else:
682
 
                        if (new_path or 
683
 
                            self.work_tree.branch.repository._format.rich_root_data):
684
 
                            # Don't report on changes to '' in non rich root
685
 
                            # repositories.
686
 
                            reporter.snapshot_change('modified', new_path)
687
 
            self._next_progress_entry()
688
 
        # Unversion IDs that were found to be deleted
689
 
        self.deleted_ids = deleted_ids
690
 
 
691
 
    def _record_unselected(self):
 
655
        # Build the new inventory
 
656
        self._populate_from_inventory(specific_files)
 
657
 
692
658
        # If specific files are selected, then all un-selected files must be
693
659
        # recorded in their previous state. For more details, see
694
660
        # https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
695
 
        if self.specific_files or self.exclude:
696
 
            specific_files = self.specific_files or []
 
661
        if specific_files:
697
662
            for path, old_ie in self.basis_inv.iter_entries():
698
663
                if old_ie.file_id in self.builder.new_inventory:
699
664
                    # already added - skip.
700
665
                    continue
701
 
                if (is_inside_any(specific_files, path)
702
 
                    and not is_inside_any(self.exclude, path)):
703
 
                    # was inside the selected path, and not excluded - if not
704
 
                    # 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.
705
669
                    continue
706
 
                # From here down it was either not selected, or was excluded:
707
 
                # 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.
708
674
                ie = old_ie.copy()
709
675
                # Note: specific file commits after a merge are currently
710
676
                # prohibited. This test is for sanity/safety in case it's
711
677
                # required after that changes.
712
678
                if len(self.parents) > 1:
713
679
                    ie.revision = None
714
 
                self.builder.record_entry_contents(ie, self.parent_invs, path,
715
 
                    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)
716
685
 
717
686
    def _report_and_accumulate_deletes(self):
718
 
        if (isinstance(self.basis_inv, Inventory)
719
 
            and isinstance(self.builder.new_inventory, Inventory)):
720
 
            # the older Inventory classes provide a _byid dict, and building a
721
 
            # set from the keys of this dict is substantially faster than even
722
 
            # getting a set of ids from the inventory
723
 
            #
724
 
            # <lifeless> set(dict) is roughly the same speed as
725
 
            # set(iter(dict)) and both are significantly slower than
726
 
            # set(dict.keys())
727
 
            deleted_ids = set(self.basis_inv._byid.keys()) - \
728
 
               set(self.builder.new_inventory._byid.keys())
729
 
        else:
730
 
            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())
731
691
        if deleted_ids:
732
692
            self.any_entries_deleted = True
733
693
            deleted = [(self.basis_tree.id2path(file_id), file_id)
735
695
            deleted.sort()
736
696
            # XXX: this is not quite directory-order sorting
737
697
            for path, file_id in deleted:
738
 
                self.builder.record_delete(path, file_id)
 
698
                self._basis_delta.append((path, None, file_id, None))
739
699
                self.reporter.deleted(path)
740
700
 
741
 
    def _check_strict(self):
742
 
        # XXX: when we use iter_changes this would likely be faster if
743
 
        # iter_changes would check for us (even in the presence of
744
 
        # selected_files).
 
701
    def _populate_from_inventory(self, specific_files):
 
702
        """Populate the CommitBuilder by walking the working tree inventory."""
745
703
        if self.strict:
746
704
            # raise an exception as soon as we find a single unknown.
747
705
            for unknown in self.work_tree.unknowns():
748
706
                raise StrictCommitFailed()
749
 
 
750
 
    def _populate_from_inventory(self):
751
 
        """Populate the CommitBuilder by walking the working tree inventory."""
752
 
        # Build the revision inventory.
753
 
        #
754
 
        # This starts by creating a new empty inventory. Depending on
755
 
        # which files are selected for commit, and what is present in the
756
 
        # current tree, the new inventory is populated. inventory entries
757
 
        # which are candidates for modification have their revision set to
758
 
        # None; inventory entries that are carried over untouched have their
759
 
        # revision set to their prior value.
760
 
        #
761
 
        # ESEPARATIONOFCONCERNS: this function is diffing and using the diff
762
 
        # results to create a new inventory at the same time, which results
763
 
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
764
 
        # ADHB 11-07-2006
765
 
 
766
 
        specific_files = self.specific_files
767
 
        exclude = self.exclude
 
707
               
768
708
        report_changes = self.reporter.is_verbose()
769
709
        deleted_ids = []
770
710
        # A tree of paths that have been deleted. E.g. if foo/bar has been
773
713
        # XXX: Note that entries may have the wrong kind because the entry does
774
714
        # not reflect the status on disk.
775
715
        work_inv = self.work_tree.inventory
776
 
        # NB: entries will include entries within the excluded ids/paths
777
 
        # because iter_entries_by_dir has no 'exclude' facility today.
778
716
        entries = work_inv.iter_entries_by_dir(
779
717
            specific_file_ids=self.specific_file_ids, yield_parents=True)
780
718
        for path, existing_ie in entries:
782
720
            name = existing_ie.name
783
721
            parent_id = existing_ie.parent_id
784
722
            kind = existing_ie.kind
 
723
            if kind == 'directory':
 
724
                self._next_progress_entry()
785
725
            # Skip files that have been deleted from the working tree.
786
726
            # The deleted path ids are also recorded so they can be explicitly
787
727
            # unversioned later.
800
740
                if deleted_dict is not None:
801
741
                    # the path has a deleted parent, do not add it.
802
742
                    continue
803
 
            if exclude and is_inside_any(exclude, path):
804
 
                # Skip excluded paths. Excluded paths are processed by
805
 
                # _update_builder_with_changes.
806
 
                continue
807
743
            content_summary = self.work_tree.path_content_summary(path)
808
 
            kind = content_summary[0]
809
744
            # Note that when a filter of specific files is given, we must only
810
745
            # skip/record deleted files matching that filter.
811
746
            if not specific_files or is_inside_any(specific_files, path):
812
 
                if kind == 'missing':
 
747
                if content_summary[0] == 'missing':
813
748
                    if not deleted_paths:
814
749
                        # path won't have been split yet.
815
750
                        path_segments = splitpath(path)
817
752
                    for segment in path_segments:
818
753
                        deleted_dict = deleted_dict.setdefault(segment, {})
819
754
                    self.reporter.missing(path)
820
 
                    self._next_progress_entry()
821
755
                    deleted_ids.append(file_id)
822
756
                    continue
823
757
            # TODO: have the builder do the nested commit just-in-time IF and
824
758
            # only if needed.
825
 
            if kind == 'tree-reference':
 
759
            if content_summary[0] == 'tree-reference':
826
760
                # enforce repository nested tree policy.
827
761
                if (not self.work_tree.supports_tree_reference() or
828
762
                    # repository does not support it either.
829
763
                    not self.branch.repository._format.supports_tree_reference):
830
 
                    kind = 'directory'
831
 
                    content_summary = (kind, None, None, None)
832
 
                elif self.recursive == 'down':
 
764
                    content_summary = ('directory',) + content_summary[1:]
 
765
            kind = content_summary[0]
 
766
            # TODO: specific_files filtering before nested tree processing
 
767
            if kind == 'tree-reference':
 
768
                if self.recursive == 'down':
833
769
                    nested_revision_id = self._commit_nested_tree(
834
770
                        file_id, path)
835
 
                    content_summary = (kind, None, None, nested_revision_id)
 
771
                    content_summary = content_summary[:3] + (
 
772
                        nested_revision_id,)
836
773
                else:
837
 
                    nested_revision_id = self.work_tree.get_reference_revision(file_id)
838
 
                    content_summary = (kind, None, None, nested_revision_id)
 
774
                    content_summary = content_summary[:3] + (
 
775
                        self.work_tree.get_reference_revision(file_id),)
839
776
 
840
777
            # Record an entry for this item
841
778
            # Note: I don't particularly want to have the existing_ie
847
784
                content_summary)
848
785
 
849
786
        # Unversion IDs that were found to be deleted
850
 
        self.deleted_ids = deleted_ids
 
787
        self.work_tree.unversion(deleted_ids)
851
788
 
852
789
    def _commit_nested_tree(self, file_id, path):
853
790
        "Commit a nested tree."
855
792
        # FIXME: be more comprehensive here:
856
793
        # this works when both trees are in --trees repository,
857
794
        # but when both are bound to a different repository,
858
 
        # it fails; a better way of approaching this is to
 
795
        # it fails; a better way of approaching this is to 
859
796
        # finally implement the explicit-caches approach design
860
797
        # a while back - RBC 20070306.
861
798
        if sub_tree.branch.repository.has_same_location(
885
822
        else:
886
823
            ie = existing_ie.copy()
887
824
            ie.revision = None
888
 
        # For carried over entries we don't care about the fs hash - the repo
889
 
        # isn't generating a sha, so we're not saving computation time.
890
 
        _, _, fs_hash = self.builder.record_entry_contents(
891
 
            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
892
831
        if report_changes:
893
832
            self._report_change(ie, path)
894
 
        if fs_hash:
895
 
            self.work_tree._observed_sha1(ie.file_id, path, fs_hash)
896
833
        return ie
897
834
 
898
835
    def _report_change(self, ie, path):
906
843
        else:
907
844
            basis_ie = None
908
845
        change = ie.describe_change(basis_ie, ie)
909
 
        if change in (InventoryEntry.RENAMED,
 
846
        if change in (InventoryEntry.RENAMED, 
910
847
            InventoryEntry.MODIFIED_AND_RENAMED):
911
848
            old_path = self.basis_inv.id2path(ie.file_id)
912
849
            self.reporter.renamed(change, old_path, path)
913
 
            self._next_progress_entry()
914
850
        else:
915
 
            if change == 'unchanged':
916
 
                return
917
851
            self.reporter.snapshot_change(change, path)
918
 
            self._next_progress_entry()
919
852
 
920
 
    def _set_progress_stage(self, name, counter=False):
 
853
    def _set_progress_stage(self, name, entries_title=None):
921
854
        """Set the progress stage and emit an update to the progress bar."""
922
855
        self.pb_stage_name = name
923
856
        self.pb_stage_count += 1
924
 
        if counter:
 
857
        self.pb_entries_title = entries_title
 
858
        if entries_title is not None:
925
859
            self.pb_entries_count = 0
926
 
        else:
927
 
            self.pb_entries_count = None
 
860
            self.pb_entries_total = '?'
928
861
        self._emit_progress()
929
862
 
930
863
    def _next_progress_entry(self):
933
866
        self._emit_progress()
934
867
 
935
868
    def _emit_progress(self):
936
 
        if self.pb_entries_count is not None:
937
 
            text = "%s [%d] - Stage" % (self.pb_stage_name,
938
 
                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))
939
877
        else:
940
 
            text = "%s - Stage" % (self.pb_stage_name, )
 
878
            text = "%s - Stage" % (self.pb_stage_name)
941
879
        self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
942
880
 
943
 
    def _set_specific_file_ids(self):
944
 
        """populate self.specific_file_ids if we will use it."""
945
 
        if not self.use_record_iter_changes:
946
 
            # If provided, ensure the specified files are versioned
947
 
            if self.specific_files is not None:
948
 
                # Note: This routine is being called because it raises
949
 
                # PathNotVersionedError as a side effect of finding the IDs. We
950
 
                # later use the ids we found as input to the working tree
951
 
                # inventory iterator, so we only consider those ids rather than
952
 
                # examining the whole tree again.
953
 
                # XXX: Dont we have filter_unversioned to do this more
954
 
                # cheaply?
955
 
                self.specific_file_ids = tree.find_ids_across_trees(
956
 
                    self.specific_files, [self.basis_tree, self.work_tree])
957
 
            else:
958
 
                self.specific_file_ids = None