335
329
self.pb.show_count = True
336
330
self.pb.show_bar = True
338
self._gather_parents()
339
332
# After a merge, a selected file commit is not supported.
340
333
# See 'bzr help merge' for an explanation as to why.
334
self.basis_inv = self.basis_tree.inventory
335
self._gather_parents()
341
336
if len(self.parents) > 1 and self.specific_files:
342
337
raise errors.CannotCommitSelectedFileMerge(self.specific_files)
343
# Excludes are a form of selected file commit.
344
if len(self.parents) > 1 and self.exclude:
345
raise errors.CannotCommitSelectedFileMerge(self.exclude)
347
339
# Collect the changes
348
self._set_progress_stage("Collecting changes", counter=True)
340
self._set_progress_stage("Collecting changes",
341
entries_title="Directory")
349
342
self.builder = self.branch.get_commit_builder(self.parents,
350
343
self.config, timestamp, timezone, committer, revprops, rev_id)
353
self.builder.will_record_deletes()
354
346
# find the location being committed to
355
347
if self.bound_branch:
356
348
master_location = self.master_branch.base
421
417
# A merge with no effect on files
422
418
if len(self.parents) > 1:
424
# TODO: we could simplify this by using self.builder.basis_delta.
420
# TODO: we could simplify this by using self._basis_delta.
426
422
# The initial commit adds a root directory, but this in itself is not
427
423
# a worthwhile commit.
428
424
if (self.basis_revid == revision.NULL_REVISION and
429
((self.builder.new_inventory is not None and
430
len(self.builder.new_inventory) == 1) or
431
len(self.builder._basis_delta) == 1)):
425
len(self.builder.new_inventory) == 1):
432
426
raise PointlessCommit()
433
if self.builder.any_changes():
427
# If length == 1, then we only have the root entry. Which means
428
# that there is no real difference (only the root could be different)
429
# unless deletes occured, in which case the length is irrelevant.
430
if (self.any_entries_deleted or
431
(len(self.builder.new_inventory) != 1 and
432
self.any_entries_changed)):
435
434
raise PointlessCommit()
437
def _check_bound_branch(self, possible_master_transports=None):
436
def _check_bound_branch(self):
438
437
"""Check to see if the local branch is bound.
440
439
If it is bound, then most of the commit will actually be
608
606
# serialiser not by commit. Then we can also add an unescaper
609
607
# in the deserializer and start roundtripping revision messages
610
608
# precisely. See repository_implementations/test_repository.py
611
self.message, escape_count = xml_serializer.escape_invalid_chars(
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'),
614
619
self.reporter.escaped(escape_count, self.message)
616
621
def _gather_parents(self):
617
622
"""Record the parents of a merge for merge detection."""
618
# TODO: Make sure that this list doesn't contain duplicate
623
# TODO: Make sure that this list doesn't contain duplicate
619
624
# entries and the order is preserved when doing this.
620
if self.use_record_iter_changes:
622
self.basis_inv = self.basis_tree.inventory
625
self.parents = self.work_tree.get_parent_ids()
623
626
self.parent_invs = [self.basis_inv]
624
627
for revision in self.parents[1:]:
625
628
if self.branch.repository.has_revision(revision):
632
635
def _update_builder_with_changes(self):
633
636
"""Update the commit builder with the data about what has changed.
635
exclude = self.exclude
636
specific_files = self.specific_files or []
638
# Build the revision inventory.
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.
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?
652
specific_files = self.specific_files
637
653
mutter("Selecting files for commit with filter %s", specific_files)
640
if self.use_record_iter_changes:
641
iter_changes = self.work_tree.iter_changes(self.basis_tree)
642
iter_changes = self._filter_iter_changes(iter_changes)
643
for file_id, path, fs_hash in self.builder.record_iter_changes(
644
self.work_tree, self.basis_revid, iter_changes):
645
self.work_tree._observed_sha1(file_id, path, fs_hash)
647
# Build the new inventory
648
self._populate_from_inventory()
649
self._record_unselected()
650
self._report_and_accumulate_deletes()
652
def _filter_iter_changes(self, iter_changes):
653
"""Process iter_changes.
655
This method reports on the changes in iter_changes to the user, and
656
converts 'missing' entries in the iter_changes iterator to 'deleted'
657
entries. 'missing' entries have their
659
:param iter_changes: An iter_changes to process.
660
:return: A generator of changes.
662
reporter = self.reporter
663
report_changes = reporter.is_verbose()
665
for change in iter_changes:
667
old_path = change[1][0]
668
new_path = change[1][1]
669
versioned = change[3][1]
671
versioned = change[3][1]
672
if kind is None and versioned:
675
reporter.missing(new_path)
676
deleted_ids.append(change[0])
677
# Reset the new path (None) and new versioned flag (False)
678
change = (change[0], (change[1][0], None), change[2],
679
(change[3][0], False)) + change[4:]
680
elif kind == 'tree-reference':
681
if self.recursive == 'down':
682
self._commit_nested_tree(change[0], change[1][1])
683
if change[3][0] or change[3][1]:
687
reporter.deleted(old_path)
688
elif old_path is None:
689
reporter.snapshot_change('added', new_path)
690
elif old_path != new_path:
691
reporter.renamed('renamed', old_path, new_path)
694
self.work_tree.branch.repository._format.rich_root_data):
695
# Don't report on changes to '' in non rich root
697
reporter.snapshot_change('modified', new_path)
698
self._next_progress_entry()
699
# Unversion IDs that were found to be deleted
700
self.work_tree.unversion(deleted_ids)
702
def _record_unselected(self):
655
# Build the new inventory
656
self._populate_from_inventory(specific_files)
703
658
# If specific files are selected, then all un-selected files must be
704
659
# recorded in their previous state. For more details, see
705
660
# https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
706
if self.specific_files or self.exclude:
707
specific_files = self.specific_files or []
708
662
for path, old_ie in self.basis_inv.iter_entries():
709
663
if old_ie.file_id in self.builder.new_inventory:
710
664
# already added - skip.
712
if (is_inside_any(specific_files, path)
713
and not is_inside_any(self.exclude, path)):
714
# was inside the selected path, and not excluded - if not
715
# present it has been deleted so skip.
666
if is_inside_any(specific_files, path):
667
# was inside the selected path, if not present it has been
717
# From here down it was either not selected, or was excluded:
718
# We preserve the entry unaltered.
670
if old_ie.kind == 'directory':
671
self._next_progress_entry()
672
# not in final inv yet, was not in the selected files, so is an
673
# entry to be preserved unaltered.
719
674
ie = old_ie.copy()
720
675
# Note: specific file commits after a merge are currently
721
676
# prohibited. This test is for sanity/safety in case it's
722
677
# required after that changes.
723
678
if len(self.parents) > 1:
724
679
ie.revision = None
725
self.builder.record_entry_contents(ie, self.parent_invs, path,
726
self.basis_tree, None)
680
delta, version_recorded = self.builder.record_entry_contents(
681
ie, self.parent_invs, path, self.basis_tree, None)
683
self.any_entries_changed = True
684
if delta: self._basis_delta.append(delta)
728
686
def _report_and_accumulate_deletes(self):
729
if (isinstance(self.basis_inv, Inventory)
730
and isinstance(self.builder.new_inventory, Inventory)):
731
# the older Inventory classes provide a _byid dict, and building a
732
# set from the keys of this dict is substantially faster than even
733
# getting a set of ids from the inventory
735
# <lifeless> set(dict) is roughly the same speed as
736
# set(iter(dict)) and both are significantly slower than
738
deleted_ids = set(self.basis_inv._byid.keys()) - \
739
set(self.builder.new_inventory._byid.keys())
741
deleted_ids = set(self.basis_inv) - set(self.builder.new_inventory)
687
# XXX: Could the list of deleted paths and ids be instead taken from
688
# _populate_from_inventory?
689
deleted_ids = set(self.basis_inv._byid.keys()) - \
690
set(self.builder.new_inventory._byid.keys())
743
692
self.any_entries_deleted = True
744
693
deleted = [(self.basis_tree.id2path(file_id), file_id)
747
696
# XXX: this is not quite directory-order sorting
748
697
for path, file_id in deleted:
749
self.builder.record_delete(path, file_id)
698
self._basis_delta.append((path, None, file_id, None))
750
699
self.reporter.deleted(path)
752
def _check_strict(self):
753
# XXX: when we use iter_changes this would likely be faster if
754
# iter_changes would check for us (even in the presence of
701
def _populate_from_inventory(self, specific_files):
702
"""Populate the CommitBuilder by walking the working tree inventory."""
757
704
# raise an exception as soon as we find a single unknown.
758
705
for unknown in self.work_tree.unknowns():
759
706
raise StrictCommitFailed()
761
def _populate_from_inventory(self):
762
"""Populate the CommitBuilder by walking the working tree inventory."""
763
# Build the revision inventory.
765
# This starts by creating a new empty inventory. Depending on
766
# which files are selected for commit, and what is present in the
767
# current tree, the new inventory is populated. inventory entries
768
# which are candidates for modification have their revision set to
769
# None; inventory entries that are carried over untouched have their
770
# revision set to their prior value.
772
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
773
# results to create a new inventory at the same time, which results
774
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
777
specific_files = self.specific_files
778
exclude = self.exclude
779
708
report_changes = self.reporter.is_verbose()
781
710
# A tree of paths that have been deleted. E.g. if foo/bar has been
921
845
change = ie.describe_change(basis_ie, ie)
922
if change in (InventoryEntry.RENAMED,
846
if change in (InventoryEntry.RENAMED,
923
847
InventoryEntry.MODIFIED_AND_RENAMED):
924
848
old_path = self.basis_inv.id2path(ie.file_id)
925
849
self.reporter.renamed(change, old_path, path)
926
self._next_progress_entry()
928
if change == 'unchanged':
930
851
self.reporter.snapshot_change(change, path)
931
self._next_progress_entry()
933
def _set_progress_stage(self, name, counter=False):
853
def _set_progress_stage(self, name, entries_title=None):
934
854
"""Set the progress stage and emit an update to the progress bar."""
935
855
self.pb_stage_name = name
936
856
self.pb_stage_count += 1
857
self.pb_entries_title = entries_title
858
if entries_title is not None:
938
859
self.pb_entries_count = 0
940
self.pb_entries_count = None
860
self.pb_entries_total = '?'
941
861
self._emit_progress()
943
863
def _next_progress_entry(self):
946
866
self._emit_progress()
948
868
def _emit_progress(self):
949
if self.pb_entries_count is not None:
950
text = "%s [%d] - Stage" % (self.pb_stage_name,
951
self.pb_entries_count)
869
if self.pb_entries_title:
870
if self.pb_entries_total == '?':
871
text = "%s [%s %d] - Stage" % (self.pb_stage_name,
872
self.pb_entries_title, self.pb_entries_count)
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))
953
text = "%s - Stage" % (self.pb_stage_name, )
878
text = "%s - Stage" % (self.pb_stage_name)
954
879
self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
956
def _set_specific_file_ids(self):
957
"""populate self.specific_file_ids if we will use it."""
958
if not self.use_record_iter_changes:
959
# If provided, ensure the specified files are versioned
960
if self.specific_files is not None:
961
# Note: This routine is being called because it raises
962
# PathNotVersionedError as a side effect of finding the IDs. We
963
# later use the ids we found as input to the working tree
964
# inventory iterator, so we only consider those ids rather than
965
# examining the whole tree again.
966
# XXX: Dont we have filter_unversioned to do this more
968
self.specific_file_ids = tree.find_ids_across_trees(
969
self.specific_files, [self.basis_tree, self.work_tree])
971
self.specific_file_ids = None