289
338
self.pb.show_count = True
290
339
self.pb.show_bar = True
341
self.basis_inv = self.basis_tree.inventory
342
self._gather_parents()
292
343
# After a merge, a selected file commit is not supported.
293
344
# See 'bzr help merge' for an explanation as to why.
294
self.basis_inv = self.basis_tree.inventory
295
self._gather_parents()
296
345
if len(self.parents) > 1 and self.specific_files:
297
346
raise errors.CannotCommitSelectedFileMerge(self.specific_files)
347
# Excludes are a form of selected file commit.
348
if len(self.parents) > 1 and self.exclude:
349
raise errors.CannotCommitSelectedFileMerge(self.exclude)
299
351
# Collect the changes
300
self._emit_progress_set_stage("Collecting changes", show_entries=True)
352
self._set_progress_stage("Collecting changes",
353
entries_title="Directory")
301
354
self.builder = self.branch.get_commit_builder(self.parents,
302
355
self.config, timestamp, timezone, committer, revprops, rev_id)
303
self._update_builder_with_changes()
304
self._check_pointless()
306
# TODO: Now the new inventory is known, check for conflicts.
307
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
308
# weave lines, because nothing should be recorded until it is known
309
# that commit will succeed.
310
self._emit_progress_set_stage("Saving data locally")
311
self.builder.finish_inventory()
313
# Prompt the user for a commit message if none provided
314
message = message_callback(self)
315
assert isinstance(message, unicode), type(message)
316
self.message = message
317
self._escape_commit_message()
319
# Add revision data to the local branch
320
self.rev_id = self.builder.commit(self.message)
358
# find the location being committed to
359
if self.bound_branch:
360
master_location = self.master_branch.base
362
master_location = self.branch.base
364
# report the start of the commit
365
self.reporter.started(new_revno, self.rev_id, master_location)
367
self._update_builder_with_changes()
368
self._report_and_accumulate_deletes()
369
self._check_pointless()
371
# TODO: Now the new inventory is known, check for conflicts.
372
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
373
# weave lines, because nothing should be recorded until it is known
374
# that commit will succeed.
375
self._set_progress_stage("Saving data locally")
376
self.builder.finish_inventory()
378
# Prompt the user for a commit message if none provided
379
message = message_callback(self)
380
self.message = message
381
self._escape_commit_message()
383
# Add revision data to the local branch
384
self.rev_id = self.builder.commit(self.message)
390
self._process_pre_hooks(old_revno, new_revno)
322
392
# Upload revision data to the master.
323
393
# this will propagate merged revisions too if needed.
324
394
if self.bound_branch:
325
self._emit_progress_set_stage("Uploading data to master branch")
326
self.master_branch.repository.fetch(self.branch.repository,
327
revision_id=self.rev_id)
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)
328
400
# now the master has the revision data
329
401
# 'commit' to the master first so a timeout here causes the
330
402
# local branch to be out of date
592
660
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
593
661
# ADHB 11-07-2006
663
exclude = self.exclude
664
specific_files = self.specific_files or []
665
mutter("Selecting files for commit with filter %s", specific_files)
667
# Build the new inventory
668
self._populate_from_inventory()
670
# If specific files are selected, then all un-selected files must be
671
# recorded in their previous state. For more details, see
672
# https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
673
if specific_files or exclude:
674
for path, old_ie in self.basis_inv.iter_entries():
675
if old_ie.file_id in self.builder.new_inventory:
676
# already added - skip.
678
if (is_inside_any(specific_files, path)
679
and not is_inside_any(exclude, path)):
680
# was inside the selected path, and not excluded - if not
681
# present it has been deleted so skip.
683
# From here down it was either not selected, or was excluded:
684
if old_ie.kind == 'directory':
685
self._next_progress_entry()
686
# We preserve the entry unaltered.
688
# Note: specific file commits after a merge are currently
689
# prohibited. This test is for sanity/safety in case it's
690
# required after that changes.
691
if len(self.parents) > 1:
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)
699
def _report_and_accumulate_deletes(self):
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())
705
self.any_entries_deleted = True
706
deleted = [(self.basis_tree.id2path(file_id), file_id)
707
for file_id in deleted_ids]
709
# XXX: this is not quite directory-order sorting
710
for path, file_id in deleted:
711
self._basis_delta.append((path, None, file_id, None))
712
self.reporter.deleted(path)
714
def _populate_from_inventory(self):
715
"""Populate the CommitBuilder by walking the working tree inventory."""
717
# raise an exception as soon as we find a single unknown.
718
for unknown in self.work_tree.unknowns():
719
raise StrictCommitFailed()
595
721
specific_files = self.specific_files
596
mutter("Selecting files for commit with filter %s", specific_files)
722
exclude = self.exclude
723
report_changes = self.reporter.is_verbose()
725
# A tree of paths that have been deleted. E.g. if foo/bar has been
726
# deleted, then we have {'foo':{'bar':{}}}
728
# XXX: Note that entries may have the wrong kind because the entry does
729
# not reflect the status on disk.
597
730
work_inv = self.work_tree.inventory
598
assert work_inv.root is not None
599
self.pb_entries_total = len(work_inv)
601
# Check and warn about old CommitBuilders
602
entries = work_inv.iter_entries()
603
if not self.builder.record_root_entry:
604
symbol_versioning.warn('CommitBuilders should support recording'
605
' the root entry as of bzr 0.10.', DeprecationWarning,
607
self.builder.new_inventory.add(self.basis_inv.root.copy())
611
deleted_paths = set()
612
for path, new_ie in entries:
613
self._emit_progress_next_entry()
614
file_id = new_ie.file_id
731
# NB: entries will include entries within the excluded ids/paths
732
# because iter_entries_by_dir has no 'exclude' facility today.
733
entries = work_inv.iter_entries_by_dir(
734
specific_file_ids=self.specific_file_ids, yield_parents=True)
735
for path, existing_ie in entries:
736
file_id = existing_ie.file_id
737
name = existing_ie.name
738
parent_id = existing_ie.parent_id
739
kind = existing_ie.kind
740
if kind == 'directory':
741
self._next_progress_entry()
616
742
# Skip files that have been deleted from the working tree.
617
# The deleted files/directories are also recorded so they
618
# can be explicitly unversioned later. Note that when a
619
# filter of specific files is given, we must only skip/record
620
# deleted files matching that filter.
621
if is_inside_any(deleted_paths, path):
743
# The deleted path ids are also recorded so they can be explicitly
746
path_segments = splitpath(path)
747
deleted_dict = deleted_paths
748
for segment in path_segments:
749
deleted_dict = deleted_dict.get(segment, None)
751
# We either took a path not present in the dict
752
# (deleted_dict was None), or we've reached an empty
753
# child dir in the dict, so are now a sub-path.
757
if deleted_dict is not None:
758
# the path has a deleted parent, do not add it.
760
if exclude and is_inside_any(exclude, path):
761
# Skip excluded paths. Excluded paths are processed by
762
# _update_builder_with_changes.
764
content_summary = self.work_tree.path_content_summary(path)
765
# Note that when a filter of specific files is given, we must only
766
# skip/record deleted files matching that filter.
623
767
if not specific_files or is_inside_any(specific_files, path):
624
if not self.work_tree.has_filename(path):
625
deleted_paths.add(path)
768
if content_summary[0] == 'missing':
769
if not deleted_paths:
770
# path won't have been split yet.
771
path_segments = splitpath(path)
772
deleted_dict = deleted_paths
773
for segment in path_segments:
774
deleted_dict = deleted_dict.setdefault(segment, {})
626
775
self.reporter.missing(path)
627
776
deleted_ids.append(file_id)
630
kind = self.work_tree.kind(file_id)
631
if kind == 'tree-reference' and self.recursive == 'down':
632
# nested tree: commit in it
633
sub_tree = WorkingTree.open(self.work_tree.abspath(path))
634
# FIXME: be more comprehensive here:
635
# this works when both trees are in --trees repository,
636
# but when both are bound to a different repository,
637
# it fails; a better way of approaching this is to
638
# finally implement the explicit-caches approach design
639
# a while back - RBC 20070306.
640
if (sub_tree.branch.repository.bzrdir.root_transport.base
642
self.work_tree.branch.repository.bzrdir.root_transport.base):
643
sub_tree.branch.repository = \
644
self.work_tree.branch.repository
646
sub_tree.commit(message=None, revprops=self.revprops,
647
recursive=self.recursive,
648
message_callback=self.message_callback,
649
timestamp=self.timestamp, timezone=self.timezone,
650
committer=self.committer,
651
allow_pointless=self.allow_pointless,
652
strict=self.strict, verbose=self.verbose,
653
local=self.local, reporter=self.reporter)
654
except errors.PointlessCommit:
656
if kind != new_ie.kind:
657
new_ie = inventory.make_entry(kind, new_ie.name,
658
new_ie.parent_id, file_id)
659
except errors.NoSuchFile:
661
# mutter('check %s {%s}', path, file_id)
662
if (not specific_files or
663
is_inside_or_parent_of_any(specific_files, path)):
664
# mutter('%s selected for commit', path)
668
# mutter('%s not selected for commit', path)
669
if self.basis_inv.has_id(file_id):
670
ie = self.basis_inv[file_id].copy()
778
# TODO: have the builder do the nested commit just-in-time IF and
780
if content_summary[0] == 'tree-reference':
781
# enforce repository nested tree policy.
782
if (not self.work_tree.supports_tree_reference() or
783
# repository does not support it either.
784
not self.branch.repository._format.supports_tree_reference):
785
content_summary = ('directory',) + content_summary[1:]
786
kind = content_summary[0]
787
# TODO: specific_files filtering before nested tree processing
788
if kind == 'tree-reference':
789
if self.recursive == 'down':
790
nested_revision_id = self._commit_nested_tree(
792
content_summary = content_summary[:3] + (
672
# this entry is new and not being committed
674
self.builder.record_entry_contents(ie, self.parent_invs,
675
path, self.work_tree)
676
# describe the nature of the change that has occurred relative to
677
# the basis inventory.
678
if (self.basis_inv.has_id(ie.file_id)):
679
basis_ie = self.basis_inv[ie.file_id]
682
change = ie.describe_change(basis_ie, ie)
683
if change in (InventoryEntry.RENAMED,
684
InventoryEntry.MODIFIED_AND_RENAMED):
685
old_path = self.basis_inv.id2path(ie.file_id)
686
self.reporter.renamed(change, old_path, path)
688
self.reporter.snapshot_change(change, path)
795
content_summary = content_summary[:3] + (
796
self.work_tree.get_reference_revision(file_id),)
798
# Record an entry for this item
799
# Note: I don't particularly want to have the existing_ie
800
# parameter but the test suite currently (28-Jun-07) breaks
801
# without it thanks to a unicode normalisation issue. :-(
802
definitely_changed = kind != existing_ie.kind
803
self._record_entry(path, file_id, specific_files, kind, name,
804
parent_id, definitely_changed, existing_ie, report_changes,
690
807
# Unversion IDs that were found to be deleted
691
808
self.work_tree.unversion(deleted_ids)
693
# If specific files/directories were nominated, it is possible
694
# that some data from outside those needs to be preserved from
695
# the basis tree. For example, if a file x is moved from out of
696
# directory foo into directory bar and the user requests
697
# ``commit foo``, then information about bar/x must also be
700
for path, new_ie in self.basis_inv.iter_entries():
701
if new_ie.file_id in work_inv:
703
if is_inside_any(specific_files, path):
707
self.builder.record_entry_contents(ie, self.parent_invs, path,
710
# Report what was deleted. We could skip this when no deletes are
711
# detected to gain a performance win, but it arguably serves as a
712
# 'safety check' by informing the user whenever anything disappears.
713
for path, ie in self.basis_inv.iter_entries():
714
if ie.file_id not in self.builder.new_inventory:
715
self.reporter.deleted(path)
717
def _emit_progress_set_stage(self, name, show_entries=False):
810
def _commit_nested_tree(self, file_id, path):
811
"Commit a nested tree."
812
sub_tree = self.work_tree.get_nested_tree(file_id, path)
813
# FIXME: be more comprehensive here:
814
# this works when both trees are in --trees repository,
815
# but when both are bound to a different repository,
816
# it fails; a better way of approaching this is to
817
# finally implement the explicit-caches approach design
818
# a while back - RBC 20070306.
819
if sub_tree.branch.repository.has_same_location(
820
self.work_tree.branch.repository):
821
sub_tree.branch.repository = \
822
self.work_tree.branch.repository
824
return sub_tree.commit(message=None, revprops=self.revprops,
825
recursive=self.recursive,
826
message_callback=self.message_callback,
827
timestamp=self.timestamp, timezone=self.timezone,
828
committer=self.committer,
829
allow_pointless=self.allow_pointless,
830
strict=self.strict, verbose=self.verbose,
831
local=self.local, reporter=self.reporter)
832
except errors.PointlessCommit:
833
return self.work_tree.get_reference_revision(file_id)
835
def _record_entry(self, path, file_id, specific_files, kind, name,
836
parent_id, definitely_changed, existing_ie, report_changes,
838
"Record the new inventory entry for a path if any."
839
# mutter('check %s {%s}', path, file_id)
840
# mutter('%s selected for commit', path)
841
if definitely_changed or existing_ie is None:
842
ie = make_entry(kind, name, parent_id, file_id)
844
ie = existing_ie.copy()
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
853
self._report_change(ie, path)
856
def _report_change(self, ie, path):
857
"""Report a change to the user.
859
The change that has occurred is described relative to the basis
862
if (self.basis_inv.has_id(ie.file_id)):
863
basis_ie = self.basis_inv[ie.file_id]
866
change = ie.describe_change(basis_ie, ie)
867
if change in (InventoryEntry.RENAMED,
868
InventoryEntry.MODIFIED_AND_RENAMED):
869
old_path = self.basis_inv.id2path(ie.file_id)
870
self.reporter.renamed(change, old_path, path)
872
self.reporter.snapshot_change(change, path)
874
def _set_progress_stage(self, name, entries_title=None):
718
875
"""Set the progress stage and emit an update to the progress bar."""
719
876
self.pb_stage_name = name
720
877
self.pb_stage_count += 1
721
self.pb_entries_show = show_entries
878
self.pb_entries_title = entries_title
879
if entries_title is not None:
723
880
self.pb_entries_count = 0
724
881
self.pb_entries_total = '?'
725
882
self._emit_progress()
727
def _emit_progress_next_entry(self):
728
"""Emit an update to the progress bar and increment the file count."""
884
def _next_progress_entry(self):
885
"""Emit an update to the progress bar and increment the entry count."""
729
886
self.pb_entries_count += 1
730
887
self._emit_progress()
732
889
def _emit_progress(self):
733
if self.pb_entries_show:
734
text = "%s [Entry %d/%s] - Stage" % (self.pb_stage_name,
735
self.pb_entries_count,str(self.pb_entries_total))
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))
737
899
text = "%s - Stage" % (self.pb_stage_name)
738
900
self.pb.update(text, self.pb_stage_count, self.pb_stage_total)