261
256
# Check that the working tree is up to date
262
257
old_revno,new_revno = self._check_out_of_date_tree()
265
# raise an exception as soon as we find a single unknown.
266
for unknown in self.work_tree.unknowns():
267
raise StrictCommitFailed()
269
259
if self.config is None:
270
260
self.config = self.branch.get_config()
272
self.work_inv = self.work_tree.inventory
273
self.basis_inv = self.basis_tree.inventory
262
# If provided, ensure the specified files are versioned
274
263
if specific_files is not None:
275
# Ensure specified files are versioned
276
# (We don't actually need the ids here)
264
# Note: We don't actually need the IDs here. This routine
265
# is being called because it raises PathNotVerisonedError
266
# as a side effect of finding the IDs.
277
267
# XXX: Dont we have filter_unversioned to do this more
279
269
tree.find_ids_across_trees(specific_files,
280
270
[self.basis_tree, self.work_tree])
282
# Setup the progress bar ...
283
# one to finish, one for rev and inventory, and one for each
284
# inventory entry, and the same for the new inventory.
285
# note that this estimate is too long when we do a partial tree
286
# commit which excludes some new files from being considered.
287
# The estimate is corrected when we populate the new inv.
288
self.pb_total = len(self.work_inv) + 5
272
# Setup the progress bar. As the number of files that need to be
273
# committed in unknown, progress is reported as stages.
274
# We keep track of entries separately though and include that
275
# information in the progress bar during the relevant stages.
276
self.pb_stage_name = ""
277
self.pb_stage_count = 0
278
self.pb_stage_total = 4
279
if self.bound_branch:
280
self.pb_stage_total += 1
281
self.pb.show_pct = False
282
self.pb.show_spinner = False
283
self.pb.show_eta = False
284
self.pb.show_count = True
285
self.pb.show_bar = True
287
# After a merge, a selected file commit is not supported.
288
# See 'bzr help merge' for an explanation as to why.
289
self.basis_inv = self.basis_tree.inventory
291
290
self._gather_parents()
292
291
if len(self.parents) > 1 and self.specific_files:
293
292
raise errors.CannotCommitSelectedFileMerge(self.specific_files)
295
# Build the new inventory
294
# Collect the changes
295
self._set_progress_stage("Collecting changes",
296
entries_title="Directory")
296
297
self.builder = self.branch.get_commit_builder(self.parents,
297
298
self.config, timestamp, timezone, committer, revprops, rev_id)
298
self._remove_deleted()
299
self._populate_new_inv()
300
self._report_deletes()
301
self._check_pointless()
302
self._emit_progress_update()
304
# TODO: Now the new inventory is known, check for conflicts and
305
# prompt the user for a commit message.
306
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
307
# weave lines, because nothing should be recorded until it is known
308
# that commit will succeed.
309
self.builder.finish_inventory()
310
self._emit_progress_update()
311
message = message_callback(self)
312
assert isinstance(message, unicode), type(message)
313
self.message = message
314
self._escape_commit_message()
316
# Add revision data to the local branch
317
self.rev_id = self.builder.commit(self.message)
318
self._emit_progress_update()
320
# upload revision data to the master.
300
self._update_builder_with_changes()
301
self._check_pointless()
303
# TODO: Now the new inventory is known, check for conflicts.
304
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
305
# weave lines, because nothing should be recorded until it is known
306
# that commit will succeed.
307
self._set_progress_stage("Saving data locally")
308
self.builder.finish_inventory()
310
# Prompt the user for a commit message if none provided
311
message = message_callback(self)
312
assert isinstance(message, unicode), type(message)
313
self.message = message
314
self._escape_commit_message()
316
# Add revision data to the local branch
317
self.rev_id = self.builder.commit(self.message)
319
# perhaps this should be done by the CommitBuilder ?
320
self.work_tree.branch.repository.abort_write_group()
323
# Upload revision data to the master.
321
324
# this will propagate merged revisions too if needed.
322
325
if self.bound_branch:
326
self._set_progress_stage("Uploading data to master branch")
323
327
self.master_branch.repository.fetch(self.branch.repository,
324
328
revision_id=self.rev_id)
325
329
# now the master has the revision data
563
577
mutter('commit parent ghost revision {%s}', revision)
565
def _remove_deleted(self):
566
"""Remove deleted files from the working inventories.
568
This is done prior to taking the working inventory as the
569
basis for the new committed inventory.
571
This returns true if any files
572
*that existed in the basis inventory* were deleted.
573
Files that were added and deleted
574
in the working copy don't matter.
576
specific = self.specific_files
578
deleted_paths = set()
579
for path, ie in self.work_inv.iter_entries():
580
if is_inside_any(deleted_paths, path):
581
# The tree will delete the required ids recursively.
583
if specific and not is_inside_any(specific, path):
585
if not self.work_tree.has_filename(path):
586
deleted_paths.add(path)
587
self.reporter.missing(path)
588
deleted_ids.append(ie.file_id)
589
self.work_tree.unversion(deleted_ids)
591
def _populate_new_inv(self):
592
"""Build revision inventory.
594
This creates a new empty inventory. Depending on
595
which files are selected for commit, and what is present in the
596
current tree, the new inventory is populated. inventory entries
597
which are candidates for modification have their revision set to
598
None; inventory entries that are carried over untouched have their
599
revision set to their prior value.
579
def _update_builder_with_changes(self):
580
"""Update the commit builder with the data about what has changed.
582
# Build the revision inventory.
584
# This starts by creating a new empty inventory. Depending on
585
# which files are selected for commit, and what is present in the
586
# current tree, the new inventory is populated. inventory entries
587
# which are candidates for modification have their revision set to
588
# None; inventory entries that are carried over untouched have their
589
# revision set to their prior value.
601
591
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
602
592
# results to create a new inventory at the same time, which results
603
593
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
604
594
# ADHB 11-07-2006
605
mutter("Selecting files for commit with filter %s", self.specific_files)
606
assert self.work_inv.root is not None
607
entries = self.work_inv.iter_entries()
596
specific_files = self.specific_files
597
mutter("Selecting files for commit with filter %s", specific_files)
599
# Check and warn about old CommitBuilders
608
600
if not self.builder.record_root_entry:
609
601
symbol_versioning.warn('CommitBuilders should support recording'
610
602
' the root entry as of bzr 0.10.', DeprecationWarning,
612
604
self.builder.new_inventory.add(self.basis_inv.root.copy())
606
# Build the new inventory
607
self._populate_from_inventory(specific_files)
609
# If specific files are selected, then all un-selected files must be
610
# recorded in their previous state. For more details, see
611
# https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
613
for path, new_ie in self.basis_inv.iter_entries():
614
if new_ie.file_id in self.builder.new_inventory:
616
if is_inside_any(specific_files, path):
620
self.builder.record_entry_contents(ie, self.parent_invs, path,
623
# Report what was deleted. We could skip this when no deletes are
624
# detected to gain a performance win, but it arguably serves as a
625
# 'safety check' by informing the user whenever anything disappears.
626
for path, ie in self.basis_inv.iter_entries():
627
if ie.file_id not in self.builder.new_inventory:
628
self.reporter.deleted(path)
630
def _populate_from_inventory(self, specific_files):
631
"""Populate the CommitBuilder by walking the working tree inventory."""
633
# raise an exception as soon as we find a single unknown.
634
for unknown in self.work_tree.unknowns():
635
raise StrictCommitFailed()
638
deleted_paths = set()
639
work_inv = self.work_tree.inventory
640
assert work_inv.root is not None
641
entries = work_inv.iter_entries()
642
if not self.builder.record_root_entry:
614
self._emit_progress_update()
615
for path, new_ie in entries:
616
self._emit_progress_update()
617
file_id = new_ie.file_id
644
for path, existing_ie in entries:
645
file_id = existing_ie.file_id
646
name = existing_ie.name
647
parent_id = existing_ie.parent_id
648
kind = existing_ie.kind
649
if kind == 'directory':
650
self._next_progress_entry()
652
# Skip files that have been deleted from the working tree.
653
# The deleted files/directories are also recorded so they
654
# can be explicitly unversioned later. Note that when a
655
# filter of specific files is given, we must only skip/record
656
# deleted files matching that filter.
657
if is_inside_any(deleted_paths, path):
659
if not specific_files or is_inside_any(specific_files, path):
660
if not self.work_tree.has_filename(path):
661
deleted_paths.add(path)
662
self.reporter.missing(path)
663
deleted_ids.append(file_id)
619
666
kind = self.work_tree.kind(file_id)
667
# TODO: specific_files filtering before nested tree processing
620
668
if kind == 'tree-reference' and self.recursive == 'down':
621
# nested tree: commit in it
622
sub_tree = WorkingTree.open(self.work_tree.abspath(path))
623
# FIXME: be more comprehensive here:
624
# this works when both trees are in --trees repository,
625
# but when both are bound to a different repository,
626
# it fails; a better way of approaching this is to
627
# finally implement the explicit-caches approach design
628
# a while back - RBC 20070306.
629
if (sub_tree.branch.repository.bzrdir.root_transport.base
631
self.work_tree.branch.repository.bzrdir.root_transport.base):
632
sub_tree.branch.repository = \
633
self.work_tree.branch.repository
635
sub_tree.commit(message=None, revprops=self.revprops,
636
recursive=self.recursive,
637
message_callback=self.message_callback,
638
timestamp=self.timestamp, timezone=self.timezone,
639
committer=self.committer,
640
allow_pointless=self.allow_pointless,
641
strict=self.strict, verbose=self.verbose,
642
local=self.local, reporter=self.reporter)
643
except errors.PointlessCommit:
645
if kind != new_ie.kind:
646
new_ie = inventory.make_entry(kind, new_ie.name,
647
new_ie.parent_id, file_id)
669
self._commit_nested_tree(file_id, path)
648
670
except errors.NoSuchFile:
650
# mutter('check %s {%s}', path, file_id)
651
if (not self.specific_files or
652
is_inside_or_parent_of_any(self.specific_files, path)):
653
# mutter('%s selected for commit', path)
673
# Record an entry for this item
674
# Note: I don't particularly want to have the existing_ie
675
# parameter but the test suite currently (28-Jun-07) breaks
676
# without it thanks to a unicode normalisation issue. :-(
677
definitely_changed = kind != existing_ie.kind
678
self._record_entry(path, file_id, specific_files, kind, name,
679
parent_id, definitely_changed, existing_ie)
681
# Unversion IDs that were found to be deleted
682
self.work_tree.unversion(deleted_ids)
684
def _commit_nested_tree(self, file_id, path):
685
"Commit a nested tree."
686
sub_tree = self.work_tree.get_nested_tree(file_id, path)
687
# FIXME: be more comprehensive here:
688
# this works when both trees are in --trees repository,
689
# but when both are bound to a different repository,
690
# it fails; a better way of approaching this is to
691
# finally implement the explicit-caches approach design
692
# a while back - RBC 20070306.
693
if sub_tree.branch.repository.has_same_location(
694
self.work_tree.branch.repository):
695
sub_tree.branch.repository = \
696
self.work_tree.branch.repository
698
sub_tree.commit(message=None, revprops=self.revprops,
699
recursive=self.recursive,
700
message_callback=self.message_callback,
701
timestamp=self.timestamp, timezone=self.timezone,
702
committer=self.committer,
703
allow_pointless=self.allow_pointless,
704
strict=self.strict, verbose=self.verbose,
705
local=self.local, reporter=self.reporter)
706
except errors.PointlessCommit:
709
def _record_entry(self, path, file_id, specific_files, kind, name,
710
parent_id, definitely_changed, existing_ie=None):
711
"Record the new inventory entry for a path if any."
712
# mutter('check %s {%s}', path, file_id)
713
if (not specific_files or
714
is_inside_or_parent_of_any(specific_files, path)):
715
# mutter('%s selected for commit', path)
716
if definitely_changed or existing_ie is None:
717
ie = inventory.make_entry(kind, name, parent_id, file_id)
719
ie = existing_ie.copy()
655
720
ie.revision = None
722
# mutter('%s not selected for commit', path)
723
if self.basis_inv.has_id(file_id):
724
ie = self.basis_inv[file_id].copy()
657
# mutter('%s not selected for commit', path)
658
if self.basis_inv.has_id(file_id):
659
ie = self.basis_inv[file_id].copy()
661
# this entry is new and not being committed
726
# this entry is new and not being committed
663
729
self.builder.record_entry_contents(ie, self.parent_invs,
664
730
path, self.work_tree)
665
# describe the nature of the change that has occurred relative to
666
# the basis inventory.
667
if (self.basis_inv.has_id(ie.file_id)):
668
basis_ie = self.basis_inv[ie.file_id]
671
change = ie.describe_change(basis_ie, ie)
672
if change in (InventoryEntry.RENAMED,
673
InventoryEntry.MODIFIED_AND_RENAMED):
674
old_path = self.basis_inv.id2path(ie.file_id)
675
self.reporter.renamed(change, old_path, path)
677
self.reporter.snapshot_change(change, path)
679
if not self.specific_files:
682
# ignore removals that don't match filespec
683
for path, new_ie in self.basis_inv.iter_entries():
684
if new_ie.file_id in self.work_inv:
686
if is_inside_any(self.specific_files, path):
690
self.builder.record_entry_contents(ie, self.parent_invs, path,
693
def _emit_progress_update(self):
694
"""Emit an update to the progress bar."""
695
self.pb.update("Committing", self.pb_count, self.pb_total)
698
def _report_deletes(self):
699
for path, ie in self.basis_inv.iter_entries():
700
if ie.file_id not in self.builder.new_inventory:
701
self.reporter.deleted(path)
731
self._report_change(ie, path)
734
def _report_change(self, ie, path):
735
"""Report a change to the user.
737
The change that has occurred is described relative to the basis
740
if (self.basis_inv.has_id(ie.file_id)):
741
basis_ie = self.basis_inv[ie.file_id]
744
change = ie.describe_change(basis_ie, ie)
745
if change in (InventoryEntry.RENAMED,
746
InventoryEntry.MODIFIED_AND_RENAMED):
747
old_path = self.basis_inv.id2path(ie.file_id)
748
self.reporter.renamed(change, old_path, path)
750
self.reporter.snapshot_change(change, path)
752
def _set_progress_stage(self, name, entries_title=None):
753
"""Set the progress stage and emit an update to the progress bar."""
754
self.pb_stage_name = name
755
self.pb_stage_count += 1
756
self.pb_entries_title = entries_title
757
if entries_title is not None:
758
self.pb_entries_count = 0
759
self.pb_entries_total = '?'
760
self._emit_progress()
762
def _next_progress_entry(self):
763
"""Emit an update to the progress bar and increment the entry count."""
764
self.pb_entries_count += 1
765
self._emit_progress()
767
def _emit_progress(self):
768
if self.pb_entries_title:
769
if self.pb_entries_total == '?':
770
text = "%s [%s %d] - Stage" % (self.pb_stage_name,
771
self.pb_entries_title, self.pb_entries_count)
773
text = "%s [%s %d/%s] - Stage" % (self.pb_stage_name,
774
self.pb_entries_title, self.pb_entries_count,
775
str(self.pb_entries_total))
777
text = "%s - Stage" % (self.pb_stage_name)
778
self.pb.update(text, self.pb_stage_count, self.pb_stage_total)