15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
# XXX: Can we do any better about making interrupted commits change
19
# nothing? Perhaps the best approach is to integrate commit of
20
# AtomicFiles with releasing the lock on the Branch.
22
# TODO: Separate 'prepare' phase where we find a list of potentially
23
# committed files. We then can then pause the commit to prompt for a
24
# commit message, knowing the summary will be the same as what's
25
# actually used for the commit. (But perhaps simpler to simply get
26
# the tree status, then use that for a selective commit?)
28
18
# The newly committed revision is going to have a shape corresponding
29
19
# to that of the working inventory. Files that are not in the
30
20
# working tree and that were in the predecessor are reported as
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
57
from cStringIO import StringIO
74
from bzrlib.atomicfile import AtomicFile
64
from bzrlib.branch import Branch
75
65
import bzrlib.config
76
import bzrlib.errors as errors
77
66
from bzrlib.errors import (BzrError, PointlessCommit,
85
73
from bzrlib.testament import Testament
86
74
from bzrlib.trace import mutter, note, warning
87
75
from bzrlib.xml5 import serializer_v5
88
from bzrlib.inventory import Inventory, ROOT_ID, InventoryEntry
89
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,
90
81
from bzrlib.workingtree import WorkingTree
93
@deprecated_function(zero_seven)
94
def commit(*args, **kwargs):
95
"""Commit a new revision to a branch.
97
Function-style interface for convenience of old callers.
99
New code should use the Commit class instead.
101
## XXX: Remove this in favor of Branch.commit?
102
Commit().commit(*args, **kwargs)
105
85
class NullCommitReporter(object):
127
107
class ReportCommitToLog(NullCommitReporter):
129
# this may be more useful if 'note' was replaced by an overridable
130
# method on self, which would allow more trivial subclassing.
131
# alternative, a callable could be passed in, allowing really trivial
132
# reuse for some uis. RBC 20060511
109
def _note(self, format, *args):
112
Messages are output by writing directly to stderr instead of
113
using bzrlib.trace.note(). The latter constantly updates the
114
log file as we go causing an unnecessary performance hit.
116
Subclasses may choose to override this method but need to be aware
117
of its potential impact on performance.
119
bzrlib.ui.ui_factory.clear_term()
120
sys.stderr.write((format + "\n") % args)
134
122
def snapshot_change(self, change, path):
135
123
if change == 'unchanged':
137
note("%s %s", change, path)
125
if change == 'added' and path == '':
127
self._note("%s %s", change, path)
139
129
def completed(self, revno, rev_id):
140
note('Committed revision %d.', revno)
130
self._note('Committed revision %d.', revno)
142
132
def deleted(self, file_id):
143
note('deleted %s', file_id)
133
self._note('deleted %s', file_id)
145
135
def escaped(self, escape_count, message):
146
note("replaced %d control characters in message", escape_count)
136
self._note("replaced %d control characters in message", escape_count)
148
138
def missing(self, path):
149
note('missing %s', path)
139
self._note('missing %s', path)
151
141
def renamed(self, change, old_path, new_path):
152
note('%s %s => %s', change, old_path, new_path)
142
self._note('%s %s => %s', change, old_path, new_path)
155
145
class Commit(object):
190
177
working_tree=None,
181
message_callback=None,
194
183
"""Commit working copy as a new revision.
196
branch -- the deprecated branch to commit to. New callers should pass in
199
message -- the commit message, a mandatory parameter
201
timestamp -- if not None, seconds-since-epoch for a
202
postdated/predated commit.
204
specific_files -- If true, commit only those files.
206
rev_id -- If set, use this as the new revision id.
185
:param message: the commit message (it or message_callback is required)
187
:param timestamp: if not None, seconds-since-epoch for a
188
postdated/predated commit.
190
:param specific_files: If true, commit only those files.
192
:param rev_id: If set, use this as the new revision id.
207
193
Useful for test or import commands that need to tightly
208
194
control what revisions are assigned. If you duplicate
209
195
a revision id that exists elsewhere it is your own fault.
210
196
If null (default), a time/random revision id is generated.
212
allow_pointless -- If true (default), commit even if nothing
198
:param allow_pointless: If true (default), commit even if nothing
213
199
has changed and no merges are recorded.
215
strict -- If true, don't allow a commit if the working tree
201
:param strict: If true, don't allow a commit if the working tree
216
202
contains unknown files.
218
revprops -- Properties for new revision
204
:param revprops: Properties for new revision
219
205
:param local: Perform a local only commit.
206
:param recursive: If set to 'down', commit in any subtrees that have
207
pending changes of any sort during this commit.
221
209
mutter('preparing to commit')
223
if deprecated_passed(branch):
224
warn("Commit.commit (branch, ...): The branch parameter is "
225
"deprecated as of bzr 0.8. Please use working_tree= instead.",
226
DeprecationWarning, stacklevel=2)
228
self.work_tree = self.branch.bzrdir.open_workingtree()
229
elif working_tree is None:
230
raise BzrError("One of branch and working_tree must be passed into commit().")
211
if working_tree is None:
212
raise BzrError("working_tree must be passed into commit().")
232
214
self.work_tree = working_tree
233
215
self.branch = self.work_tree.branch
235
raise BzrError("The message keyword parameter is required for commit().")
216
if getattr(self.work_tree, 'requires_rich_root', lambda: False)():
217
if not self.branch.repository.supports_rich_root():
218
raise errors.RootNotRich()
219
if message_callback is None:
220
if message is not None:
221
if isinstance(message, str):
222
message = message.decode(bzrlib.user_encoding)
223
message_callback = lambda x: message
225
raise BzrError("The message or message_callback keyword"
226
" parameter is required for commit().")
237
228
self.bound_branch = None
238
229
self.local = local
250
249
self.work_tree.lock_write()
251
250
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
251
self.basis_tree = self.work_tree.basis_tree()
252
self.basis_tree.lock_read()
253
254
# Cannot commit with conflicts present.
254
if len(self.work_tree.conflicts())>0:
255
if len(self.work_tree.conflicts()) > 0:
255
256
raise ConflictsInTree
257
# setup the bound branch variables as needed.
258
# Setup the bound branch variables as needed.
258
259
self._check_bound_branch()
260
# check for out of date working trees
261
# if we are bound, then self.branch is the master branch and this
262
# test is thus all we need.
263
if self.work_tree.last_revision() != self.master_branch.last_revision():
264
raise errors.OutOfDateTree(self.work_tree)
261
# Check that the working tree is up to date
262
old_revno,new_revno = self._check_out_of_date_tree()
267
265
# raise an exception as soon as we find a single unknown.
268
266
for unknown in self.work_tree.unknowns():
269
267
raise StrictCommitFailed()
271
269
if self.config is None:
272
self.config = bzrlib.config.BranchConfig(self.branch)
274
if isinstance(message, str):
275
message = message.decode(bzrlib.user_encoding)
276
assert isinstance(message, unicode), type(message)
277
self.message = message
278
self._escape_commit_message()
270
self.config = self.branch.get_config()
280
272
self.work_inv = self.work_tree.inventory
281
self.basis_tree = self.work_tree.basis_tree()
282
273
self.basis_inv = self.basis_tree.inventory
274
if specific_files is not None:
275
# Ensure specified files are versioned
276
# (We don't actually need the ids here)
277
# XXX: Dont we have filter_unversioned to do this more
279
tree.find_ids_across_trees(specific_files,
280
[self.basis_tree, self.work_tree])
282
# Setup the progress bar ...
283
283
# one to finish, one for rev and inventory, and one for each
284
284
# inventory entry, and the same for the new inventory.
285
285
# note that this estimate is too long when we do a partial tree
291
291
self._gather_parents()
292
292
if len(self.parents) > 1 and self.specific_files:
293
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
295
self._check_parents_present()
296
self.builder = self.branch.get_commit_builder(self.parents,
293
raise errors.CannotCommitSelectedFileMerge(self.specific_files)
295
# Build the new inventory
296
self.builder = self.branch.get_commit_builder(self.parents,
297
297
self.config, timestamp, timezone, committer, revprops, rev_id)
299
298
self._remove_deleted()
300
299
self._populate_new_inv()
301
300
self._report_deletes()
303
if not (self.allow_pointless
304
or len(self.parents) > 1
305
or self.builder.new_inventory != self.basis_inv):
306
raise PointlessCommit()
301
self._check_pointless()
308
302
self._emit_progress_update()
309
# TODO: Now the new inventory is known, check for conflicts and prompt the
310
# user for a commit message.
304
# TODO: Now the new inventory is known, check for conflicts and
305
# prompt the user for a commit message.
306
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
307
# weave lines, because nothing should be recorded until it is known
308
# that commit will succeed.
311
309
self.builder.finish_inventory()
312
310
self._emit_progress_update()
311
message = message_callback(self)
312
assert isinstance(message, unicode), type(message)
313
self.message = message
314
self._escape_commit_message()
316
# Add revision data to the local branch
313
317
self.rev_id = self.builder.commit(self.message)
314
318
self._emit_progress_update()
315
# revision data is in the local branch now.
317
320
# upload revision data to the master.
318
321
# this will propagate merged revisions too if needed.
320
323
self.master_branch.repository.fetch(self.branch.repository,
321
324
revision_id=self.rev_id)
322
325
# now the master has the revision data
323
# 'commit' to the master first so a timeout here causes the local
324
# branch to be out of date
325
self.master_branch.append_revision(self.rev_id)
326
# 'commit' to the master first so a timeout here causes the
327
# local branch to be out of date
328
self.master_branch.set_last_revision_info(new_revno,
327
331
# and now do the commit locally.
328
self.branch.append_revision(self.rev_id)
330
self.work_tree.set_pending_merges([])
331
self.work_tree.set_last_revision(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)
335
if self.config.post_commit() is not None:
336
hooks = self.config.post_commit().split(' ')
337
# this would be nicer with twisted.python.reflect.namedAny
339
result = eval(hook + '(branch, rev_id)',
340
{'branch':self.branch,
342
'rev_id':self.rev_id})
332
self.branch.set_last_revision_info(new_revno, self.rev_id)
334
# Make the working tree up to date with the branch
335
rev_tree = self.builder.revision_tree()
336
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
337
self.reporter.completed(new_revno, self.rev_id)
339
# Process the post commit hooks, if any
340
self._process_hooks(old_revno, new_revno)
343
341
self._emit_progress_update()
346
def _any_real_changes(self):
347
"""Are there real changes between new_inventory and basis?
349
For trees without rich roots, inv.root.revision changes every commit.
350
But if that is the only change, we want to treat it as though there
353
new_entries = self.builder.new_inventory.iter_entries()
354
basis_entries = self.basis_inv.iter_entries()
355
new_path, new_root_ie = new_entries.next()
356
basis_path, basis_root_ie = basis_entries.next()
358
# This is a copy of InventoryEntry.__eq__ only leaving out .revision
359
def ie_equal_no_revision(this, other):
360
return ((this.file_id == other.file_id)
361
and (this.name == other.name)
362
and (this.symlink_target == other.symlink_target)
363
and (this.text_sha1 == other.text_sha1)
364
and (this.text_size == other.text_size)
365
and (this.text_id == other.text_id)
366
and (this.parent_id == other.parent_id)
367
and (this.kind == other.kind)
368
and (this.executable == other.executable)
369
and (this.reference_revision == other.reference_revision)
371
if not ie_equal_no_revision(new_root_ie, basis_root_ie):
374
for new_ie, basis_ie in zip(new_entries, basis_entries):
375
if new_ie != basis_ie:
378
# No actual changes present
381
def _check_pointless(self):
382
if self.allow_pointless:
384
# A merge with no effect on files
385
if len(self.parents) > 1:
387
# work around the fact that a newly-initted tree does differ from its
389
if len(self.basis_inv) == 0 and len(self.builder.new_inventory) == 1:
390
raise PointlessCommit()
391
# Shortcut, if the number of entries changes, then we obviously have
393
if len(self.builder.new_inventory) != len(self.basis_inv):
395
# If length == 1, then we only have the root entry. Which means
396
# that there is no real difference (only the root could be different)
397
if (len(self.builder.new_inventory) != 1 and self._any_real_changes()):
399
raise PointlessCommit()
347
401
def _check_bound_branch(self):
348
402
"""Check to see if the local branch is bound.
385
439
self.bound_branch = self.branch
386
440
self.master_branch.lock_write()
387
441
self.master_locked = True
389
#### # Check to see if we have any pending merges. If we do
390
#### # those need to be pushed into the master branch
391
#### pending_merges = self.work_tree.pending_merges()
392
#### if pending_merges:
393
#### for revision_id in pending_merges:
394
#### self.master_branch.repository.fetch(self.bound_branch.repository,
395
#### revision_id=revision_id)
443
def _check_out_of_date_tree(self):
444
"""Check that the working tree is up to date.
446
:return: old_revision_number,new_revision_number tuple
449
first_tree_parent = self.work_tree.get_parent_ids()[0]
451
# if there are no parents, treat our parent as 'None'
452
# this is so that we still consider the master branch
453
# - in a checkout scenario the tree may have no
454
# parents but the branch may do.
455
first_tree_parent = bzrlib.revision.NULL_REVISION
456
old_revno, master_last = self.master_branch.last_revision_info()
457
if master_last != first_tree_parent:
458
if master_last != bzrlib.revision.NULL_REVISION:
459
raise errors.OutOfDateTree(self.work_tree)
460
if self.branch.repository.has_revision(first_tree_parent):
461
new_revno = old_revno + 1
463
# ghost parents never appear in revision history.
465
return old_revno,new_revno
467
def _process_hooks(self, old_revno, new_revno):
468
"""Process any registered commit hooks."""
469
# old style commit hooks - should be deprecated ? (obsoleted in
471
if self.config.post_commit() is not None:
472
hooks = self.config.post_commit().split(' ')
473
# this would be nicer with twisted.python.reflect.namedAny
475
result = eval(hook + '(branch, rev_id)',
476
{'branch':self.branch,
478
'rev_id':self.rev_id})
479
# new style commit hooks:
480
if not self.bound_branch:
481
hook_master = self.branch
484
hook_master = self.master_branch
485
hook_local = self.branch
486
# With bound branches, when the master is behind the local branch,
487
# the 'old_revno' and old_revid values here are incorrect.
488
# XXX: FIXME ^. RBC 20060206
490
old_revid = self.parents[0]
492
old_revid = bzrlib.revision.NULL_REVISION
493
for hook in Branch.hooks['post_commit']:
494
hook(hook_local, hook_master, old_revno, old_revid, new_revno,
397
497
def _cleanup(self):
398
498
"""Cleanup any open locks, progress bars etc."""
399
499
cleanups = [self._cleanup_bound_branch,
500
self.basis_tree.unlock,
400
501
self.work_tree.unlock,
401
502
self.pb.finished]
402
503
found_exception = None
451
552
"""Record the parents of a merge for merge detection."""
452
553
# TODO: Make sure that this list doesn't contain duplicate
453
554
# entries and the order is preserved when doing this.
454
pending_merges = self.work_tree.pending_merges()
456
self.parent_invs = []
457
precursor_id = self.branch.last_revision()
459
self.parents.append(precursor_id)
460
self.parents += pending_merges
461
for revision in self.parents:
555
self.parents = self.work_tree.get_parent_ids()
556
self.parent_invs = [self.basis_inv]
557
for revision in self.parents[1:]:
462
558
if self.branch.repository.has_revision(revision):
559
mutter('commit parent revision {%s}', revision)
463
560
inventory = self.branch.repository.get_inventory(revision)
464
561
self.parent_invs.append(inventory)
563
mutter('commit parent ghost revision {%s}', revision)
466
def _check_parents_present(self):
467
for parent_id in self.parents:
468
mutter('commit parent revision {%s}', parent_id)
469
if not self.branch.repository.has_revision(parent_id):
470
if parent_id == self.branch.last_revision():
471
warning("parent is missing %r", parent_id)
472
raise HistoryMissing(self.branch, 'revision', parent_id)
474
mutter("commit will ghost revision %r", parent_id)
476
565
def _remove_deleted(self):
477
566
"""Remove deleted files from the working inventories.
487
576
specific = self.specific_files
578
deleted_paths = set()
489
579
for path, ie in self.work_inv.iter_entries():
580
if is_inside_any(deleted_paths, path):
581
# The tree will delete the required ids recursively.
490
583
if specific and not is_inside_any(specific, path):
492
585
if not self.work_tree.has_filename(path):
586
deleted_paths.add(path)
493
587
self.reporter.missing(path)
494
deleted_ids.append((path, ie.file_id))
496
deleted_ids.sort(reverse=True)
497
for path, file_id in deleted_ids:
498
del self.work_inv[file_id]
499
self.work_tree._write_inventory(self.work_inv)
588
deleted_ids.append(ie.file_id)
589
self.work_tree.unversion(deleted_ids)
501
591
def _populate_new_inv(self):
502
592
"""Build revision inventory.
508
598
None; inventory entries that are carried over untouched have their
509
599
revision set to their prior value.
601
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
602
# results to create a new inventory at the same time, which results
603
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
511
605
mutter("Selecting files for commit with filter %s", self.specific_files)
512
# iter_entries does not visit the ROOT_ID node so we need to call
513
# self._emit_progress_update once by hand.
514
self._emit_progress_update()
515
for path, new_ie in self.work_inv.iter_entries():
606
assert self.work_inv.root is not None
607
entries = self.work_inv.iter_entries()
608
if not self.builder.record_root_entry:
609
symbol_versioning.warn('CommitBuilders should support recording'
610
' the root entry as of bzr 0.10.', DeprecationWarning,
612
self.builder.new_inventory.add(self.basis_inv.root.copy())
614
self._emit_progress_update()
615
for path, new_ie in entries:
516
616
self._emit_progress_update()
517
617
file_id = new_ie.file_id
518
mutter('check %s {%s}', path, file_id)
619
kind = self.work_tree.kind(file_id)
620
if kind == 'tree-reference' and self.recursive == 'down':
621
# nested tree: commit in it
622
sub_tree = WorkingTree.open(self.work_tree.abspath(path))
623
# FIXME: be more comprehensive here:
624
# this works when both trees are in --trees repository,
625
# but when both are bound to a different repository,
626
# it fails; a better way of approaching this is to
627
# finally implement the explicit-caches approach design
628
# a while back - RBC 20070306.
629
if (sub_tree.branch.repository.bzrdir.root_transport.base
631
self.work_tree.branch.repository.bzrdir.root_transport.base):
632
sub_tree.branch.repository = \
633
self.work_tree.branch.repository
635
sub_tree.commit(message=None, revprops=self.revprops,
636
recursive=self.recursive,
637
message_callback=self.message_callback,
638
timestamp=self.timestamp, timezone=self.timezone,
639
committer=self.committer,
640
allow_pointless=self.allow_pointless,
641
strict=self.strict, verbose=self.verbose,
642
local=self.local, reporter=self.reporter)
643
except errors.PointlessCommit:
645
if kind != new_ie.kind:
646
new_ie = inventory.make_entry(kind, new_ie.name,
647
new_ie.parent_id, file_id)
648
except errors.NoSuchFile:
650
# mutter('check %s {%s}', path, file_id)
519
651
if (not self.specific_files or
520
652
is_inside_or_parent_of_any(self.specific_files, path)):
521
mutter('%s selected for commit', path)
653
# mutter('%s selected for commit', path)
522
654
ie = new_ie.copy()
523
655
ie.revision = None
525
mutter('%s not selected for commit', path)
657
# mutter('%s not selected for commit', path)
526
658
if self.basis_inv.has_id(file_id):
527
659
ie = self.basis_inv[file_id].copy()
529
661
# this entry is new and not being committed
532
663
self.builder.record_entry_contents(ie, self.parent_invs,
533
664
path, self.work_tree)
534
665
# describe the nature of the change that has occurred relative to
546
677
self.reporter.snapshot_change(change, path)
679
if not self.specific_files:
682
# ignore removals that don't match filespec
683
for path, new_ie in self.basis_inv.iter_entries():
684
if new_ie.file_id in self.work_inv:
686
if is_inside_any(self.specific_files, path):
690
self.builder.record_entry_contents(ie, self.parent_invs, path,
548
693
def _emit_progress_update(self):
549
694
"""Emit an update to the progress bar."""
550
695
self.pb.update("Committing", self.pb_count, self.pb_total)