56
46
# merges from, then it should still be reported as newly added
57
47
# relative to the basis revision.
59
# TODO: Do checks that the tree can be committed *before* running the
60
# editor; this should include checks for a pointless commit and for
61
# unknown or missing files.
63
# TODO: If commit fails, leave the message in a file somewhere.
49
# TODO: Change the parameter 'rev_id' to 'revision_id' to be consistent with
50
# the rest of the code; add a deprecation of the old name.
72
from binascii import hexlify
73
57
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_file, isdir, isfile,
64
from bzrlib.branch import Branch
81
65
import bzrlib.config
82
import bzrlib.errors as errors
83
66
from bzrlib.errors import (BzrError, PointlessCommit,
88
from bzrlib.revision import Revision
70
from bzrlib.osutils import (kind_marker, isdir,isfile, is_inside_any,
71
is_inside_or_parent_of_any,
72
quotefn, sha_file, split_lines)
89
73
from bzrlib.testament import Testament
90
74
from bzrlib.trace import mutter, note, warning
91
75
from bzrlib.xml5 import serializer_v5
92
from bzrlib.inventory import Inventory, ROOT_ID
93
from bzrlib.symbol_versioning import *
76
from bzrlib.inventory import Inventory, InventoryEntry
77
from bzrlib import symbol_versioning
78
from bzrlib.symbol_versioning import (deprecated_passed,
94
81
from bzrlib.workingtree import WorkingTree
97
@deprecated_function(zero_seven)
98
def commit(*args, **kwargs):
99
"""Commit a new revision to a branch.
101
Function-style interface for convenience of old callers.
103
New code should use the Commit class instead.
105
## XXX: Remove this in favor of Branch.commit?
106
Commit().commit(*args, **kwargs)
109
85
class NullCommitReporter(object):
225
218
self.work_tree = working_tree
226
219
self.branch = self.work_tree.branch
228
raise BzrError("The message keyword parameter is required for commit().")
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().")
230
self.weave_store = self.branch.repository.weave_store
231
232
self.bound_branch = None
232
233
self.local = local
233
234
self.master_branch = None
234
235
self.master_locked = False
236
237
self.specific_files = specific_files
237
238
self.allow_pointless = allow_pointless
239
if revprops is not None:
240
self.revprops.update(revprops)
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
242
250
if reporter is None and self.reporter is None:
243
251
self.reporter = NullCommitReporter()
245
253
self.reporter = reporter
247
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()
260
# Cannot commit with conflicts present.
261
if len(self.work_tree.conflicts())>0:
262
raise ConflictsInTree
249
264
# setup the bound branch variables as needed.
250
265
self._check_bound_branch()
252
267
# check for out of date working trees
253
# if we are bound, then self.branch is the master branch and this
254
# test is thus all we need.
255
if self.work_tree.last_revision() != self.master_branch.last_revision():
256
raise errors.OutOfDateTree(self.work_tree)
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.
259
286
# raise an exception as soon as we find a single unknown.
260
287
for unknown in self.work_tree.unknowns():
261
288
raise StrictCommitFailed()
263
if timestamp is None:
264
self.timestamp = time.time()
266
self.timestamp = long(timestamp)
268
290
if self.config is None:
269
self.config = bzrlib.config.BranchConfig(self.branch)
272
self.rev_id = _gen_revision_id(self.config, self.timestamp)
276
if committer is None:
277
self.committer = self.config.username()
279
assert isinstance(committer, basestring), type(committer)
280
self.committer = committer
283
self.timezone = local_time_offset()
285
self.timezone = int(timezone)
287
if isinstance(message, str):
288
message = message.decode(bzrlib.user_encoding)
289
assert isinstance(message, unicode), type(message)
290
self.message = message
291
self._escape_commit_message()
291
self.config = self.branch.get_config()
293
293
self.work_inv = self.work_tree.inventory
294
self.basis_tree = self.work_tree.basis_tree()
295
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
297
310
self._gather_parents()
298
311
if len(self.parents) > 1 and self.specific_files:
299
raise NotImplementedError('selected-file commit of merges is not supported yet')
300
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)
302
318
self._remove_deleted()
303
319
self._populate_new_inv()
304
self._store_snapshot()
305
320
self._report_deletes()
307
if not (self.allow_pointless
308
or len(self.parents) > 1
309
or self.new_inv != self.basis_inv):
310
raise PointlessCommit()
312
if len(self.work_tree.conflicts())>0:
313
raise ConflictsInTree
315
self.inv_sha1 = self.branch.repository.add_inventory(
320
self._make_revision()
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()
321
339
# revision data is in the local branch now.
323
341
# upload revision data to the master.
324
# this will propogate merged revisions too if needed.
342
# this will propagate merged revisions too if needed.
325
343
if self.bound_branch:
326
344
self.master_branch.repository.fetch(self.branch.repository,
327
345
revision_id=self.rev_id)
328
346
# now the master has the revision data
329
347
# 'commit' to the master first so a timeout here causes the local
330
348
# branch to be out of date
331
self.master_branch.append_revision(self.rev_id)
349
self.master_branch.set_last_revision_info(new_revno,
333
352
# and now do the commit locally.
334
self.branch.append_revision(self.rev_id)
353
self.branch.set_last_revision_info(new_revno, self.rev_id)
336
self.work_tree.set_pending_merges([])
337
self.work_tree.set_last_revision(self.rev_id)
355
rev_tree = self.builder.revision_tree()
356
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
338
357
# now the work tree is up to date with the branch
340
self.reporter.completed(self.branch.revno(), self.rev_id)
359
self.reporter.completed(new_revno, self.rev_id)
360
# old style commit hooks - should be deprecated ? (obsoleted in
341
362
if self.config.post_commit() is not None:
342
363
hooks = self.config.post_commit().split(' ')
343
364
# this would be nicer with twisted.python.reflect.namedAny
346
367
{'branch':self.branch,
348
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()
350
self._cleanup_bound_branch()
351
self.work_tree.unlock()
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()
353
447
def _check_bound_branch(self):
354
448
"""Check to see if the local branch is bound.
391
485
self.bound_branch = self.branch
392
486
self.master_branch.lock_write()
393
487
self.master_locked = True
395
#### # Check to see if we have any pending merges. If we do
396
#### # those need to be pushed into the master branch
397
#### pending_merges = self.work_tree.pending_merges()
398
#### if pending_merges:
399
#### for revision_id in pending_merges:
400
#### self.master_branch.repository.fetch(self.bound_branch.repository,
401
#### revision_id=revision_id)
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.
403
513
def _cleanup_bound_branch(self):
404
514
"""Executed at the end of a try/finally to cleanup a bound branch.
428
543
def _gather_parents(self):
429
544
"""Record the parents of a merge for merge detection."""
430
pending_merges = self.work_tree.pending_merges()
432
self.parent_invs = []
433
self.present_parents = []
434
precursor_id = self.branch.last_revision()
436
self.parents.append(precursor_id)
437
self.parents += pending_merges
438
for revision in self.parents:
545
# TODO: Make sure that this list doesn't contain duplicate
546
# entries and the order is preserved when doing this.
547
self.parents = self.work_tree.get_parent_ids()
548
self.parent_invs = [self.basis_inv]
549
for revision in self.parents[1:]:
439
550
if self.branch.repository.has_revision(revision):
551
mutter('commit parent revision {%s}', revision)
440
552
inventory = self.branch.repository.get_inventory(revision)
441
553
self.parent_invs.append(inventory)
442
self.present_parents.append(revision)
444
def _check_parents_present(self):
445
for parent_id in self.parents:
446
mutter('commit parent revision {%s}', parent_id)
447
if not self.branch.repository.has_revision(parent_id):
448
if parent_id == self.branch.last_revision():
449
warning("parent is missing %r", parent_id)
450
raise HistoryMissing(self.branch, 'revision', parent_id)
452
mutter("commit will ghost revision %r", parent_id)
454
def _make_revision(self):
455
"""Record a new revision object for this commit."""
456
rev = Revision(timestamp=self.timestamp,
457
timezone=self.timezone,
458
committer=self.committer,
459
message=self.message,
460
inventory_sha1=self.inv_sha1,
461
revision_id=self.rev_id,
462
properties=self.revprops)
463
rev.parent_ids = self.parents
464
self.branch.repository.add_revision(self.rev_id, rev, self.new_inv, self.config)
555
mutter('commit parent ghost revision {%s}', revision)
466
557
def _remove_deleted(self):
467
558
"""Remove deleted files from the working inventories.
477
568
specific = self.specific_files
570
deleted_paths = set()
479
571
for path, ie in self.work_inv.iter_entries():
572
if is_inside_any(deleted_paths, path):
573
# The tree will delete the required ids recursively.
480
575
if specific and not is_inside_any(specific, path):
482
577
if not self.work_tree.has_filename(path):
578
deleted_paths.add(path)
483
579
self.reporter.missing(path)
484
deleted_ids.append((path, ie.file_id))
486
deleted_ids.sort(reverse=True)
487
for path, file_id in deleted_ids:
488
del self.work_inv[file_id]
489
self.work_tree._write_inventory(self.work_inv)
491
def _store_snapshot(self):
492
"""Pass over inventory and record a snapshot.
494
Entries get a new revision when they are modified in
495
any way, which includes a merge with a new set of
496
parents that have the same entry.
498
# XXX: Need to think more here about when the user has
499
# made a specific decision on a particular value -- c.f.
501
for path, ie in self.new_inv.iter_entries():
502
previous_entries = ie.find_previous_heads(
505
self.branch.repository.get_transaction())
506
if ie.revision is None:
507
change = ie.snapshot(self.rev_id, path, previous_entries,
508
self.work_tree, self.weave_store,
509
self.branch.get_transaction())
512
self.reporter.snapshot_change(change, path)
580
deleted_ids.append(ie.file_id)
581
self.work_tree.unversion(deleted_ids)
514
583
def _populate_new_inv(self):
515
584
"""Build revision inventory.
521
590
None; inventory entries that are carried over untouched have their
522
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?
524
597
mutter("Selecting files for commit with filter %s", self.specific_files)
525
self.new_inv = Inventory(revision_id=self.rev_id)
526
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()
527
609
file_id = new_ie.file_id
528
mutter('check %s {%s}', path, new_ie.file_id)
529
if self.specific_files:
530
if not is_inside_any(self.specific_files, path):
531
mutter('%s not selected for commit', path)
532
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
535
# this is selected, ensure its parents are too.
536
parent_id = new_ie.parent_id
537
while parent_id != ROOT_ID:
538
if not self.new_inv.has_id(parent_id):
539
ie = self._select_entry(self.work_inv[parent_id])
540
mutter('%s selected for commit because of %s',
541
self.new_inv.id2path(parent_id), path)
543
ie = self.new_inv[parent_id]
544
if ie.revision is not None:
546
mutter('%s selected for commit because of %s',
547
self.new_inv.id2path(parent_id), path)
548
parent_id = ie.parent_id
549
mutter('%s selected for commit', path)
550
self._select_entry(new_ie)
552
def _select_entry(self, new_ie):
553
"""Make new_ie be considered for committing."""
559
def _carry_entry(self, file_id):
560
"""Carry the file unchanged from the basis revision."""
561
if self.basis_inv.has_id(file_id):
562
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)
564
690
def _report_deletes(self):
565
for file_id in self.basis_inv:
566
if file_id not in self.new_inv:
567
self.reporter.deleted(self.basis_inv.id2path(file_id))
569
def _gen_revision_id(config, when):
570
"""Return new revision-id."""
571
s = '%s-%s-' % (config.user_email(), compact_date(when))
572
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)