1
# Copyright (C) 2005, 2006 Canonical Ltd
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
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
21
# TODO: Separate 'prepare' phase where we find a list of potentially
22
# committed files. We then can then pause the commit to prompt for a
23
# commit message, knowing the summary will be the same as what's
24
# actually used for the commit. (But perhaps simpler to simply get
25
# the tree status, then use that for a selective commit?)
27
18
# The newly committed revision is going to have a shape corresponding
28
19
# to that of the working inventory. Files that are not in the
29
20
# working tree and that were in the predecessor are reported as
55
46
# merges from, then it should still be reported as newly added
56
47
# relative to the basis revision.
58
# TODO: Do checks that the tree can be committed *before* running the
59
# editor; this should include checks for a pointless commit and for
60
# unknown or missing files.
62
# 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.
81
73
from bzrlib.testament import Testament
82
74
from bzrlib.trace import mutter, note, warning
83
75
from bzrlib.xml5 import serializer_v5
84
from bzrlib.inventory import Inventory, ROOT_ID, InventoryEntry
76
from bzrlib.inventory import Inventory, InventoryEntry
85
77
from bzrlib import symbol_versioning
86
78
from bzrlib.symbol_versioning import (deprecated_passed,
87
79
deprecated_function,
88
80
DEPRECATED_PARAMETER)
89
81
from bzrlib.workingtree import WorkingTree
92
85
class NullCommitReporter(object):
121
114
def snapshot_change(self, change, path):
122
115
if change == 'unchanged':
117
if change == 'added' and path == '':
124
119
note("%s %s", change, path)
126
121
def completed(self, revno, rev_id):
177
172
working_tree=None,
176
message_callback=None,
181
178
"""Commit working copy as a new revision.
183
branch -- the deprecated branch to commit to. New callers should pass in
186
message -- the commit message, a mandatory parameter
180
message -- the commit message (it or message_callback is required)
188
182
timestamp -- if not None, seconds-since-epoch for a
189
183
postdated/predated commit.
205
199
revprops -- Properties for new revision
206
200
:param local: Perform a local only commit.
201
:param recursive: If set to 'down', commit in any subtrees that have
202
pending changes of any sort during this commit.
208
204
mutter('preparing to commit')
210
if deprecated_passed(branch):
211
symbol_versioning.warn("Commit.commit (branch, ...): The branch parameter is "
212
"deprecated as of bzr 0.8. Please use working_tree= instead.",
213
DeprecationWarning, stacklevel=2)
215
self.work_tree = self.branch.bzrdir.open_workingtree()
216
elif working_tree is None:
217
raise BzrError("One of branch and working_tree must be passed into commit().")
206
if working_tree is None:
207
raise BzrError("working_tree must be passed into commit().")
219
209
self.work_tree = working_tree
220
210
self.branch = self.work_tree.branch
222
raise BzrError("The message keyword parameter is required for commit().")
211
if getattr(self.work_tree, 'requires_rich_root', lambda: False)():
212
if not self.branch.repository.supports_rich_root():
213
raise errors.RootNotRich()
214
if message_callback is None:
215
if message is not None:
216
if isinstance(message, str):
217
message = message.decode(bzrlib.user_encoding)
218
message_callback = lambda x: message
220
raise BzrError("The message or message_callback keyword"
221
" parameter is required for commit().")
224
223
self.bound_branch = None
225
224
self.local = local
228
227
self.rev_id = None
229
228
self.specific_files = specific_files
230
229
self.allow_pointless = allow_pointless
230
self.recursive = recursive
231
self.revprops = revprops
232
self.message_callback = message_callback
233
self.timestamp = timestamp
234
self.timezone = timezone
235
self.committer = committer
237
self.verbose = verbose
232
240
if reporter is None and self.reporter is None:
233
241
self.reporter = NullCommitReporter()
237
245
self.work_tree.lock_write()
238
246
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
247
self.basis_tree = self.work_tree.basis_tree()
248
self.basis_tree.lock_read()
240
250
# Cannot commit with conflicts present.
241
251
if len(self.work_tree.conflicts())>0:
252
262
# this is so that we still consier the master branch
253
263
# - in a checkout scenario the tree may have no
254
264
# parents but the branch may do.
255
first_tree_parent = None
256
master_last = self.master_branch.last_revision()
257
if (master_last is not None and
258
master_last != first_tree_parent):
259
raise errors.OutOfDateTree(self.work_tree)
265
first_tree_parent = bzrlib.revision.NULL_REVISION
266
old_revno, master_last = self.master_branch.last_revision_info()
267
if master_last != first_tree_parent:
268
if master_last != bzrlib.revision.NULL_REVISION:
269
raise errors.OutOfDateTree(self.work_tree)
270
if self.branch.repository.has_revision(first_tree_parent):
271
new_revno = old_revno + 1
273
# ghost parents never appear in revision history.
262
276
# raise an exception as soon as we find a single unknown.
263
277
for unknown in self.work_tree.unknowns():
266
280
if self.config is None:
267
281
self.config = self.branch.get_config()
269
if isinstance(message, str):
270
message = message.decode(bzrlib.user_encoding)
271
assert isinstance(message, unicode), type(message)
272
self.message = message
273
self._escape_commit_message()
275
283
self.work_inv = self.work_tree.inventory
276
self.basis_tree = self.work_tree.basis_tree()
277
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
# XXX: Dont we have filter_unversioned to do this more
290
tree.find_ids_across_trees(specific_files,
291
[self.basis_tree, self.work_tree])
278
292
# one to finish, one for rev and inventory, and one for each
279
293
# inventory entry, and the same for the new inventory.
280
294
# note that this estimate is too long when we do a partial tree
286
300
self._gather_parents()
287
301
if len(self.parents) > 1 and self.specific_files:
288
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
302
raise errors.CannotCommitSelectedFileMerge(self.specific_files)
291
self.builder = self.branch.get_commit_builder(self.parents,
304
self.builder = self.branch.get_commit_builder(self.parents,
292
305
self.config, timestamp, timezone, committer, revprops, rev_id)
294
307
self._remove_deleted()
305
318
# that commit will succeed.
306
319
self.builder.finish_inventory()
307
320
self._emit_progress_update()
321
message = message_callback(self)
322
assert isinstance(message, unicode), type(message)
323
self.message = message
324
self._escape_commit_message()
308
326
self.rev_id = self.builder.commit(self.message)
309
327
self._emit_progress_update()
310
328
# revision data is in the local branch now.
317
335
# now the master has the revision data
318
336
# 'commit' to the master first so a timeout here causes the local
319
337
# branch to be out of date
320
self.master_branch.append_revision(self.rev_id)
338
self.master_branch.set_last_revision_info(new_revno,
322
341
# and now do the commit locally.
323
self.branch.append_revision(self.rev_id)
342
self.branch.set_last_revision_info(new_revno, self.rev_id)
325
# if the builder gave us the revisiontree it created back, we
326
# could use it straight away here.
327
# TODO: implement this.
328
self.work_tree.set_parent_trees([(self.rev_id,
329
self.branch.repository.revision_tree(self.rev_id))])
344
rev_tree = self.builder.revision_tree()
345
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
330
346
# now the work tree is up to date with the branch
332
self.reporter.completed(self.branch.revno(), self.rev_id)
348
self.reporter.completed(new_revno, self.rev_id)
349
# old style commit hooks - should be deprecated ? (obsoleted in
333
351
if self.config.post_commit() is not None:
334
352
hooks = self.config.post_commit().split(' ')
335
353
# this would be nicer with twisted.python.reflect.namedAny
338
356
{'branch':self.branch,
340
358
'rev_id':self.rev_id})
359
# new style commit hooks:
360
if not self.bound_branch:
361
hook_master = self.branch
364
hook_master = self.master_branch
365
hook_local = self.branch
366
# With bound branches, when the master is behind the local branch,
367
# the 'old_revno' and old_revid values here are incorrect.
368
# XXX: FIXME ^. RBC 20060206
370
old_revid = self.parents[0]
372
old_revid = bzrlib.revision.NULL_REVISION
373
for hook in Branch.hooks['post_commit']:
374
hook(hook_local, hook_master, old_revno, old_revid, new_revno,
341
376
self._emit_progress_update()
344
379
return self.rev_id
381
def _any_real_changes(self):
382
"""Are there real changes between new_inventory and basis?
384
For trees without rich roots, inv.root.revision changes every commit.
385
But if that is the only change, we want to treat it as though there
388
new_entries = self.builder.new_inventory.iter_entries()
389
basis_entries = self.basis_inv.iter_entries()
390
new_path, new_root_ie = new_entries.next()
391
basis_path, basis_root_ie = basis_entries.next()
393
# This is a copy of InventoryEntry.__eq__ only leaving out .revision
394
def ie_equal_no_revision(this, other):
395
return ((this.file_id == other.file_id)
396
and (this.name == other.name)
397
and (this.symlink_target == other.symlink_target)
398
and (this.text_sha1 == other.text_sha1)
399
and (this.text_size == other.text_size)
400
and (this.text_id == other.text_id)
401
and (this.parent_id == other.parent_id)
402
and (this.kind == other.kind)
403
and (this.executable == other.executable)
404
and (this.reference_revision == other.reference_revision)
406
if not ie_equal_no_revision(new_root_ie, basis_root_ie):
409
for new_ie, basis_ie in zip(new_entries, basis_entries):
410
if new_ie != basis_ie:
413
# No actual changes present
346
416
def _check_pointless(self):
347
417
if self.allow_pointless:
352
422
# work around the fact that a newly-initted tree does differ from its
424
if len(self.basis_inv) == 0 and len(self.builder.new_inventory) == 1:
425
raise PointlessCommit()
426
# Shortcut, if the number of entries changes, then we obviously have
354
428
if len(self.builder.new_inventory) != len(self.basis_inv):
356
if (len(self.builder.new_inventory) != 1 and
357
self.builder.new_inventory != self.basis_inv):
430
# If length == 1, then we only have the root entry. Which means
431
# that there is no real difference (only the root could be different)
432
if (len(self.builder.new_inventory) != 1 and self._any_real_changes()):
359
434
raise PointlessCommit()
390
465
# Make sure the local branch is identical to the master
391
master_rh = self.master_branch.revision_history()
392
local_rh = self.branch.revision_history()
393
if local_rh != master_rh:
466
master_info = self.master_branch.last_revision_info()
467
local_info = self.branch.last_revision_info()
468
if local_info != master_info:
394
469
raise errors.BoundBranchOutOfDate(self.branch,
395
470
self.master_branch)
458
534
# TODO: Make sure that this list doesn't contain duplicate
459
535
# entries and the order is preserved when doing this.
460
536
self.parents = self.work_tree.get_parent_ids()
461
self.parent_invs = []
462
for revision in self.parents:
537
self.parent_invs = [self.basis_inv]
538
for revision in self.parents[1:]:
463
539
if self.branch.repository.has_revision(revision):
464
540
mutter('commit parent revision {%s}', revision)
465
541
inventory = self.branch.repository.get_inventory(revision)
508
584
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
509
585
# ADHB 11-07-2006
510
586
mutter("Selecting files for commit with filter %s", self.specific_files)
587
assert self.work_inv.root is not None
511
588
entries = self.work_inv.iter_entries()
512
589
if not self.builder.record_root_entry:
513
590
symbol_versioning.warn('CommitBuilders should support recording'
519
596
for path, new_ie in entries:
520
597
self._emit_progress_update()
521
598
file_id = new_ie.file_id
600
kind = self.work_tree.kind(file_id)
601
if kind == 'tree-reference' and self.recursive == 'down':
602
# nested tree: commit in it
603
sub_tree = WorkingTree.open(self.work_tree.abspath(path))
604
# FIXME: be more comprehensive here:
605
# this works when both trees are in --trees repository,
606
# but when both are bound to a different repository,
607
# it fails; a better way of approaching this is to
608
# finally implement the explicit-caches approach design
609
# a while back - RBC 20070306.
610
if (sub_tree.branch.repository.bzrdir.root_transport.base
612
self.work_tree.branch.repository.bzrdir.root_transport.base):
613
sub_tree.branch.repository = \
614
self.work_tree.branch.repository
616
sub_tree.commit(message=None, revprops=self.revprops,
617
recursive=self.recursive,
618
message_callback=self.message_callback,
619
timestamp=self.timestamp, timezone=self.timezone,
620
committer=self.committer,
621
allow_pointless=self.allow_pointless,
622
strict=self.strict, verbose=self.verbose,
623
local=self.local, reporter=self.reporter)
624
except errors.PointlessCommit:
626
if kind != new_ie.kind:
627
new_ie = inventory.make_entry(kind, new_ie.name,
628
new_ie.parent_id, file_id)
629
except errors.NoSuchFile:
522
631
# mutter('check %s {%s}', path, file_id)
523
632
if (not self.specific_files or
524
633
is_inside_or_parent_of_any(self.specific_files, path)):