~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/commit.py

  • Committer: Ian Clatworthy
  • Date: 2007-12-07 04:21:59 UTC
  • mto: This revision was merged to the branch mainline in revision 3092.
  • Revision ID: ian.clatworthy@internode.on.net-20071207042159-n9rmhanqid1l7olh
Better PDF for Qiock Start Card (Ian Clatworthy)

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
 
387
382
            # this will propagate merged revisions too if needed.
388
383
            if self.bound_branch:
389
384
                self._set_progress_stage("Uploading data to master branch")
 
385
                self.master_branch.repository.fetch(self.branch.repository,
 
386
                                                    revision_id=self.rev_id)
 
387
                # now the master has the revision data
390
388
                # 'commit' to the master first so a timeout here causes the
391
389
                # local branch to be out of date
392
 
                self.master_branch.import_last_revision_info(
393
 
                    self.branch.repository, new_revno, self.rev_id)
 
390
                self.master_branch.set_last_revision_info(new_revno,
 
391
                                                          self.rev_id)
394
392
 
395
393
            # and now do the commit locally.
396
394
            self.branch.set_last_revision_info(new_revno, self.rev_id)
397
395
 
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)
 
396
            # Make the working tree up to date with the branch
402
397
            self._set_progress_stage("Updating the working tree")
403
398
            self.work_tree.update_basis_by_delta(self.rev_id,
404
 
                 self.builder.get_basis_delta())
 
399
                 self._basis_delta)
405
400
            self.reporter.completed(new_revno, self.rev_id)
406
401
            self._process_post_hooks(old_revno, new_revno)
407
402
        finally:
420
415
        # A merge with no effect on files
421
416
        if len(self.parents) > 1:
422
417
            return
423
 
        # TODO: we could simplify this by using self.builder.basis_delta.
 
418
        # TODO: we could simplify this by using self._basis_delta.
424
419
 
425
420
        # The initial commit adds a root directory, but this in itself is not
426
421
        # a worthwhile commit.
427
422
        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)):
 
423
            len(self.builder.new_inventory) == 1):
431
424
            raise PointlessCommit()
432
 
        if self.builder.any_changes():
 
425
        # If length == 1, then we only have the root entry. Which means
 
426
        # that there is no real difference (only the root could be different)
 
427
        # unless deletes occured, in which case the length is irrelevant.
 
428
        if (self.any_entries_deleted or 
 
429
            (len(self.builder.new_inventory) != 1 and
 
430
             self.any_entries_changed)):
433
431
            return
434
432
        raise PointlessCommit()
435
433
 
436
 
    def _check_bound_branch(self, possible_master_transports=None):
 
434
    def _check_bound_branch(self):
