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, InventoryEntry
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):
236
218
self.work_tree = working_tree
237
219
self.branch = self.work_tree.branch
239
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().")
241
self.weave_store = self.branch.repository.weave_store
242
232
self.bound_branch = None
243
233
self.local = local
244
234
self.master_branch = None
245
235
self.master_locked = False
247
237
self.specific_files = specific_files
248
238
self.allow_pointless = allow_pointless
250
if revprops is not None:
251
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
253
250
if reporter is None and self.reporter is None:
254
251
self.reporter = NullCommitReporter()
266
265
self._check_bound_branch()
268
267
# check for out of date working trees
269
# if we are bound, then self.branch is the master branch and this
270
# test is thus all we need.
271
if self.work_tree.last_revision() != self.master_branch.last_revision():
272
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.
275
286
# raise an exception as soon as we find a single unknown.
276
287
for unknown in self.work_tree.unknowns():
277
288
raise StrictCommitFailed()
279
if timestamp is None:
280
self.timestamp = time.time()
282
self.timestamp = long(timestamp)
284
290
if self.config is None:
285
self.config = bzrlib.config.BranchConfig(self.branch)
288
self.rev_id = _gen_revision_id(self.config, self.timestamp)
292
if committer is None:
293
self.committer = self.config.username()
295
assert isinstance(committer, basestring), type(committer)
296
self.committer = committer
299
self.timezone = local_time_offset()
301
self.timezone = int(timezone)
303
if isinstance(message, str):
304
message = message.decode(bzrlib.user_encoding)
305
assert isinstance(message, unicode), type(message)
306
self.message = message
307
self._escape_commit_message()
291
self.config = self.branch.get_config()
309
293
self.work_inv = self.work_tree.inventory
310
self.basis_tree = self.work_tree.basis_tree()
311
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])
312
302
# one to finish, one for rev and inventory, and one for each
313
303
# inventory entry, and the same for the new inventory.
314
304
# note that this estimate is too long when we do a partial tree
315
305
# commit which excludes some new files from being considered.
316
306
# The estimate is corrected when we populate the new inv.
317
self.pb_total = len(self.basis_inv) + len(self.work_inv) + 3 - 1
307
self.pb_total = len(self.work_inv) + 5
318
308
self.pb_count = 0
320
310
self._gather_parents()
321
311
if len(self.parents) > 1 and self.specific_files:
322
312
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
323
313
self.specific_files)
324
self._check_parents_present()
315
self.builder = self.branch.get_commit_builder(self.parents,
316
self.config, timestamp, timezone, committer, revprops, rev_id)
326
318
self._remove_deleted()
327
319
self._populate_new_inv()
328
self._store_snapshot()
329
320
self._report_deletes()
331
if not (self.allow_pointless
332
or len(self.parents) > 1
333
or self.new_inv != self.basis_inv):
334
raise PointlessCommit()
336
self._emit_progress_update()
337
self.inv_sha1 = self.branch.repository.add_inventory(
342
self._emit_progress_update()
343
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()
344
339
# revision data is in the local branch now.
346
341
# upload revision data to the master.
347
# this will propogate merged revisions too if needed.
342
# this will propagate merged revisions too if needed.
348
343
if self.bound_branch:
349
344
self.master_branch.repository.fetch(self.branch.repository,
350
345
revision_id=self.rev_id)
351
346
# now the master has the revision data
352
347
# 'commit' to the master first so a timeout here causes the local
353
348
# branch to be out of date
354
self.master_branch.append_revision(self.rev_id)
349
self.master_branch.set_last_revision_info(new_revno,
356
352
# and now do the commit locally.
357
self.branch.append_revision(self.rev_id)
353
self.branch.set_last_revision_info(new_revno, self.rev_id)
359
self.work_tree.set_pending_merges([])
360
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)])
361
357
# now the work tree is up to date with the branch
363
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
364
362
if self.config.post_commit() is not None:
365
363
hooks = self.config.post_commit().split(' ')
366
364
# this would be nicer with twisted.python.reflect.namedAny
369
367
{'branch':self.branch,
371
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,
372
387
self._emit_progress_update()
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()
376
447
def _check_bound_branch(self):
377
448
"""Check to see if the local branch is bound.
479
543
def _gather_parents(self):
480
544
"""Record the parents of a merge for merge detection."""
481
pending_merges = self.work_tree.pending_merges()
483
self.parent_invs = []
484
self.present_parents = []
485
precursor_id = self.branch.last_revision()
487
self.parents.append(precursor_id)
488
self.parents += pending_merges
489
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:]:
490
550
if self.branch.repository.has_revision(revision):
551
mutter('commit parent revision {%s}', revision)
491
552
inventory = self.branch.repository.get_inventory(revision)
492
553
self.parent_invs.append(inventory)
493
self.present_parents.append(revision)
495
def _check_parents_present(self):
496
for parent_id in self.parents:
497
mutter('commit parent revision {%s}', parent_id)
498
if not self.branch.repository.has_revision(parent_id):
499
if parent_id == self.branch.last_revision():
500
warning("parent is missing %r", parent_id)
501
raise HistoryMissing(self.branch, 'revision', parent_id)
503
mutter("commit will ghost revision %r", parent_id)
505
def _make_revision(self):
506
"""Record a new revision object for this commit."""
507
rev = Revision(timestamp=self.timestamp,
508
timezone=self.timezone,
509
committer=self.committer,
510
message=self.message,
511
inventory_sha1=self.inv_sha1,
512
revision_id=self.rev_id,
513
properties=self.revprops)
514
rev.parent_ids = self.parents
515
self.branch.repository.add_revision(self.rev_id, rev, self.new_inv, self.config)
555
mutter('commit parent ghost revision {%s}', revision)
517
557
def _remove_deleted(self):
518
558
"""Remove deleted files from the working inventories.
528
568
specific = self.specific_files
570
deleted_paths = set()
530
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.
531
575
if specific and not is_inside_any(specific, path):
533
577
if not self.work_tree.has_filename(path):
578
deleted_paths.add(path)
534
579
self.reporter.missing(path)
535
deleted_ids.append((path, ie.file_id))
537
deleted_ids.sort(reverse=True)
538
for path, file_id in deleted_ids:
539
del self.work_inv[file_id]
540
self.work_tree._write_inventory(self.work_inv)
542
def _store_snapshot(self):
543
"""Pass over inventory and record a snapshot.
545
Entries get a new revision when they are modified in
546
any way, which includes a merge with a new set of
547
parents that have the same entry.
549
# XXX: Need to think more here about when the user has
550
# made a specific decision on a particular value -- c.f.
553
# iter_entries does not visit the ROOT_ID node so we need to call
554
# self._emit_progress_update once by hand.
555
self._emit_progress_update()
556
for path, ie in self.new_inv.iter_entries():
557
self._emit_progress_update()
558
previous_entries = ie.find_previous_heads(
561
self.branch.repository.get_transaction())
562
if ie.revision is None:
563
# we are creating a new revision for ie in the history store
565
ie.snapshot(self.rev_id, path, previous_entries,
566
self.work_tree, self.weave_store,
567
self.branch.repository.get_transaction())
568
# describe the nature of the change that has occured relative to
569
# the basis inventory.
570
if (self.basis_inv.has_id(ie.file_id)):
571
basis_ie = self.basis_inv[ie.file_id]
574
change = ie.describe_change(basis_ie, ie)
575
if change in (InventoryEntry.RENAMED,
576
InventoryEntry.MODIFIED_AND_RENAMED):
577
old_path = self.basis_inv.id2path(ie.file_id)
578
self.reporter.renamed(change, old_path, path)
580
self.reporter.snapshot_change(change, path)
580
deleted_ids.append(ie.file_id)
581
self.work_tree.unversion(deleted_ids)
582
583
def _populate_new_inv(self):
583
584
"""Build revision inventory.
589
590
None; inventory entries that are carried over untouched have their
590
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?
592
597
mutter("Selecting files for commit with filter %s", self.specific_files)
593
self.new_inv = Inventory(revision_id=self.rev_id)
594
# iter_entries does not visit the ROOT_ID node so we need to call
595
# self._emit_progress_update once by hand.
596
self._emit_progress_update()
597
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:
598
608
self._emit_progress_update()
599
609
file_id = new_ie.file_id
600
mutter('check %s {%s}', path, new_ie.file_id)
601
if self.specific_files:
602
if not is_inside_any(self.specific_files, path):
603
mutter('%s not selected for commit', path)
604
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
607
# this is selected, ensure its parents are too.
608
parent_id = new_ie.parent_id
609
while parent_id != ROOT_ID:
610
if not self.new_inv.has_id(parent_id):
611
ie = self._select_entry(self.work_inv[parent_id])
612
mutter('%s selected for commit because of %s',
613
self.new_inv.id2path(parent_id), path)
615
ie = self.new_inv[parent_id]
616
if ie.revision is not None:
618
mutter('%s selected for commit because of %s',
619
self.new_inv.id2path(parent_id), path)
620
parent_id = ie.parent_id
621
mutter('%s selected for commit', path)
622
self._select_entry(new_ie)
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,
624
685
def _emit_progress_update(self):
625
686
"""Emit an update to the progress bar."""
626
687
self.pb.update("Committing", self.pb_count, self.pb_total)
627
688
self.pb_count += 1
629
def _select_entry(self, new_ie):
630
"""Make new_ie be considered for committing."""
636
def _carry_entry(self, file_id):
637
"""Carry the file unchanged from the basis revision."""
638
if self.basis_inv.has_id(file_id):
639
self.new_inv.add(self.basis_inv[file_id].copy())
641
# this entry is new and not being committed
644
690
def _report_deletes(self):
645
691
for path, ie in self.basis_inv.iter_entries():
646
if ie.file_id not in self.new_inv:
692
if ie.file_id not in self.builder.new_inventory:
647
693
self.reporter.deleted(path)
649
def _gen_revision_id(config, when):
650
"""Return new revision-id."""
651
s = '%s-%s-' % (config.user_email(), compact_date(when))
652
s += hexlify(rand_bytes(8))