119
107
class ReportCommitToLog(NullCommitReporter):
121
# this may be more useful if 'note' was replaced by an overridable
122
# method on self, which would allow more trivial subclassing.
123
# alternative, a callable could be passed in, allowing really trivial
124
# reuse for some uis. RBC 20060511
109
def _note(self, format, *args):
112
Subclasses may choose to override this method.
126
116
def snapshot_change(self, change, path):
127
117
if change == 'unchanged':
129
note("%s %s", change, path)
119
if change == 'added' and path == '':
121
self._note("%s %s", change, path)
131
123
def completed(self, revno, rev_id):
132
note('Committed revision %d.', revno)
124
self._note('Committed revision %d.', revno)
134
126
def deleted(self, file_id):
135
note('deleted %s', file_id)
127
self._note('deleted %s', file_id)
137
129
def escaped(self, escape_count, message):
138
note("replaced %d control characters in message", escape_count)
130
self._note("replaced %d control characters in message", escape_count)
140
132
def missing(self, path):
141
note('missing %s', path)
133
self._note('missing %s', path)
143
135
def renamed(self, change, old_path, new_path):
144
note('%s %s => %s', change, old_path, new_path)
136
self._note('%s %s => %s', change, old_path, new_path)
147
139
class Commit(object):
182
171
working_tree=None,
175
message_callback=None,
186
177
"""Commit working copy as a new revision.
188
branch -- the deprecated branch to commit to. New callers should pass in
191
message -- the commit message, a mandatory parameter
193
timestamp -- if not None, seconds-since-epoch for a
194
postdated/predated commit.
196
specific_files -- If true, commit only those files.
198
rev_id -- If set, use this as the new revision id.
179
:param message: the commit message (it or message_callback is required)
181
:param timestamp: if not None, seconds-since-epoch for a
182
postdated/predated commit.
184
:param specific_files: If true, commit only those files.
186
:param rev_id: If set, use this as the new revision id.
199
187
Useful for test or import commands that need to tightly
200
188
control what revisions are assigned. If you duplicate
201
189
a revision id that exists elsewhere it is your own fault.
202
190
If null (default), a time/random revision id is generated.
204
allow_pointless -- If true (default), commit even if nothing
192
:param allow_pointless: If true (default), commit even if nothing
205
193
has changed and no merges are recorded.
207
strict -- If true, don't allow a commit if the working tree
195
:param strict: If true, don't allow a commit if the working tree
208
196
contains unknown files.
210
revprops -- Properties for new revision
198
:param revprops: Properties for new revision
211
199
:param local: Perform a local only commit.
200
:param recursive: If set to 'down', commit in any subtrees that have
201
pending changes of any sort during this commit.
213
203
mutter('preparing to commit')
215
if deprecated_passed(branch):
216
symbol_versioning.warn("Commit.commit (branch, ...): The branch parameter is "
217
"deprecated as of bzr 0.8. Please use working_tree= instead.",
218
DeprecationWarning, stacklevel=2)
220
self.work_tree = self.branch.bzrdir.open_workingtree()
221
elif working_tree is None:
222
raise BzrError("One of branch and working_tree must be passed into commit().")
205
if working_tree is None:
206
raise BzrError("working_tree must be passed into commit().")
224
208
self.work_tree = working_tree
225
209
self.branch = self.work_tree.branch
227
raise BzrError("The message keyword parameter is required for commit().")
210
if getattr(self.work_tree, 'requires_rich_root', lambda: False)():
211
if not self.branch.repository.supports_rich_root():
212
raise errors.RootNotRich()
213
if message_callback is None:
214
if message is not None:
215
if isinstance(message, str):
216
message = message.decode(bzrlib.user_encoding)
217
message_callback = lambda x: message
219
raise BzrError("The message or message_callback keyword"
220
" parameter is required for commit().")
229
222
self.bound_branch = None
230
223
self.local = local
242
243
self.work_tree.lock_write()
243
244
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
245
self.basis_tree = self.work_tree.basis_tree()
246
self.basis_tree.lock_read()
245
248
# Cannot commit with conflicts present.
246
if len(self.work_tree.conflicts())>0:
249
if len(self.work_tree.conflicts()) > 0:
247
250
raise ConflictsInTree
249
# setup the bound branch variables as needed.
252
# Setup the bound branch variables as needed.
250
253
self._check_bound_branch()
252
# check for out of date working trees
254
first_tree_parent = self.work_tree.get_parent_ids()[0]
256
# if there are no parents, treat our parent as 'None'
257
# this is so that we still consier the master branch
258
# - in a checkout scenario the tree may have no
259
# parents but the branch may do.
260
first_tree_parent = None
261
master_last = self.master_branch.last_revision()
262
if (master_last is not None and
263
master_last != first_tree_parent):
264
raise errors.OutOfDateTree(self.work_tree)
255
# Check that the working tree is up to date
256
old_revno,new_revno = self._check_out_of_date_tree()
267
259
# raise an exception as soon as we find a single unknown.
268
260
for unknown in self.work_tree.unknowns():
271
263
if self.config is None:
272
264
self.config = self.branch.get_config()
274
if isinstance(message, str):
275
message = message.decode(bzrlib.user_encoding)
276
assert isinstance(message, unicode), type(message)
277
self.message = message
278
self._escape_commit_message()
280
self.work_inv = self.work_tree.inventory
281
self.basis_tree = self.work_tree.basis_tree()
282
self.basis_inv = self.basis_tree.inventory
266
# If provided, ensure the specified files are versioned
283
267
if specific_files is not None:
284
# Ensure specified files are versioned
285
# (We don't actually need the ids here)
286
tree.find_ids_across_trees(specific_files,
268
# Note: We don't actually need the IDs here. This routine
269
# is being called because it raises PathNotVerisonedError
270
# as a side effect of finding the IDs.
271
# XXX: Dont we have filter_unversioned to do this more
273
tree.find_ids_across_trees(specific_files,
287
274
[self.basis_tree, self.work_tree])
288
# one to finish, one for rev and inventory, and one for each
289
# inventory entry, and the same for the new inventory.
290
# note that this estimate is too long when we do a partial tree
291
# commit which excludes some new files from being considered.
292
# The estimate is corrected when we populate the new inv.
293
self.pb_total = len(self.work_inv) + 5
276
# Setup the progress bar. As the number of files that need to be
277
# committed in unknown, progress is reported as stages.
278
# We keep track of entries separately though and include that
279
# information in the progress bar during the relevant stages.
280
self.pb_stage_name = ""
281
self.pb_stage_count = 0
282
self.pb_stage_total = 4
283
if self.bound_branch:
284
self.pb_stage_total += 1
285
self.pb.show_pct = False
286
self.pb.show_spinner = False
287
self.pb.show_eta = False
288
self.pb.show_count = True
289
self.pb.show_bar = False
291
# After a merge, a selected file commit is not supported.
292
# See 'bzr help merge' for an explanation as to why.
293
self.basis_inv = self.basis_tree.inventory
296
294
self._gather_parents()
297
295
if len(self.parents) > 1 and self.specific_files:
298
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
296
raise errors.CannotCommitSelectedFileMerge(self.specific_files)
301
self.builder = self.branch.get_commit_builder(self.parents,
298
# Collect the changes
299
self._emit_progress_set_stage("Collecting changes", show_entries=True)
300
self.builder = self.branch.get_commit_builder(self.parents,
302
301
self.config, timestamp, timezone, committer, revprops, rev_id)
304
self._remove_deleted()
305
self._populate_new_inv()
306
self._report_deletes()
302
self._update_builder_with_changes()
308
303
self._check_pointless()
310
self._emit_progress_update()
311
# TODO: Now the new inventory is known, check for conflicts and
312
# prompt the user for a commit message.
305
# TODO: Now the new inventory is known, check for conflicts.
313
306
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
314
307
# weave lines, because nothing should be recorded until it is known
315
308
# that commit will succeed.
309
self._emit_progress_set_stage("Saving data locally")
316
310
self.builder.finish_inventory()
317
self._emit_progress_update()
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
318
319
self.rev_id = self.builder.commit(self.message)
319
self._emit_progress_update()
320
# revision data is in the local branch now.
322
# upload revision data to the master.
321
# Upload revision data to the master.
323
322
# this will propagate merged revisions too if needed.
324
323
if self.bound_branch:
324
self._emit_progress_set_stage("Uploading data to master branch")
325
325
self.master_branch.repository.fetch(self.branch.repository,
326
326
revision_id=self.rev_id)
327
327
# now the master has the revision data
328
# 'commit' to the master first so a timeout here causes the local
329
# branch to be out of date
330
self.master_branch.append_revision(self.rev_id)
328
# 'commit' to the master first so a timeout here causes the
329
# local branch to be out of date
330
self.master_branch.set_last_revision_info(new_revno,
332
333
# and now do the commit locally.
333
self.branch.append_revision(self.rev_id)
334
self.branch.set_last_revision_info(new_revno, self.rev_id)
336
# Make the working tree up to date with the branch
337
self._emit_progress_set_stage("Updating the working tree")
335
338
rev_tree = self.builder.revision_tree()
336
339
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
337
# now the work tree is up to date with the branch
339
self.reporter.completed(self.branch.revno(), self.rev_id)
340
if self.config.post_commit() is not None:
341
hooks = self.config.post_commit().split(' ')
342
# this would be nicer with twisted.python.reflect.namedAny
344
result = eval(hook + '(branch, rev_id)',
345
{'branch':self.branch,
347
'rev_id':self.rev_id})
348
self._emit_progress_update()
340
self.reporter.completed(new_revno, self.rev_id)
341
self._process_hooks(old_revno, new_revno)
351
344
return self.rev_id
442
440
self.master_branch.lock_write()
443
441
self.master_locked = True
443
def _check_out_of_date_tree(self):
444
"""Check that the working tree is up to date.
446
:return: old_revision_number,new_revision_number tuple
449
first_tree_parent = self.work_tree.get_parent_ids()[0]
451
# if there are no parents, treat our parent as 'None'
452
# this is so that we still consider the master branch
453
# - in a checkout scenario the tree may have no
454
# parents but the branch may do.
455
first_tree_parent = bzrlib.revision.NULL_REVISION
456
old_revno, master_last = self.master_branch.last_revision_info()
457
if master_last != first_tree_parent:
458
if master_last != bzrlib.revision.NULL_REVISION:
459
raise errors.OutOfDateTree(self.work_tree)
460
if self.branch.repository.has_revision(first_tree_parent):
461
new_revno = old_revno + 1
463
# ghost parents never appear in revision history.
465
return old_revno,new_revno
467
def _process_hooks(self, old_revno, new_revno):
468
"""Process any registered commit hooks."""
469
# Process the post commit hooks, if any
470
self._emit_progress_set_stage("Running post commit hooks")
471
# old style commit hooks - should be deprecated ? (obsoleted in
473
if self.config.post_commit() is not None:
474
hooks = self.config.post_commit().split(' ')
475
# this would be nicer with twisted.python.reflect.namedAny
477
result = eval(hook + '(branch, rev_id)',
478
{'branch':self.branch,
480
'rev_id':self.rev_id})
481
# new style commit hooks:
482
if not self.bound_branch:
483
hook_master = self.branch
486
hook_master = self.master_branch
487
hook_local = self.branch
488
# With bound branches, when the master is behind the local branch,
489
# the 'old_revno' and old_revid values here are incorrect.
490
# XXX: FIXME ^. RBC 20060206
492
old_revid = self.parents[0]
494
old_revid = bzrlib.revision.NULL_REVISION
495
for hook in Branch.hooks['post_commit']:
496
# show the running hook in the progress bar. As hooks may
497
# end up doing nothing (e.g. because they are not configured by
498
# the user) this is still showing progress, not showing overall
499
# actions - its up to each plugin to show a UI if it want's to
500
# (such as 'Emailing diff to foo@example.com').
501
self.pb_stage_name = "Running post commit hooks [%s]" % \
502
Branch.hooks.get_hook_name(hook)
503
self._emit_progress()
504
hook(hook_local, hook_master, old_revno, old_revid, new_revno,
445
507
def _cleanup(self):
446
508
"""Cleanup any open locks, progress bars etc."""
447
509
cleanups = [self._cleanup_bound_branch,
510
self.basis_tree.unlock,
448
511
self.work_tree.unlock,
449
512
self.pb.finished]
450
513
found_exception = None
510
573
mutter('commit parent ghost revision {%s}', revision)
512
def _remove_deleted(self):
513
"""Remove deleted files from the working inventories.
515
This is done prior to taking the working inventory as the
516
basis for the new committed inventory.
518
This returns true if any files
519
*that existed in the basis inventory* were deleted.
520
Files that were added and deleted
521
in the working copy don't matter.
523
specific = self.specific_files
525
deleted_paths = set()
526
for path, ie in self.work_inv.iter_entries():
527
if is_inside_any(deleted_paths, path):
528
# The tree will delete the required ids recursively.
530
if specific and not is_inside_any(specific, path):
532
if not self.work_tree.has_filename(path):
533
deleted_paths.add(path)
534
self.reporter.missing(path)
535
deleted_ids.append(ie.file_id)
536
self.work_tree.unversion(deleted_ids)
538
def _populate_new_inv(self):
539
"""Build revision inventory.
541
This creates a new empty inventory. Depending on
542
which files are selected for commit, and what is present in the
543
current tree, the new inventory is populated. inventory entries
544
which are candidates for modification have their revision set to
545
None; inventory entries that are carried over untouched have their
546
revision set to their prior value.
575
def _update_builder_with_changes(self):
576
"""Update the commit builder with the data about what has changed.
578
# Build the revision inventory.
580
# This starts by creating a new empty inventory. Depending on
581
# which files are selected for commit, and what is present in the
582
# current tree, the new inventory is populated. inventory entries
583
# which are candidates for modification have their revision set to
584
# None; inventory entries that are carried over untouched have their
585
# revision set to their prior value.
548
587
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
549
588
# results to create a new inventory at the same time, which results
550
589
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
551
590
# ADHB 11-07-2006
552
mutter("Selecting files for commit with filter %s", self.specific_files)
553
entries = self.work_inv.iter_entries()
592
specific_files = self.specific_files
593
mutter("Selecting files for commit with filter %s", specific_files)
594
work_inv = self.work_tree.inventory
595
assert work_inv.root is not None
596
self.pb_entries_total = len(work_inv)
598
# Check and warn about old CommitBuilders
599
entries = work_inv.iter_entries()
554
600
if not self.builder.record_root_entry:
555
601
symbol_versioning.warn('CommitBuilders should support recording'
556
602
' the root entry as of bzr 0.10.', DeprecationWarning,
558
604
self.builder.new_inventory.add(self.basis_inv.root.copy())
560
self._emit_progress_update()
608
deleted_paths = set()
561
609
for path, new_ie in entries:
562
self._emit_progress_update()
610
self._emit_progress_next_entry()
563
611
file_id = new_ie.file_id
613
# Skip files that have been deleted from the working tree.
614
# The deleted files/directories are also recorded so they
615
# can be explicitly unversioned later. Note that when a
616
# filter of specific files is given, we must only skip/record
617
# deleted files matching that filter.
618
if is_inside_any(deleted_paths, path):
620
if not specific_files or is_inside_any(specific_files, path):
621
if not self.work_tree.has_filename(path):
622
deleted_paths.add(path)
623
self.reporter.missing(path)
624
deleted_ids.append(file_id)
627
kind = self.work_tree.kind(file_id)
628
if kind == 'tree-reference' and self.recursive == 'down':
629
# nested tree: commit in it
630
sub_tree = WorkingTree.open(self.work_tree.abspath(path))
631
# FIXME: be more comprehensive here:
632
# this works when both trees are in --trees repository,
633
# but when both are bound to a different repository,
634
# it fails; a better way of approaching this is to
635
# finally implement the explicit-caches approach design
636
# a while back - RBC 20070306.
637
if (sub_tree.branch.repository.bzrdir.root_transport.base
639
self.work_tree.branch.repository.bzrdir.root_transport.base):
640
sub_tree.branch.repository = \
641
self.work_tree.branch.repository
643
sub_tree.commit(message=None, revprops=self.revprops,
644
recursive=self.recursive,
645
message_callback=self.message_callback,
646
timestamp=self.timestamp, timezone=self.timezone,
647
committer=self.committer,
648
allow_pointless=self.allow_pointless,
649
strict=self.strict, verbose=self.verbose,
650
local=self.local, reporter=self.reporter)
651
except errors.PointlessCommit:
653
if kind != new_ie.kind:
654
new_ie = inventory.make_entry(kind, new_ie.name,
655
new_ie.parent_id, file_id)
656
except errors.NoSuchFile:
564
658
# mutter('check %s {%s}', path, file_id)
565
if (not self.specific_files or
566
is_inside_or_parent_of_any(self.specific_files, path)):
659
if (not specific_files or
660
is_inside_or_parent_of_any(specific_files, path)):
567
661
# mutter('%s selected for commit', path)
568
662
ie = new_ie.copy()
569
663
ie.revision = None
592
685
self.reporter.snapshot_change(change, path)
594
if not self.specific_files:
597
# ignore removals that don't match filespec
598
for path, new_ie in self.basis_inv.iter_entries():
599
if new_ie.file_id in self.work_inv:
601
if is_inside_any(self.specific_files, path):
605
self.builder.record_entry_contents(ie, self.parent_invs, path,
608
def _emit_progress_update(self):
609
"""Emit an update to the progress bar."""
610
self.pb.update("Committing", self.pb_count, self.pb_total)
613
def _report_deletes(self):
687
# Unversion IDs that were found to be deleted
688
self.work_tree.unversion(deleted_ids)
690
# If specific files/directories were nominated, it is possible
691
# that some data from outside those needs to be preserved from
692
# the basis tree. For example, if a file x is moved from out of
693
# directory foo into directory bar and the user requests
694
# ``commit foo``, then information about bar/x must also be
697
for path, new_ie in self.basis_inv.iter_entries():
698
if new_ie.file_id in work_inv:
700
if is_inside_any(specific_files, path):
704
self.builder.record_entry_contents(ie, self.parent_invs, path,
707
# Report what was deleted. We could skip this when no deletes are
708
# detected to gain a performance win, but it arguably serves as a
709
# 'safety check' by informing the user whenever anything disappears.
614
710
for path, ie in self.basis_inv.iter_entries():
615
711
if ie.file_id not in self.builder.new_inventory:
616
712
self.reporter.deleted(path)
714
def _emit_progress_set_stage(self, name, show_entries=False):
715
"""Set the progress stage and emit an update to the progress bar."""
716
self.pb_stage_name = name
717
self.pb_stage_count += 1
718
self.pb_entries_show = show_entries
720
self.pb_entries_count = 0
721
self.pb_entries_total = '?'
722
self._emit_progress()
724
def _emit_progress_next_entry(self):
725
"""Emit an update to the progress bar and increment the file count."""
726
self.pb_entries_count += 1
727
self._emit_progress()
729
def _emit_progress(self):
730
if self.pb_entries_show:
731
text = "%s [Entry %d/%s] - Stage" % (self.pb_stage_name,
732
self.pb_entries_count,str(self.pb_entries_total))
734
text = "%s - Stage" % (self.pb_stage_name)
735
self.pb.update(text, self.pb_stage_count, self.pb_stage_total)