173
177
allow_pointless=True,
181
message_callback=None,
183
181
"""Commit working copy as a new revision.
185
:param message: the commit message (it or message_callback is required)
187
:param timestamp: if not None, seconds-since-epoch for a
188
postdated/predated commit.
190
:param specific_files: If true, commit only those files.
192
:param rev_id: If set, use this as the new revision id.
183
timestamp -- if not None, seconds-since-epoch for a
184
postdated/predated commit.
186
specific_files -- If true, commit only those files.
188
rev_id -- If set, use this as the new revision id.
193
189
Useful for test or import commands that need to tightly
194
190
control what revisions are assigned. If you duplicate
195
191
a revision id that exists elsewhere it is your own fault.
196
192
If null (default), a time/random revision id is generated.
198
:param allow_pointless: If true (default), commit even if nothing
194
allow_pointless -- If true (default), commit even if nothing
199
195
has changed and no merges are recorded.
201
:param strict: If true, don't allow a commit if the working tree
197
strict -- If true, don't allow a commit if the working tree
202
198
contains unknown files.
204
:param revprops: Properties for new revision
205
:param local: Perform a local only commit.
206
:param recursive: If set to 'down', commit in any subtrees that have
207
pending changes of any sort during this commit.
200
revprops -- Properties for new revision
209
202
mutter('preparing to commit')
211
if working_tree is None:
212
raise BzrError("working_tree must be passed into commit().")
214
self.work_tree = working_tree
215
self.branch = self.work_tree.branch
216
if getattr(self.work_tree, 'requires_rich_root', lambda: False)():
217
if not self.branch.repository.supports_rich_root():
218
raise errors.RootNotRich()
219
if message_callback is None:
220
if message is not None:
221
if isinstance(message, str):
222
message = message.decode(bzrlib.user_encoding)
223
message_callback = lambda x: message
225
raise BzrError("The message or message_callback keyword"
226
" parameter is required for commit().")
228
self.bound_branch = None
230
self.master_branch = None
231
self.master_locked = False
205
self.weave_store = branch.weave_store
233
207
self.specific_files = specific_files
234
208
self.allow_pointless = allow_pointless
235
self.recursive = recursive
236
self.revprops = revprops
237
self.message_callback = message_callback
238
self.timestamp = timestamp
239
self.timezone = timezone
240
self.committer = committer
242
self.verbose = verbose
244
if reporter is None and self.reporter is None:
245
self.reporter = NullCommitReporter()
246
elif reporter is not None:
247
self.reporter = reporter
249
self.work_tree.lock_write()
250
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
251
self.basis_tree = self.work_tree.basis_tree()
252
self.basis_tree.lock_read()
209
self.revprops = {'branch-nick': branch.nick}
211
self.revprops.update(revprops)
212
self.work_tree = WorkingTree(branch.base, branch)
215
# raise an exception as soon as we find a single unknown.
216
for unknown in self.work_tree.unknowns():
217
raise StrictCommitFailed()
219
if timestamp is None:
220
self.timestamp = time.time()
222
self.timestamp = long(timestamp)
224
if self.config is None:
225
self.config = bzrlib.config.BranchConfig(self.branch)
228
self.rev_id = _gen_revision_id(self.config, self.timestamp)
232
if committer is None:
233
self.committer = self.config.username()
235
assert isinstance(committer, basestring), type(committer)
236
self.committer = committer
239
self.timezone = local_time_offset()
241
self.timezone = int(timezone)
243
if isinstance(message, str):
244
message = message.decode(bzrlib.user_encoding)
245
assert isinstance(message, unicode), type(message)
246
self.message = message
247
self._escape_commit_message()
249
self.branch.lock_write()
254
# Cannot commit with conflicts present.
255
if len(self.work_tree.conflicts()) > 0:
256
raise ConflictsInTree
258
# Setup the bound branch variables as needed.
259
self._check_bound_branch()
261
# Check that the working tree is up to date
262
old_revno,new_revno = self._check_out_of_date_tree()
265
# raise an exception as soon as we find a single unknown.
266
for unknown in self.work_tree.unknowns():
267
raise StrictCommitFailed()
269
if self.config is None:
270
self.config = self.branch.get_config()
272
251
self.work_inv = self.work_tree.inventory
252
self.basis_tree = self.branch.basis_tree()
273
253
self.basis_inv = self.basis_tree.inventory
274
if specific_files is not None:
275
# Ensure specified files are versioned
276
# (We don't actually need the ids here)
277
# XXX: Dont we have filter_unversioned to do this more
279
tree.find_ids_across_trees(specific_files,
280
[self.basis_tree, self.work_tree])
282
# Setup the progress bar ...
283
# one to finish, one for rev and inventory, and one for each
284
# inventory entry, and the same for the new inventory.
285
# note that this estimate is too long when we do a partial tree
286
# commit which excludes some new files from being considered.
287
# The estimate is corrected when we populate the new inv.
288
self.pb_total = len(self.work_inv) + 5
291
255
self._gather_parents()
292
256
if len(self.parents) > 1 and self.specific_files:
293
raise errors.CannotCommitSelectedFileMerge(self.specific_files)
257
raise NotImplementedError('selected-file commit of merges is not supported yet')
258
self._check_parents_present()
295
# Build the new inventory
296
self.builder = self.branch.get_commit_builder(self.parents,
297
self.config, timestamp, timezone, committer, revprops, rev_id)
298
260
self._remove_deleted()
299
261
self._populate_new_inv()
262
self._store_snapshot()
300
263
self._report_deletes()
301
self._check_pointless()
302
self._emit_progress_update()
304
# TODO: Now the new inventory is known, check for conflicts and
305
# prompt the user for a commit message.
306
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
307
# weave lines, because nothing should be recorded until it is known
308
# that commit will succeed.
309
self.builder.finish_inventory()
310
self._emit_progress_update()
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)
318
self._emit_progress_update()
320
# upload revision data to the master.
321
# this will propagate merged revisions too if needed.
322
if self.bound_branch:
323
self.master_branch.repository.fetch(self.branch.repository,
324
revision_id=self.rev_id)
325
# now the master has the revision data
326
# 'commit' to the master first so a timeout here causes the
327
# local branch to be out of date
328
self.master_branch.set_last_revision_info(new_revno,
331
# and now do the commit locally.
332
self.branch.set_last_revision_info(new_revno, self.rev_id)
334
# Make the working tree up to date with the branch
335
rev_tree = self.builder.revision_tree()
336
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
337
self.reporter.completed(new_revno, self.rev_id)
339
# Process the post commit hooks, if any
340
self._process_hooks(old_revno, new_revno)
341
self._emit_progress_update()
265
if not (self.allow_pointless
266
or len(self.parents) > 1
267
or self.new_inv != self.basis_inv):
268
raise PointlessCommit()
270
if len(list(self.work_tree.iter_conflicts()))>0:
271
raise ConflictsInTree
273
self._record_inventory()
274
self._make_revision()
275
self.work_tree.set_pending_merges([])
276
self.branch.append_revision(self.rev_id)
277
self.reporter.completed(self.branch.revno()+1, self.rev_id)
278
if self.config.post_commit() is not None:
279
hooks = self.config.post_commit().split(' ')
280
# this would be nicer with twisted.python.reflect.namedAny
282
result = eval(hook + '(branch, rev_id)',
283
{'branch':self.branch,
285
'rev_id':self.rev_id})
346
def _any_real_changes(self):
347
"""Are there real changes between new_inventory and basis?
349
For trees without rich roots, inv.root.revision changes every commit.
350
But if that is the only change, we want to treat it as though there
353
new_entries = self.builder.new_inventory.iter_entries()
354
basis_entries = self.basis_inv.iter_entries()
355
new_path, new_root_ie = new_entries.next()
356
basis_path, basis_root_ie = basis_entries.next()
358
# This is a copy of InventoryEntry.__eq__ only leaving out .revision
359
def ie_equal_no_revision(this, other):
360
return ((this.file_id == other.file_id)
361
and (this.name == other.name)
362
and (this.symlink_target == other.symlink_target)
363
and (this.text_sha1 == other.text_sha1)
364
and (this.text_size == other.text_size)
365
and (this.text_id == other.text_id)
366
and (this.parent_id == other.parent_id)
367
and (this.kind == other.kind)
368
and (this.executable == other.executable)
369
and (this.reference_revision == other.reference_revision)
371
if not ie_equal_no_revision(new_root_ie, basis_root_ie):
374
for new_ie, basis_ie in zip(new_entries, basis_entries):
375
if new_ie != basis_ie:
378
# No actual changes present
381
def _check_pointless(self):
382
if self.allow_pointless:
384
# A merge with no effect on files
385
if len(self.parents) > 1:
387
# work around the fact that a newly-initted tree does differ from its
389
if len(self.basis_inv) == 0 and len(self.builder.new_inventory) == 1:
390
raise PointlessCommit()
391
# Shortcut, if the number of entries changes, then we obviously have
393
if len(self.builder.new_inventory) != len(self.basis_inv):
395
# If length == 1, then we only have the root entry. Which means
396
# that there is no real difference (only the root could be different)
397
if (len(self.builder.new_inventory) != 1 and self._any_real_changes()):
399
raise PointlessCommit()
401
def _check_bound_branch(self):
402
"""Check to see if the local branch is bound.
404
If it is bound, then most of the commit will actually be
405
done using the remote branch as the target branch.
406
Only at the end will the local branch be updated.
408
if self.local and not self.branch.get_bound_location():
409
raise errors.LocalRequiresBoundBranch()
412
self.master_branch = self.branch.get_master_branch()
414
if not self.master_branch:
415
# make this branch the reference branch for out of date checks.
416
self.master_branch = self.branch
419
# If the master branch is bound, we must fail
420
master_bound_location = self.master_branch.get_bound_location()
421
if master_bound_location:
422
raise errors.CommitToDoubleBoundBranch(self.branch,
423
self.master_branch, master_bound_location)
425
# TODO: jam 20051230 We could automatically push local
426
# commits to the remote branch if they would fit.
427
# But for now, just require remote to be identical
430
# Make sure the local branch is identical to the master
431
master_info = self.master_branch.last_revision_info()
432
local_info = self.branch.last_revision_info()
433
if local_info != master_info:
434
raise errors.BoundBranchOutOfDate(self.branch,
437
# Now things are ready to change the master branch
439
self.bound_branch = self.branch
440
self.master_branch.lock_write()
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
# old style commit hooks - should be deprecated ? (obsoleted in
471
if self.config.post_commit() is not None:
472
hooks = self.config.post_commit().split(' ')
473
# this would be nicer with twisted.python.reflect.namedAny
475
result = eval(hook + '(branch, rev_id)',
476
{'branch':self.branch,
478
'rev_id':self.rev_id})
479
# new style commit hooks:
480
if not self.bound_branch:
481
hook_master = self.branch
484
hook_master = self.master_branch
485
hook_local = self.branch
486
# With bound branches, when the master is behind the local branch,
487
# the 'old_revno' and old_revid values here are incorrect.
488
# XXX: FIXME ^. RBC 20060206
490
old_revid = self.parents[0]
492
old_revid = bzrlib.revision.NULL_REVISION
493
for hook in Branch.hooks['post_commit']:
494
hook(hook_local, hook_master, old_revno, old_revid, new_revno,
498
"""Cleanup any open locks, progress bars etc."""
499
cleanups = [self._cleanup_bound_branch,
500
self.basis_tree.unlock,
501
self.work_tree.unlock,
503
found_exception = None
504
for cleanup in cleanups:
507
# we want every cleanup to run no matter what.
508
# so we have a catchall here, but we will raise the
509
# last encountered exception up the stack: and
510
# typically this will be useful enough.
513
if found_exception is not None:
514
# don't do a plan raise, because the last exception may have been
515
# trashed, e is our sure-to-work exception even though it loses the
516
# full traceback. XXX: RBC 20060421 perhaps we could check the
517
# exc_info and if its the same one do a plain raise otherwise
518
# 'raise e' as we do now.
521
def _cleanup_bound_branch(self):
522
"""Executed at the end of a try/finally to cleanup a bound branch.
524
If the branch wasn't bound, this is a no-op.
525
If it was, it resents self.branch to the local branch, instead
528
if not self.bound_branch:
530
if self.master_locked:
531
self.master_branch.unlock()
289
def _record_inventory(self):
290
"""Store the inventory for the new revision."""
291
inv_text = serializer_v5.write_inventory_to_string(self.new_inv)
292
self.inv_sha1 = sha_string(inv_text)
293
s = self.branch.control_weaves
294
s.add_text('inventory', self.rev_id,
295
split_lines(inv_text), self.present_parents,
296
self.branch.get_transaction())
533
298
def _escape_commit_message(self):
534
299
"""Replace xml-incompatible control characters."""
535
# FIXME: RBC 20060419 this should be done by the revision
536
# serialiser not by commit. Then we can also add an unescaper
537
# in the deserializer and start roundtripping revision messages
538
# precisely. See repository_implementations/test_repository.py
540
300
# Python strings can include characters that can't be
541
301
# represented in well-formed XML; escape characters that
542
302
# aren't listed in the XML specification
598
411
None; inventory entries that are carried over untouched have their
599
412
revision set to their prior value.
601
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
602
# results to create a new inventory at the same time, which results
603
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
605
414
mutter("Selecting files for commit with filter %s", self.specific_files)
606
assert self.work_inv.root is not None
607
entries = self.work_inv.iter_entries()
608
if not self.builder.record_root_entry:
609
symbol_versioning.warn('CommitBuilders should support recording'
610
' the root entry as of bzr 0.10.', DeprecationWarning,
612
self.builder.new_inventory.add(self.basis_inv.root.copy())
614
self._emit_progress_update()
615
for path, new_ie in entries:
616
self._emit_progress_update()
415
self.new_inv = Inventory()
416
for path, new_ie in self.work_inv.iter_entries():
617
417
file_id = new_ie.file_id
619
kind = self.work_tree.kind(file_id)
620
if kind == 'tree-reference' and self.recursive == 'down':
621
# nested tree: commit in it
622
sub_tree = WorkingTree.open(self.work_tree.abspath(path))
623
# FIXME: be more comprehensive here:
624
# this works when both trees are in --trees repository,
625
# but when both are bound to a different repository,
626
# it fails; a better way of approaching this is to
627
# finally implement the explicit-caches approach design
628
# a while back - RBC 20070306.
629
if (sub_tree.branch.repository.bzrdir.root_transport.base
631
self.work_tree.branch.repository.bzrdir.root_transport.base):
632
sub_tree.branch.repository = \
633
self.work_tree.branch.repository
635
sub_tree.commit(message=None, revprops=self.revprops,
636
recursive=self.recursive,
637
message_callback=self.message_callback,
638
timestamp=self.timestamp, timezone=self.timezone,
639
committer=self.committer,
640
allow_pointless=self.allow_pointless,
641
strict=self.strict, verbose=self.verbose,
642
local=self.local, reporter=self.reporter)
643
except errors.PointlessCommit:
645
if kind != new_ie.kind:
646
new_ie = inventory.make_entry(kind, new_ie.name,
647
new_ie.parent_id, file_id)
648
except errors.NoSuchFile:
650
# mutter('check %s {%s}', path, file_id)
651
if (not self.specific_files or
652
is_inside_or_parent_of_any(self.specific_files, path)):
653
# mutter('%s selected for commit', path)
657
# mutter('%s not selected for commit', path)
658
if self.basis_inv.has_id(file_id):
659
ie = self.basis_inv[file_id].copy()
661
# this entry is new and not being committed
418
mutter('check %s {%s}', path, new_ie.file_id)
419
if self.specific_files:
420
if not is_inside_any(self.specific_files, path):
421
mutter('%s not selected for commit', path)
422
self._carry_entry(file_id)
663
self.builder.record_entry_contents(ie, self.parent_invs,
664
path, self.work_tree)
665
# describe the nature of the change that has occurred relative to
666
# the basis inventory.
667
if (self.basis_inv.has_id(ie.file_id)):
668
basis_ie = self.basis_inv[ie.file_id]
671
change = ie.describe_change(basis_ie, ie)
672
if change in (InventoryEntry.RENAMED,
673
InventoryEntry.MODIFIED_AND_RENAMED):
674
old_path = self.basis_inv.id2path(ie.file_id)
675
self.reporter.renamed(change, old_path, path)
677
self.reporter.snapshot_change(change, path)
679
if not self.specific_files:
682
# ignore removals that don't match filespec
683
for path, new_ie in self.basis_inv.iter_entries():
684
if new_ie.file_id in self.work_inv:
686
if is_inside_any(self.specific_files, path):
690
self.builder.record_entry_contents(ie, self.parent_invs, path,
693
def _emit_progress_update(self):
694
"""Emit an update to the progress bar."""
695
self.pb.update("Committing", self.pb_count, self.pb_total)
425
# this is selected, ensure its parents are too.
426
parent_id = new_ie.parent_id
427
while parent_id != ROOT_ID:
428
if not self.new_inv.has_id(parent_id):
429
ie = self._select_entry(self.work_inv[parent_id])
430
mutter('%s selected for commit because of %s',
431
self.new_inv.id2path(parent_id), path)
433
ie = self.new_inv[parent_id]
434
if ie.revision is not None:
436
mutter('%s selected for commit because of %s',
437
self.new_inv.id2path(parent_id), path)
438
parent_id = ie.parent_id
439
mutter('%s selected for commit', path)
440
self._select_entry(new_ie)
442
def _select_entry(self, new_ie):
443
"""Make new_ie be considered for committing."""
449
def _carry_entry(self, file_id):
450
"""Carry the file unchanged from the basis revision."""
451
if self.basis_inv.has_id(file_id):
452
self.new_inv.add(self.basis_inv[file_id].copy())
698
454
def _report_deletes(self):
699
for path, ie in self.basis_inv.iter_entries():
700
if ie.file_id not in self.builder.new_inventory:
701
self.reporter.deleted(path)
455
for file_id in self.basis_inv:
456
if file_id not in self.new_inv:
457
self.reporter.deleted(self.basis_inv.id2path(file_id))
459
def _gen_revision_id(config, when):
460
"""Return new revision-id."""
461
s = '%s-%s-' % (config.user_email(), compact_date(when))
462
s += hexlify(rand_bytes(8))