107
107
class ReportCommitToLog(NullCommitReporter):
109
# this may be more useful if 'note' was replaced by an overridable
110
# method on self, which would allow more trivial subclassing.
111
# alternative, a callable could be passed in, allowing really trivial
112
# reuse for some uis. RBC 20060511
109
def _note(self, format, *args):
112
Subclasses may choose to override this method.
114
116
def snapshot_change(self, change, path):
115
117
if change == 'unchanged':
117
119
if change == 'added' and path == '':
119
note("%s %s", change, path)
121
self._note("%s %s", change, path)
121
123
def completed(self, revno, rev_id):
122
note('Committed revision %d.', revno)
124
self._note('Committed revision %d.', revno)
124
126
def deleted(self, file_id):
125
note('deleted %s', file_id)
127
self._note('deleted %s', file_id)
127
129
def escaped(self, escape_count, message):
128
note("replaced %d control characters in message", escape_count)
130
self._note("replaced %d control characters in message", escape_count)
130
132
def missing(self, path):
131
note('missing %s', path)
133
self._note('missing %s', path)
133
135
def renamed(self, change, old_path, new_path):
134
note('%s %s => %s', change, old_path, new_path)
136
self._note('%s %s => %s', change, old_path, new_path)
137
139
class Commit(object):
177
176
recursive='down'):
178
177
"""Commit working copy as a new revision.
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.
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.
188
187
Useful for test or import commands that need to tightly
189
188
control what revisions are assigned. If you duplicate
190
189
a revision id that exists elsewhere it is your own fault.
191
190
If null (default), a time/random revision id is generated.
193
allow_pointless -- If true (default), commit even if nothing
192
:param allow_pointless: If true (default), commit even if nothing
194
193
has changed and no merges are recorded.
196
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
197
196
contains unknown files.
199
revprops -- Properties for new revision
198
:param revprops: Properties for new revision
200
199
:param local: Perform a local only commit.
201
200
:param recursive: If set to 'down', commit in any subtrees that have
202
201
pending changes of any sort during this commit.
249
246
self.basis_tree.lock_read()
251
248
# Cannot commit with conflicts present.
252
if len(self.work_tree.conflicts())>0:
249
if len(self.work_tree.conflicts()) > 0:
253
250
raise ConflictsInTree
255
# setup the bound branch variables as needed.
252
# Setup the bound branch variables as needed.
256
253
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.
255
# Check that the working tree is up to date
256
old_revno,new_revno = self._check_out_of_date_tree()
277
259
# raise an exception as soon as we find a single unknown.
278
260
for unknown in self.work_tree.unknowns():
281
263
if self.config is None:
282
264
self.config = self.branch.get_config()
284
self.work_inv = self.work_tree.inventory
285
self.basis_inv = self.basis_tree.inventory
266
# If provided, ensure the specified files are versioned
286
267
if specific_files is not None:
287
# Ensure specified files are versioned
288
# (We don't actually need the ids here)
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.
289
271
# XXX: Dont we have filter_unversioned to do this more
291
273
tree.find_ids_across_trees(specific_files,
292
274
[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
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
301
294
self._gather_parents()
302
295
if len(self.parents) > 1 and self.specific_files:
303
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
296
raise errors.CannotCommitSelectedFileMerge(self.specific_files)
298
# Collect the changes
299
self._emit_progress_set_stage("Collecting changes", show_entries=True)
306
300
self.builder = self.branch.get_commit_builder(self.parents,
307
301
self.config, timestamp, timezone, committer, revprops, rev_id)
309
self._remove_deleted()
310
self._populate_new_inv()
311
self._report_deletes()
302
self._update_builder_with_changes()
313
303
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.
305
# TODO: Now the new inventory is known, check for conflicts.
318
306
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
319
307
# weave lines, because nothing should be recorded until it is known
320
308
# that commit will succeed.
309
self._emit_progress_set_stage("Saving data locally")
321
310
self.builder.finish_inventory()
322
self._emit_progress_update()
312
# Prompt the user for a commit message if none provided
323
313
message = message_callback(self)
324
314
assert isinstance(message, unicode), type(message)
325
315
self.message = message
326
316
self._escape_commit_message()
318
# Add revision data to the local branch
328
319
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.
321
# Upload revision data to the master.
333
322
# this will propagate merged revisions too if needed.
334
323
if self.bound_branch:
324
self._emit_progress_set_stage("Uploading data to master branch")
335
325
self.master_branch.repository.fetch(self.branch.repository,
336
326
revision_id=self.rev_id)
337
327
# 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
328
# 'commit' to the master first so a timeout here causes the
329
# local branch to be out of date
340
330
self.master_branch.set_last_revision_info(new_revno,
343
333
# and now do the commit locally.
344
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")
346
338
rev_tree = self.builder.revision_tree()
347
339
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
348
# now the work tree is up to date with the branch
350
340
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()
341
self._process_hooks(old_revno, new_revno)
381
344
return self.rev_id
477
440
self.master_branch.lock_write()
478
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,
480
507
def _cleanup(self):
481
508
"""Cleanup any open locks, progress bars etc."""
482
509
cleanups = [self._cleanup_bound_branch,
546
573
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.
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.
584
587
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
585
588
# results to create a new inventory at the same time, which results
586
589
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
587
590
# 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()
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()
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())
597
self._emit_progress_update()
608
deleted_paths = set()
598
609
for path, new_ie in entries:
599
self._emit_progress_update()
610
self._emit_progress_next_entry()
600
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)
602
627
kind = self.work_tree.kind(file_id)
603
628
if kind == 'tree-reference' and self.recursive == 'down':
660
685
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):
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.
682
710
for path, ie in self.basis_inv.iter_entries():
683
711
if ie.file_id not in self.builder.new_inventory:
684
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)