62
63
# TODO: If commit fails, leave the message in a file somewhere.
64
# TODO: Change the parameter 'rev_id' to 'revision_id' to be consistent with
65
# the rest of the code; add a deprecation of the old name.
72
from binascii import hexlify
72
73
from cStringIO import StringIO
75
from bzrlib.atomicfile import AtomicFile
76
from bzrlib.osutils import (local_time_offset,
77
rand_bytes, compact_date,
78
kind_marker, is_inside_any, quotefn,
79
sha_string, sha_strings, sha_file, isdir, isfile,
74
81
import bzrlib.config
75
82
import bzrlib.errors as errors
76
83
from bzrlib.errors import (BzrError, PointlessCommit,
80
from bzrlib.osutils import (kind_marker, isdir,isfile, is_inside_any,
81
is_inside_or_parent_of_any,
82
quotefn, sha_file, split_lines)
88
import bzrlib.gpg as gpg
89
from bzrlib.revision import Revision
83
90
from bzrlib.testament import Testament
84
91
from bzrlib.trace import mutter, note, warning
85
92
from bzrlib.xml5 import serializer_v5
86
from bzrlib.inventory import Inventory, ROOT_ID, InventoryEntry
87
from bzrlib import symbol_versioning
88
from bzrlib.symbol_versioning import (deprecated_passed,
93
from bzrlib.inventory import Inventory, ROOT_ID
94
from bzrlib.symbol_versioning import *
95
from bzrlib.weave import Weave
96
from bzrlib.weavefile import read_weave, write_weave_v5
91
97
from bzrlib.workingtree import WorkingTree
100
@deprecated_function(zero_seven)
101
def commit(*args, **kwargs):
102
"""Commit a new revision to a branch.
104
Function-style interface for convenience of old callers.
106
New code should use the Commit class instead.
108
## XXX: Remove this in favor of Branch.commit?
109
Commit().commit(*args, **kwargs)
94
112
class NullCommitReporter(object):
95
113
"""I report on progress of a commit."""
109
127
def missing(self, path):
112
def renamed(self, change, old_path, new_path):
116
131
class ReportCommitToLog(NullCommitReporter):
118
# this may be more useful if 'note' was replaced by an overridable
119
# method on self, which would allow more trivial subclassing.
120
# alternative, a callable could be passed in, allowing really trivial
121
# reuse for some uis. RBC 20060511
123
133
def snapshot_change(self, change, path):
124
if change == 'unchanged':
126
134
note("%s %s", change, path)
128
136
def completed(self, revno, rev_id):
129
note('Committed revision %d.', revno)
137
note('committed r%d {%s}', revno, rev_id)
131
139
def deleted(self, file_id):
132
140
note('deleted %s', file_id)
223
224
if message is None:
224
225
raise BzrError("The message keyword parameter is required for commit().")
226
self.bound_branch = None
228
self.master_branch = None
229
self.master_locked = False
227
self.weave_store = self.branch.repository.weave_store
231
229
self.specific_files = specific_files
232
230
self.allow_pointless = allow_pointless
234
if reporter is None and self.reporter is None:
235
self.reporter = NullCommitReporter()
236
elif reporter is not None:
237
self.reporter = reporter
239
self.work_tree.lock_write()
240
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
231
self.revprops = {'branch-nick': self.branch.nick}
233
self.revprops.update(revprops)
235
# check for out of date working trees
236
if self.work_tree.last_revision() != self.branch.last_revision():
237
raise errors.OutOfDateTree(self.work_tree)
240
# raise an exception as soon as we find a single unknown.
241
for unknown in self.work_tree.unknowns():
242
raise StrictCommitFailed()
244
if timestamp is None:
245
self.timestamp = time.time()
247
self.timestamp = long(timestamp)
249
if self.config is None:
250
self.config = bzrlib.config.BranchConfig(self.branch)
253
self.rev_id = _gen_revision_id(self.config, self.timestamp)
257
if committer is None:
258
self.committer = self.config.username()
260
assert isinstance(committer, basestring), type(committer)
261
self.committer = committer
264
self.timezone = local_time_offset()
266
self.timezone = int(timezone)
268
if isinstance(message, str):
269
message = message.decode(bzrlib.user_encoding)
270
assert isinstance(message, unicode), type(message)
271
self.message = message
272
self._escape_commit_message()
274
self.branch.lock_write()
242
# Cannot commit with conflicts present.
243
if len(self.work_tree.conflicts())>0:
244
raise ConflictsInTree
246
# setup the bound branch variables as needed.
247
self._check_bound_branch()
249
# check for out of date working trees
251
first_tree_parent = self.work_tree.get_parent_ids()[0]
253
# if there are no parents, treat our parent as 'None'
254
# this is so that we still consier the master branch
255
# - in a checkout scenario the tree may have no
256
# parents but the branch may do.
257
first_tree_parent = None
258
master_last = self.master_branch.last_revision()
259
if (master_last is not None and
260
master_last != first_tree_parent):
261
raise errors.OutOfDateTree(self.work_tree)
264
# raise an exception as soon as we find a single unknown.
265
for unknown in self.work_tree.unknowns():
266
raise StrictCommitFailed()
268
if self.config is None:
269
self.config = self.branch.get_config()
271
if isinstance(message, str):
272
message = message.decode(bzrlib.user_encoding)
273
assert isinstance(message, unicode), type(message)
274
self.message = message
275
self._escape_commit_message()
277
276
self.work_inv = self.work_tree.inventory
278
277
self.basis_tree = self.work_tree.basis_tree()
279
278
self.basis_inv = self.basis_tree.inventory
280
# one to finish, one for rev and inventory, and one for each
281
# inventory entry, and the same for the new inventory.
282
# note that this estimate is too long when we do a partial tree
283
# commit which excludes some new files from being considered.
284
# The estimate is corrected when we populate the new inv.
285
self.pb_total = len(self.work_inv) + 5
288
280
self._gather_parents()
289
281
if len(self.parents) > 1 and self.specific_files:
290
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
293
self.builder = self.branch.get_commit_builder(self.parents,
294
self.config, timestamp, timezone, committer, revprops, rev_id)
282
raise NotImplementedError('selected-file commit of merges is not supported yet')
283
self._check_parents_present()
296
285
self._remove_deleted()
297
286
self._populate_new_inv()
287
self._store_snapshot()
298
288
self._report_deletes()
300
self._check_pointless()
302
self._emit_progress_update()
303
# TODO: Now the new inventory is known, check for conflicts and
304
# prompt the user for a commit message.
305
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
306
# weave lines, because nothing should be recorded until it is known
307
# that commit will succeed.
308
self.builder.finish_inventory()
309
self._emit_progress_update()
310
self.rev_id = self.builder.commit(self.message)
311
self._emit_progress_update()
312
# revision data is in the local branch now.
314
# upload revision data to the master.
315
# this will propagate merged revisions too if needed.
316
if self.bound_branch:
317
self.master_branch.repository.fetch(self.branch.repository,
318
revision_id=self.rev_id)
319
# now the master has the revision data
320
# 'commit' to the master first so a timeout here causes the local
321
# branch to be out of date
322
self.master_branch.append_revision(self.rev_id)
324
# and now do the commit locally.
290
if not (self.allow_pointless
291
or len(self.parents) > 1
292
or self.new_inv != self.basis_inv):
293
raise PointlessCommit()
295
if len(list(self.work_tree.iter_conflicts()))>0:
296
raise ConflictsInTree
298
self._record_inventory()
299
self._make_revision()
300
self.work_tree.set_pending_merges([])
325
301
self.branch.append_revision(self.rev_id)
327
# if the builder gave us the revisiontree it created back, we
328
# could use it straight away here.
329
# TODO: implement this.
330
self.work_tree.set_parent_trees([(self.rev_id,
331
self.branch.repository.revision_tree(self.rev_id))])
332
# now the work tree is up to date with the branch
334
self.reporter.completed(self.branch.revno(), self.rev_id)
302
if len(self.parents):
303
precursor = self.parents[0]
306
self.work_tree.set_last_revision(self.rev_id, precursor)
307
self.reporter.completed(self.branch.revno()+1, self.rev_id)
335
308
if self.config.post_commit() is not None:
336
309
hooks = self.config.post_commit().split(' ')
337
310
# this would be nicer with twisted.python.reflect.namedAny
340
313
{'branch':self.branch,
342
315
'rev_id':self.rev_id})
343
self._emit_progress_update()
348
def _check_pointless(self):
349
if self.allow_pointless:
351
# A merge with no effect on files
352
if len(self.parents) > 1:
354
# work around the fact that a newly-initted tree does differ from its
356
if len(self.builder.new_inventory) != len(self.basis_inv):
358
if (len(self.builder.new_inventory) != 1 and
359
self.builder.new_inventory != self.basis_inv):
361
raise PointlessCommit()
363
def _check_bound_branch(self):
364
"""Check to see if the local branch is bound.
366
If it is bound, then most of the commit will actually be
367
done using the remote branch as the target branch.
368
Only at the end will the local branch be updated.
370
if self.local and not self.branch.get_bound_location():
371
raise errors.LocalRequiresBoundBranch()
374
self.master_branch = self.branch.get_master_branch()
376
if not self.master_branch:
377
# make this branch the reference branch for out of date checks.
378
self.master_branch = self.branch
381
# If the master branch is bound, we must fail
382
master_bound_location = self.master_branch.get_bound_location()
383
if master_bound_location:
384
raise errors.CommitToDoubleBoundBranch(self.branch,
385
self.master_branch, master_bound_location)
387
# TODO: jam 20051230 We could automatically push local
388
# commits to the remote branch if they would fit.
389
# But for now, just require remote to be identical
392
# Make sure the local branch is identical to the master
393
master_rh = self.master_branch.revision_history()
394
local_rh = self.branch.revision_history()
395
if local_rh != master_rh:
396
raise errors.BoundBranchOutOfDate(self.branch,
399
# Now things are ready to change the master branch
401
self.bound_branch = self.branch
402
self.master_branch.lock_write()
403
self.master_locked = True
406
"""Cleanup any open locks, progress bars etc."""
407
cleanups = [self._cleanup_bound_branch,
408
self.work_tree.unlock,
410
found_exception = None
411
for cleanup in cleanups:
414
# we want every cleanup to run no matter what.
415
# so we have a catchall here, but we will raise the
416
# last encountered exception up the stack: and
417
# typically this will be useful enough.
420
if found_exception is not None:
421
# don't do a plan raise, because the last exception may have been
422
# trashed, e is our sure-to-work exception even though it loses the
423
# full traceback. XXX: RBC 20060421 perhaps we could check the
424
# exc_info and if its the same one do a plain raise otherwise
425
# 'raise e' as we do now.
428
def _cleanup_bound_branch(self):
429
"""Executed at the end of a try/finally to cleanup a bound branch.
431
If the branch wasn't bound, this is a no-op.
432
If it was, it resents self.branch to the local branch, instead
435
if not self.bound_branch:
437
if self.master_locked:
438
self.master_branch.unlock()
319
def _record_inventory(self):
320
"""Store the inventory for the new revision."""
321
inv_text = serializer_v5.write_inventory_to_string(self.new_inv)
322
self.inv_sha1 = sha_string(inv_text)
323
s = self.branch.repository.control_weaves
324
s.add_text('inventory', self.rev_id,
325
split_lines(inv_text), self.present_parents,
326
self.branch.get_transaction())
440
328
def _escape_commit_message(self):
441
329
"""Replace xml-incompatible control characters."""
442
# FIXME: RBC 20060419 this should be done by the revision
443
# serialiser not by commit. Then we can also add an unescaper
444
# in the deserializer and start roundtripping revision messages
445
# precisely. See repository_implementations/test_repository.py
447
330
# Python strings can include characters that can't be
448
331
# represented in well-formed XML; escape characters that
449
332
# aren't listed in the XML specification
458
341
def _gather_parents(self):
459
342
"""Record the parents of a merge for merge detection."""
460
# TODO: Make sure that this list doesn't contain duplicate
461
# entries and the order is preserved when doing this.
462
self.parents = self.work_tree.get_parent_ids()
343
pending_merges = self.work_tree.pending_merges()
463
345
self.parent_invs = []
346
self.present_parents = []
347
precursor_id = self.branch.last_revision()
349
self.parents.append(precursor_id)
350
self.parents += pending_merges
464
351
for revision in self.parents:
465
352
if self.branch.repository.has_revision(revision):
466
mutter('commit parent revision {%s}', revision)
467
353
inventory = self.branch.repository.get_inventory(revision)
468
354
self.parent_invs.append(inventory)
470
mutter('commit parent ghost revision {%s}', revision)
355
self.present_parents.append(revision)
357
def _check_parents_present(self):
358
for parent_id in self.parents:
359
mutter('commit parent revision {%s}', parent_id)
360
if not self.branch.repository.has_revision(parent_id):
361
if parent_id == self.branch.last_revision():
362
warning("parent is missing %r", parent_id)
363
raise HistoryMissing(self.branch, 'revision', parent_id)
365
mutter("commit will ghost revision %r", parent_id)
367
def _make_revision(self):
368
"""Record a new revision object for this commit."""
369
self.rev = Revision(timestamp=self.timestamp,
370
timezone=self.timezone,
371
committer=self.committer,
372
message=self.message,
373
inventory_sha1=self.inv_sha1,
374
revision_id=self.rev_id,
375
properties=self.revprops)
376
self.rev.parent_ids = self.parents
378
serializer_v5.write_revision(self.rev, rev_tmp)
380
if self.config.signature_needed():
381
plaintext = Testament(self.rev, self.new_inv).as_short_text()
382
self.branch.repository.store_revision_signature(
383
gpg.GPGStrategy(self.config), plaintext, self.rev_id)
384
self.branch.repository.revision_store.add(rev_tmp, self.rev_id)
385
mutter('new revision_id is {%s}', self.rev_id)
472
387
def _remove_deleted(self):
473
388
"""Remove deleted files from the working inventories.
483
398
specific = self.specific_files
485
deleted_paths = set()
486
400
for path, ie in self.work_inv.iter_entries():
487
if is_inside_any(deleted_paths, path):
488
# The tree will delete the required ids recursively.
490
401
if specific and not is_inside_any(specific, path):
492
403
if not self.work_tree.has_filename(path):
493
deleted_paths.add(path)
494
404
self.reporter.missing(path)
495
deleted_ids.append(ie.file_id)
496
self.work_tree.unversion(deleted_ids)
405
deleted_ids.append((path, ie.file_id))
407
deleted_ids.sort(reverse=True)
408
for path, file_id in deleted_ids:
409
del self.work_inv[file_id]
410
self.work_tree._write_inventory(self.work_inv)
412
def _store_snapshot(self):
413
"""Pass over inventory and record a snapshot.
415
Entries get a new revision when they are modified in
416
any way, which includes a merge with a new set of
417
parents that have the same entry.
419
# XXX: Need to think more here about when the user has
420
# made a specific decision on a particular value -- c.f.
422
for path, ie in self.new_inv.iter_entries():
423
previous_entries = ie.find_previous_heads(
425
self.weave_store.get_weave_prelude_or_empty(ie.file_id,
426
self.branch.get_transaction()))
427
if ie.revision is None:
428
change = ie.snapshot(self.rev_id, path, previous_entries,
429
self.work_tree, self.weave_store,
430
self.branch.get_transaction())
433
self.reporter.snapshot_change(change, path)
498
435
def _populate_new_inv(self):
499
436
"""Build revision inventory.
505
442
None; inventory entries that are carried over untouched have their
506
443
revision set to their prior value.
508
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
509
# results to create a new inventory at the same time, which results
510
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
512
445
mutter("Selecting files for commit with filter %s", self.specific_files)
513
entries = self.work_inv.iter_entries()
514
if not self.builder.record_root_entry:
515
symbol_versioning.warn('CommitBuilders should support recording'
516
' the root entry as of bzr 0.10.', DeprecationWarning,
518
self.builder.new_inventory.add(self.basis_inv.root.copy())
520
self._emit_progress_update()
521
for path, new_ie in entries:
522
self._emit_progress_update()
446
self.new_inv = Inventory()
447
for path, new_ie in self.work_inv.iter_entries():
523
448
file_id = new_ie.file_id
524
# mutter('check %s {%s}', path, file_id)
525
if (not self.specific_files or
526
is_inside_or_parent_of_any(self.specific_files, path)):
527
# mutter('%s selected for commit', path)
531
# mutter('%s not selected for commit', path)
532
if self.basis_inv.has_id(file_id):
533
ie = self.basis_inv[file_id].copy()
535
# this entry is new and not being committed
449
mutter('check %s {%s}', path, new_ie.file_id)
450
if self.specific_files:
451
if not is_inside_any(self.specific_files, path):
452
mutter('%s not selected for commit', path)
453
self._carry_entry(file_id)
538
self.builder.record_entry_contents(ie, self.parent_invs,
539
path, self.work_tree)
540
# describe the nature of the change that has occurred relative to
541
# the basis inventory.
542
if (self.basis_inv.has_id(ie.file_id)):
543
basis_ie = self.basis_inv[ie.file_id]
546
change = ie.describe_change(basis_ie, ie)
547
if change in (InventoryEntry.RENAMED,
548
InventoryEntry.MODIFIED_AND_RENAMED):
549
old_path = self.basis_inv.id2path(ie.file_id)
550
self.reporter.renamed(change, old_path, path)
552
self.reporter.snapshot_change(change, path)
554
if not self.specific_files:
557
# ignore removals that don't match filespec
558
for path, new_ie in self.basis_inv.iter_entries():
559
if new_ie.file_id in self.work_inv:
561
if is_inside_any(self.specific_files, path):
565
self.builder.record_entry_contents(ie, self.parent_invs, path,
568
def _emit_progress_update(self):
569
"""Emit an update to the progress bar."""
570
self.pb.update("Committing", self.pb_count, self.pb_total)
456
# this is selected, ensure its parents are too.
457
parent_id = new_ie.parent_id
458
while parent_id != ROOT_ID:
459
if not self.new_inv.has_id(parent_id):
460
ie = self._select_entry(self.work_inv[parent_id])
461
mutter('%s selected for commit because of %s',
462
self.new_inv.id2path(parent_id), path)
464
ie = self.new_inv[parent_id]
465
if ie.revision is not None:
467
mutter('%s selected for commit because of %s',
468
self.new_inv.id2path(parent_id), path)
469
parent_id = ie.parent_id
470
mutter('%s selected for commit', path)
471
self._select_entry(new_ie)
473
def _select_entry(self, new_ie):
474
"""Make new_ie be considered for committing."""
480
def _carry_entry(self, file_id):
481
"""Carry the file unchanged from the basis revision."""
482
if self.basis_inv.has_id(file_id):
483
self.new_inv.add(self.basis_inv[file_id].copy())
573
485
def _report_deletes(self):
574
for path, ie in self.basis_inv.iter_entries():
575
if ie.file_id not in self.builder.new_inventory:
576
self.reporter.deleted(path)
486
for file_id in self.basis_inv:
487
if file_id not in self.new_inv:
488
self.reporter.deleted(self.basis_inv.id2path(file_id))
490
def _gen_revision_id(config, when):
491
"""Return new revision-id."""
492
s = '%s-%s-' % (config.user_email(), compact_date(when))
493
s += hexlify(rand_bytes(8))