71
from bzrlib.osutils import (kind_marker, isdir,isfile, is_inside_any,
71
from bzrlib.osutils import (kind_marker, isdir,isfile, is_inside_any,
72
72
is_inside_or_parent_of_any,
73
minimum_path_selection,
74
quotefn, sha_file, split_lines,
73
quotefn, sha_file, split_lines)
77
74
from bzrlib.testament import Testament
78
from bzrlib.trace import mutter, note, warning, is_quiet
75
from bzrlib.trace import mutter, note, warning
79
76
from bzrlib.xml5 import serializer_v5
80
from bzrlib.inventory import InventoryEntry, make_entry
77
from bzrlib.inventory import Inventory, InventoryEntry
81
78
from bzrlib import symbol_versioning
82
79
from bzrlib.symbol_versioning import (deprecated_passed,
83
80
deprecated_function,
84
81
DEPRECATED_PARAMETER)
85
82
from bzrlib.workingtree import WorkingTree
86
from bzrlib.urlutils import unescape_for_display
90
86
class NullCommitReporter(object):
91
87
"""I report on progress of a commit."""
93
def started(self, revno, revid, location=None):
95
symbol_versioning.warn("As of bzr 1.0 you must pass a location "
96
"to started.", DeprecationWarning,
100
89
def snapshot_change(self, change, path):
136
122
self._note("%s %s", change, path)
138
def started(self, revno, rev_id, location=None):
139
if location is not None:
140
location = ' to: ' + unescape_for_display(location, 'utf-8')
142
# When started was added, location was only made optional by
143
# accident. Matt Nordhoff 20071129
144
symbol_versioning.warn("As of bzr 1.0 you must pass a location "
145
"to started.", DeprecationWarning,
148
self._note('Committing%s', location)
150
124
def completed(self, revno, rev_id):
151
125
self._note('Committed revision %d.', revno)
153
127
def deleted(self, file_id):
154
128
self._note('deleted %s', file_id)
303
255
# Check that the working tree is up to date
304
256
old_revno, new_revno = self._check_out_of_date_tree()
306
# Complete configuration setup
307
if reporter is not None:
308
self.reporter = reporter
309
elif self.reporter is None:
310
self.reporter = self._select_reporter()
311
258
if self.config is None:
312
259
self.config = self.branch.get_config()
314
261
# 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.
262
if specific_files is not None:
263
# Note: We don't actually need the IDs here. This routine
264
# is being called because it raises PathNotVerisonedError
265
# as a side effect of finding the IDs.
321
266
# 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])
268
tree.find_ids_across_trees(specific_files,
269
[self.basis_tree, self.work_tree])
326
271
# Setup the progress bar. As the number of files that need to be
327
272
# committed in unknown, progress is reported as stages.
338
283
self.pb.show_count = True
339
284
self.pb.show_bar = True
286
# After a merge, a selected file commit is not supported.
287
# See 'bzr help merge' for an explanation as to why.
341
288
self.basis_inv = self.basis_tree.inventory
342
289
self._gather_parents()
343
# After a merge, a selected file commit is not supported.
344
# See 'bzr help merge' for an explanation as to why.
345
290
if len(self.parents) > 1 and self.specific_files:
346
291
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)
351
293
# Collect the changes
352
294
self._set_progress_stage("Collecting changes",
353
295
entries_title="Directory")
354
296
self.builder = self.branch.get_commit_builder(self.parents,
355
297
self.config, timestamp, timezone, committer, revprops, rev_id)
298
# tell the builder about the chosen recursive behaviour
299
self.builder.recursive = recursive
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
302
self._update_builder_with_changes()
368
self._report_and_accumulate_deletes()
369
303
self._check_pointless()
371
305
# TODO: Now the new inventory is known, check for conflicts.
409
342
# Make the working tree up to date with the branch
410
343
self._set_progress_stage("Updating the working tree")
411
self.work_tree.update_basis_by_delta(self.rev_id,
344
rev_tree = self.builder.revision_tree()
345
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
413
346
self.reporter.completed(new_revno, self.rev_id)
414
347
self._process_post_hooks(old_revno, new_revno)
417
350
return self.rev_id
419
def _select_reporter(self):
420
"""Select the CommitReporter to use."""
422
return NullCommitReporter()
423
return ReportCommitToLog()
352
def _any_real_changes(self):
353
"""Are there real changes between new_inventory and basis?
355
For trees without rich roots, inv.root.revision changes every commit.
356
But if that is the only change, we want to treat it as though there
359
new_entries = self.builder.new_inventory.iter_entries()
360
basis_entries = self.basis_inv.iter_entries()
361
new_path, new_root_ie = new_entries.next()
362
basis_path, basis_root_ie = basis_entries.next()
364
# This is a copy of InventoryEntry.__eq__ only leaving out .revision
365
def ie_equal_no_revision(this, other):
366
return ((this.file_id == other.file_id)
367
and (this.name == other.name)
368
and (this.symlink_target == other.symlink_target)
369
and (this.text_sha1 == other.text_sha1)
370
and (this.text_size == other.text_size)
371
and (this.text_id == other.text_id)
372
and (this.parent_id == other.parent_id)
373
and (this.kind == other.kind)
374
and (this.executable == other.executable)
375
and (this.reference_revision == other.reference_revision)
377
if not ie_equal_no_revision(new_root_ie, basis_root_ie):
380
for new_ie, basis_ie in zip(new_entries, basis_entries):
381
if new_ie != basis_ie:
384
# No actual changes present
425
387
def _check_pointless(self):
426
388
if self.allow_pointless:
428
390
# A merge with no effect on files
429
391
if len(self.parents) > 1:
431
# TODO: we could simplify this by using self._basis_delta.
433
# The initial commit adds a root directory, but this in itself is not
434
# a worthwhile commit.
435
if (self.basis_revid == revision.NULL_REVISION and
436
len(self.builder.new_inventory) == 1):
393
# work around the fact that a newly-initted tree does differ from its
395
if len(self.basis_inv) == 0 and len(self.builder.new_inventory) == 1:
437
396
raise PointlessCommit()
397
# Shortcut, if the number of entries changes, then we obviously have
399
if len(self.builder.new_inventory) != len(self.basis_inv):
438
401
# If length == 1, then we only have the root entry. Which means
439
402
# 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)):
403
if (len(self.builder.new_inventory) != 1 and self._any_real_changes()):
445
405
raise PointlessCommit()
660
620
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
661
621
# ADHB 11-07-2006
663
exclude = self.exclude
664
specific_files = self.specific_files or []
623
specific_files = self.specific_files
665
624
mutter("Selecting files for commit with filter %s", specific_files)
626
# Check and warn about old CommitBuilders
627
if not self.builder.record_root_entry:
628
symbol_versioning.warn('CommitBuilders should support recording'
629
' the root entry as of bzr 0.10.', DeprecationWarning,
631
self.builder.new_inventory.add(self.basis_inv.root.copy())
667
633
# Build the new inventory
668
self._populate_from_inventory()
634
self._populate_from_inventory(specific_files)
670
636
# If specific files are selected, then all un-selected files must be
671
637
# recorded in their previous state. For more details, see
672
638
# 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)
640
for path, new_ie in self.basis_inv.iter_entries():
641
if new_ie.file_id in self.builder.new_inventory:
643
if is_inside_any(specific_files, path):
647
self.builder.record_entry_contents(ie, self.parent_invs, path,
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))
650
# Report what was deleted. We could skip this when no deletes are
651
# detected to gain a performance win, but it arguably serves as a
652
# 'safety check' by informing the user whenever anything disappears.
653
for path, ie in self.basis_inv.iter_entries():
654
if ie.file_id not in self.builder.new_inventory:
712
655
self.reporter.deleted(path)
714
def _populate_from_inventory(self):
657
def _populate_from_inventory(self, specific_files):
715
658
"""Populate the CommitBuilder by walking the working tree inventory."""
717
660
# raise an exception as soon as we find a single unknown.
718
661
for unknown in self.work_tree.unknowns():
719
662
raise StrictCommitFailed()
721
specific_files = self.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.
665
deleted_paths = set()
730
666
work_inv = self.work_tree.inventory
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)
667
assert work_inv.root is not None
668
entries = work_inv.iter_entries()
669
# XXX: Note that entries may have the wrong kind.
670
if not self.builder.record_root_entry:
735
672
for path, existing_ie in entries:
736
673
file_id = existing_ie.file_id
737
674
name = existing_ie.name
739
676
kind = existing_ie.kind
740
677
if kind == 'directory':
741
678
self._next_progress_entry()
742
680
# Skip files that have been deleted from the working tree.
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.
681
# The deleted files/directories are also recorded so they
682
# can be explicitly unversioned later. Note that when a
683
# filter of specific files is given, we must only skip/record
684
# deleted files matching that filter.
685
if is_inside_any(deleted_paths, path):
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.
767
687
if not specific_files or is_inside_any(specific_files, 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, {})
688
# TODO: fix double-stat here.
689
if not self.work_tree.has_filename(path):
690
deleted_paths.add(path)
775
691
self.reporter.missing(path)
776
692
deleted_ids.append(file_id)
778
694
# TODO: have the builder do the nested commit just-in-time IF and
779
695
# only if needed.
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] + (
795
content_summary = content_summary[:3] + (
796
self.work_tree.get_reference_revision(file_id),)
697
kind = self.work_tree.kind(file_id)
698
# TODO: specific_files filtering before nested tree processing
699
if kind == 'tree-reference' and self.builder.recursive == 'down':
700
self._commit_nested_tree(file_id, path)
701
except errors.NoSuchFile:
798
704
# Record an entry for this item
799
705
# Note: I don't particularly want to have the existing_ie
830
735
strict=self.strict, verbose=self.verbose,
831
736
local=self.local, reporter=self.reporter)
832
737
except errors.PointlessCommit:
833
return self.work_tree.get_reference_revision(file_id)
835
740
def _record_entry(self, path, file_id, specific_files, kind, name,
836
parent_id, definitely_changed, existing_ie, report_changes,
741
parent_id, definitely_changed, existing_ie=None):
838
742
"Record the new inventory entry for a path if any."
839
743
# 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)
744
if (not specific_files or
745
is_inside_or_parent_of_any(specific_files, path)):
746
# mutter('%s selected for commit', path)
747
if definitely_changed or existing_ie is None:
748
ie = inventory.make_entry(kind, name, parent_id, file_id)
750
ie = existing_ie.copy()
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
753
# mutter('%s not selected for commit', path)
754
if self.basis_inv.has_id(file_id):
755
ie = self.basis_inv[file_id].copy()
757
# this entry is new and not being committed
760
self.builder.record_entry_contents(ie, self.parent_invs,
761
path, self.work_tree)
853
762
self._report_change(ie, path)