18
18
# The newly committed revision is going to have a shape corresponding
19
# to that of the working tree. Files that are not in the
19
# to that of the working inventory. Files that are not in the
20
20
# working tree and that were in the predecessor are reported as
21
21
# removed --- this can include files that were either removed from the
22
22
# inventory or deleted in the working tree. If they were only
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 tree, we check to see what
28
# recorded. For each parent revision inventory, 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
71
from bzrlib.osutils import (kind_marker, isdir,isfile, is_inside_any,
72
72
is_inside_or_parent_of_any,
73
73
minimum_path_selection,
74
quotefn, sha_file, split_lines,
74
quotefn, sha_file, split_lines)
77
75
from bzrlib.testament import Testament
78
76
from bzrlib.trace import mutter, note, warning, is_quiet
79
77
from bzrlib.xml5 import serializer_v5
80
from bzrlib.inventory import InventoryEntry, make_entry
78
from bzrlib.inventory import Inventory, InventoryEntry
81
79
from bzrlib import symbol_versioning
82
80
from bzrlib.symbol_versioning import (deprecated_passed,
83
81
deprecated_function,
91
89
"""I report on progress of a commit."""
93
91
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
94
def snapshot_change(self, change, path):
138
132
def started(self, revno, rev_id, location=None):
139
133
if location is not None:
140
location = ' to: ' + unescape_for_display(location, 'utf-8')
134
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)
137
self._note('Committing revision %d%s.', revno, location)
150
139
def completed(self, revno, rev_id):
151
140
self._note('Committed revision %d.', revno)
233
221
:param verbose: if True and the reporter is not None, report everything
234
222
:param recursive: If set to 'down', commit in any subtrees that have
235
223
pending changes of any sort during this commit.
236
:param exclude: None or a list of relative paths to exclude from the
237
commit. Pending changes to excluded files will be ignored by the
240
225
mutter('preparing to commit')
259
244
self.bound_branch = None
260
245
self.any_entries_changed = False
261
246
self.any_entries_deleted = False
262
if exclude is not None:
263
self.exclude = sorted(
264
minimum_path_selection(exclude))
267
247
self.local = local
268
248
self.master_branch = None
269
249
self.master_locked = False
290
270
self.work_tree.lock_write()
291
271
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
292
self.basis_revid = self.work_tree.last_revision()
293
272
self.basis_tree = self.work_tree.basis_tree()
294
273
self.basis_tree.lock_read()
338
317
self.pb.show_count = True
339
318
self.pb.show_bar = True
320
# After a merge, a selected file commit is not supported.
321
# See 'bzr help merge' for an explanation as to why.
341
322
self.basis_inv = self.basis_tree.inventory
342
323
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
324
if len(self.parents) > 1 and self.specific_files:
346
325
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
327
# Collect the changes
352
328
self._set_progress_stage("Collecting changes",
378
354
# Prompt the user for a commit message if none provided
379
355
message = message_callback(self)
356
assert isinstance(message, unicode), type(message)
380
357
self.message = message
381
358
self._escape_commit_message()
392
369
# Upload revision data to the master.
393
370
# this will propagate merged revisions too if needed.
394
371
if self.bound_branch:
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)
372
self._set_progress_stage("Uploading data to master branch")
373
self.master_branch.repository.fetch(self.branch.repository,
374
revision_id=self.rev_id)
400
375
# now the master has the revision data
401
376
# 'commit' to the master first so a timeout here causes the
402
377
# local branch to be out of date
409
384
# Make the working tree up to date with the branch
410
385
self._set_progress_stage("Updating the working tree")
411
self.work_tree.update_basis_by_delta(self.rev_id,
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)
396
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
413
397
self.reporter.completed(new_revno, self.rev_id)
414
398
self._process_post_hooks(old_revno, new_revno)
431
415
# 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):
417
# The inital commit adds a root directory, but this in itself is not
418
# a worthwhile commit.
419
if len(self.basis_inv) == 0 and len(self.builder.new_inventory) == 1:
437
420
raise PointlessCommit()
421
# Shortcut, if the number of entries changes, then we obviously have
423
if len(self.builder.new_inventory) != len(self.basis_inv):
438
425
# If length == 1, then we only have the root entry. Which means
439
426
# 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)):
427
if len(self.builder.new_inventory) != 1 and (self.any_entries_changed
428
or self.any_entries_deleted):
445
430
raise PointlessCommit()
660
645
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
661
646
# ADHB 11-07-2006
663
exclude = self.exclude
664
specific_files = self.specific_files or []
648
specific_files = self.specific_files
665
649
mutter("Selecting files for commit with filter %s", specific_files)
667
651
# Build the new inventory
668
self._populate_from_inventory()
652
self._populate_from_inventory(specific_files)
670
654
# If specific files are selected, then all un-selected files must be
671
655
# recorded in their previous state. For more details, see
672
656
# https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
673
if specific_files or exclude:
674
658
for path, old_ie in self.basis_inv.iter_entries():
675
659
if old_ie.file_id in self.builder.new_inventory:
676
660
# 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.
662
if is_inside_any(specific_files, path):
663
# was inside the selected path, if not present it has been
683
# From here down it was either not selected, or was excluded:
684
666
if old_ie.kind == 'directory':
685
667
self._next_progress_entry()
686
# We preserve the entry unaltered.
668
# not in final inv yet, was not in the selected files, so is an
669
# entry to be preserved unaltered.
687
670
ie = old_ie.copy()
688
671
# Note: specific file commits after a merge are currently
689
672
# prohibited. This test is for sanity/safety in case it's
703
686
set(self.builder.new_inventory._byid.keys())
705
688
self.any_entries_deleted = True
706
deleted = [(self.basis_tree.id2path(file_id), file_id)
689
deleted = [(self.basis_inv.id2path(file_id), file_id)
707
690
for file_id in deleted_ids]
709
692
# XXX: this is not quite directory-order sorting
711
694
self._basis_delta.append((path, None, file_id, None))
712
695
self.reporter.deleted(path)
714
def _populate_from_inventory(self):
697
def _populate_from_inventory(self, specific_files):
715
698
"""Populate the CommitBuilder by walking the working tree inventory."""
717
700
# raise an exception as soon as we find a single unknown.
718
701
for unknown in self.work_tree.unknowns():
719
702
raise StrictCommitFailed()
721
specific_files = self.specific_files
722
exclude = self.exclude
723
704
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.
706
deleted_paths = set()
730
707
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.
708
assert work_inv.root is not None
709
# XXX: Note that entries may have the wrong kind.
733
710
entries = work_inv.iter_entries_by_dir(
734
711
specific_file_ids=self.specific_file_ids, yield_parents=True)
735
712
for path, existing_ie in entries:
740
717
if kind == 'directory':
741
718
self._next_progress_entry()
742
719
# 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.
720
# The deleted files/directories are also recorded so they
721
# can be explicitly unversioned later. Note that when a
722
# filter of specific files is given, we must only skip/record
723
# deleted files matching that filter.
724
if is_inside_any(deleted_paths, path):
764
726
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
727
if not specific_files or is_inside_any(specific_files, path):
768
728
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, {})
729
deleted_paths.add(path)
775
730
self.reporter.missing(path)
776
731
deleted_ids.append(file_id)
839
794
# mutter('check %s {%s}', path, file_id)
840
795
# mutter('%s selected for commit', path)
841
796
if definitely_changed or existing_ie is None:
842
ie = make_entry(kind, name, parent_id, file_id)
797
ie = inventory.make_entry(kind, name, parent_id, file_id)
844
799
ie = existing_ie.copy()
845
800
ie.revision = None