437
435
        """Check to see if the local branch is bound.
438
436
 
439
437
        If it is bound, then most of the commit will actually be
444
442
            raise errors.LocalRequiresBoundBranch()
445
443
 
446
444
        if not self.local:
447
 
            self.master_branch = self.branch.get_master_branch(
448
 
                possible_master_transports)
 
445
            self.master_branch = self.branch.get_master_branch()
449
446
 
450
447
        if not self.master_branch:
451
448
            # make this branch the reference branch for out of date checks.
462
459
        #       commits to the remote branch if they would fit.
463
460
        #       But for now, just require remote to be identical
464
461
        #       to local.
465
 
 
 
462
        
466
463
        # Make sure the local branch is identical to the master
467
464
        master_info = self.master_branch.last_revision_info()
468
465
        local_info = self.branch.last_revision_info()
525
522
    def _process_hooks(self, hook_name, old_revno, new_revno):
526
523
        if not Branch.hooks[hook_name]:
527
524
            return
528
 
 
 
525
        
529
526
        # new style commit hooks:
530
527
        if not self.bound_branch:
531
528
            hook_master = self.branch
540
537
            old_revid = self.parents[0]
541
538
        else:
542
539
            old_revid = bzrlib.revision.NULL_REVISION
543
 
 
 
540
        
544
541
        if hook_name == "pre_commit":
545
542
            future_tree = self.builder.revision_tree()
546
543
            tree_delta = future_tree.changes_from(self.basis_tree,
547
544
                                             include_root=True)
548
 
 
 
545
        
549
546
        for hook in Branch.hooks[hook_name]:
550
547
            # show the running hook in the progress bar. As hooks may
551
548
            # end up doing nothing (e.g. because they are not configured by
581
578
            # typically this will be useful enough.
582
579
            except Exception, e:
583
580
                found_exception = e
584
 
        if found_exception is not None:
 
581
        if found_exception is not None: 
585
582
            # don't do a plan raise, because the last exception may have been
586
583
            # trashed, e is our sure-to-work exception even though it loses the
587
584
            # full traceback. XXX: RBC 20060421 perhaps we could check the
588
 
            # exc_info and if its the same one do a plain raise otherwise
 
585
            # exc_info and if its the same one do a plain raise otherwise 
589
586
            # 'raise e' as we do now.
590
587
            raise e
591
588
 
601
598
        if self.master_locked:
602
599
            self.master_branch.unlock()
603
600
 
 
601
    def _escape_commit_message(self):
 
602
        """Replace xml-incompatible control characters."""
 
603
        # FIXME: RBC 20060419 this should be done by the revision
 
604
        # serialiser not by commit. Then we can also add an unescaper
 
605
        # in the deserializer and start roundtripping revision messages
 
606
        # precisely. See repository_implementations/test_repository.py
 
607
        
 
608
        # Python strings can include characters that can't be
 
609
        # represented in well-formed XML; escape characters that
 
610
        # aren't listed in the XML specification
 
611
        # (http://www.w3.org/TR/REC-xml/#NT-Char).
 
612
        self.message, escape_count = re.subn(
 
613
            u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
 
614
            lambda match: match.group(0).encode('unicode_escape'),
 
615
            self.message)
 
616
        if escape_count:
 
617
            self.reporter.escaped(escape_count, self.message)
 
618
 
604
619
    def _gather_parents(self):
605
620
        """Record the parents of a merge for merge detection."""
606
 
        # TODO: Make sure that this list doesn't contain duplicate
 
621
        # TODO: Make sure that this list doesn't contain duplicate 
607
622
        # 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
 
623
        self.parents = self.work_tree.get_parent_ids()
611
624
        self.parent_invs = [self.basis_inv]
612
625
        for revision in self.parents[1:]:
613
626
            if self.branch.repository.has_revision(revision):
620
633
    def _update_builder_with_changes(self):
621
634
        """Update the commit builder with the data about what has changed.
