176
196
allow_pointless -- If true (default), commit even if nothing
177
197
has changed and no merges are recorded.
199
strict -- If true, don't allow a commit if the working tree
200
contains unknown files.
202
revprops -- Properties for new revision
203
:param local: Perform a local only commit.
204
:param recursive: If set to 'down', commit in any subtrees that have
205
pending changes of any sort during this commit.
179
207
mutter('preparing to commit')
182
self.weave_store = branch.weave_store
209
if deprecated_passed(branch):
210
symbol_versioning.warn("Commit.commit (branch, ...): The branch parameter is "
211
"deprecated as of bzr 0.8. Please use working_tree= instead.",
212
DeprecationWarning, stacklevel=2)
214
self.work_tree = self.branch.bzrdir.open_workingtree()
215
elif working_tree is None:
216
raise BzrError("One of branch and working_tree must be passed into commit().")
218
self.work_tree = working_tree
219
self.branch = self.work_tree.branch
220
if getattr(self.work_tree, 'requires_rich_root', lambda: False)():
221
if not self.branch.repository.supports_rich_root():
222
raise errors.RootNotRich()
223
if message_callback is None:
224
if message is not None:
225
if isinstance(message, str):
226
message = message.decode(bzrlib.user_encoding)
227
message_callback = lambda x: message
229
raise BzrError("The message or message_callback keyword"
230
" parameter is required for commit().")
232
self.bound_branch = None
234
self.master_branch = None
235
self.master_locked = False
184
237
self.specific_files = specific_files
185
238
self.allow_pointless = allow_pointless
187
if timestamp is None:
188
self.timestamp = time.time()
190
self.timestamp = long(timestamp)
193
self.rev_id = _gen_revision_id(self.branch, self.timestamp)
197
if committer is None:
198
self.committer = username(self.branch)
200
assert isinstance(committer, basestring), type(committer)
201
self.committer = committer
204
self.timezone = local_time_offset()
206
self.timezone = int(timezone)
208
assert isinstance(message, basestring), type(message)
209
self.message = message
210
self._escape_commit_message()
212
self.branch.lock_write()
239
self.recursive = recursive
240
self.revprops = revprops
241
self.message_callback = message_callback
242
self.timestamp = timestamp
243
self.timezone = timezone
244
self.committer = committer
245
self.specific_files = specific_files
247
self.verbose = verbose
250
if reporter is None and self.reporter is None:
251
self.reporter = NullCommitReporter()
252
elif reporter is not None:
253
self.reporter = reporter
255
self.work_tree.lock_write()
256
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
257
self.basis_tree = self.work_tree.basis_tree()
258
self.basis_tree.lock_read()
214
self.work_tree = self.branch.working_tree()
260
# Cannot commit with conflicts present.
261
if len(self.work_tree.conflicts())>0:
262
raise ConflictsInTree
264
# setup the bound branch variables as needed.
265
self._check_bound_branch()
267
# check for out of date working trees
269
first_tree_parent = self.work_tree.get_parent_ids()[0]
271
# if there are no parents, treat our parent as 'None'
272
# this is so that we still consier the master branch
273
# - in a checkout scenario the tree may have no
274
# parents but the branch may do.
275
first_tree_parent = bzrlib.revision.NULL_REVISION
276
old_revno, master_last = self.master_branch.last_revision_info()
277
if master_last != first_tree_parent:
278
if master_last != bzrlib.revision.NULL_REVISION:
279
raise errors.OutOfDateTree(self.work_tree)
280
if self.branch.repository.has_revision(first_tree_parent):
281
new_revno = old_revno + 1
283
# ghost parents never appear in revision history.
286
# raise an exception as soon as we find a single unknown.
287
for unknown in self.work_tree.unknowns():
288
raise StrictCommitFailed()
290
if self.config is None:
291
self.config = self.branch.get_config()
215
293
self.work_inv = self.work_tree.inventory
216
self.basis_tree = self.branch.basis_tree()
217
294
self.basis_inv = self.basis_tree.inventory
295
if specific_files is not None:
296
# Ensure specified files are versioned
297
# (We don't actually need the ids here)
298
# XXX: Dont we have filter_unversioned to do this more
300
tree.find_ids_across_trees(specific_files,
301
[self.basis_tree, self.work_tree])
302
# one to finish, one for rev and inventory, and one for each
303
# inventory entry, and the same for the new inventory.
304
# note that this estimate is too long when we do a partial tree
305
# commit which excludes some new files from being considered.
306
# The estimate is corrected when we populate the new inv.
307
self.pb_total = len(self.work_inv) + 5
219
310
self._gather_parents()
220
311
if len(self.parents) > 1 and self.specific_files:
221
raise NotImplementedError('selected-file commit of merges is not supported yet')
222
self._check_parents_present()
312
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
315
self.builder = self.branch.get_commit_builder(self.parents,
316
self.config, timestamp, timezone, committer, revprops, rev_id)
224
318
self._remove_deleted()
225
319
self._populate_new_inv()
226
self._store_snapshot()
227
320
self._report_deletes()
229
if not (self.allow_pointless
230
or len(self.parents) > 1
231
or self.new_inv != self.basis_inv):
232
raise PointlessCommit()
234
if len(list(self.work_tree.iter_conflicts()))>0:
235
raise ConflictsInTree
237
self._record_inventory()
238
self._make_revision()
239
self.reporter.completed(self.branch.revno()+1, self.rev_id)
240
self.branch.append_revision(self.rev_id)
241
self.branch.set_pending_merges([])
322
self._check_pointless()
324
self._emit_progress_update()
325
# TODO: Now the new inventory is known, check for conflicts and
326
# prompt the user for a commit message.
327
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
328
# weave lines, because nothing should be recorded until it is known
329
# that commit will succeed.
330
self.builder.finish_inventory()
331
self._emit_progress_update()
332
message = message_callback(self)
333
assert isinstance(message, unicode), type(message)
334
self.message = message
335
self._escape_commit_message()
337
self.rev_id = self.builder.commit(self.message)
338
self._emit_progress_update()
339
# revision data is in the local branch now.
341
# upload revision data to the master.
342
# this will propagate merged revisions too if needed.
343
if self.bound_branch:
344
self.master_branch.repository.fetch(self.branch.repository,
345
revision_id=self.rev_id)
346
# now the master has the revision data
347
# 'commit' to the master first so a timeout here causes the local
348
# branch to be out of date
349
self.master_branch.set_last_revision_info(new_revno,
352
# and now do the commit locally.
353
self.branch.set_last_revision_info(new_revno, self.rev_id)
355
rev_tree = self.builder.revision_tree()
356
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
357
# now the work tree is up to date with the branch
359
self.reporter.completed(new_revno, self.rev_id)
360
# old style commit hooks - should be deprecated ? (obsoleted in
362
if self.config.post_commit() is not None:
363
hooks = self.config.post_commit().split(' ')
364
# this would be nicer with twisted.python.reflect.namedAny
366
result = eval(hook + '(branch, rev_id)',
367
{'branch':self.branch,
369
'rev_id':self.rev_id})
370
# new style commit hooks:
371
if not self.bound_branch:
372
hook_master = self.branch
375
hook_master = self.master_branch
376
hook_local = self.branch
377
# With bound branches, when the master is behind the local branch,
378
# the 'old_revno' and old_revid values here are incorrect.
379
# XXX: FIXME ^. RBC 20060206
381
old_revid = self.parents[0]
383
old_revid = bzrlib.revision.NULL_REVISION
384
for hook in Branch.hooks['post_commit']:
385
hook(hook_local, hook_master, old_revno, old_revid, new_revno,
387
self._emit_progress_update()
245
def _record_inventory(self):
246
"""Store the inventory for the new revision."""
247
inv_text = serializer_v5.write_inventory_to_string(self.new_inv)
248
self.inv_sha1 = sha_string(inv_text)
249
s = self.branch.control_weaves
250
s.add_text('inventory', self.rev_id,
251
split_lines(inv_text), self.present_parents)
392
def _any_real_changes(self):
393
"""Are there real changes between new_inventory and basis?
395
For trees without rich roots, inv.root.revision changes every commit.
396
But if that is the only change, we want to treat it as though there
399
new_entries = self.builder.new_inventory.iter_entries()
400
basis_entries = self.basis_inv.iter_entries()
401
new_path, new_root_ie = new_entries.next()
402
basis_path, basis_root_ie = basis_entries.next()
404
# This is a copy of InventoryEntry.__eq__ only leaving out .revision
405
def ie_equal_no_revision(this, other):
406
return ((this.file_id == other.file_id)
407
and (this.name == other.name)
408
and (this.symlink_target == other.symlink_target)
409
and (this.text_sha1 == other.text_sha1)
410
and (this.text_size == other.text_size)
411
and (this.text_id == other.text_id)
412
and (this.parent_id == other.parent_id)
413
and (this.kind == other.kind)
414
and (this.executable == other.executable)
415
and (this.reference_revision == other.reference_revision)
417
if not ie_equal_no_revision(new_root_ie, basis_root_ie):
420
for new_ie, basis_ie in zip(new_entries, basis_entries):
421
if new_ie != basis_ie:
424
# No actual changes present
427
def _check_pointless(self):
428
if self.allow_pointless:
430
# A merge with no effect on files
431
if len(self.parents) > 1:
433
# work around the fact that a newly-initted tree does differ from its
435
if len(self.basis_inv) == 0 and len(self.builder.new_inventory) == 1:
436
raise PointlessCommit()
437
# Shortcut, if the number of entries changes, then we obviously have
439
if len(self.builder.new_inventory) != len(self.basis_inv):
441
# If length == 1, then we only have the root entry. Which means
442
# that there is no real difference (only the root could be different)
443
if (len(self.builder.new_inventory) != 1 and self._any_real_changes()):
445
raise PointlessCommit()
447
def _check_bound_branch(self):
448
"""Check to see if the local branch is bound.
450
If it is bound, then most of the commit will actually be
451
done using the remote branch as the target branch.
452
Only at the end will the local branch be updated.
454
if self.local and not self.branch.get_bound_location():
455
raise errors.LocalRequiresBoundBranch()
458
self.master_branch = self.branch.get_master_branch()
460
if not self.master_branch:
461
# make this branch the reference branch for out of date checks.
462
self.master_branch = self.branch
465
# If the master branch is bound, we must fail
466
master_bound_location = self.master_branch.get_bound_location()
467
if master_bound_location:
468
raise errors.CommitToDoubleBoundBranch(self.branch,
469
self.master_branch, master_bound_location)
471
# TODO: jam 20051230 We could automatically push local
472
# commits to the remote branch if they would fit.
473
# But for now, just require remote to be identical
476
# Make sure the local branch is identical to the master
477
master_info = self.master_branch.last_revision_info()
478
local_info = self.branch.last_revision_info()
479
if local_info != master_info:
480
raise errors.BoundBranchOutOfDate(self.branch,
483
# Now things are ready to change the master branch
485
self.bound_branch = self.branch
486
self.master_branch.lock_write()
487
self.master_locked = True
490
"""Cleanup any open locks, progress bars etc."""
491
cleanups = [self._cleanup_bound_branch,
492
self.basis_tree.unlock,
493
self.work_tree.unlock,
495
found_exception = None
496
for cleanup in cleanups:
499
# we want every cleanup to run no matter what.
500
# so we have a catchall here, but we will raise the
501
# last encountered exception up the stack: and
502
# typically this will be useful enough.
505
if found_exception is not None:
506
# don't do a plan raise, because the last exception may have been
507
# trashed, e is our sure-to-work exception even though it loses the
508
# full traceback. XXX: RBC 20060421 perhaps we could check the
509
# exc_info and if its the same one do a plain raise otherwise
510
# 'raise e' as we do now.
513
def _cleanup_bound_branch(self):
514
"""Executed at the end of a try/finally to cleanup a bound branch.
516
If the branch wasn't bound, this is a no-op.
517
If it was, it resents self.branch to the local branch, instead
520
if not self.bound_branch:
522
if self.master_locked:
523
self.master_branch.unlock()
253
525
def _escape_commit_message(self):
254
526
"""Replace xml-incompatible control characters."""
527
# FIXME: RBC 20060419 this should be done by the revision
528
# serialiser not by commit. Then we can also add an unescaper
529
# in the deserializer and start roundtripping revision messages
530
# precisely. See repository_implementations/test_repository.py
255
532
# Python strings can include characters that can't be
256
533
# represented in well-formed XML; escape characters that
257
534
# aren't listed in the XML specification
258
535
# (http://www.w3.org/TR/REC-xml/#NT-Char).
259
if isinstance(self.message, unicode):
260
char_pattern = u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]'
262
# Use a regular 'str' as pattern to avoid having re.subn
263
# return 'unicode' results.
264
char_pattern = '[^x09\x0A\x0D\x20-\xFF]'
265
536
self.message, escape_count = re.subn(
537
u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
267
538
lambda match: match.group(0).encode('unicode_escape'),
365
590
None; inventory entries that are carried over untouched have their
366
591
revision set to their prior value.
593
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
594
# results to create a new inventory at the same time, which results
595
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
368
597
mutter("Selecting files for commit with filter %s", self.specific_files)
369
self.new_inv = Inventory()
370
for path, new_ie in self.work_inv.iter_entries():
598
assert self.work_inv.root is not None
599
entries = self.work_inv.iter_entries()
600
if not self.builder.record_root_entry:
601
symbol_versioning.warn('CommitBuilders should support recording'
602
' the root entry as of bzr 0.10.', DeprecationWarning,
604
self.builder.new_inventory.add(self.basis_inv.root.copy())
606
self._emit_progress_update()
607
for path, new_ie in entries:
608
self._emit_progress_update()
371
609
file_id = new_ie.file_id
372
mutter('check %s {%s}', path, new_ie.file_id)
373
if self.specific_files:
374
if not is_inside_any(self.specific_files, path):
375
mutter('%s not selected for commit', path)
376
self._carry_entry(file_id)
611
kind = self.work_tree.kind(file_id)
612
if kind == 'tree-reference' and self.recursive == 'down':
613
# nested tree: commit in it
614
sub_tree = WorkingTree.open(self.work_tree.abspath(path))
615
# FIXME: be more comprehensive here:
616
# this works when both trees are in --trees repository,
617
# but when both are bound to a different repository,
618
# it fails; a better way of approaching this is to
619
# finally implement the explicit-caches approach design
620
# a while back - RBC 20070306.
621
if (sub_tree.branch.repository.bzrdir.root_transport.base
623
self.work_tree.branch.repository.bzrdir.root_transport.base):
624
sub_tree.branch.repository = \
625
self.work_tree.branch.repository
627
sub_tree.commit(message=None, revprops=self.revprops,
628
recursive=self.recursive,
629
message_callback=self.message_callback,
630
timestamp=self.timestamp, timezone=self.timezone,
631
committer=self.committer,
632
allow_pointless=self.allow_pointless,
633
strict=self.strict, verbose=self.verbose,
634
local=self.local, reporter=self.reporter)
635
except errors.PointlessCommit:
637
if kind != new_ie.kind:
638
new_ie = inventory.make_entry(kind, new_ie.name,
639
new_ie.parent_id, file_id)
640
except errors.NoSuchFile:
642
# mutter('check %s {%s}', path, file_id)
643
if (not self.specific_files or
644
is_inside_or_parent_of_any(self.specific_files, path)):
645
# mutter('%s selected for commit', path)
649
# mutter('%s not selected for commit', path)
650
if self.basis_inv.has_id(file_id):
651
ie = self.basis_inv[file_id].copy()
653
# this entry is new and not being committed
379
# this is selected, ensure its parents are too.
380
parent_id = new_ie.parent_id
381
while parent_id != ROOT_ID:
382
if not self.new_inv.has_id(parent_id):
383
ie = self._select_entry(self.work_inv[parent_id])
384
mutter('%s selected for commit because of %s',
385
self.new_inv.id2path(parent_id), path)
387
ie = self.new_inv[parent_id]
388
if ie.revision is not None:
390
mutter('%s selected for commit because of %s',
391
self.new_inv.id2path(parent_id), path)
392
parent_id = ie.parent_id
393
mutter('%s selected for commit', path)
394
self._select_entry(new_ie)
396
def _select_entry(self, new_ie):
397
"""Make new_ie be considered for committing."""
403
def _carry_entry(self, file_id):
404
"""Carry the file unchanged from the basis revision."""
405
if self.basis_inv.has_id(file_id):
406
self.new_inv.add(self.basis_inv[file_id].copy())
655
self.builder.record_entry_contents(ie, self.parent_invs,
656
path, self.work_tree)
657
# describe the nature of the change that has occurred relative to
658
# the basis inventory.
659
if (self.basis_inv.has_id(ie.file_id)):
660
basis_ie = self.basis_inv[ie.file_id]
663
change = ie.describe_change(basis_ie, ie)
664
if change in (InventoryEntry.RENAMED,
665
InventoryEntry.MODIFIED_AND_RENAMED):
666
old_path = self.basis_inv.id2path(ie.file_id)
667
self.reporter.renamed(change, old_path, path)
669
self.reporter.snapshot_change(change, path)
671
if not self.specific_files:
674
# ignore removals that don't match filespec
675
for path, new_ie in self.basis_inv.iter_entries():
676
if new_ie.file_id in self.work_inv:
678
if is_inside_any(self.specific_files, path):
682
self.builder.record_entry_contents(ie, self.parent_invs, path,
685
def _emit_progress_update(self):
686
"""Emit an update to the progress bar."""
687
self.pb.update("Committing", self.pb_count, self.pb_total)
408
690
def _report_deletes(self):
409
for file_id in self.basis_inv:
410
if file_id not in self.new_inv:
411
self.reporter.deleted(self.basis_inv.id2path(file_id))
413
def _gen_revision_id(branch, when):
414
"""Return new revision-id."""
415
s = '%s-%s-' % (user_email(branch), compact_date(when))
416
s += hexlify(rand_bytes(8))
691
for path, ie in self.basis_inv.iter_entries():
692
if ie.file_id not in self.builder.new_inventory:
693
self.reporter.deleted(path)