196
210
contains unknown files.
198
212
revprops -- Properties for new revision
213
:param local: Perform a local only commit.
200
215
mutter('preparing to commit')
203
self.weave_store = branch.weave_store
217
if deprecated_passed(branch):
218
symbol_versioning.warn("Commit.commit (branch, ...): The branch parameter is "
219
"deprecated as of bzr 0.8. Please use working_tree= instead.",
220
DeprecationWarning, stacklevel=2)
222
self.work_tree = self.branch.bzrdir.open_workingtree()
223
elif working_tree is None:
224
raise BzrError("One of branch and working_tree must be passed into commit().")
226
self.work_tree = working_tree
227
self.branch = self.work_tree.branch
229
raise BzrError("The message keyword parameter is required for commit().")
231
self.bound_branch = None
233
self.master_branch = None
234
self.master_locked = False
205
236
self.specific_files = specific_files
206
237
self.allow_pointless = allow_pointless
207
self.revprops = revprops
209
if strict and branch.unknowns():
210
raise StrictCommitFailed()
212
if timestamp is None:
213
self.timestamp = time.time()
215
self.timestamp = long(timestamp)
217
if self.config is None:
218
self.config = bzrlib.config.BranchConfig(self.branch)
221
self.rev_id = _gen_revision_id(self.config, self.timestamp)
225
if committer is None:
226
self.committer = self.config.username()
228
assert isinstance(committer, basestring), type(committer)
229
self.committer = committer
232
self.timezone = local_time_offset()
234
self.timezone = int(timezone)
236
assert isinstance(message, basestring), type(message)
237
self.message = message
238
self._escape_commit_message()
240
self.branch.lock_write()
239
if reporter is None and self.reporter is None:
240
self.reporter = NullCommitReporter()
241
elif reporter is not None:
242
self.reporter = reporter
244
self.work_tree.lock_write()
245
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
242
self.work_tree = self.branch.working_tree()
247
# Cannot commit with conflicts present.
248
if len(self.work_tree.conflicts())>0:
249
raise ConflictsInTree
251
# setup the bound branch variables as needed.
252
self._check_bound_branch()
254
# check for out of date working trees
256
first_tree_parent = self.work_tree.get_parent_ids()[0]
258
# if there are no parents, treat our parent as 'None'
259
# this is so that we still consier the master branch
260
# - in a checkout scenario the tree may have no
261
# parents but the branch may do.
262
first_tree_parent = None
263
master_last = self.master_branch.last_revision()
264
if (master_last is not None and
265
master_last != first_tree_parent):
266
raise errors.OutOfDateTree(self.work_tree)
269
# raise an exception as soon as we find a single unknown.
270
for unknown in self.work_tree.unknowns():
271
raise StrictCommitFailed()
273
if self.config is None:
274
self.config = self.branch.get_config()
276
if isinstance(message, str):
277
message = message.decode(bzrlib.user_encoding)
278
assert isinstance(message, unicode), type(message)
279
self.message = message
280
self._escape_commit_message()
243
282
self.work_inv = self.work_tree.inventory
244
self.basis_tree = self.branch.basis_tree()
283
self.basis_tree = self.work_tree.basis_tree()
245
284
self.basis_inv = self.basis_tree.inventory
285
if specific_files is not None:
286
# Ensure specified files are versioned
287
# (We don't actually need the ids here)
288
tree.find_ids_across_trees(specific_files,
289
[self.basis_tree, self.work_tree])
290
# one to finish, one for rev and inventory, and one for each
291
# inventory entry, and the same for the new inventory.
292
# note that this estimate is too long when we do a partial tree
293
# commit which excludes some new files from being considered.
294
# The estimate is corrected when we populate the new inv.
295
self.pb_total = len(self.work_inv) + 5
247
298
self._gather_parents()
248
299
if len(self.parents) > 1 and self.specific_files:
249
raise NotImplementedError('selected-file commit of merges is not supported yet')
250
self._check_parents_present()
300
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
303
self.builder = self.branch.get_commit_builder(self.parents,
304
self.config, timestamp, timezone, committer, revprops, rev_id)
252
306
self._remove_deleted()
253
307
self._populate_new_inv()
254
self._store_snapshot()
255
308
self._report_deletes()
257
if not (self.allow_pointless
258
or len(self.parents) > 1
259
or self.new_inv != self.basis_inv):
260
raise PointlessCommit()
262
if len(list(self.work_tree.iter_conflicts()))>0:
263
raise ConflictsInTree
265
self._record_inventory()
266
self._make_revision()
267
self.reporter.completed(self.branch.revno()+1, self.rev_id)
310
self._check_pointless()
312
self._emit_progress_update()
313
# TODO: Now the new inventory is known, check for conflicts and
314
# prompt the user for a commit message.
315
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
316
# weave lines, because nothing should be recorded until it is known
317
# that commit will succeed.
318
self.builder.finish_inventory()
319
self._emit_progress_update()
320
self.rev_id = self.builder.commit(self.message)
321
self._emit_progress_update()
322
# revision data is in the local branch now.
324
# upload revision data to the master.
325
# this will propagate merged revisions too if needed.
326
if self.bound_branch:
327
self.master_branch.repository.fetch(self.branch.repository,
328
revision_id=self.rev_id)
329
# now the master has the revision data
330
# 'commit' to the master first so a timeout here causes the local
331
# branch to be out of date
332
self.master_branch.append_revision(self.rev_id)
334
# and now do the commit locally.
268
335
self.branch.append_revision(self.rev_id)
269
self.branch.set_pending_merges([])
337
rev_tree = self.builder.revision_tree()
338
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
339
# now the work tree is up to date with the branch
341
self.reporter.completed(self.branch.revno(), self.rev_id)
342
if self.config.post_commit() is not None:
343
hooks = self.config.post_commit().split(' ')
344
# this would be nicer with twisted.python.reflect.namedAny
346
result = eval(hook + '(branch, rev_id)',
347
{'branch':self.branch,
349
'rev_id':self.rev_id})
350
self._emit_progress_update()
273
def _record_inventory(self):
274
"""Store the inventory for the new revision."""
275
inv_text = serializer_v5.write_inventory_to_string(self.new_inv)
276
self.inv_sha1 = sha_string(inv_text)
277
s = self.branch.control_weaves
278
s.add_text('inventory', self.rev_id,
279
split_lines(inv_text), self.present_parents,
280
self.branch.get_transaction())
355
def _any_real_changes(self):
356
"""Are there real changes between new_inventory and basis?
358
For trees without rich roots, inv.root.revision changes every commit.
359
But if that is the only change, we want to treat it as though there
362
new_entries = self.builder.new_inventory.iter_entries()
363
basis_entries = self.basis_inv.iter_entries()
364
new_path, new_root_ie = new_entries.next()
365
basis_path, basis_root_ie = basis_entries.next()
367
# This is a copy of InventoryEntry.__eq__ only leaving out .revision
368
def ie_equal_no_revision(this, other):
369
return ((this.file_id == other.file_id)
370
and (this.name == other.name)
371
and (this.symlink_target == other.symlink_target)
372
and (this.text_sha1 == other.text_sha1)
373
and (this.text_size == other.text_size)
374
and (this.text_id == other.text_id)
375
and (this.parent_id == other.parent_id)
376
and (this.kind == other.kind)
377
and (this.executable == other.executable)
379
if not ie_equal_no_revision(new_root_ie, basis_root_ie):
382
for new_ie, basis_ie in zip(new_entries, basis_entries):
383
if new_ie != basis_ie:
386
# No actual changes present
389
def _check_pointless(self):
390
if self.allow_pointless:
392
# A merge with no effect on files
393
if len(self.parents) > 1:
395
# work around the fact that a newly-initted tree does differ from its
397
if len(self.basis_inv) == 0 and len(self.builder.new_inventory) == 1:
398
raise PointlessCommit()
399
# Shortcut, if the number of entries changes, then we obviously have
401
if len(self.builder.new_inventory) != len(self.basis_inv):
403
# If length == 1, then we only have the root entry. Which means
404
# that there is no real difference (only the root could be different)
405
if (len(self.builder.new_inventory) != 1 and self._any_real_changes()):
407
raise PointlessCommit()
409
def _check_bound_branch(self):
410
"""Check to see if the local branch is bound.
412
If it is bound, then most of the commit will actually be
413
done using the remote branch as the target branch.
414
Only at the end will the local branch be updated.
416
if self.local and not self.branch.get_bound_location():
417
raise errors.LocalRequiresBoundBranch()
420
self.master_branch = self.branch.get_master_branch()
422
if not self.master_branch:
423
# make this branch the reference branch for out of date checks.
424
self.master_branch = self.branch
427
# If the master branch is bound, we must fail
428
master_bound_location = self.master_branch.get_bound_location()
429
if master_bound_location:
430
raise errors.CommitToDoubleBoundBranch(self.branch,
431
self.master_branch, master_bound_location)
433
# TODO: jam 20051230 We could automatically push local
434
# commits to the remote branch if they would fit.
435
# But for now, just require remote to be identical
438
# Make sure the local branch is identical to the master
439
master_rh = self.master_branch.revision_history()
440
local_rh = self.branch.revision_history()
441
if local_rh != master_rh:
442
raise errors.BoundBranchOutOfDate(self.branch,
445
# Now things are ready to change the master branch
447
self.bound_branch = self.branch
448
self.master_branch.lock_write()
449
self.master_locked = True
452
"""Cleanup any open locks, progress bars etc."""
453
cleanups = [self._cleanup_bound_branch,
454
self.work_tree.unlock,
456
found_exception = None
457
for cleanup in cleanups:
460
# we want every cleanup to run no matter what.
461
# so we have a catchall here, but we will raise the
462
# last encountered exception up the stack: and
463
# typically this will be useful enough.
466
if found_exception is not None:
467
# don't do a plan raise, because the last exception may have been
468
# trashed, e is our sure-to-work exception even though it loses the
469
# full traceback. XXX: RBC 20060421 perhaps we could check the
470
# exc_info and if its the same one do a plain raise otherwise
471
# 'raise e' as we do now.
474
def _cleanup_bound_branch(self):
475
"""Executed at the end of a try/finally to cleanup a bound branch.
477
If the branch wasn't bound, this is a no-op.
478
If it was, it resents self.branch to the local branch, instead
481
if not self.bound_branch:
483
if self.master_locked:
484
self.master_branch.unlock()
282
486
def _escape_commit_message(self):
283
487
"""Replace xml-incompatible control characters."""
488
# FIXME: RBC 20060419 this should be done by the revision
489
# serialiser not by commit. Then we can also add an unescaper
490
# in the deserializer and start roundtripping revision messages
491
# precisely. See repository_implementations/test_repository.py
284
493
# Python strings can include characters that can't be
285
494
# represented in well-formed XML; escape characters that
286
495
# aren't listed in the XML specification
287
496
# (http://www.w3.org/TR/REC-xml/#NT-Char).
288
if isinstance(self.message, unicode):
289
char_pattern = u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]'
291
# Use a regular 'str' as pattern to avoid having re.subn
292
# return 'unicode' results.
293
char_pattern = '[^x09\x0A\x0D\x20-\xFF]'
294
497
self.message, escape_count = re.subn(
498
u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
296
499
lambda match: match.group(0).encode('unicode_escape'),
301
504
def _gather_parents(self):
302
505
"""Record the parents of a merge for merge detection."""
303
pending_merges = self.branch.pending_merges()
506
# TODO: Make sure that this list doesn't contain duplicate
507
# entries and the order is preserved when doing this.
508
self.parents = self.work_tree.get_parent_ids()
305
509
self.parent_invs = []
306
self.present_parents = []
307
precursor_id = self.branch.last_revision()
309
self.parents.append(precursor_id)
310
self.parents += pending_merges
311
510
for revision in self.parents:
312
if self.branch.has_revision(revision):
313
self.parent_invs.append(self.branch.get_inventory(revision))
314
self.present_parents.append(revision)
316
def _check_parents_present(self):
317
for parent_id in self.parents:
318
mutter('commit parent revision {%s}', parent_id)
319
if not self.branch.has_revision(parent_id):
320
if parent_id == self.branch.last_revision():
321
warning("parent is missing %r", parent_id)
322
raise HistoryMissing(self.branch, 'revision', parent_id)
324
mutter("commit will ghost revision %r", parent_id)
326
def _make_revision(self):
327
"""Record a new revision object for this commit."""
328
self.rev = Revision(timestamp=self.timestamp,
329
timezone=self.timezone,
330
committer=self.committer,
331
message=self.message,
332
inventory_sha1=self.inv_sha1,
333
revision_id=self.rev_id,
334
properties=self.revprops)
335
self.rev.parent_ids = self.parents
337
serializer_v5.write_revision(self.rev, rev_tmp)
339
if self.config.signature_needed():
340
plaintext = Testament(self.rev, self.new_inv).as_short_text()
341
self.branch.store_revision_signature(gpg.GPGStrategy(self.config),
342
plaintext, self.rev_id)
343
self.branch.revision_store.add(rev_tmp, self.rev_id)
344
mutter('new revision_id is {%s}', self.rev_id)
511
if self.branch.repository.has_revision(revision):
512
mutter('commit parent revision {%s}', revision)
513
inventory = self.branch.repository.get_inventory(revision)
514
self.parent_invs.append(inventory)
516
mutter('commit parent ghost revision {%s}', revision)
346
518
def _remove_deleted(self):
347
519
"""Remove deleted files from the working inventories.
401
551
None; inventory entries that are carried over untouched have their
402
552
revision set to their prior value.
554
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
555
# results to create a new inventory at the same time, which results
556
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
404
558
mutter("Selecting files for commit with filter %s", self.specific_files)
405
self.new_inv = Inventory()
406
for path, new_ie in self.work_inv.iter_entries():
559
assert self.work_inv.root is not None
560
entries = self.work_inv.iter_entries()
561
if not self.builder.record_root_entry:
562
symbol_versioning.warn('CommitBuilders should support recording'
563
' the root entry as of bzr 0.10.', DeprecationWarning,
565
self.builder.new_inventory.add(self.basis_inv.root.copy())
567
self._emit_progress_update()
568
for path, new_ie in entries:
569
self._emit_progress_update()
407
570
file_id = new_ie.file_id
408
mutter('check %s {%s}', path, new_ie.file_id)
409
if self.specific_files:
410
if not is_inside_any(self.specific_files, path):
411
mutter('%s not selected for commit', path)
412
self._carry_entry(file_id)
571
# mutter('check %s {%s}', path, file_id)
572
if (not self.specific_files or
573
is_inside_or_parent_of_any(self.specific_files, path)):
574
# mutter('%s selected for commit', path)
578
# mutter('%s not selected for commit', path)
579
if self.basis_inv.has_id(file_id):
580
ie = self.basis_inv[file_id].copy()
582
# this entry is new and not being committed
415
# this is selected, ensure its parents are too.
416
parent_id = new_ie.parent_id
417
while parent_id != ROOT_ID:
418
if not self.new_inv.has_id(parent_id):
419
ie = self._select_entry(self.work_inv[parent_id])
420
mutter('%s selected for commit because of %s',
421
self.new_inv.id2path(parent_id), path)
423
ie = self.new_inv[parent_id]
424
if ie.revision is not None:
426
mutter('%s selected for commit because of %s',
427
self.new_inv.id2path(parent_id), path)
428
parent_id = ie.parent_id
429
mutter('%s selected for commit', path)
430
self._select_entry(new_ie)
432
def _select_entry(self, new_ie):
433
"""Make new_ie be considered for committing."""
439
def _carry_entry(self, file_id):
440
"""Carry the file unchanged from the basis revision."""
441
if self.basis_inv.has_id(file_id):
442
self.new_inv.add(self.basis_inv[file_id].copy())
584
self.builder.record_entry_contents(ie, self.parent_invs,
585
path, self.work_tree)
586
# describe the nature of the change that has occurred relative to
587
# the basis inventory.
588
if (self.basis_inv.has_id(ie.file_id)):
589
basis_ie = self.basis_inv[ie.file_id]
592
change = ie.describe_change(basis_ie, ie)
593
if change in (InventoryEntry.RENAMED,
594
InventoryEntry.MODIFIED_AND_RENAMED):
595
old_path = self.basis_inv.id2path(ie.file_id)
596
self.reporter.renamed(change, old_path, path)
598
self.reporter.snapshot_change(change, path)
600
if not self.specific_files:
603
# ignore removals that don't match filespec
604
for path, new_ie in self.basis_inv.iter_entries():
605
if new_ie.file_id in self.work_inv:
607
if is_inside_any(self.specific_files, path):
611
self.builder.record_entry_contents(ie, self.parent_invs, path,
614
def _emit_progress_update(self):
615
"""Emit an update to the progress bar."""
616
self.pb.update("Committing", self.pb_count, self.pb_total)
444
619
def _report_deletes(self):
445
for file_id in self.basis_inv:
446
if file_id not in self.new_inv:
447
self.reporter.deleted(self.basis_inv.id2path(file_id))
449
def _gen_revision_id(config, when):
450
"""Return new revision-id."""
451
s = '%s-%s-' % (config.user_email(), compact_date(when))
452
s += hexlify(rand_bytes(8))
620
for path, ie in self.basis_inv.iter_entries():
621
if ie.file_id not in self.builder.new_inventory:
622
self.reporter.deleted(path)