622
635
        """
623
 
        exclude = self.exclude
 
636
        # Build the revision inventory.
 
637
        #
 
638
        # This starts by creating a new empty inventory. Depending on
 
639
        # which files are selected for commit, and what is present in the
 
640
        # current tree, the new inventory is populated. inventory entries 
 
641
        # which are candidates for modification have their revision set to
 
642
        # None; inventory entries that are carried over untouched have their
 
643
        # revision set to their prior value.
 
644
        #
 
645
        # ESEPARATIONOFCONCERNS: this function is diffing and using the diff
 
646
        # results to create a new inventory at the same time, which results
 
647
        # in bugs like #46635.  Any reason not to use/enhance Tree.changes_from?
 
648
        # ADHB 11-07-2006
 
649
 
624
650
        specific_files = self.specific_files
625
651
        mutter("Selecting files for commit with filter %s", specific_files)
626
652
 
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):
 
653
        # Build the new inventory
 
654
        self._populate_from_inventory(specific_files)
 
655
 
692
656
        # If specific files are selected, then all un-selected files must be
693
657
        # recorded in their previous state. For more details, see
694
658
        # https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
695
 
        if self.specific_files or self.exclude:
696
 
            specific_files = self.specific_files or []
 
659
        if specific_files:
697
660
            for path, old_ie in self.basis_inv.iter_entries():
698
661
                if old_ie.file_id in self.builder.new_inventory:
699
662
                    # already added - skip.
700
663
                    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.
 
664
                if is_inside_any(specific_files, path):
 
665
                    # was inside the selected path, if not present it has been
 
666
                    # deleted so skip.
705
667
                    continue
706
 
                # From here down it was either not selected, or was excluded:
707
 
                # We preserve the entry unaltered.
 
668
                if old_ie.kind == 'directory':
 
669
                    self._next_progress_entry()
 
670
                # not in final inv yet, was not in the selected files, so is an
 
671
                # entry to be preserved unaltered.
708
672
                ie = old_ie.copy()
709
673
                # Note: specific file commits after a merge are currently
710
674
                # prohibited. This test is for sanity/safety in case it's
711
675
                # required after that changes.
712
676
                if len(self.parents) > 1:
713
677
                    ie.revision = None
714
 
                self.builder.record_entry_contents(ie, self.parent_invs, path,
715
 
                    self.basis_tree, None)
 
678
                delta, version_recorded = self.builder.record_entry_contents(
 
679
                    ie, self.parent_invs, path, self.basis_tree, None)
 
680
                if version_recorded:
 
681
                    self.any_entries_changed = True
 
682
                if delta: self._basis_delta.append(delta)
716
683
 
717
684
    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)
 
685
        # XXX: Could the list of deleted paths and ids be instead taken from
 
686
        # _populate_from_inventory?
 
687
        deleted_ids = set(self.basis_inv._byid.keys()) - \
 
688
            set(self.builder.new_inventory._byid.keys())
731
689
        if deleted_ids:
732
690
            self.any_entries_deleted = True
733
691
            deleted = [(self.basis_tree.id2path(file_id), file_id)
735
693
            deleted.sort()
736
694
            # XXX: this is not quite directory-order sorting
737
695
            for path, file_id in deleted:
738
 
                self.builder.record_delete(path, file_id)
 
696
                self._basis_delta.append((path, None, file_id, None))
739
697
                self.reporter.deleted(path)
740
698
 
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).
 
699
    def _populate_from_inventory(self, specific_files):
 
700
        """Populate the CommitBuilder by walking the working tree inventory."""
745
701
        if self.strict:
746
702
            # raise an exception as soon as we find a single unknown.
747
703
            for unknown in self.work_tree.unknowns():
748
704
                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
 
705
               
768
706
        report_changes = self.reporter.is_verbose()
769
707
        deleted_ids = []
770
708
        # A tree of paths that have been deleted. E.g. if foo/bar has been
773
711
        # XXX: Note that entries may have the wrong kind because the entry does
774
712
        # not reflect the status on disk.
775
713
        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
714
        entries = work_inv.iter_entries_by_dir(
779
715
            specific_file_ids=self.specific_file_ids, yield_parents=True)
780
716
        for path, existing_ie in entries:
782
718
            name = existing_ie.name
783
719
            parent_id = existing_ie.parent_id
784
720
            kind = existing_ie.kind
 
721
            if kind == 'directory':
 
722
                self._next_progress_entry()
785
723
            # Skip files that have been deleted from the working tree.
786
724
            # The deleted path ids are also recorded so they can be explicitly
787
725
            # unversioned later.
800
738
                if deleted_dict is not None:
801
739
                    # the path has a deleted parent, do not add it.
802
740
                    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
741
            content_summary = self.work_tree.path_content_summary(path)
808
 
            kind = content_summary[0]
809
742
            # Note that when a filter of specific files is given, we must only
810
743
            # skip/record deleted files matching that filter.
811
744
            if not specific_files or is_inside_any(specific_files, path):
812
 
                if kind == 'missing':
 
745
                if content_summary[0] == 'missing':
813
746
                    if not deleted_paths:
814
747
                        # path won't have been split yet.
815
748
                        path_segments = splitpath(path)
817
750
                    for segment in path_segments:
818
751
                        deleted_dict = deleted_dict.setdefault(segment, {})
819
752
                    self.reporter.missing(path)
820
 
                    self._next_progress_entry()
821
753
                    deleted_ids.append(file_id)
822
754
                    continue
823
755
            # TODO: have the builder do the nested commit just-in-time IF and
824
756
            # only if needed.
825
 
            if kind == 'tree-reference':
 
757
            if content_summary[0] == 'tree-reference':
826
758
                # enforce repository nested tree policy.
827
759
                if (not self.work_tree.supports_tree_reference() or
828
760
                    # repository does not support it either.
829
761
                    not self.branch.repository._format.supports_tree_reference):
830
 
                    kind = 'directory'
831
 
                    content_summary = (kind, None, None, None)
832
 
                elif self.recursive == 'down':
 
762
                    content_summary = ('directory',) + content_summary[1:]
 
763
            kind = content_summary[0]
 
764
            # TODO: specific_files filtering before nested tree processing
 
765
            if kind == 'tree-reference':
 
766
                if self.recursive == 'down':
833
767
                    nested_revision_id = self._commit_nested_tree(
834
768
                        file_id, path)
835
 
                    content_summary = (kind, None, None, nested_revision_id)
 
769
                    content_summary = content_summary[:3] + (
 
770
                        nested_revision_id,)
836
771
                else:
837
 
                    nested_revision_id = self.work_tree.get_reference_revision(file_id)
838
 
                    content_summary = (kind, None, None, nested_revision_id)
 
772
                    content_summary = content_summary[:3] + (
 
773
                        self.work_tree.get_reference_revision(file_id),)
839
774
 
840
775
            # Record an entry for this item
841
776
            # Note: I don't particularly want to have the existing_ie
847
782
                content_summary)
848
783
 
849
784
        # Unversion IDs that were found to be deleted
850
 
        self.deleted_ids = deleted_ids
 
785
        self.work_tree.unversion(deleted_ids)
851
786
 
852
787
    def _commit_nested_tree(self, file_id, path):
853
788
        "Commit a nested tree."
855
790
        # FIXME: be more comprehensive here:
856
791
        # this works when both trees are in --trees repository,
857
792
        # but when both are bound to a different repository,
858
 
        # it fails; a better way of approaching this is to
 
793
        # it fails; a better way of approaching this is to 
859
794
        # finally implement the explicit-caches approach design
860
795
        # a while back - RBC 20070306.
861
796
        if sub_tree.branch.repository.has_same_location(
885
820
        else:
886
821
            ie = existing_ie.copy()
887
822
            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)
 
823
        delta, version_recorded = self.builder.record_entry_contents(ie,
 
824
            self.parent_invs, path, self.work_tree, content_summary)
 
825
        if delta:
 
826
            self._basis_delta.append(delta)
 
827
        if version_recorded:
 
828
            self.any_entries_changed = True
892
829
        if report_changes:
893
830
            self._report_change(ie, path)
894
 
        if fs_hash:
895
 
            self.work_tree._observed_sha1(ie.file_id, path, fs_hash)
896
831
        return ie
897
832
 
898
833
    def _report_change(self, ie, path):
906
841
        else:
907
842
            basis_ie = None
908
843
        change = ie.describe_change(basis_ie, ie)
909
 
        if change in (InventoryEntry.RENAMED,
 
844
        if change in (InventoryEntry.RENAMED, 
910
845
            InventoryEntry.MODIFIED_AND_RENAMED):
911
846
            old_path = self.basis_inv.id2path(ie.file_id)
912
847
            self.reporter.renamed(change, old_path, path)
913
 
            self._next_progress_entry()
914
848
        else:
915
 
            if change == 'unchanged':
916
 
                return
917
849
            self.reporter.snapshot_change(change, path)
918
 
            self._next_progress_entry()
919
850
 
920
 
    def _set_progress_stage(self, name, counter=False):
 
851
    def _set_progress_stage(self, name, entries_title=None):
921
852
        """Set the progress stage and emit an update to the progress bar."""
922
853
        self.pb_stage_name = name
923
854
        self.pb_stage_count += 1
924
 
        if counter:
 
855
        self.pb_entries_title = entries_title
 
856
        if entries_title is not None:
925
857
            self.pb_entries_count = 0
926
 
        else:
927
 
            self.pb_entries_count = None
 
858
            self.pb_entries_total = '?'
928
859
        self._emit_progress()
929
860
 
930
861
    def _next_progress_entry(self):
933
864
        self._emit_progress()
934
865
 
935
866
    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)
 
867
        if self.pb_entries_title:
 
868
            if self.pb_entries_total == '?':
 
869
                text = "%s [%s %d] - Stage" % (self.pb_stage_name,
 
870
                    self.pb_entries_title, self.pb_entries_count)
 
871
            else:
 
872
                text = "%s [%s %d/%s] - Stage" % (self.pb_stage_name,
 
873
                    self.pb_entries_title, self.pb_entries_count,
 
874
                    str(self.pb_entries_total))
939
875
        else:
940
 
            text = "%s - Stage" % (self.pb_stage_name, )
 
876
            text = "%s - Stage" % (self.pb_stage_name)
941
877
        self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
942
878
 
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