25
25
# We then consider the remaining entries, which will be in the new
26
26
# version. Directory entries are simply copied across. File entries
27
27
# must be checked to see if a new version of the file should be
28
# recorded. For each parent revision inventory, we check to see what
28
# recorded. For each parent revision tree, we check to see what
29
29
# version of the file was present. If the file was present in at
30
30
# least one tree, and if it was the same version in all the trees,
31
31
# then we can just refer to that version. Otherwise, a new version
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,
73
74
quotefn, sha_file, split_lines)
74
75
from bzrlib.testament import Testament
75
76
from bzrlib.trace import mutter, note, warning, is_quiet
76
77
from bzrlib.xml5 import serializer_v5
77
from bzrlib.inventory import Inventory, InventoryEntry
78
from bzrlib.inventory import InventoryEntry, make_entry
78
79
from bzrlib import symbol_versioning
79
80
from bzrlib.symbol_versioning import (deprecated_passed,
80
81
deprecated_function,
241
242
" parameter is required for commit().")
243
244
self.bound_branch = None
245
self.any_entries_changed = False
246
self.any_entries_deleted = False
244
247
self.local = local
245
248
self.master_branch = None
246
249
self.master_locked = False
250
self.recursive = recursive
247
251
self.rev_id = None
248
self.specific_files = specific_files
252
if specific_files is not None:
253
self.specific_files = sorted(
254
minimum_path_selection(specific_files))
256
self.specific_files = None
257
self.specific_file_ids = None
249
258
self.allow_pointless = allow_pointless
250
self.recursive = recursive
251
259
self.revprops = revprops
252
260
self.message_callback = message_callback
253
261
self.timestamp = timestamp
255
263
self.committer = committer
256
264
self.strict = strict
257
265
self.verbose = verbose
266
# accumulates an inventory delta to the basis entry, so we can make
267
# just the necessary updates to the workingtree's cached basis.
268
self._basis_delta = []
259
270
self.work_tree.lock_write()
260
271
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
272
self.basis_revid = self.work_tree.last_revision()
261
273
self.basis_tree = self.work_tree.basis_tree()
262
274
self.basis_tree.lock_read()
280
292
self.config = self.branch.get_config()
282
294
# If provided, ensure the specified files are versioned
283
if specific_files is not None:
284
# Note: We don't actually need the IDs here. This routine
285
# is being called because it raises PathNotVerisonedError
286
# as a side effect of finding the IDs.
295
if self.specific_files is not None:
296
# Note: This routine is being called because it raises
297
# PathNotVersionedError as a side effect of finding the IDs. We
298
# later use the ids we found as input to the working tree
299
# inventory iterator, so we only consider those ids rather than
300
# examining the whole tree again.
287
301
# XXX: Dont we have filter_unversioned to do this more
289
tree.find_ids_across_trees(specific_files,
290
[self.basis_tree, self.work_tree])
303
self.specific_file_ids = tree.find_ids_across_trees(
304
specific_files, [self.basis_tree, self.work_tree])
292
306
# Setup the progress bar. As the number of files that need to be
293
307
# committed in unknown, progress is reported as stages.
370
385
# Make the working tree up to date with the branch
371
386
self._set_progress_stage("Updating the working tree")
372
rev_tree = self.builder.revision_tree()
373
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
387
self.work_tree.update_basis_by_delta(self.rev_id,
374
389
self.reporter.completed(new_revno, self.rev_id)
375
390
self._process_post_hooks(old_revno, new_revno)
383
398
return NullCommitReporter()
384
399
return ReportCommitToLog()
386
def _any_real_changes(self):
387
"""Are there real changes between new_inventory and basis?
389
For trees without rich roots, inv.root.revision changes every commit.
390
But if that is the only change, we want to treat it as though there
393
new_entries = self.builder.new_inventory.iter_entries()
394
basis_entries = self.basis_inv.iter_entries()
395
new_path, new_root_ie = new_entries.next()
396
basis_path, basis_root_ie = basis_entries.next()
398
# This is a copy of InventoryEntry.__eq__ only leaving out .revision
399
def ie_equal_no_revision(this, other):
400
return ((this.file_id == other.file_id)
401
and (this.name == other.name)
402
and (this.symlink_target == other.symlink_target)
403
and (this.text_sha1 == other.text_sha1)
404
and (this.text_size == other.text_size)
405
and (this.text_id == other.text_id)
406
and (this.parent_id == other.parent_id)
407
and (this.kind == other.kind)
408
and (this.executable == other.executable)
409
and (this.reference_revision == other.reference_revision)
411
if not ie_equal_no_revision(new_root_ie, basis_root_ie):
414
for new_ie, basis_ie in zip(new_entries, basis_entries):
415
if new_ie != basis_ie:
418
# No actual changes present
421
401
def _check_pointless(self):
422
402
if self.allow_pointless:
424
404
# A merge with no effect on files
425
405
if len(self.parents) > 1:
427
# work around the fact that a newly-initted tree does differ from its
429
if len(self.basis_inv) == 0 and len(self.builder.new_inventory) == 1:
407
# TODO: we could simplify this by using self._basis_delta.
409
# The initial commit adds a root directory, but this in itself is not
410
# a worthwhile commit.
411
if (self.basis_revid == revision.NULL_REVISION and
412
len(self.builder.new_inventory) == 1):
430
413
raise PointlessCommit()
431
# Shortcut, if the number of entries changes, then we obviously have
433
if len(self.builder.new_inventory) != len(self.basis_inv):
435
414
# If length == 1, then we only have the root entry. Which means
436
415
# that there is no real difference (only the root could be different)
437
if (len(self.builder.new_inventory) != 1 and self._any_real_changes()):
416
# unless deletes occured, in which case the length is irrelevant.
417
if (self.any_entries_deleted or
418
(len(self.builder.new_inventory) != 1 and
419
self.any_entries_changed)):
439
421
raise PointlessCommit()
657
639
specific_files = self.specific_files
658
640
mutter("Selecting files for commit with filter %s", specific_files)
660
# Check and warn about old CommitBuilders
661
if not self.builder.record_root_entry:
662
symbol_versioning.warn('CommitBuilders should support recording'
663
' the root entry as of bzr 0.10.', DeprecationWarning,
665
self.builder.new_inventory.add(self.basis_inv.root.copy())
667
642
# Build the new inventory
668
643
self._populate_from_inventory(specific_files)
671
646
# recorded in their previous state. For more details, see
672
647
# https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
673
648
if specific_files:
674
for path, new_ie in self.basis_inv.iter_entries():
675
if new_ie.file_id in self.builder.new_inventory:
649
for path, old_ie in self.basis_inv.iter_entries():
650
if old_ie.file_id in self.builder.new_inventory:
651
# already added - skip.
677
653
if is_inside_any(specific_files, path):
654
# was inside the selected path, if not present it has been
681
self.builder.record_entry_contents(ie, self.parent_invs, path,
657
if old_ie.kind == 'directory':
658
self._next_progress_entry()
659
# not in final inv yet, was not in the selected files, so is an
660
# entry to be preserved unaltered.
662
# Note: specific file commits after a merge are currently
663
# prohibited. This test is for sanity/safety in case it's
664
# required after that changes.
665
if len(self.parents) > 1:
667
delta, version_recorded = self.builder.record_entry_contents(
668
ie, self.parent_invs, path, self.basis_tree, None)
670
self.any_entries_changed = True
671
if delta: self._basis_delta.append(delta)
684
# Report what was deleted.
685
if self.reporter.is_verbose():
686
for path, ie in self.basis_inv.iter_entries():
687
if ie.file_id not in self.builder.new_inventory:
688
self.reporter.deleted(path)
673
def _report_and_accumulate_deletes(self):
674
# XXX: Could the list of deleted paths and ids be instead taken from
675
# _populate_from_inventory?
676
deleted_ids = set(self.basis_inv._byid.keys()) - \
677
set(self.builder.new_inventory._byid.keys())
679
self.any_entries_deleted = True
680
deleted = [(self.basis_tree.id2path(file_id), file_id)
681
for file_id in deleted_ids]
683
# XXX: this is not quite directory-order sorting
684
for path, file_id in deleted:
685
self._basis_delta.append((path, None, file_id, None))
686
self.reporter.deleted(path)
690
688
def _populate_from_inventory(self, specific_files):
691
689
"""Populate the CommitBuilder by walking the working tree inventory."""
697
695
report_changes = self.reporter.is_verbose()
699
697
deleted_paths = set()
698
# XXX: Note that entries may have the wrong kind because the entry does
699
# not reflect the status on disk.
700
700
work_inv = self.work_tree.inventory
701
assert work_inv.root is not None
702
entries = work_inv.iter_entries()
703
if not self.builder.record_root_entry:
701
entries = work_inv.iter_entries_by_dir(
702
specific_file_ids=self.specific_file_ids, yield_parents=True)
705
703
for path, existing_ie in entries:
706
704
file_id = existing_ie.file_id
707
705
name = existing_ie.name
717
714
# deleted files matching that filter.
718
715
if is_inside_any(deleted_paths, path):
717
content_summary = self.work_tree.path_content_summary(path)
720
718
if not specific_files or is_inside_any(specific_files, path):
721
if not self.work_tree.has_filename(path):
719
if content_summary[0] == 'missing':
722
720
deleted_paths.add(path)
723
721
self.reporter.missing(path)
724
722
deleted_ids.append(file_id)
727
kind = self.work_tree.kind(file_id)
728
# TODO: specific_files filtering before nested tree processing
729
if kind == 'tree-reference' and self.recursive == 'down':
730
self._commit_nested_tree(file_id, path)
731
except errors.NoSuchFile:
724
# TODO: have the builder do the nested commit just-in-time IF and
726
if content_summary[0] == 'tree-reference':
727
# enforce repository nested tree policy.
728
if (not self.work_tree.supports_tree_reference() or
729
# repository does not support it either.
730
not self.branch.repository._format.supports_tree_reference):
731
content_summary = ('directory',) + content_summary[1:]
732
kind = content_summary[0]
733
# TODO: specific_files filtering before nested tree processing
734
if kind == 'tree-reference':
735
if self.recursive == 'down':
736
nested_revision_id = self._commit_nested_tree(
738
content_summary = content_summary[:3] + (
741
content_summary = content_summary[:3] + (
742
self.work_tree.get_reference_revision(file_id),)
734
744
# Record an entry for this item
735
745
# Note: I don't particularly want to have the existing_ie
736
746
# parameter but the test suite currently (28-Jun-07) breaks
737
747
# without it thanks to a unicode normalisation issue. :-(
738
definitely_changed = kind != existing_ie.kind
748
definitely_changed = kind != existing_ie.kind
739
749
self._record_entry(path, file_id, specific_files, kind, name,
740
parent_id, definitely_changed, existing_ie, report_changes)
750
parent_id, definitely_changed, existing_ie, report_changes,
742
753
# Unversion IDs that were found to be deleted
743
754
self.work_tree.unversion(deleted_ids)
756
767
sub_tree.branch.repository = \
757
768
self.work_tree.branch.repository
759
sub_tree.commit(message=None, revprops=self.revprops,
770
return sub_tree.commit(message=None, revprops=self.revprops,
760
771
recursive=self.recursive,
761
772
message_callback=self.message_callback,
762
773
timestamp=self.timestamp, timezone=self.timezone,
765
776
strict=self.strict, verbose=self.verbose,
766
777
local=self.local, reporter=self.reporter)
767
778
except errors.PointlessCommit:
779
return self.work_tree.get_reference_revision(file_id)
770
781
def _record_entry(self, path, file_id, specific_files, kind, name,
771
parent_id, definitely_changed, existing_ie=None,
772
report_changes=True):
782
parent_id, definitely_changed, existing_ie, report_changes,
773
784
"Record the new inventory entry for a path if any."
774
785
# mutter('check %s {%s}', path, file_id)
775
if (not specific_files or
776
is_inside_or_parent_of_any(specific_files, path)):
777
# mutter('%s selected for commit', path)
778
if definitely_changed or existing_ie is None:
779
ie = inventory.make_entry(kind, name, parent_id, file_id)
781
ie = existing_ie.copy()
786
# mutter('%s selected for commit', path)
787
if definitely_changed or existing_ie is None:
788
ie = make_entry(kind, name, parent_id, file_id)
784
# mutter('%s not selected for commit', path)
785
if self.basis_inv.has_id(file_id):
786
ie = self.basis_inv[file_id].copy()
788
# this entry is new and not being committed
791
self.builder.record_entry_contents(ie, self.parent_invs,
792
path, self.work_tree)
794
self._report_change(ie, path)
790
ie = existing_ie.copy()
792
delta, version_recorded = self.builder.record_entry_contents(ie,
793
self.parent_invs, path, self.work_tree, content_summary)
795
self._basis_delta.append(delta)
797
self.any_entries_changed = True
799
self._report_change(ie, path)
797
802
def _report_change(self, ie, path):