335
247
self.strict = strict
336
248
self.verbose = verbose
250
if reporter is None and self.reporter is None:
251
self.reporter = NullCommitReporter()
252
elif reporter is not None:
253
self.reporter = reporter
338
255
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
256
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
257
self.basis_tree = self.work_tree.basis_tree()
353
258
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)
260
# Cannot commit with conflicts present.
261
if len(self.work_tree.conflicts()) > 0:
262
raise ConflictsInTree
264
# Setup the bound branch variables as needed.
265
self._check_bound_branch()
267
# Check that the working tree is up to date
268
old_revno, new_revno = self._check_out_of_date_tree()
270
if self.config is None:
271
self.config = self.branch.get_config()
273
# If provided, ensure the specified files are versioned
274
if specific_files is not None:
275
# Note: We don't actually need the IDs here. This routine
276
# is being called because it raises PathNotVerisonedError
277
# as a side effect of finding the IDs.
278
# XXX: Dont we have filter_unversioned to do this more
280
tree.find_ids_across_trees(specific_files,
281
[self.basis_tree, self.work_tree])
283
# Setup the progress bar. As the number of files that need to be
284
# committed in unknown, progress is reported as stages.
285
# We keep track of entries separately though and include that
286
# information in the progress bar during the relevant stages.
287
self.pb_stage_name = ""
288
self.pb_stage_count = 0
289
self.pb_stage_total = 5
290
if self.bound_branch:
291
self.pb_stage_total += 1
292
self.pb.show_pct = False
293
self.pb.show_spinner = False
294
self.pb.show_eta = False
295
self.pb.show_count = True
296
self.pb.show_bar = True
298
# After a merge, a selected file commit is not supported.
299
# See 'bzr help merge' for an explanation as to why.
300
self.basis_inv = self.basis_tree.inventory
301
self._gather_parents()
302
if len(self.parents) > 1 and self.specific_files:
303
raise errors.CannotCommitSelectedFileMerge(self.specific_files)
305
# Collect the changes
306
self._set_progress_stage("Collecting changes",
307
entries_title="Directory")
308
self.builder = self.branch.get_commit_builder(self.parents,
309
self.config, timestamp, timezone, committer, revprops, rev_id)
312
# find the location being committed to
313
if self.bound_branch:
314
master_location = self.master_branch.base
316
master_location = self.branch.base
318
# report the start of the commit
319
self.reporter.started(new_revno, self.rev_id, master_location)
321
self._update_builder_with_changes()
322
self._check_pointless()
324
# TODO: Now the new inventory is known, check for conflicts.
325
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
326
# weave lines, because nothing should be recorded until it is known
327
# that commit will succeed.
328
self._set_progress_stage("Saving data locally")
329
self.builder.finish_inventory()
331
# Prompt the user for a commit message if none provided
332
message = message_callback(self)
333
assert isinstance(message, unicode), type(message)
334
self.message = message
335
self._escape_commit_message()
337
# Add revision data to the local branch
338
self.rev_id = self.builder.commit(self.message)
344
self._process_pre_hooks(old_revno, new_revno)
346
# Upload revision data to the master.
347
# this will propagate merged revisions too if needed.
348
if self.bound_branch:
349
self._set_progress_stage("Uploading data to master branch")
350
self.master_branch.repository.fetch(self.branch.repository,
351
revision_id=self.rev_id)
352
# now the master has the revision data
353
# 'commit' to the master first so a timeout here causes the
354
# local branch to be out of date
355
self.master_branch.set_last_revision_info(new_revno,
358
# and now do the commit locally.
359
self.branch.set_last_revision_info(new_revno, self.rev_id)
361
# Make the working tree up to date with the branch
362
self._set_progress_stage("Updating the working tree")
363
rev_tree = self.builder.revision_tree()
364
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
365
self.reporter.completed(new_revno, self.rev_id)
366
self._process_post_hooks(old_revno, new_revno)
462
369
return self.rev_id
464
def _select_reporter(self):
465
"""Select the CommitReporter to use."""
467
return NullCommitReporter()
468
return ReportCommitToLog()
371
def _any_real_changes(self):
372
"""Are there real changes between new_inventory and basis?
374
For trees without rich roots, inv.root.revision changes every commit.
375
But if that is the only change, we want to treat it as though there
378
new_entries = self.builder.new_inventory.iter_entries()
379
basis_entries = self.basis_inv.iter_entries()
380
new_path, new_root_ie = new_entries.next()
381
basis_path, basis_root_ie = basis_entries.next()
383
# This is a copy of InventoryEntry.__eq__ only leaving out .revision
384
def ie_equal_no_revision(this, other):
385
return ((this.file_id == other.file_id)
386
and (this.name == other.name)
387
and (this.symlink_target == other.symlink_target)
388
and (this.text_sha1 == other.text_sha1)
389
and (this.text_size == other.text_size)
390
and (this.text_id == other.text_id)
391
and (this.parent_id == other.parent_id)
392
and (this.kind == other.kind)
393
and (this.executable == other.executable)
394
and (this.reference_revision == other.reference_revision)
396
if not ie_equal_no_revision(new_root_ie, basis_root_ie):
399
for new_ie, basis_ie in zip(new_entries, basis_entries):
400
if new_ie != basis_ie:
403
# No actual changes present
470
406
def _check_pointless(self):
471
407
if self.allow_pointless:
618
554
old_revno, old_revid, new_revno, self.rev_id,
619
555
tree_delta, future_tree)
558
"""Cleanup any open locks, progress bars etc."""
559
cleanups = [self._cleanup_bound_branch,
560
self.basis_tree.unlock,
561
self.work_tree.unlock,
563
found_exception = None
564
for cleanup in cleanups:
567
# we want every cleanup to run no matter what.
568
# so we have a catchall here, but we will raise the
569
# last encountered exception up the stack: and
570
# typically this will be useful enough.
573
if found_exception is not None:
574
# don't do a plan raise, because the last exception may have been
575
# trashed, e is our sure-to-work exception even though it loses the
576
# full traceback. XXX: RBC 20060421 perhaps we could check the
577
# exc_info and if its the same one do a plain raise otherwise
578
# 'raise e' as we do now.
581
def _cleanup_bound_branch(self):
582
"""Executed at the end of a try/finally to cleanup a bound branch.
584
If the branch wasn't bound, this is a no-op.
585
If it was, it resents self.branch to the local branch, instead
588
if not self.bound_branch:
590
if self.master_locked:
591
self.master_branch.unlock()
593
def _escape_commit_message(self):
594
"""Replace xml-incompatible control characters."""
595
# FIXME: RBC 20060419 this should be done by the revision
596
# serialiser not by commit. Then we can also add an unescaper
597
# in the deserializer and start roundtripping revision messages
598
# precisely. See repository_implementations/test_repository.py
600
# Python strings can include characters that can't be
601
# represented in well-formed XML; escape characters that
602
# aren't listed in the XML specification
603
# (http://www.w3.org/TR/REC-xml/#NT-Char).
604
self.message, escape_count = re.subn(
605
u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
606
lambda match: match.group(0).encode('unicode_escape'),
609
self.reporter.escaped(escape_count, self.message)
621
611
def _gather_parents(self):
622
612
"""Record the parents of a merge for merge detection."""
623
# TODO: Make sure that this list doesn't contain duplicate
613
# TODO: Make sure that this list doesn't contain duplicate
624
614
# entries and the order is preserved when doing this.
625
if self.use_record_iter_changes:
627
self.basis_inv = self.basis_tree.inventory
615
self.parents = self.work_tree.get_parent_ids()
628
616
self.parent_invs = [self.basis_inv]
629
617
for revision in self.parents[1:]:
630
618
if self.branch.repository.has_revision(revision):
637
625
def _update_builder_with_changes(self):
638
626
"""Update the commit builder with the data about what has changed.
640
exclude = self.exclude
628
# Build the revision inventory.
630
# This starts by creating a new empty inventory. Depending on
631
# which files are selected for commit, and what is present in the
632
# current tree, the new inventory is populated. inventory entries
633
# which are candidates for modification have their revision set to
634
# None; inventory entries that are carried over untouched have their
635
# revision set to their prior value.
637
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
638
# results to create a new inventory at the same time, which results
639
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
641
642
specific_files = self.specific_files
642
643
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):
645
# Check and warn about old CommitBuilders
646
if not self.builder.record_root_entry:
647
symbol_versioning.warn('CommitBuilders should support recording'
648
' the root entry as of bzr 0.10.', DeprecationWarning,
650
self.builder.new_inventory.add(self.basis_inv.root.copy())
652
# Build the new inventory
653
self._populate_from_inventory(specific_files)
709
655
# If specific files are selected, then all un-selected files must be
710
656
# recorded in their previous state. For more details, see
711
657
# 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:
659
for path, new_ie in self.basis_inv.iter_entries():
660
if new_ie.file_id in self.builder.new_inventory:
662
if is_inside_any(specific_files, path):
731
666
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)
669
# Report what was deleted. We could skip this when no deletes are
670
# detected to gain a performance win, but it arguably serves as a
671
# 'safety check' by informing the user whenever anything disappears.
672
for path, ie in self.basis_inv.iter_entries():
673
if ie.file_id not in self.builder.new_inventory:
756
674
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
676
def _populate_from_inventory(self, specific_files):
677
"""Populate the CommitBuilder by walking the working tree inventory."""
763
679
# raise an exception as soon as we find a single unknown.
764
680
for unknown in self.work_tree.unknowns():
765
681
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.
684
deleted_paths = set()
792
685
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)
686
assert work_inv.root is not None
687
entries = work_inv.iter_entries()
688
if not self.builder.record_root_entry:
797
690
for path, existing_ie in entries:
798
691
file_id = existing_ie.file_id
799
692
name = existing_ie.name
800
693
parent_id = existing_ie.parent_id
801
694
kind = existing_ie.kind
695
if kind == 'directory':
696
self._next_progress_entry()
802
698
# 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.
699
# The deleted files/directories are also recorded so they
700
# can be explicitly unversioned later. Note that when a
701
# filter of specific files is given, we must only skip/record
702
# deleted files matching that filter.
703
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
705
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, {})
706
if not self.work_tree.has_filename(path):
707
deleted_paths.add(path)
836
708
self.reporter.missing(path)
837
self._next_progress_entry()
838
709
deleted_ids.append(file_id)
840
# TODO: have the builder do the nested commit just-in-time IF and
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)
712
kind = self.work_tree.kind(file_id)
713
# TODO: specific_files filtering before nested tree processing
714
if kind == 'tree-reference' and self.recursive == 'down':
715
self._commit_nested_tree(file_id, path)
716
except errors.NoSuchFile:
857
719
# Record an entry for this item
858
720
# Note: I don't particularly want to have the existing_ie
859
721
# parameter but the test suite currently (28-Jun-07) breaks
860
722
# without it thanks to a unicode normalisation issue. :-(
861
definitely_changed = kind != existing_ie.kind
723
definitely_changed = kind != existing_ie.kind
862
724
self._record_entry(path, file_id, specific_files, kind, name,
863
parent_id, definitely_changed, existing_ie, report_changes,
725
parent_id, definitely_changed, existing_ie)
866
727
# Unversion IDs that were found to be deleted
867
self.deleted_ids = deleted_ids
728
self.work_tree.unversion(deleted_ids)
869
730
def _commit_nested_tree(self, file_id, path):
870
731
"Commit a nested tree."
889
750
strict=self.strict, verbose=self.verbose,
890
751
local=self.local, reporter=self.reporter)
891
752
except errors.PointlessCommit:
892
return self.work_tree.get_reference_revision(file_id)
894
755
def _record_entry(self, path, file_id, specific_files, kind, name,
895
parent_id, definitely_changed, existing_ie, report_changes,
756
parent_id, definitely_changed, existing_ie=None):
897
757
"Record the new inventory entry for a path if any."
898
758
# 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)
759
if (not specific_files or
760
is_inside_or_parent_of_any(specific_files, path)):
761
# mutter('%s selected for commit', path)
762
if definitely_changed or existing_ie is None:
763
ie = inventory.make_entry(kind, name, parent_id, file_id)
765
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)
768
# mutter('%s not selected for commit', path)
769
if self.basis_inv.has_id(file_id):
770
ie = self.basis_inv[file_id].copy()
772
# this entry is new and not being committed
775
self.builder.record_entry_contents(ie, self.parent_invs,
776
path, self.work_tree)
910
777
self._report_change(ie, path)
912
self.work_tree._observed_sha1(ie.file_id, path, fs_hash)
915
780
def _report_change(self, ie, path):