176
message_callback=None,
174
message_callback=None):
178
175
"""Commit working copy as a new revision.
180
:param message: the commit message (it or message_callback is required)
182
:param timestamp: if not None, seconds-since-epoch for a
183
postdated/predated commit.
185
:param specific_files: If true, commit only those files.
187
:param rev_id: If set, use this as the new revision id.
177
branch -- the deprecated branch to commit to. New callers should pass in
180
message -- the commit message (it or message_callback is required)
182
timestamp -- if not None, seconds-since-epoch for a
183
postdated/predated commit.
185
specific_files -- If true, commit only those files.
187
rev_id -- If set, use this as the new revision id.
188
188
Useful for test or import commands that need to tightly
189
189
control what revisions are assigned. If you duplicate
190
190
a revision id that exists elsewhere it is your own fault.
191
191
If null (default), a time/random revision id is generated.
193
:param allow_pointless: If true (default), commit even if nothing
193
allow_pointless -- If true (default), commit even if nothing
194
194
has changed and no merges are recorded.
196
:param strict: If true, don't allow a commit if the working tree
196
strict -- If true, don't allow a commit if the working tree
197
197
contains unknown files.
199
:param revprops: Properties for new revision
199
revprops -- Properties for new revision
200
200
:param local: Perform a local only commit.
201
:param recursive: If set to 'down', commit in any subtrees that have
202
pending changes of any sort during this commit.
204
202
mutter('preparing to commit')
206
if working_tree is None:
207
raise BzrError("working_tree must be passed into commit().")
204
if deprecated_passed(branch):
205
symbol_versioning.warn("Commit.commit (branch, ...): The branch parameter is "
206
"deprecated as of bzr 0.8. Please use working_tree= instead.",
207
DeprecationWarning, stacklevel=2)
209
self.work_tree = self.branch.bzrdir.open_workingtree()
210
elif working_tree is None:
211
raise BzrError("One of branch and working_tree must be passed into commit().")
209
213
self.work_tree = working_tree
210
214
self.branch = self.work_tree.branch
211
if getattr(self.work_tree, 'requires_rich_root', lambda: False)():
212
if not self.branch.repository.supports_rich_root():
213
raise errors.RootNotRich()
214
215
if message_callback is None:
215
216
if message is not None:
216
217
if isinstance(message, str):
244
237
self.work_tree.lock_write()
245
238
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
246
self.basis_tree = self.work_tree.basis_tree()
247
self.basis_tree.lock_read()
249
240
# Cannot commit with conflicts present.
250
if len(self.work_tree.conflicts()) > 0:
241
if len(self.work_tree.conflicts())>0:
251
242
raise ConflictsInTree
253
# Setup the bound branch variables as needed.
244
# setup the bound branch variables as needed.
254
245
self._check_bound_branch()
256
# Check that the working tree is up to date
257
old_revno,new_revno = self._check_out_of_date_tree()
247
# check for out of date working trees
249
first_tree_parent = self.work_tree.get_parent_ids()[0]
251
# if there are no parents, treat our parent as 'None'
252
# this is so that we still consier the master branch
253
# - in a checkout scenario the tree may have no
254
# parents but the branch may do.
255
first_tree_parent = None
256
master_last = self.master_branch.last_revision()
257
if (master_last is not None and
258
master_last != first_tree_parent):
259
raise errors.OutOfDateTree(self.work_tree)
262
# raise an exception as soon as we find a single unknown.
263
for unknown in self.work_tree.unknowns():
264
raise StrictCommitFailed()
259
266
if self.config is None:
260
267
self.config = self.branch.get_config()
262
# If provided, ensure the specified files are versioned
269
self.work_inv = self.work_tree.inventory
270
self.basis_tree = self.work_tree.basis_tree()
271
self.basis_inv = self.basis_tree.inventory
263
272
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.
267
# XXX: Dont we have filter_unversioned to do this more
269
tree.find_ids_across_trees(specific_files,
273
# Ensure specified files are versioned
274
# (We don't actually need the ids here)
275
tree.find_ids_across_trees(specific_files,
270
276
[self.basis_tree, self.work_tree])
272
# Setup the progress bar. As the number of files that need to be
273
# committed in unknown, progress is reported as stages.
274
# We keep track of entries separately though and include that
275
# information in the progress bar during the relevant stages.
276
self.pb_stage_name = ""
277
self.pb_stage_count = 0
278
self.pb_stage_total = 4
279
if self.bound_branch:
280
self.pb_stage_total += 1
281
self.pb.show_pct = False
282
self.pb.show_spinner = False
283
self.pb.show_eta = False
284
self.pb.show_count = True
285
self.pb.show_bar = True
287
# After a merge, a selected file commit is not supported.
288
# See 'bzr help merge' for an explanation as to why.
289
self.basis_inv = self.basis_tree.inventory
277
# one to finish, one for rev and inventory, and one for each
278
# inventory entry, and the same for the new inventory.
279
# note that this estimate is too long when we do a partial tree
280
# commit which excludes some new files from being considered.
281
# The estimate is corrected when we populate the new inv.
282
self.pb_total = len(self.work_inv) + 5
290
285
self._gather_parents()
291
286
if len(self.parents) > 1 and self.specific_files:
292
raise errors.CannotCommitSelectedFileMerge(self.specific_files)
287
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
294
# Collect the changes
295
self._set_progress_stage("Collecting changes",
296
entries_title="Directory")
297
self.builder = self.branch.get_commit_builder(self.parents,
290
self.builder = self.branch.get_commit_builder(self.parents,
298
291
self.config, timestamp, timezone, committer, revprops, rev_id)
299
self._update_builder_with_changes()
293
self._remove_deleted()
294
self._populate_new_inv()
295
self._report_deletes()
300
297
self._check_pointless()
302
# TODO: Now the new inventory is known, check for conflicts.
299
self._emit_progress_update()
300
# TODO: Now the new inventory is known, check for conflicts and
301
# prompt the user for a commit message.
303
302
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
304
303
# weave lines, because nothing should be recorded until it is known
305
304
# that commit will succeed.
306
self._set_progress_stage("Saving data locally")
307
305
self.builder.finish_inventory()
309
# Prompt the user for a commit message if none provided
306
self._emit_progress_update()
310
307
message = message_callback(self)
311
308
assert isinstance(message, unicode), type(message)
312
309
self.message = message
313
310
self._escape_commit_message()
315
# Add revision data to the local branch
316
312
self.rev_id = self.builder.commit(self.message)
313
self._emit_progress_update()
314
# revision data is in the local branch now.
318
# Upload revision data to the master.
316
# upload revision data to the master.
319
317
# this will propagate merged revisions too if needed.
320
318
if self.bound_branch:
321
self._set_progress_stage("Uploading data to master branch")
322
319
self.master_branch.repository.fetch(self.branch.repository,
323
320
revision_id=self.rev_id)
324
321
# now the master has the revision data
325
# 'commit' to the master first so a timeout here causes the
326
# local branch to be out of date
327
self.master_branch.set_last_revision_info(new_revno,
322
# 'commit' to the master first so a timeout here causes the local
323
# branch to be out of date
324
self.master_branch.append_revision(self.rev_id)
330
326
# and now do the commit locally.
331
self.branch.set_last_revision_info(new_revno, self.rev_id)
327
self.branch.append_revision(self.rev_id)
333
# Make the working tree up to date with the branch
334
self._set_progress_stage("Updating the working tree")
335
329
rev_tree = self.builder.revision_tree()
336
330
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
337
self.reporter.completed(new_revno, self.rev_id)
338
self._process_hooks(old_revno, new_revno)
331
# now the work tree is up to date with the branch
333
self.reporter.completed(self.branch.revno(), self.rev_id)
334
if self.config.post_commit() is not None:
335
hooks = self.config.post_commit().split(' ')
336
# this would be nicer with twisted.python.reflect.namedAny
338
result = eval(hook + '(branch, rev_id)',
339
{'branch':self.branch,
341
'rev_id':self.rev_id})
342
self._emit_progress_update()
341
345
return self.rev_id
437
440
self.master_branch.lock_write()
438
441
self.master_locked = True
440
def _check_out_of_date_tree(self):
441
"""Check that the working tree is up to date.
443
:return: old_revision_number,new_revision_number tuple
446
first_tree_parent = self.work_tree.get_parent_ids()[0]
448
# if there are no parents, treat our parent as 'None'
449
# this is so that we still consider the master branch
450
# - in a checkout scenario the tree may have no
451
# parents but the branch may do.
452
first_tree_parent = bzrlib.revision.NULL_REVISION
453
old_revno, master_last = self.master_branch.last_revision_info()
454
if master_last != first_tree_parent:
455
if master_last != bzrlib.revision.NULL_REVISION:
456
raise errors.OutOfDateTree(self.work_tree)
457
if self.branch.repository.has_revision(first_tree_parent):
458
new_revno = old_revno + 1
460
# ghost parents never appear in revision history.
462
return old_revno,new_revno
464
def _process_hooks(self, old_revno, new_revno):
465
"""Process any registered commit hooks."""
466
# Process the post commit hooks, if any
467
self._set_progress_stage("Running post commit hooks")
468
# old style commit hooks - should be deprecated ? (obsoleted in
470
if self.config.post_commit() is not None:
471
hooks = self.config.post_commit().split(' ')
472
# this would be nicer with twisted.python.reflect.namedAny
474
result = eval(hook + '(branch, rev_id)',
475
{'branch':self.branch,
477
'rev_id':self.rev_id})
478
# new style commit hooks:
479
if not self.bound_branch:
480
hook_master = self.branch
483
hook_master = self.master_branch
484
hook_local = self.branch
485
# With bound branches, when the master is behind the local branch,
486
# the 'old_revno' and old_revid values here are incorrect.
487
# XXX: FIXME ^. RBC 20060206
489
old_revid = self.parents[0]
491
old_revid = bzrlib.revision.NULL_REVISION
492
for hook in Branch.hooks['post_commit']:
493
# show the running hook in the progress bar. As hooks may
494
# end up doing nothing (e.g. because they are not configured by
495
# the user) this is still showing progress, not showing overall
496
# actions - its up to each plugin to show a UI if it want's to
497
# (such as 'Emailing diff to foo@example.com').
498
self.pb_stage_name = "Running post commit hooks [%s]" % \
499
Branch.hooks.get_hook_name(hook)
500
self._emit_progress()
501
if 'hooks' in debug.debug_flags:
502
mutter("Invoking commit hook: %r", hook)
503
hook(hook_local, hook_master, old_revno, old_revid, new_revno,
506
443
def _cleanup(self):
507
444
"""Cleanup any open locks, progress bars etc."""
508
445
cleanups = [self._cleanup_bound_branch,
509
self.basis_tree.unlock,
510
446
self.work_tree.unlock,
511
447
self.pb.finished]
512
448
found_exception = None
572
508
mutter('commit parent ghost revision {%s}', revision)
574
def _update_builder_with_changes(self):
575
"""Update the commit builder with the data about what has changed.
577
# Build the revision inventory.
579
# This starts by creating a new empty inventory. Depending on
580
# which files are selected for commit, and what is present in the
581
# current tree, the new inventory is populated. inventory entries
582
# which are candidates for modification have their revision set to
583
# None; inventory entries that are carried over untouched have their
584
# revision set to their prior value.
510
def _remove_deleted(self):
511
"""Remove deleted files from the working inventories.
513
This is done prior to taking the working inventory as the
514
basis for the new committed inventory.
516
This returns true if any files
517
*that existed in the basis inventory* were deleted.
518
Files that were added and deleted
519
in the working copy don't matter.
521
specific = self.specific_files
523
deleted_paths = set()
524
for path, ie in self.work_inv.iter_entries():
525
if is_inside_any(deleted_paths, path):
526
# The tree will delete the required ids recursively.
528
if specific and not is_inside_any(specific, path):
530
if not self.work_tree.has_filename(path):
531
deleted_paths.add(path)
532
self.reporter.missing(path)
533
deleted_ids.append(ie.file_id)
534
self.work_tree.unversion(deleted_ids)
536
def _populate_new_inv(self):
537
"""Build revision inventory.
539
This creates a new empty inventory. Depending on
540
which files are selected for commit, and what is present in the
541
current tree, the new inventory is populated. inventory entries
542
which are candidates for modification have their revision set to
543
None; inventory entries that are carried over untouched have their
544
revision set to their prior value.
586
546
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
587
547
# results to create a new inventory at the same time, which results
588
548
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
589
549
# ADHB 11-07-2006
591
specific_files = self.specific_files
592
mutter("Selecting files for commit with filter %s", specific_files)
594
# Check and warn about old CommitBuilders
550
mutter("Selecting files for commit with filter %s", self.specific_files)
551
assert self.work_inv.root is not None
552
entries = self.work_inv.iter_entries()
595
553
if not self.builder.record_root_entry:
596
554
symbol_versioning.warn('CommitBuilders should support recording'
597
555
' the root entry as of bzr 0.10.', DeprecationWarning,
599
557
self.builder.new_inventory.add(self.basis_inv.root.copy())
601
# Build the new inventory
602
self._populate_from_inventory(specific_files)
604
# If specific files are selected, then all un-selected files must be
605
# recorded in their previous state. For more details, see
606
# https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
608
for path, new_ie in self.basis_inv.iter_entries():
609
if new_ie.file_id in self.builder.new_inventory:
611
if is_inside_any(specific_files, path):
615
self.builder.record_entry_contents(ie, self.parent_invs, path,
618
# Report what was deleted. We could skip this when no deletes are
619
# detected to gain a performance win, but it arguably serves as a
620
# 'safety check' by informing the user whenever anything disappears.
621
for path, ie in self.basis_inv.iter_entries():
622
if ie.file_id not in self.builder.new_inventory:
623
self.reporter.deleted(path)
625
def _populate_from_inventory(self, specific_files):
626
"""Populate the CommitBuilder by walking the working tree inventory."""
628
# raise an exception as soon as we find a single unknown.
629
for unknown in self.work_tree.unknowns():
630
raise StrictCommitFailed()
633
deleted_paths = set()
634
work_inv = self.work_tree.inventory
635
assert work_inv.root is not None
636
entries = work_inv.iter_entries()
637
if not self.builder.record_root_entry:
639
for path, existing_ie in entries:
640
file_id = existing_ie.file_id
641
name = existing_ie.name
642
parent_id = existing_ie.parent_id
643
kind = existing_ie.kind
644
if kind == 'directory':
645
self._next_progress_entry()
647
# Skip files that have been deleted from the working tree.
648
# The deleted files/directories are also recorded so they
649
# can be explicitly unversioned later. Note that when a
650
# filter of specific files is given, we must only skip/record
651
# deleted files matching that filter.
652
if is_inside_any(deleted_paths, path):
654
if not specific_files or is_inside_any(specific_files, path):
655
if not self.work_tree.has_filename(path):
656
deleted_paths.add(path)
657
self.reporter.missing(path)
658
deleted_ids.append(file_id)
661
kind = self.work_tree.kind(file_id)
662
# TODO: specific_files filtering before nested tree processing
663
if kind == 'tree-reference' and self.recursive == 'down':
664
self._commit_nested_tree(file_id, path)
665
except errors.NoSuchFile:
668
# Record an entry for this item
669
# Note: I don't particularly want to have the existing_ie
670
# parameter but the test suite currently (28-Jun-07) breaks
671
# without it thanks to a unicode normalisation issue. :-(
672
definitely_changed = kind != existing_ie.kind
673
self._record_entry(path, file_id, specific_files, kind, name,
674
parent_id, definitely_changed, existing_ie)
676
# Unversion IDs that were found to be deleted
677
self.work_tree.unversion(deleted_ids)
679
def _commit_nested_tree(self, file_id, path):
680
"Commit a nested tree."
681
sub_tree = self.work_tree.get_nested_tree(file_id, path)
682
# FIXME: be more comprehensive here:
683
# this works when both trees are in --trees repository,
684
# but when both are bound to a different repository,
685
# it fails; a better way of approaching this is to
686
# finally implement the explicit-caches approach design
687
# a while back - RBC 20070306.
688
if (sub_tree.branch.repository.bzrdir.root_transport.base
690
self.work_tree.branch.repository.bzrdir.root_transport.base):
691
sub_tree.branch.repository = \
692
self.work_tree.branch.repository
694
sub_tree.commit(message=None, revprops=self.revprops,
695
recursive=self.recursive,
696
message_callback=self.message_callback,
697
timestamp=self.timestamp, timezone=self.timezone,
698
committer=self.committer,
699
allow_pointless=self.allow_pointless,
700
strict=self.strict, verbose=self.verbose,
701
local=self.local, reporter=self.reporter)
702
except errors.PointlessCommit:
705
def _record_entry(self, path, file_id, specific_files, kind, name,
706
parent_id, definitely_changed, existing_ie=None):
707
"Record the new inventory entry for a path if any."
708
# mutter('check %s {%s}', path, file_id)
709
if (not specific_files or
710
is_inside_or_parent_of_any(specific_files, path)):
711
# mutter('%s selected for commit', path)
712
if definitely_changed or existing_ie is None:
713
ie = inventory.make_entry(kind, name, parent_id, file_id)
559
self._emit_progress_update()
560
for path, new_ie in entries:
561
self._emit_progress_update()
562
file_id = new_ie.file_id
563
# mutter('check %s {%s}', path, file_id)
564
if (not self.specific_files or
565
is_inside_or_parent_of_any(self.specific_files, path)):
566
# mutter('%s selected for commit', path)
570
# mutter('%s not selected for commit', path)
571
if self.basis_inv.has_id(file_id):
572
ie = self.basis_inv[file_id].copy()
715
ie = existing_ie.copy()
718
# mutter('%s not selected for commit', path)
719
if self.basis_inv.has_id(file_id):
720
ie = self.basis_inv[file_id].copy()
722
# this entry is new and not being committed
574
# this entry is new and not being committed
725
576
self.builder.record_entry_contents(ie, self.parent_invs,
726
577
path, self.work_tree)
727
self._report_change(ie, path)
730
def _report_change(self, ie, path):
731
"""Report a change to the user.
733
The change that has occurred is described relative to the basis
736
if (self.basis_inv.has_id(ie.file_id)):
737
basis_ie = self.basis_inv[ie.file_id]
740
change = ie.describe_change(basis_ie, ie)
741
if change in (InventoryEntry.RENAMED,
742
InventoryEntry.MODIFIED_AND_RENAMED):
743
old_path = self.basis_inv.id2path(ie.file_id)
744
self.reporter.renamed(change, old_path, path)
746
self.reporter.snapshot_change(change, path)
748
def _set_progress_stage(self, name, entries_title=None):
749
"""Set the progress stage and emit an update to the progress bar."""
750
self.pb_stage_name = name
751
self.pb_stage_count += 1
752
self.pb_entries_title = entries_title
753
if entries_title is not None:
754
self.pb_entries_count = 0
755
self.pb_entries_total = '?'
756
self._emit_progress()
758
def _next_progress_entry(self):
759
"""Emit an update to the progress bar and increment the entry count."""
760
self.pb_entries_count += 1
761
self._emit_progress()
763
def _emit_progress(self):
764
if self.pb_entries_title:
765
if self.pb_entries_total == '?':
766
text = "%s [%s %d] - Stage" % (self.pb_stage_name,
767
self.pb_entries_title, self.pb_entries_count)
769
text = "%s [%s %d/%s] - Stage" % (self.pb_stage_name,
770
self.pb_entries_title, self.pb_entries_count,
771
str(self.pb_entries_total))
773
text = "%s - Stage" % (self.pb_stage_name)
774
self.pb.update(text, self.pb_stage_count, self.pb_stage_total)
578
# describe the nature of the change that has occurred relative to
579
# the basis inventory.
580
if (self.basis_inv.has_id(ie.file_id)):
581
basis_ie = self.basis_inv[ie.file_id]
584
change = ie.describe_change(basis_ie, ie)
585
if change in (InventoryEntry.RENAMED,
586
InventoryEntry.MODIFIED_AND_RENAMED):
587
old_path = self.basis_inv.id2path(ie.file_id)
588
self.reporter.renamed(change, old_path, path)
590
self.reporter.snapshot_change(change, path)
592
if not self.specific_files:
595
# ignore removals that don't match filespec
596
for path, new_ie in self.basis_inv.iter_entries():
597
if new_ie.file_id in self.work_inv:
599
if is_inside_any(self.specific_files, path):
603
self.builder.record_entry_contents(ie, self.parent_invs, path,
606
def _emit_progress_update(self):
607
"""Emit an update to the progress bar."""
608
self.pb.update("Committing", self.pb_count, self.pb_total)
611
def _report_deletes(self):
612
for path, ie in self.basis_inv.iter_entries():
613
if ie.file_id not in self.builder.new_inventory:
614
self.reporter.deleted(path)