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
from bzrlib.trace import mutter, note, warning
76
from bzrlib.trace import mutter, note, warning, is_quiet
76
77
from bzrlib.xml5 import serializer_v5
77
78
from bzrlib.inventory import Inventory, InventoryEntry
78
79
from bzrlib import symbol_versioning
80
81
deprecated_function,
81
82
DEPRECATED_PARAMETER)
82
83
from bzrlib.workingtree import WorkingTree
84
from bzrlib.urlutils import unescape_for_display
86
88
class NullCommitReporter(object):
87
89
"""I report on progress of a commit."""
91
def started(self, revno, revid, location=None):
89
94
def snapshot_change(self, change, path):
122
130
self._note("%s %s", change, path)
132
def started(self, revno, rev_id, location=None):
133
if location is not None:
134
location = ' to "' + unescape_for_display(location, 'utf-8') + '"'
137
self._note('Committing revision %d%s.', revno, location)
124
139
def completed(self, revno, rev_id):
125
140
self._note('Committed revision %d.', revno)
127
142
def deleted(self, file_id):
128
143
self._note('deleted %s', file_id)
199
218
:param revprops: Properties for new revision
200
219
:param local: Perform a local only commit.
220
:param reporter: the reporter to use or None for the default
221
:param verbose: if True and the reporter is not None, report everything
201
222
:param recursive: If set to 'down', commit in any subtrees that have
202
223
pending changes of any sort during this commit.
221
242
" parameter is required for commit().")
223
244
self.bound_branch = None
245
self.any_entries_changed = False
246
self.any_entries_deleted = False
224
247
self.local = local
225
248
self.master_branch = None
226
249
self.master_locked = False
250
self.recursive = recursive
227
251
self.rev_id = None
228
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
229
258
self.allow_pointless = allow_pointless
230
self.recursive = recursive
231
259
self.revprops = revprops
232
260
self.message_callback = message_callback
233
261
self.timestamp = timestamp
235
263
self.committer = committer
236
264
self.strict = strict
237
265
self.verbose = verbose
239
if reporter is None and self.reporter is None:
240
self.reporter = NullCommitReporter()
241
elif reporter is not None:
242
self.reporter = reporter
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 = []
244
270
self.work_tree.lock_write()
245
271
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
256
282
# Check that the working tree is up to date
257
283
old_revno, new_revno = self._check_out_of_date_tree()
285
# Complete configuration setup
286
if reporter is not None:
287
self.reporter = reporter
288
elif self.reporter is None:
289
self.reporter = self._select_reporter()
259
290
if self.config is None:
260
291
self.config = self.branch.get_config()
262
293
# If provided, ensure the specified files are versioned
263
if specific_files is not None:
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.
294
if self.specific_files is not None:
295
# Note: This routine is being called because it raises
296
# PathNotVersionedError as a side effect of finding the IDs. We
297
# later use the ids we found as input to the working tree
298
# inventory iterator, so we only consider those ids rather than
299
# examining the whole tree again.
267
300
# XXX: Dont we have filter_unversioned to do this more
269
tree.find_ids_across_trees(specific_files,
270
[self.basis_tree, self.work_tree])
302
self.specific_file_ids = tree.find_ids_across_trees(
303
specific_files, [self.basis_tree, self.work_tree])
272
305
# Setup the progress bar. As the number of files that need to be
273
306
# committed in unknown, progress is reported as stages.
298
331
self.config, timestamp, timezone, committer, revprops, rev_id)
334
# find the location being committed to
335
if self.bound_branch:
336
master_location = self.master_branch.base
338
master_location = self.branch.base
340
# report the start of the commit
341
self.reporter.started(new_revno, self.rev_id, master_location)
301
343
self._update_builder_with_changes()
344
self._report_and_accumulate_deletes()
302
345
self._check_pointless()
304
347
# TODO: Now the new inventory is known, check for conflicts.
341
384
# Make the working tree up to date with the branch
342
385
self._set_progress_stage("Updating the working tree")
343
386
rev_tree = self.builder.revision_tree()
387
# XXX: This will need to be changed if we support doing a
388
# selective commit while a merge is still pending - then we'd
389
# still have multiple parents after the commit.
391
# XXX: update_basis_by_delta is slower at present because it works
392
# on inventories, so this is not active until there's a native
393
# dirstate implementation.
394
## self.work_tree.update_basis_by_delta(self.rev_id,
395
## self._basis_delta)
344
396
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
345
397
self.reporter.completed(new_revno, self.rev_id)
346
398
self._process_post_hooks(old_revno, new_revno)
349
401
return self.rev_id
351
def _any_real_changes(self):
352
"""Are there real changes between new_inventory and basis?
354
For trees without rich roots, inv.root.revision changes every commit.
355
But if that is the only change, we want to treat it as though there
358
new_entries = self.builder.new_inventory.iter_entries()
359
basis_entries = self.basis_inv.iter_entries()
360
new_path, new_root_ie = new_entries.next()
361
basis_path, basis_root_ie = basis_entries.next()
363
# This is a copy of InventoryEntry.__eq__ only leaving out .revision
364
def ie_equal_no_revision(this, other):
365
return ((this.file_id == other.file_id)
366
and (this.name == other.name)
367
and (this.symlink_target == other.symlink_target)
368
and (this.text_sha1 == other.text_sha1)
369
and (this.text_size == other.text_size)
370
and (this.text_id == other.text_id)
371
and (this.parent_id == other.parent_id)
372
and (this.kind == other.kind)
373
and (this.executable == other.executable)
374
and (this.reference_revision == other.reference_revision)
376
if not ie_equal_no_revision(new_root_ie, basis_root_ie):
379
for new_ie, basis_ie in zip(new_entries, basis_entries):
380
if new_ie != basis_ie:
383
# No actual changes present
403
def _select_reporter(self):
404
"""Select the CommitReporter to use."""
406
return NullCommitReporter()
407
return ReportCommitToLog()
386
409
def _check_pointless(self):
387
410
if self.allow_pointless:
389
412
# A merge with no effect on files
390
413
if len(self.parents) > 1:
392
# work around the fact that a newly-initted tree does differ from its
415
# TODO: we could simplify this by using self._basis_delta.
417
# The inital commit adds a root directory, but this in itself is not
418
# a worthwhile commit.
394
419
if len(self.basis_inv) == 0 and len(self.builder.new_inventory) == 1:
395
420
raise PointlessCommit()
396
421
# Shortcut, if the number of entries changes, then we obviously have
622
648
specific_files = self.specific_files
623
649
mutter("Selecting files for commit with filter %s", specific_files)
625
# Check and warn about old CommitBuilders
626
if not self.builder.record_root_entry:
627
symbol_versioning.warn('CommitBuilders should support recording'
628
' the root entry as of bzr 0.10.', DeprecationWarning,
630
self.builder.new_inventory.add(self.basis_inv.root.copy())
632
651
# Build the new inventory
633
652
self._populate_from_inventory(specific_files)
636
655
# recorded in their previous state. For more details, see
637
656
# https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
638
657
if specific_files:
639
for path, new_ie in self.basis_inv.iter_entries():
640
if new_ie.file_id in self.builder.new_inventory:
658
for path, old_ie in self.basis_inv.iter_entries():
659
if old_ie.file_id in self.builder.new_inventory:
660
# already added - skip.
642
662
if is_inside_any(specific_files, path):
663
# was inside the selected path, if not present it has been
646
self.builder.record_entry_contents(ie, self.parent_invs, path,
666
if old_ie.kind == 'directory':
667
self._next_progress_entry()
668
# not in final inv yet, was not in the selected files, so is an
669
# entry to be preserved unaltered.
671
# Note: specific file commits after a merge are currently
672
# prohibited. This test is for sanity/safety in case it's
673
# required after that changes.
674
if len(self.parents) > 1:
676
delta, version_recorded = self.builder.record_entry_contents(
677
ie, self.parent_invs, path, self.basis_tree, None)
679
self.any_entries_changed = True
680
if delta: self._basis_delta.append(delta)
649
# Report what was deleted. We could skip this when no deletes are
650
# detected to gain a performance win, but it arguably serves as a
651
# 'safety check' by informing the user whenever anything disappears.
652
for path, ie in self.basis_inv.iter_entries():
653
if ie.file_id not in self.builder.new_inventory:
682
def _report_and_accumulate_deletes(self):
683
# XXX: Could the list of deleted paths and ids be instead taken from
684
# _populate_from_inventory?
685
deleted_ids = set(self.basis_inv._byid.keys()) - \
686
set(self.builder.new_inventory._byid.keys())
688
self.any_entries_deleted = True
689
deleted = [(self.basis_inv.id2path(file_id), file_id)
690
for file_id in deleted_ids]
692
# XXX: this is not quite directory-order sorting
693
for path, file_id in deleted:
694
self._basis_delta.append((path, None, file_id, None))
654
695
self.reporter.deleted(path)
656
697
def _populate_from_inventory(self, specific_files):
660
701
for unknown in self.work_tree.unknowns():
661
702
raise StrictCommitFailed()
704
report_changes = self.reporter.is_verbose()
664
706
deleted_paths = set()
665
707
work_inv = self.work_tree.inventory
666
708
assert work_inv.root is not None
667
entries = work_inv.iter_entries()
668
if not self.builder.record_root_entry:
709
# XXX: Note that entries may have the wrong kind.
710
entries = work_inv.iter_entries_by_dir(
711
specific_file_ids=self.specific_file_ids, yield_parents=True)
670
712
for path, existing_ie in entries:
671
713
file_id = existing_ie.file_id
672
714
name = existing_ie.name
682
723
# deleted files matching that filter.
683
724
if is_inside_any(deleted_paths, path):
726
content_summary = self.work_tree.path_content_summary(path)
685
727
if not specific_files or is_inside_any(specific_files, path):
686
if not self.work_tree.has_filename(path):
728
if content_summary[0] == 'missing':
687
729
deleted_paths.add(path)
688
730
self.reporter.missing(path)
689
731
deleted_ids.append(file_id)
692
kind = self.work_tree.kind(file_id)
693
# TODO: specific_files filtering before nested tree processing
694
if kind == 'tree-reference' and self.recursive == 'down':
695
self._commit_nested_tree(file_id, path)
696
except errors.NoSuchFile:
733
# TODO: have the builder do the nested commit just-in-time IF and
735
if content_summary[0] == 'tree-reference':
736
# enforce repository nested tree policy.
737
if (not self.work_tree.supports_tree_reference() or
738
# repository does not support it either.
739
not self.branch.repository._format.supports_tree_reference):
740
content_summary = ('directory',) + content_summary[1:]
741
kind = content_summary[0]
742
# TODO: specific_files filtering before nested tree processing
743
if kind == 'tree-reference':
744
if self.recursive == 'down':
745
nested_revision_id = self._commit_nested_tree(
747
content_summary = content_summary[:3] + (
750
content_summary = content_summary[:3] + (
751
self.work_tree.get_reference_revision(file_id),)
699
753
# Record an entry for this item
700
754
# Note: I don't particularly want to have the existing_ie
701
755
# parameter but the test suite currently (28-Jun-07) breaks
702
756
# without it thanks to a unicode normalisation issue. :-(
703
definitely_changed = kind != existing_ie.kind
757
definitely_changed = kind != existing_ie.kind
704
758
self._record_entry(path, file_id, specific_files, kind, name,
705
parent_id, definitely_changed, existing_ie)
759
parent_id, definitely_changed, existing_ie, report_changes,
707
762
# Unversion IDs that were found to be deleted
708
763
self.work_tree.unversion(deleted_ids)
721
776
sub_tree.branch.repository = \
722
777
self.work_tree.branch.repository
724
sub_tree.commit(message=None, revprops=self.revprops,
779
return sub_tree.commit(message=None, revprops=self.revprops,
725
780
recursive=self.recursive,
726
781
message_callback=self.message_callback,
727
782
timestamp=self.timestamp, timezone=self.timezone,
730
785
strict=self.strict, verbose=self.verbose,
731
786
local=self.local, reporter=self.reporter)
732
787
except errors.PointlessCommit:
788
return self.work_tree.get_reference_revision(file_id)
735
790
def _record_entry(self, path, file_id, specific_files, kind, name,
736
parent_id, definitely_changed, existing_ie=None):
791
parent_id, definitely_changed, existing_ie, report_changes,
737
793
"Record the new inventory entry for a path if any."
738
794
# mutter('check %s {%s}', path, file_id)
739
if (not specific_files or
740
is_inside_or_parent_of_any(specific_files, path)):
741
# mutter('%s selected for commit', path)
742
if definitely_changed or existing_ie is None:
743
ie = inventory.make_entry(kind, name, parent_id, file_id)
745
ie = existing_ie.copy()
795
# mutter('%s selected for commit', path)
796
if definitely_changed or existing_ie is None:
797
ie = inventory.make_entry(kind, name, parent_id, file_id)
748
# mutter('%s not selected for commit', path)
749
if self.basis_inv.has_id(file_id):
750
ie = self.basis_inv[file_id].copy()
752
# this entry is new and not being committed
755
self.builder.record_entry_contents(ie, self.parent_invs,
756
path, self.work_tree)
799
ie = existing_ie.copy()
801
delta, version_recorded = self.builder.record_entry_contents(ie,
802
self.parent_invs, path, self.work_tree, content_summary)
804
self._basis_delta.append(delta)
806
self.any_entries_changed = True
757
808
self._report_change(ie, path)