278
283
self.committer = committer
279
284
self.strict = strict
280
285
self.verbose = verbose
286
# accumulates an inventory delta to the basis entry, so we can make
287
# just the necessary updates to the workingtree's cached basis.
288
self._basis_delta = []
282
290
self.work_tree.lock_write()
283
self.parents = self.work_tree.get_parent_ids()
284
# We can use record_iter_changes IFF iter_changes is compatible with
285
# the command line parameters, and the repository has fast delta
286
# generation. See bug 347649.
287
self.use_record_iter_changes = (
288
not self.specific_files and
290
not self.branch.repository._format.supports_tree_reference and
291
(self.branch.repository._format.fast_deltas or
292
len(self.parents) < 2))
293
291
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
294
292
self.basis_revid = self.work_tree.last_revision()
295
293
self.basis_tree = self.work_tree.basis_tree()
313
311
if self.config is None:
314
312
self.config = self.branch.get_config()
316
self._set_specific_file_ids()
314
# If provided, ensure the specified files are versioned
315
if self.specific_files is not None:
316
# Note: This routine is being called because it raises
317
# PathNotVersionedError as a side effect of finding the IDs. We
318
# later use the ids we found as input to the working tree
319
# inventory iterator, so we only consider those ids rather than
320
# examining the whole tree again.
321
# XXX: Dont we have filter_unversioned to do this more
323
self.specific_file_ids = tree.find_ids_across_trees(
324
specific_files, [self.basis_tree, self.work_tree])
318
326
# Setup the progress bar. As the number of files that need to be
319
327
# committed in unknown, progress is reported as stages.
384
392
# Upload revision data to the master.
385
393
# this will propagate merged revisions too if needed.
386
394
if self.bound_branch:
387
self._set_progress_stage("Uploading data to master branch")
395
if not self.master_branch.repository.has_same_location(
396
self.branch.repository):
397
self._set_progress_stage("Uploading data to master branch")
398
self.master_branch.repository.fetch(self.branch.repository,
399
revision_id=self.rev_id)
400
# now the master has the revision data
388
401
# 'commit' to the master first so a timeout here causes the
389
402
# local branch to be out of date
390
self.master_branch.import_last_revision_info(
391
self.branch.repository, new_revno, self.rev_id)
403
self.master_branch.set_last_revision_info(new_revno,
393
406
# and now do the commit locally.
394
407
self.branch.set_last_revision_info(new_revno, self.rev_id)
396
# Make the working tree be up to date with the branch. This
397
# includes automatic changes scheduled to be made to the tree, such
398
# as updating its basis and unversioning paths that were missing.
399
self.work_tree.unversion(self.deleted_ids)
409
# Make the working tree up to date with the branch
400
410
self._set_progress_stage("Updating the working tree")
401
411
self.work_tree.update_basis_by_delta(self.rev_id,
402
self.builder.get_basis_delta())
403
413
self.reporter.completed(new_revno, self.rev_id)
404
414
self._process_post_hooks(old_revno, new_revno)
418
428
# A merge with no effect on files
419
429
if len(self.parents) > 1:
421
# TODO: we could simplify this by using self.builder.basis_delta.
431
# TODO: we could simplify this by using self._basis_delta.
423
433
# The initial commit adds a root directory, but this in itself is not
424
434
# a worthwhile commit.
425
435
if (self.basis_revid == revision.NULL_REVISION and
426
((self.builder.new_inventory is not None and
427
len(self.builder.new_inventory) == 1) or
428
len(self.builder._basis_delta) == 1)):
436
len(self.builder.new_inventory) == 1):
429
437
raise PointlessCommit()
430
if self.builder.any_changes():
438
# If length == 1, then we only have the root entry. Which means
439
# that there is no real difference (only the root could be different)
440
# unless deletes occured, in which case the length is irrelevant.
441
if (self.any_entries_deleted or
442
(len(self.builder.new_inventory) != 1 and
443
self.any_entries_changed)):
432
445
raise PointlessCommit()
434
def _check_bound_branch(self, possible_master_transports=None):
447
def _check_bound_branch(self):
435
448
"""Check to see if the local branch is bound.
437
450
If it is bound, then most of the commit will actually be
599
611
if self.master_locked:
600
612
self.master_branch.unlock()
614
def _escape_commit_message(self):
615
"""Replace xml-incompatible control characters."""
616
# FIXME: RBC 20060419 this should be done by the revision
617
# serialiser not by commit. Then we can also add an unescaper
618
# in the deserializer and start roundtripping revision messages
619
# precisely. See repository_implementations/test_repository.py
621
# Python strings can include characters that can't be
622
# represented in well-formed XML; escape characters that
623
# aren't listed in the XML specification
624
# (http://www.w3.org/TR/REC-xml/#NT-Char).
625
self.message, escape_count = re.subn(
626
u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
627
lambda match: match.group(0).encode('unicode_escape'),
630
self.reporter.escaped(escape_count, self.message)
602
632
def _gather_parents(self):
603
633
"""Record the parents of a merge for merge detection."""
604
# TODO: Make sure that this list doesn't contain duplicate
634
# TODO: Make sure that this list doesn't contain duplicate
605
635
# entries and the order is preserved when doing this.
606
if self.use_record_iter_changes:
608
self.basis_inv = self.basis_tree.inventory
636
self.parents = self.work_tree.get_parent_ids()
609
637
self.parent_invs = [self.basis_inv]
610
638
for revision in self.parents[1:]:
611
639
if self.branch.repository.has_revision(revision):
618
646
def _update_builder_with_changes(self):
619
647
"""Update the commit builder with the data about what has changed.
649
# Build the revision inventory.
651
# This starts by creating a new empty inventory. Depending on
652
# which files are selected for commit, and what is present in the
653
# current tree, the new inventory is populated. inventory entries
654
# which are candidates for modification have their revision set to
655
# None; inventory entries that are carried over untouched have their
656
# revision set to their prior value.
658
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
659
# results to create a new inventory at the same time, which results
660
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
621
663
exclude = self.exclude
622
664
specific_files = self.specific_files or []
623
665
mutter("Selecting files for commit with filter %s", specific_files)
626
if self.use_record_iter_changes:
627
iter_changes = self.work_tree.iter_changes(self.basis_tree)
628
iter_changes = self._filter_iter_changes(iter_changes)
629
for file_id, path, fs_hash in self.builder.record_iter_changes(
630
self.work_tree, self.basis_revid, iter_changes):
631
self.work_tree._observed_sha1(file_id, path, fs_hash)
633
# Build the new inventory
634
self._populate_from_inventory()
635
self._record_unselected()
636
self._report_and_accumulate_deletes()
638
def _filter_iter_changes(self, iter_changes):
639
"""Process iter_changes.
641
This method reports on the changes in iter_changes to the user, and
642
converts 'missing' entries in the iter_changes iterator to 'deleted'
643
entries. 'missing' entries have their
645
:param iter_changes: An iter_changes to process.
646
:return: A generator of changes.
648
reporter = self.reporter
649
report_changes = reporter.is_verbose()
651
for change in iter_changes:
653
old_path = change[1][0]
654
new_path = change[1][1]
655
versioned = change[3][1]
657
versioned = change[3][1]
658
if kind is None and versioned:
661
reporter.missing(new_path)
662
deleted_ids.append(change[0])
663
# Reset the new path (None) and new versioned flag (False)
664
change = (change[0], (change[1][0], None), change[2],
665
(change[3][0], False)) + change[4:]
666
elif kind == 'tree-reference':
667
if self.recursive == 'down':
668
self._commit_nested_tree(change[0], change[1][1])
669
if change[3][0] or change[3][1]:
673
reporter.deleted(old_path)
674
elif old_path is None:
675
reporter.snapshot_change('added', new_path)
676
elif old_path != new_path:
677
reporter.renamed('renamed', old_path, new_path)
680
self.work_tree.branch.repository._format.rich_root_data):
681
# Don't report on changes to '' in non rich root
683
reporter.snapshot_change('modified', new_path)
684
self._next_progress_entry()
685
# Unversion IDs that were found to be deleted
686
self.deleted_ids = deleted_ids
688
def _record_unselected(self):
667
# Build the new inventory
668
self._populate_from_inventory()
689
670
# If specific files are selected, then all un-selected files must be
690
671
# recorded in their previous state. For more details, see
691
672
# https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
692
if self.specific_files or self.exclude:
693
specific_files = self.specific_files or []
673
if specific_files or exclude:
694
674
for path, old_ie in self.basis_inv.iter_entries():
695
675
if old_ie.file_id in self.builder.new_inventory:
696
676
# already added - skip.
698
678
if (is_inside_any(specific_files, path)
699
and not is_inside_any(self.exclude, path)):
679
and not is_inside_any(exclude, path)):
700
680
# was inside the selected path, and not excluded - if not
701
681
# present it has been deleted so skip.
703
683
# From here down it was either not selected, or was excluded:
684
if old_ie.kind == 'directory':
685
self._next_progress_entry()
704
686
# We preserve the entry unaltered.
705
687
ie = old_ie.copy()
706
688
# Note: specific file commits after a merge are currently
708
690
# required after that changes.
709
691
if len(self.parents) > 1:
710
692
ie.revision = None
711
self.builder.record_entry_contents(ie, self.parent_invs, path,
712
self.basis_tree, None)
693
delta, version_recorded = self.builder.record_entry_contents(
694
ie, self.parent_invs, path, self.basis_tree, None)
696
self.any_entries_changed = True
697
if delta: self._basis_delta.append(delta)
714
699
def _report_and_accumulate_deletes(self):
715
if (isinstance(self.basis_inv, Inventory)
716
and isinstance(self.builder.new_inventory, Inventory)):
717
# the older Inventory classes provide a _byid dict, and building a
718
# set from the keys of this dict is substantially faster than even
719
# getting a set of ids from the inventory
721
# <lifeless> set(dict) is roughly the same speed as
722
# set(iter(dict)) and both are significantly slower than
724
deleted_ids = set(self.basis_inv._byid.keys()) - \
725
set(self.builder.new_inventory._byid.keys())
727
deleted_ids = set(self.basis_inv) - set(self.builder.new_inventory)
700
# XXX: Could the list of deleted paths and ids be instead taken from
701
# _populate_from_inventory?
702
deleted_ids = set(self.basis_inv._byid.keys()) - \
703
set(self.builder.new_inventory._byid.keys())
729
705
self.any_entries_deleted = True
730
706
deleted = [(self.basis_tree.id2path(file_id), file_id)
733
709
# XXX: this is not quite directory-order sorting
734
710
for path, file_id in deleted:
735
self.builder.record_delete(path, file_id)
711
self._basis_delta.append((path, None, file_id, None))
736
712
self.reporter.deleted(path)
738
def _check_strict(self):
739
# XXX: when we use iter_changes this would likely be faster if
740
# iter_changes would check for us (even in the presence of
714
def _populate_from_inventory(self):
715
"""Populate the CommitBuilder by walking the working tree inventory."""
743
717
# raise an exception as soon as we find a single unknown.
744
718
for unknown in self.work_tree.unknowns():
745
719
raise StrictCommitFailed()
747
def _populate_from_inventory(self):
748
"""Populate the CommitBuilder by walking the working tree inventory."""
749
# Build the revision inventory.
751
# This starts by creating a new empty inventory. Depending on
752
# which files are selected for commit, and what is present in the
753
# current tree, the new inventory is populated. inventory entries
754
# which are candidates for modification have their revision set to
755
# None; inventory entries that are carried over untouched have their
756
# revision set to their prior value.
758
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
759
# results to create a new inventory at the same time, which results
760
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
763
721
specific_files = self.specific_files
764
722
exclude = self.exclude
765
723
report_changes = self.reporter.is_verbose()
885
844
ie = existing_ie.copy()
886
845
ie.revision = None
887
# For carried over entries we don't care about the fs hash - the repo
888
# isn't generating a sha, so we're not saving computation time.
889
_, _, fs_hash = self.builder.record_entry_contents(
890
ie, self.parent_invs, path, self.work_tree, content_summary)
846
delta, version_recorded = self.builder.record_entry_contents(ie,
847
self.parent_invs, path, self.work_tree, content_summary)
849
self._basis_delta.append(delta)
851
self.any_entries_changed = True
891
852
if report_changes:
892
853
self._report_change(ie, path)
894
self.work_tree._observed_sha1(ie.file_id, path, fs_hash)
897
856
def _report_change(self, ie, path):
907
866
change = ie.describe_change(basis_ie, ie)
908
if change in (InventoryEntry.RENAMED,
867
if change in (InventoryEntry.RENAMED,
909
868
InventoryEntry.MODIFIED_AND_RENAMED):
910
869
old_path = self.basis_inv.id2path(ie.file_id)
911
870
self.reporter.renamed(change, old_path, path)
912
self._next_progress_entry()
914
if change == 'unchanged':
916
872
self.reporter.snapshot_change(change, path)
917
self._next_progress_entry()
919
def _set_progress_stage(self, name, counter=False):
874
def _set_progress_stage(self, name, entries_title=None):
920
875
"""Set the progress stage and emit an update to the progress bar."""
921
876
self.pb_stage_name = name
922
877
self.pb_stage_count += 1
878
self.pb_entries_title = entries_title
879
if entries_title is not None:
924
880
self.pb_entries_count = 0
926
self.pb_entries_count = None
881
self.pb_entries_total = '?'
927
882
self._emit_progress()
929
884
def _next_progress_entry(self):
932
887
self._emit_progress()
934
889
def _emit_progress(self):
935
if self.pb_entries_count is not None:
936
text = "%s [%d] - Stage" % (self.pb_stage_name,
937
self.pb_entries_count)
890
if self.pb_entries_title:
891
if self.pb_entries_total == '?':
892
text = "%s [%s %d] - Stage" % (self.pb_stage_name,
893
self.pb_entries_title, self.pb_entries_count)
895
text = "%s [%s %d/%s] - Stage" % (self.pb_stage_name,
896
self.pb_entries_title, self.pb_entries_count,
897
str(self.pb_entries_total))
939
text = "%s - Stage" % (self.pb_stage_name, )
899
text = "%s - Stage" % (self.pb_stage_name)
940
900
self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
942
def _set_specific_file_ids(self):
943
"""populate self.specific_file_ids if we will use it."""
944
if not self.use_record_iter_changes:
945
# If provided, ensure the specified files are versioned
946
if self.specific_files is not None:
947
# Note: This routine is being called because it raises
948
# PathNotVersionedError as a side effect of finding the IDs. We
949
# later use the ids we found as input to the working tree
950
# inventory iterator, so we only consider those ids rather than
951
# examining the whole tree again.
952
# XXX: Dont we have filter_unversioned to do this more
954
self.specific_file_ids = tree.find_ids_across_trees(
955
self.specific_files, [self.basis_tree, self.work_tree])
957
self.specific_file_ids = None