335
235
self.strict = strict
336
236
self.verbose = verbose
238
if reporter is None and self.reporter is None:
239
self.reporter = NullCommitReporter()
240
elif reporter is not None:
241
self.reporter = reporter
338
243
self.work_tree.lock_write()
339
operation.add_cleanup(self.work_tree.unlock)
340
self.parents = self.work_tree.get_parent_ids()
341
# We can use record_iter_changes IFF iter_changes is compatible with
342
# the command line parameters, and the repository has fast delta
343
# generation. See bug 347649.
344
self.use_record_iter_changes = (
346
not self.branch.repository._format.supports_tree_reference and
347
(self.branch.repository._format.fast_deltas or
348
len(self.parents) < 2))
349
244
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
350
operation.add_cleanup(self.pb.finished)
351
self.basis_revid = self.work_tree.last_revision()
352
245
self.basis_tree = self.work_tree.basis_tree()
353
246
self.basis_tree.lock_read()
354
operation.add_cleanup(self.basis_tree.unlock)
355
# Cannot commit with conflicts present.
356
if len(self.work_tree.conflicts()) > 0:
357
raise ConflictsInTree
359
# Setup the bound branch variables as needed.
360
self._check_bound_branch(operation, possible_master_transports)
362
# Check that the working tree is up to date
363
old_revno, new_revno = self._check_out_of_date_tree()
365
# Complete configuration setup
366
if reporter is not None:
367
self.reporter = reporter
368
elif self.reporter is None:
369
self.reporter = self._select_reporter()
370
if self.config is None:
371
self.config = self.branch.get_config()
373
self._set_specific_file_ids()
375
# Setup the progress bar. As the number of files that need to be
376
# committed in unknown, progress is reported as stages.
377
# We keep track of entries separately though and include that
378
# information in the progress bar during the relevant stages.
379
self.pb_stage_name = ""
380
self.pb_stage_count = 0
381
self.pb_stage_total = 5
382
if self.bound_branch:
383
self.pb_stage_total += 1
384
self.pb.show_pct = False
385
self.pb.show_spinner = False
386
self.pb.show_eta = False
387
self.pb.show_count = True
388
self.pb.show_bar = True
390
self._gather_parents()
391
# After a merge, a selected file commit is not supported.
392
# See 'bzr help merge' for an explanation as to why.
393
if len(self.parents) > 1 and self.specific_files is not None:
394
raise errors.CannotCommitSelectedFileMerge(self.specific_files)
395
# Excludes are a form of selected file commit.
396
if len(self.parents) > 1 and self.exclude:
397
raise errors.CannotCommitSelectedFileMerge(self.exclude)
399
# Collect the changes
400
self._set_progress_stage("Collecting changes", counter=True)
401
self.builder = self.branch.get_commit_builder(self.parents,
402
self.config, timestamp, timezone, committer, self.revprops, rev_id)
405
self.builder.will_record_deletes()
406
# find the location being committed to
407
if self.bound_branch:
408
master_location = self.master_branch.base
410
master_location = self.branch.base
412
# report the start of the commit
413
self.reporter.started(new_revno, self.rev_id, master_location)
415
self._update_builder_with_changes()
416
self._check_pointless()
418
# TODO: Now the new inventory is known, check for conflicts.
419
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
420
# weave lines, because nothing should be recorded until it is known
421
# that commit will succeed.
422
self._set_progress_stage("Saving data locally")
423
self.builder.finish_inventory()
425
# Prompt the user for a commit message if none provided
426
message = message_callback(self)
427
self.message = message
429
# Add revision data to the local branch
430
self.rev_id = self.builder.commit(self.message)
433
mutter("aborting commit write group because of exception:")
434
trace.log_exception_quietly()
435
note("aborting commit write group: %r" % (e,))
439
self._process_pre_hooks(old_revno, new_revno)
441
# Upload revision data to the master.
442
# this will propagate merged revisions too if needed.
443
if self.bound_branch:
444
self._set_progress_stage("Uploading data to master branch")
445
# 'commit' to the master first so a timeout here causes the
446
# local branch to be out of date
447
self.master_branch.import_last_revision_info(
448
self.branch.repository, new_revno, self.rev_id)
450
# and now do the commit locally.
451
self.branch.set_last_revision_info(new_revno, self.rev_id)
453
# Make the working tree be up to date with the branch. This
454
# includes automatic changes scheduled to be made to the tree, such
455
# as updating its basis and unversioning paths that were missing.
456
self.work_tree.unversion(self.deleted_ids)
457
self._set_progress_stage("Updating the working tree")
458
self.work_tree.update_basis_by_delta(self.rev_id,
459
self.builder.get_basis_delta())
460
self.reporter.completed(new_revno, self.rev_id)
461
self._process_post_hooks(old_revno, new_revno)
248
# Cannot commit with conflicts present.
249
if len(self.work_tree.conflicts()) > 0:
250
raise ConflictsInTree
252
# Setup the bound branch variables as needed.
253
self._check_bound_branch()
255
# Check that the working tree is up to date
256
old_revno, new_revno = self._check_out_of_date_tree()
258
if self.config is None:
259
self.config = self.branch.get_config()
261
# If provided, ensure the specified files are versioned
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.
266
# XXX: Dont we have filter_unversioned to do this more
268
tree.find_ids_across_trees(specific_files,
269
[self.basis_tree, self.work_tree])
271
# Setup the progress bar. As the number of files that need to be
272
# committed in unknown, progress is reported as stages.
273
# We keep track of entries separately though and include that
274
# information in the progress bar during the relevant stages.
275
self.pb_stage_name = ""
276
self.pb_stage_count = 0
277
self.pb_stage_total = 5
278
if self.bound_branch:
279
self.pb_stage_total += 1
280
self.pb.show_pct = False
281
self.pb.show_spinner = False
282
self.pb.show_eta = False
283
self.pb.show_count = True
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.
288
self.basis_inv = self.basis_tree.inventory
289
self._gather_parents()
290
if len(self.parents) > 1 and self.specific_files:
291
raise errors.CannotCommitSelectedFileMerge(self.specific_files)
293
# Collect the changes
294
self._set_progress_stage("Collecting changes",
295
entries_title="Directory")
296
self.builder = self.branch.get_commit_builder(self.parents,
297
self.config, timestamp, timezone, committer, revprops, rev_id)
298
# tell the builder about the chosen recursive behaviour
299
self.builder.recursive = recursive
302
self._update_builder_with_changes()
303
self._check_pointless()
305
# TODO: Now the new inventory is known, check for conflicts.
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._set_progress_stage("Saving data locally")
310
self.builder.finish_inventory()
312
# Prompt the user for a commit message if none provided
313
message = message_callback(self)
314
assert isinstance(message, unicode), type(message)
315
self.message = message
316
self._escape_commit_message()
318
# Add revision data to the local branch
319
self.rev_id = self.builder.commit(self.message)
325
self._process_pre_hooks(old_revno, new_revno)
327
# Upload revision data to the master.
328
# this will propagate merged revisions too if needed.
329
if self.bound_branch:
330
self._set_progress_stage("Uploading data to master branch")
331
self.master_branch.repository.fetch(self.branch.repository,
332
revision_id=self.rev_id)
333
# now the master has the revision data
334
# 'commit' to the master first so a timeout here causes the
335
# local branch to be out of date
336
self.master_branch.set_last_revision_info(new_revno,
339
# and now do the commit locally.
340
self.branch.set_last_revision_info(new_revno, self.rev_id)
342
# Make the working tree up to date with the branch
343
self._set_progress_stage("Updating the working tree")
344
rev_tree = self.builder.revision_tree()
345
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
346
self.reporter.completed(new_revno, self.rev_id)
347
self._process_post_hooks(old_revno, new_revno)
462
350
return self.rev_id
464
def _select_reporter(self):
465
"""Select the CommitReporter to use."""
467
return NullCommitReporter()
468
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
470
387
def _check_pointless(self):
471
388
if self.allow_pointless:
618
535
old_revno, old_revid, new_revno, self.rev_id,
619
536
tree_delta, future_tree)
539
"""Cleanup any open locks, progress bars etc."""
540
cleanups = [self._cleanup_bound_branch,
541
self.basis_tree.unlock,
542
self.work_tree.unlock,
544
found_exception = None
545
for cleanup in cleanups:
548
# we want every cleanup to run no matter what.
549
# so we have a catchall here, but we will raise the
550
# last encountered exception up the stack: and
551
# typically this will be useful enough.
554
if found_exception is not None:
555
# don't do a plan raise, because the last exception may have been
556
# trashed, e is our sure-to-work exception even though it loses the
557
# full traceback. XXX: RBC 20060421 perhaps we could check the
558
# exc_info and if its the same one do a plain raise otherwise
559
# 'raise e' as we do now.
562
def _cleanup_bound_branch(self):
563
"""Executed at the end of a try/finally to cleanup a bound branch.
565
If the branch wasn't bound, this is a no-op.
566
If it was, it resents self.branch to the local branch, instead
569
if not self.bound_branch:
571
if self.master_locked:
572
self.master_branch.unlock()
574
def _escape_commit_message(self):
575
"""Replace xml-incompatible control characters."""
576
# FIXME: RBC 20060419 this should be done by the revision
577
# serialiser not by commit. Then we can also add an unescaper
578
# in the deserializer and start roundtripping revision messages
579
# precisely. See repository_implementations/test_repository.py
581
# Python strings can include characters that can't be
582
# represented in well-formed XML; escape characters that
583
# aren't listed in the XML specification
584
# (http://www.w3.org/TR/REC-xml/#NT-Char).
585
self.message, escape_count = re.subn(
586
u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
587
lambda match: match.group(0).encode('unicode_escape'),
590
self.reporter.escaped(escape_count, self.message)
621
592
def _gather_parents(self):
622
593
"""Record the parents of a merge for merge detection."""
623
# TODO: Make sure that this list doesn't contain duplicate
594
# TODO: Make sure that this list doesn't contain duplicate
624
595
# entries and the order is preserved when doing this.
625
if self.use_record_iter_changes:
627
self.basis_inv = self.basis_tree.inventory
596
self.parents = self.work_tree.get_parent_ids()
628
597
self.parent_invs = [self.basis_inv]
629
598
for revision in self.parents[1:]:
630
599
if self.branch.repository.has_revision(revision):
637
606
def _update_builder_with_changes(self):
638
607
"""Update the commit builder with the data about what has changed.
640
exclude = self.exclude
609
# Build the revision inventory.
611
# This starts by creating a new empty inventory. Depending on
612
# which files are selected for commit, and what is present in the
613
# current tree, the new inventory is populated. inventory entries
614
# which are candidates for modification have their revision set to
615
# None; inventory entries that are carried over untouched have their
616
# revision set to their prior value.
618
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
619
# results to create a new inventory at the same time, which results
620
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
641
623
specific_files = self.specific_files
642
624
mutter("Selecting files for commit with filter %s", specific_files)
645
if self.use_record_iter_changes:
646
iter_changes = self.work_tree.iter_changes(self.basis_tree,
647
specific_files=specific_files)
648
iter_changes = self._filter_iter_changes(iter_changes)
649
for file_id, path, fs_hash in self.builder.record_iter_changes(
650
self.work_tree, self.basis_revid, iter_changes):
651
self.work_tree._observed_sha1(file_id, path, fs_hash)
653
# Build the new inventory
654
self._populate_from_inventory()
655
self._record_unselected()
656
self._report_and_accumulate_deletes()
658
def _filter_iter_changes(self, iter_changes):
659
"""Process iter_changes.
661
This method reports on the changes in iter_changes to the user, and
662
converts 'missing' entries in the iter_changes iterator to 'deleted'
663
entries. 'missing' entries have their
665
:param iter_changes: An iter_changes to process.
666
:return: A generator of changes.
668
reporter = self.reporter
669
report_changes = reporter.is_verbose()
671
for change in iter_changes:
673
old_path = change[1][0]
674
new_path = change[1][1]
675
versioned = change[3][1]
677
versioned = change[3][1]
678
if kind is None and versioned:
681
reporter.missing(new_path)
682
deleted_ids.append(change[0])
683
# Reset the new path (None) and new versioned flag (False)
684
change = (change[0], (change[1][0], None), change[2],
685
(change[3][0], False)) + change[4:]
686
elif kind == 'tree-reference':
687
if self.recursive == 'down':
688
self._commit_nested_tree(change[0], change[1][1])
689
if change[3][0] or change[3][1]:
693
reporter.deleted(old_path)
694
elif old_path is None:
695
reporter.snapshot_change('added', new_path)
696
elif old_path != new_path:
697
reporter.renamed('renamed', old_path, new_path)
700
self.work_tree.branch.repository._format.rich_root_data):
701
# Don't report on changes to '' in non rich root
703
reporter.snapshot_change('modified', new_path)
704
self._next_progress_entry()
705
# Unversion IDs that were found to be deleted
706
self.deleted_ids = deleted_ids
708
def _record_unselected(self):
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())
633
# Build the new inventory
634
self._populate_from_inventory(specific_files)
709
636
# If specific files are selected, then all un-selected files must be
710
637
# recorded in their previous state. For more details, see
711
638
# https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
712
if self.specific_files or self.exclude:
713
specific_files = self.specific_files or []
714
for path, old_ie in self.basis_inv.iter_entries():
715
if old_ie.file_id in self.builder.new_inventory:
716
# already added - skip.
718
if (is_inside_any(specific_files, path)
719
and not is_inside_any(self.exclude, path)):
720
# was inside the selected path, and not excluded - if not
721
# present it has been deleted so skip.
723
# From here down it was either not selected, or was excluded:
724
# We preserve the entry unaltered.
726
# Note: specific file commits after a merge are currently
727
# prohibited. This test is for sanity/safety in case it's
728
# required after that changes.
729
if len(self.parents) > 1:
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):
731
647
self.builder.record_entry_contents(ie, self.parent_invs, path,
732
self.basis_tree, None)
734
def _report_and_accumulate_deletes(self):
735
if (isinstance(self.basis_inv, Inventory)
736
and isinstance(self.builder.new_inventory, Inventory)):
737
# the older Inventory classes provide a _byid dict, and building a
738
# set from the keys of this dict is substantially faster than even
739
# getting a set of ids from the inventory
741
# <lifeless> set(dict) is roughly the same speed as
742
# set(iter(dict)) and both are significantly slower than
744
deleted_ids = set(self.basis_inv._byid.keys()) - \
745
set(self.builder.new_inventory._byid.keys())
747
deleted_ids = set(self.basis_inv) - set(self.builder.new_inventory)
749
self.any_entries_deleted = True
750
deleted = [(self.basis_tree.id2path(file_id), file_id)
751
for file_id in deleted_ids]
753
# XXX: this is not quite directory-order sorting
754
for path, file_id in deleted:
755
self.builder.record_delete(path, file_id)
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:
756
655
self.reporter.deleted(path)
758
def _check_strict(self):
759
# XXX: when we use iter_changes this would likely be faster if
760
# iter_changes would check for us (even in the presence of
657
def _populate_from_inventory(self, specific_files):
658
"""Populate the CommitBuilder by walking the working tree inventory."""
763
660
# raise an exception as soon as we find a single unknown.
764
661
for unknown in self.work_tree.unknowns():
765
662
raise StrictCommitFailed()
767
def _populate_from_inventory(self):
768
"""Populate the CommitBuilder by walking the working tree inventory."""
769
# Build the revision inventory.
771
# This starts by creating a new empty inventory. Depending on
772
# which files are selected for commit, and what is present in the
773
# current tree, the new inventory is populated. inventory entries
774
# which are candidates for modification have their revision set to
775
# None; inventory entries that are carried over untouched have their
776
# revision set to their prior value.
778
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
779
# results to create a new inventory at the same time, which results
780
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
783
specific_files = self.specific_files
784
exclude = self.exclude
785
report_changes = self.reporter.is_verbose()
787
# A tree of paths that have been deleted. E.g. if foo/bar has been
788
# deleted, then we have {'foo':{'bar':{}}}
790
# XXX: Note that entries may have the wrong kind because the entry does
791
# not reflect the status on disk.
665
deleted_paths = set()
792
666
work_inv = self.work_tree.inventory
793
# NB: entries will include entries within the excluded ids/paths
794
# because iter_entries_by_dir has no 'exclude' facility today.
795
entries = work_inv.iter_entries_by_dir(
796
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:
797
672
for path, existing_ie in entries:
798
673
file_id = existing_ie.file_id
799
674
name = existing_ie.name
800
675
parent_id = existing_ie.parent_id
801
676
kind = existing_ie.kind
677
if kind == 'directory':
678
self._next_progress_entry()
802
680
# Skip files that have been deleted from the working tree.
803
# The deleted path ids are also recorded so they can be explicitly
806
path_segments = splitpath(path)
807
deleted_dict = deleted_paths
808
for segment in path_segments:
809
deleted_dict = deleted_dict.get(segment, None)
811
# We either took a path not present in the dict
812
# (deleted_dict was None), or we've reached an empty
813
# child dir in the dict, so are now a sub-path.
817
if deleted_dict is not None:
818
# the path has a deleted parent, do not add it.
820
if exclude and is_inside_any(exclude, path):
821
# Skip excluded paths. Excluded paths are processed by
822
# _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):
824
content_summary = self.work_tree.path_content_summary(path)
825
kind = content_summary[0]
826
# Note that when a filter of specific files is given, we must only
827
# skip/record deleted files matching that filter.
828
687
if not specific_files or is_inside_any(specific_files, path):
829
if kind == 'missing':
830
if not deleted_paths:
831
# path won't have been split yet.
832
path_segments = splitpath(path)
833
deleted_dict = deleted_paths
834
for segment in path_segments:
835
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)
836
691
self.reporter.missing(path)
837
self._next_progress_entry()
838
692
deleted_ids.append(file_id)
840
694
# TODO: have the builder do the nested commit just-in-time IF and
841
695
# only if needed.
842
if kind == 'tree-reference':
843
# enforce repository nested tree policy.
844
if (not self.work_tree.supports_tree_reference() or
845
# repository does not support it either.
846
not self.branch.repository._format.supports_tree_reference):
848
content_summary = (kind, None, None, None)
849
elif self.recursive == 'down':
850
nested_revision_id = self._commit_nested_tree(
852
content_summary = (kind, None, None, nested_revision_id)
854
nested_revision_id = self.work_tree.get_reference_revision(file_id)
855
content_summary = (kind, None, None, nested_revision_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:
857
704
# Record an entry for this item
858
705
# Note: I don't particularly want to have the existing_ie
889
735
strict=self.strict, verbose=self.verbose,
890
736
local=self.local, reporter=self.reporter)
891
737
except errors.PointlessCommit:
892
return self.work_tree.get_reference_revision(file_id)
894
740
def _record_entry(self, path, file_id, specific_files, kind, name,
895
parent_id, definitely_changed, existing_ie, report_changes,
741
parent_id, definitely_changed, existing_ie=None):
897
742
"Record the new inventory entry for a path if any."
898
743
# mutter('check %s {%s}', path, file_id)
899
# mutter('%s selected for commit', path)
900
if definitely_changed or existing_ie is None:
901
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()
903
ie = existing_ie.copy()
905
# For carried over entries we don't care about the fs hash - the repo
906
# isn't generating a sha, so we're not saving computation time.
907
_, _, fs_hash = self.builder.record_entry_contents(
908
ie, self.parent_invs, path, self.work_tree, content_summary)
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)
910
762
self._report_change(ie, path)
912
self.work_tree._observed_sha1(ie.file_id, path, fs_hash)
915
765
def _report_change(self, ie, path):