249
247
self.basis_tree.lock_read()
251
249
# Cannot commit with conflicts present.
252
if len(self.work_tree.conflicts())>0:
250
if len(self.work_tree.conflicts()) > 0:
253
251
raise ConflictsInTree
255
# setup the bound branch variables as needed.
253
# Setup the bound branch variables as needed.
256
254
self._check_bound_branch()
258
# check for out of date working trees
260
first_tree_parent = self.work_tree.get_parent_ids()[0]
262
# if there are no parents, treat our parent as 'None'
263
# this is so that we still consier the master branch
264
# - in a checkout scenario the tree may have no
265
# parents but the branch may do.
266
first_tree_parent = bzrlib.revision.NULL_REVISION
267
old_revno, master_last = self.master_branch.last_revision_info()
268
if master_last != first_tree_parent:
269
if master_last != bzrlib.revision.NULL_REVISION:
270
raise errors.OutOfDateTree(self.work_tree)
271
if self.branch.repository.has_revision(first_tree_parent):
272
new_revno = old_revno + 1
274
# ghost parents never appear in revision history.
277
# raise an exception as soon as we find a single unknown.
278
for unknown in self.work_tree.unknowns():
279
raise StrictCommitFailed()
256
# Check that the working tree is up to date
257
old_revno,new_revno = self._check_out_of_date_tree()
281
259
if self.config is None:
282
260
self.config = self.branch.get_config()
284
self.work_inv = self.work_tree.inventory
285
self.basis_inv = self.basis_tree.inventory
262
# If provided, ensure the specified files are versioned
286
263
if specific_files is not None:
287
# Ensure specified files are versioned
288
# (We don't actually need the ids here)
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.
289
267
# XXX: Dont we have filter_unversioned to do this more
291
269
tree.find_ids_across_trees(specific_files,
292
270
[self.basis_tree, self.work_tree])
293
# one to finish, one for rev and inventory, and one for each
294
# inventory entry, and the same for the new inventory.
295
# note that this estimate is too long when we do a partial tree
296
# commit which excludes some new files from being considered.
297
# The estimate is corrected when we populate the new inv.
298
self.pb_total = len(self.work_inv) + 5
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
301
290
self._gather_parents()
302
291
if len(self.parents) > 1 and self.specific_files:
303
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
292
raise errors.CannotCommitSelectedFileMerge(self.specific_files)
294
# Collect the changes
295
self._set_progress_stage("Collecting changes",
296
entries_title="Directory")
306
297
self.builder = self.branch.get_commit_builder(self.parents,
307
298
self.config, timestamp, timezone, committer, revprops, rev_id)
309
self._remove_deleted()
310
self._populate_new_inv()
311
self._report_deletes()
313
self._check_pointless()
315
self._emit_progress_update()
316
# TODO: Now the new inventory is known, check for conflicts and
317
# prompt the user for a commit message.
318
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
319
# weave lines, because nothing should be recorded until it is known
320
# that commit will succeed.
321
self.builder.finish_inventory()
322
self._emit_progress_update()
323
message = message_callback(self)
324
assert isinstance(message, unicode), type(message)
325
self.message = message
326
self._escape_commit_message()
328
self.rev_id = self.builder.commit(self.message)
329
self._emit_progress_update()
330
# revision data is in the local branch now.
332
# upload revision data to the master.
300
self._update_builder_with_changes()
301
self._check_pointless()
303
# TODO: Now the new inventory is known, check for conflicts.
304
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
305
# weave lines, because nothing should be recorded until it is known
306
# that commit will succeed.
307
self._set_progress_stage("Saving data locally")
308
self.builder.finish_inventory()
310
# Prompt the user for a commit message if none provided
311
message = message_callback(self)
312
assert isinstance(message, unicode), type(message)
313
self.message = message
314
self._escape_commit_message()
316
# Add revision data to the local branch
317
self.rev_id = self.builder.commit(self.message)
319
# perhaps this should be done by the CommitBuilder ?
320
self.work_tree.branch.repository.abort_write_group()
323
# Upload revision data to the master.
333
324
# this will propagate merged revisions too if needed.
334
325
if self.bound_branch:
326
self._set_progress_stage("Uploading data to master branch")
335
327
self.master_branch.repository.fetch(self.branch.repository,
336
328
revision_id=self.rev_id)
337
329
# now the master has the revision data
338
# 'commit' to the master first so a timeout here causes the local
339
# branch to be out of date
330
# 'commit' to the master first so a timeout here causes the
331
# local branch to be out of date
340
332
self.master_branch.set_last_revision_info(new_revno,
343
335
# and now do the commit locally.
344
336
self.branch.set_last_revision_info(new_revno, self.rev_id)
338
# Make the working tree up to date with the branch
339
self._set_progress_stage("Updating the working tree")
346
340
rev_tree = self.builder.revision_tree()
347
341
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
348
# now the work tree is up to date with the branch
350
342
self.reporter.completed(new_revno, self.rev_id)
351
# old style commit hooks - should be deprecated ? (obsoleted in
353
if self.config.post_commit() is not None:
354
hooks = self.config.post_commit().split(' ')
355
# this would be nicer with twisted.python.reflect.namedAny
357
result = eval(hook + '(branch, rev_id)',
358
{'branch':self.branch,
360
'rev_id':self.rev_id})
361
# new style commit hooks:
362
if not self.bound_branch:
363
hook_master = self.branch
366
hook_master = self.master_branch
367
hook_local = self.branch
368
# With bound branches, when the master is behind the local branch,
369
# the 'old_revno' and old_revid values here are incorrect.
370
# XXX: FIXME ^. RBC 20060206
372
old_revid = self.parents[0]
374
old_revid = bzrlib.revision.NULL_REVISION
375
for hook in Branch.hooks['post_commit']:
376
hook(hook_local, hook_master, old_revno, old_revid, new_revno,
378
self._emit_progress_update()
343
self._process_hooks(old_revno, new_revno)
381
346
return self.rev_id
477
442
self.master_branch.lock_write()
478
443
self.master_locked = True
445
def _check_out_of_date_tree(self):
446
"""Check that the working tree is up to date.
448
:return: old_revision_number,new_revision_number tuple
451
first_tree_parent = self.work_tree.get_parent_ids()[0]
453
# if there are no parents, treat our parent as 'None'
454
# this is so that we still consider the master branch
455
# - in a checkout scenario the tree may have no
456
# parents but the branch may do.
457
first_tree_parent = bzrlib.revision.NULL_REVISION
458
old_revno, master_last = self.master_branch.last_revision_info()
459
if master_last != first_tree_parent:
460
if master_last != bzrlib.revision.NULL_REVISION:
461
raise errors.OutOfDateTree(self.work_tree)
462
if self.branch.repository.has_revision(first_tree_parent):
463
new_revno = old_revno + 1
465
# ghost parents never appear in revision history.
467
return old_revno,new_revno
469
def _process_hooks(self, old_revno, new_revno):
470
"""Process any registered commit hooks."""
471
# Process the post commit hooks, if any
472
self._set_progress_stage("Running post commit hooks")
473
# old style commit hooks - should be deprecated ? (obsoleted in
475
if self.config.post_commit() is not None:
476
hooks = self.config.post_commit().split(' ')
477
# this would be nicer with twisted.python.reflect.namedAny
479
result = eval(hook + '(branch, rev_id)',
480
{'branch':self.branch,
482
'rev_id':self.rev_id})
483
# new style commit hooks:
484
if not self.bound_branch:
485
hook_master = self.branch
488
hook_master = self.master_branch
489
hook_local = self.branch
490
# With bound branches, when the master is behind the local branch,
491
# the 'old_revno' and old_revid values here are incorrect.
492
# XXX: FIXME ^. RBC 20060206
494
old_revid = self.parents[0]
496
old_revid = bzrlib.revision.NULL_REVISION
497
for hook in Branch.hooks['post_commit']:
498
# show the running hook in the progress bar. As hooks may
499
# end up doing nothing (e.g. because they are not configured by
500
# the user) this is still showing progress, not showing overall
501
# actions - its up to each plugin to show a UI if it want's to
502
# (such as 'Emailing diff to foo@example.com').
503
self.pb_stage_name = "Running post commit hooks [%s]" % \
504
Branch.hooks.get_hook_name(hook)
505
self._emit_progress()
506
if 'hooks' in debug.debug_flags:
507
mutter("Invoking commit hook: %r", hook)
508
hook(hook_local, hook_master, old_revno, old_revid, new_revno,
480
511
def _cleanup(self):
481
512
"""Cleanup any open locks, progress bars etc."""
482
513
cleanups = [self._cleanup_bound_branch,
546
577
mutter('commit parent ghost revision {%s}', revision)
548
def _remove_deleted(self):
549
"""Remove deleted files from the working inventories.
551
This is done prior to taking the working inventory as the
552
basis for the new committed inventory.
554
This returns true if any files
555
*that existed in the basis inventory* were deleted.
556
Files that were added and deleted
557
in the working copy don't matter.
559
specific = self.specific_files
561
deleted_paths = set()
562
for path, ie in self.work_inv.iter_entries():
563
if is_inside_any(deleted_paths, path):
564
# The tree will delete the required ids recursively.
566
if specific and not is_inside_any(specific, path):
568
if not self.work_tree.has_filename(path):
569
deleted_paths.add(path)
570
self.reporter.missing(path)
571
deleted_ids.append(ie.file_id)
572
self.work_tree.unversion(deleted_ids)
574
def _populate_new_inv(self):
575
"""Build revision inventory.
577
This creates a new empty inventory. Depending on
578
which files are selected for commit, and what is present in the
579
current tree, the new inventory is populated. inventory entries
580
which are candidates for modification have their revision set to
581
None; inventory entries that are carried over untouched have their
582
revision set to their prior value.
579
def _update_builder_with_changes(self):
580
"""Update the commit builder with the data about what has changed.
582
# Build the revision inventory.
584
# This starts by creating a new empty inventory. Depending on
585
# which files are selected for commit, and what is present in the
586
# current tree, the new inventory is populated. inventory entries
587
# which are candidates for modification have their revision set to
588
# None; inventory entries that are carried over untouched have their
589
# revision set to their prior value.
584
591
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
585
592
# results to create a new inventory at the same time, which results
586
593
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
587
594
# ADHB 11-07-2006
588
mutter("Selecting files for commit with filter %s", self.specific_files)
589
assert self.work_inv.root is not None
590
entries = self.work_inv.iter_entries()
596
specific_files = self.specific_files
597
mutter("Selecting files for commit with filter %s", specific_files)
599
# Check and warn about old CommitBuilders
591
600
if not self.builder.record_root_entry:
592
601
symbol_versioning.warn('CommitBuilders should support recording'
593
602
' the root entry as of bzr 0.10.', DeprecationWarning,
595
604
self.builder.new_inventory.add(self.basis_inv.root.copy())
606
# Build the new inventory
607
self._populate_from_inventory(specific_files)
609
# If specific files are selected, then all un-selected files must be
610
# recorded in their previous state. For more details, see
611
# https://lists.ubuntu.com/archives/bazaar/2007q3/028476.html.
613
for path, new_ie in self.basis_inv.iter_entries():
614
if new_ie.file_id in self.builder.new_inventory:
616
if is_inside_any(specific_files, path):
620
self.builder.record_entry_contents(ie, self.parent_invs, path,
623
# Report what was deleted. We could skip this when no deletes are
624
# detected to gain a performance win, but it arguably serves as a
625
# 'safety check' by informing the user whenever anything disappears.
626
for path, ie in self.basis_inv.iter_entries():
627
if ie.file_id not in self.builder.new_inventory:
628
self.reporter.deleted(path)
630
def _populate_from_inventory(self, specific_files):
631
"""Populate the CommitBuilder by walking the working tree inventory."""
633
# raise an exception as soon as we find a single unknown.
634
for unknown in self.work_tree.unknowns():
635
raise StrictCommitFailed()
638
deleted_paths = set()
639
work_inv = self.work_tree.inventory
640
assert work_inv.root is not None
641
entries = work_inv.iter_entries()
642
if not self.builder.record_root_entry:
597
self._emit_progress_update()
598
for path, new_ie in entries:
599
self._emit_progress_update()
600
file_id = new_ie.file_id
644
for path, existing_ie in entries:
645
file_id = existing_ie.file_id
646
name = existing_ie.name
647
parent_id = existing_ie.parent_id
648
kind = existing_ie.kind
649
if kind == 'directory':
650
self._next_progress_entry()
652
# Skip files that have been deleted from the working tree.
653
# The deleted files/directories are also recorded so they
654
# can be explicitly unversioned later. Note that when a
655
# filter of specific files is given, we must only skip/record
656
# deleted files matching that filter.
657
if is_inside_any(deleted_paths, path):
659
if not specific_files or is_inside_any(specific_files, path):
660
if not self.work_tree.has_filename(path):
661
deleted_paths.add(path)
662
self.reporter.missing(path)
663
deleted_ids.append(file_id)
602
666
kind = self.work_tree.kind(file_id)
667
# TODO: specific_files filtering before nested tree processing
603
668
if kind == 'tree-reference' and self.recursive == 'down':
604
# nested tree: commit in it
605
sub_tree = WorkingTree.open(self.work_tree.abspath(path))
606
# FIXME: be more comprehensive here:
607
# this works when both trees are in --trees repository,
608
# but when both are bound to a different repository,
609
# it fails; a better way of approaching this is to
610
# finally implement the explicit-caches approach design
611
# a while back - RBC 20070306.
612
if (sub_tree.branch.repository.bzrdir.root_transport.base
614
self.work_tree.branch.repository.bzrdir.root_transport.base):
615
sub_tree.branch.repository = \
616
self.work_tree.branch.repository
618
sub_tree.commit(message=None, revprops=self.revprops,
619
recursive=self.recursive,
620
message_callback=self.message_callback,
621
timestamp=self.timestamp, timezone=self.timezone,
622
committer=self.committer,
623
allow_pointless=self.allow_pointless,
624
strict=self.strict, verbose=self.verbose,
625
local=self.local, reporter=self.reporter)
626
except errors.PointlessCommit:
628
if kind != new_ie.kind:
629
new_ie = inventory.make_entry(kind, new_ie.name,
630
new_ie.parent_id, file_id)
669
self._commit_nested_tree(file_id, path)
631
670
except errors.NoSuchFile:
633
# mutter('check %s {%s}', path, file_id)
634
if (not self.specific_files or
635
is_inside_or_parent_of_any(self.specific_files, path)):
636
# mutter('%s selected for commit', path)
673
# Record an entry for this item
674
# Note: I don't particularly want to have the existing_ie
675
# parameter but the test suite currently (28-Jun-07) breaks
676
# without it thanks to a unicode normalisation issue. :-(
677
definitely_changed = kind != existing_ie.kind
678
self._record_entry(path, file_id, specific_files, kind, name,
679
parent_id, definitely_changed, existing_ie)
681
# Unversion IDs that were found to be deleted
682
self.work_tree.unversion(deleted_ids)
684
def _commit_nested_tree(self, file_id, path):
685
"Commit a nested tree."
686
sub_tree = self.work_tree.get_nested_tree(file_id, path)
687
# FIXME: be more comprehensive here:
688
# this works when both trees are in --trees repository,
689
# but when both are bound to a different repository,
690
# it fails; a better way of approaching this is to
691
# finally implement the explicit-caches approach design
692
# a while back - RBC 20070306.
693
if (sub_tree.branch.repository.bzrdir.root_transport.base
695
self.work_tree.branch.repository.bzrdir.root_transport.base):
696
sub_tree.branch.repository = \
697
self.work_tree.branch.repository
699
sub_tree.commit(message=None, revprops=self.revprops,
700
recursive=self.recursive,
701
message_callback=self.message_callback,
702
timestamp=self.timestamp, timezone=self.timezone,
703
committer=self.committer,
704
allow_pointless=self.allow_pointless,
705
strict=self.strict, verbose=self.verbose,
706
local=self.local, reporter=self.reporter)
707
except errors.PointlessCommit:
710
def _record_entry(self, path, file_id, specific_files, kind, name,
711
parent_id, definitely_changed, existing_ie=None):
712
"Record the new inventory entry for a path if any."
713
# mutter('check %s {%s}', path, file_id)
714
if (not specific_files or
715
is_inside_or_parent_of_any(specific_files, path)):
716
# mutter('%s selected for commit', path)
717
if definitely_changed or existing_ie is None:
718
ie = inventory.make_entry(kind, name, parent_id, file_id)
720
ie = existing_ie.copy()
638
721
ie.revision = None
723
# mutter('%s not selected for commit', path)
724
if self.basis_inv.has_id(file_id):
725
ie = self.basis_inv[file_id].copy()
640
# mutter('%s not selected for commit', path)
641
if self.basis_inv.has_id(file_id):
642
ie = self.basis_inv[file_id].copy()
644
# this entry is new and not being committed
727
# this entry is new and not being committed
646
730
self.builder.record_entry_contents(ie, self.parent_invs,
647
731
path, self.work_tree)
648
# describe the nature of the change that has occurred relative to
649
# the basis inventory.
650
if (self.basis_inv.has_id(ie.file_id)):
651
basis_ie = self.basis_inv[ie.file_id]
654
change = ie.describe_change(basis_ie, ie)
655
if change in (InventoryEntry.RENAMED,
656
InventoryEntry.MODIFIED_AND_RENAMED):
657
old_path = self.basis_inv.id2path(ie.file_id)
658
self.reporter.renamed(change, old_path, path)
660
self.reporter.snapshot_change(change, path)
662
if not self.specific_files:
665
# ignore removals that don't match filespec
666
for path, new_ie in self.basis_inv.iter_entries():
667
if new_ie.file_id in self.work_inv:
669
if is_inside_any(self.specific_files, path):
673
self.builder.record_entry_contents(ie, self.parent_invs, path,
676
def _emit_progress_update(self):
677
"""Emit an update to the progress bar."""
678
self.pb.update("Committing", self.pb_count, self.pb_total)
681
def _report_deletes(self):
682
for path, ie in self.basis_inv.iter_entries():
683
if ie.file_id not in self.builder.new_inventory:
684
self.reporter.deleted(path)
732
self._report_change(ie, path)
735
def _report_change(self, ie, path):
736
"""Report a change to the user.
738
The change that has occurred is described relative to the basis
741
if (self.basis_inv.has_id(ie.file_id)):
742
basis_ie = self.basis_inv[ie.file_id]
745
change = ie.describe_change(basis_ie, ie)
746
if change in (InventoryEntry.RENAMED,
747
InventoryEntry.MODIFIED_AND_RENAMED):
748
old_path = self.basis_inv.id2path(ie.file_id)
749
self.reporter.renamed(change, old_path, path)
751
self.reporter.snapshot_change(change, path)
753
def _set_progress_stage(self, name, entries_title=None):
754
"""Set the progress stage and emit an update to the progress bar."""
755
self.pb_stage_name = name
756
self.pb_stage_count += 1
757
self.pb_entries_title = entries_title
758
if entries_title is not None:
759
self.pb_entries_count = 0
760
self.pb_entries_total = '?'
761
self._emit_progress()
763
def _next_progress_entry(self):
764
"""Emit an update to the progress bar and increment the entry count."""
765
self.pb_entries_count += 1
766
self._emit_progress()
768
def _emit_progress(self):
769
if self.pb_entries_title:
770
if self.pb_entries_total == '?':
771
text = "%s [%s %d] - Stage" % (self.pb_stage_name,
772
self.pb_entries_title, self.pb_entries_count)
774
text = "%s [%s %d/%s] - Stage" % (self.pb_stage_name,
775
self.pb_entries_title, self.pb_entries_count,
776
str(self.pb_entries_total))
778
text = "%s - Stage" % (self.pb_stage_name)
779
self.pb.update(text, self.pb_stage_count, self.pb_stage_total)