56
55
# merges from, then it should still be reported as newly added
57
56
# 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.
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.
66
from binascii import hexlify
67
72
from cStringIO import StringIO
69
from bzrlib.osutils import (local_time_offset, username,
70
rand_bytes, compact_date, user_email,
71
kind_marker, is_inside_any, quotefn,
72
sha_string, sha_strings, sha_file, isdir, isfile,
74
from bzrlib.branch import gen_file_id
75
79
from bzrlib.errors import (BzrError, PointlessCommit,
79
from bzrlib.revision import Revision
83
from bzrlib.osutils import (kind_marker, isdir,isfile, is_inside_any,
84
is_inside_or_parent_of_any,
85
quotefn, sha_file, split_lines)
86
from bzrlib.testament import Testament
80
87
from bzrlib.trace import mutter, note, warning
81
88
from bzrlib.xml5 import serializer_v5
82
from bzrlib.inventory import Inventory, ROOT_ID
83
from bzrlib.weave import Weave
84
from bzrlib.weavefile import read_weave, write_weave_v5
85
from bzrlib.atomicfile import AtomicFile
88
def commit(*args, **kwargs):
89
"""Commit a new revision to a branch.
91
Function-style interface for convenience of old callers.
93
New code should use the Commit class instead.
95
## XXX: Remove this in favor of Branch.commit?
96
Commit().commit(*args, **kwargs)
89
from bzrlib.inventory import Inventory, InventoryEntry
90
from bzrlib import symbol_versioning
91
from bzrlib.symbol_versioning import (deprecated_passed,
94
from bzrlib.workingtree import WorkingTree
99
97
class NullCommitReporter(object):
176
206
allow_pointless -- If true (default), commit even if nothing
177
207
has changed and no merges are recorded.
209
strict -- If true, don't allow a commit if the working tree
210
contains unknown files.
212
revprops -- Properties for new revision
213
:param local: Perform a local only commit.
179
215
mutter('preparing to commit')
182
self.weave_store = branch.weave_store
217
if deprecated_passed(branch):
218
symbol_versioning.warn("Commit.commit (branch, ...): The branch parameter is "
219
"deprecated as of bzr 0.8. Please use working_tree= instead.",
220
DeprecationWarning, stacklevel=2)
222
self.work_tree = self.branch.bzrdir.open_workingtree()
223
elif working_tree is None:
224
raise BzrError("One of branch and working_tree must be passed into commit().")
226
self.work_tree = working_tree
227
self.branch = self.work_tree.branch
229
raise BzrError("The message keyword parameter is required for commit().")
231
self.bound_branch = None
233
self.master_branch = None
234
self.master_locked = False
184
236
self.specific_files = specific_files
185
237
self.allow_pointless = allow_pointless
187
if timestamp is None:
188
self.timestamp = time.time()
190
self.timestamp = long(timestamp)
193
self.rev_id = _gen_revision_id(self.branch, self.timestamp)
197
if committer is None:
198
self.committer = username(self.branch)
200
assert isinstance(committer, basestring), type(committer)
201
self.committer = committer
204
self.timezone = local_time_offset()
206
self.timezone = int(timezone)
208
assert isinstance(message, basestring), type(message)
209
self.message = message
210
self._escape_commit_message()
212
self.branch.lock_write()
239
if reporter is None and self.reporter is None:
240
self.reporter = NullCommitReporter()
241
elif reporter is not None:
242
self.reporter = reporter
244
self.work_tree.lock_write()
245
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
214
self.work_tree = self.branch.working_tree()
247
# Cannot commit with conflicts present.
248
if len(self.work_tree.conflicts())>0:
249
raise ConflictsInTree
251
# setup the bound branch variables as needed.
252
self._check_bound_branch()
254
# check for out of date working trees
256
first_tree_parent = self.work_tree.get_parent_ids()[0]
258
# if there are no parents, treat our parent as 'None'
259
# this is so that we still consier the master branch
260
# - in a checkout scenario the tree may have no
261
# parents but the branch may do.
262
first_tree_parent = None
263
master_last = self.master_branch.last_revision()
264
if (master_last is not None and
265
master_last != first_tree_parent):
266
raise errors.OutOfDateTree(self.work_tree)
269
# raise an exception as soon as we find a single unknown.
270
for unknown in self.work_tree.unknowns():
271
raise StrictCommitFailed()
273
if self.config is None:
274
self.config = self.branch.get_config()
276
if isinstance(message, str):
277
message = message.decode(bzrlib.user_encoding)
278
assert isinstance(message, unicode), type(message)
279
self.message = message
280
self._escape_commit_message()
215
282
self.work_inv = self.work_tree.inventory
216
self.basis_tree = self.branch.basis_tree()
283
self.basis_tree = self.work_tree.basis_tree()
217
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
tree.find_ids_across_trees(specific_files,
289
[self.basis_tree, self.work_tree])
290
# one to finish, one for rev and inventory, and one for each
291
# inventory entry, and the same for the new inventory.
292
# note that this estimate is too long when we do a partial tree
293
# commit which excludes some new files from being considered.
294
# The estimate is corrected when we populate the new inv.
295
self.pb_total = len(self.work_inv) + 5
219
298
self._gather_parents()
220
299
if len(self.parents) > 1 and self.specific_files:
221
raise NotImplementedError('selected-file commit of merges is not supported yet')
222
self._check_parents_present()
300
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
303
self.builder = self.branch.get_commit_builder(self.parents,
304
self.config, timestamp, timezone, committer, revprops, rev_id)
224
306
self._remove_deleted()
225
307
self._populate_new_inv()
226
self._store_snapshot()
227
308
self._report_deletes()
229
if not (self.allow_pointless
230
or len(self.parents) > 1
231
or self.new_inv != self.basis_inv):
232
raise PointlessCommit()
234
if len(list(self.work_tree.iter_conflicts()))>0:
235
raise ConflictsInTree
237
self._record_inventory()
238
self._make_revision()
239
self.reporter.completed(self.branch.revno()+1, self.rev_id)
310
self._check_pointless()
312
self._emit_progress_update()
313
# TODO: Now the new inventory is known, check for conflicts and
314
# prompt the user for a commit message.
315
# ADHB 2006-08-08: If this is done, populate_new_inv should not add
316
# weave lines, because nothing should be recorded until it is known
317
# that commit will succeed.
318
self.builder.finish_inventory()
319
self._emit_progress_update()
320
self.rev_id = self.builder.commit(self.message)
321
self._emit_progress_update()
322
# revision data is in the local branch now.
324
# upload revision data to the master.
325
# this will propagate merged revisions too if needed.
326
if self.bound_branch:
327
self.master_branch.repository.fetch(self.branch.repository,
328
revision_id=self.rev_id)
329
# now the master has the revision data
330
# 'commit' to the master first so a timeout here causes the local
331
# branch to be out of date
332
self.master_branch.append_revision(self.rev_id)
334
# and now do the commit locally.
240
335
self.branch.append_revision(self.rev_id)
241
self.branch.set_pending_merges([])
337
rev_tree = self.builder.revision_tree()
338
self.work_tree.set_parent_trees([(self.rev_id, rev_tree)])
339
# now the work tree is up to date with the branch
341
self.reporter.completed(self.branch.revno(), self.rev_id)
342
if self.config.post_commit() is not None:
343
hooks = self.config.post_commit().split(' ')
344
# this would be nicer with twisted.python.reflect.namedAny
346
result = eval(hook + '(branch, rev_id)',
347
{'branch':self.branch,
349
'rev_id':self.rev_id})
350
self._emit_progress_update()
245
def _record_inventory(self):
246
"""Store the inventory for the new revision."""
247
inv_text = serializer_v5.write_inventory_to_string(self.new_inv)
248
self.inv_sha1 = sha_string(inv_text)
249
s = self.branch.control_weaves
250
s.add_text('inventory', self.rev_id,
251
split_lines(inv_text), self.present_parents)
355
def _any_real_changes(self):
356
"""Are there real changes between new_inventory and basis?
358
For trees without rich roots, inv.root.revision changes every commit.
359
But if that is the only change, we want to treat it as though there
362
new_entries = self.builder.new_inventory.iter_entries()
363
basis_entries = self.basis_inv.iter_entries()
364
new_path, new_root_ie = new_entries.next()
365
basis_path, basis_root_ie = basis_entries.next()
367
# This is a copy of InventoryEntry.__eq__ only leaving out .revision
368
def ie_equal_no_revision(this, other):
369
return ((this.file_id == other.file_id)
370
and (this.name == other.name)
371
and (this.symlink_target == other.symlink_target)
372
and (this.text_sha1 == other.text_sha1)
373
and (this.text_size == other.text_size)
374
and (this.text_id == other.text_id)
375
and (this.parent_id == other.parent_id)
376
and (this.kind == other.kind)
377
and (this.executable == other.executable)
379
if not ie_equal_no_revision(new_root_ie, basis_root_ie):
382
for new_ie, basis_ie in zip(new_entries, basis_entries):
383
if new_ie != basis_ie:
386
# No actual changes present
389
def _check_pointless(self):
390
if self.allow_pointless:
392
# A merge with no effect on files
393
if len(self.parents) > 1:
395
# work around the fact that a newly-initted tree does differ from its
397
if len(self.basis_inv) == 0 and len(self.builder.new_inventory) == 1:
398
raise PointlessCommit()
399
# Shortcut, if the number of entries changes, then we obviously have
401
if len(self.builder.new_inventory) != len(self.basis_inv):
403
# If length == 1, then we only have the root entry. Which means
404
# that there is no real difference (only the root could be different)
405
if (len(self.builder.new_inventory) != 1 and self._any_real_changes()):
407
raise PointlessCommit()
409
def _check_bound_branch(self):
410
"""Check to see if the local branch is bound.
412
If it is bound, then most of the commit will actually be
413
done using the remote branch as the target branch.
414
Only at the end will the local branch be updated.
416
if self.local and not self.branch.get_bound_location():
417
raise errors.LocalRequiresBoundBranch()
420
self.master_branch = self.branch.get_master_branch()
422
if not self.master_branch:
423
# make this branch the reference branch for out of date checks.
424
self.master_branch = self.branch
427
# If the master branch is bound, we must fail
428
master_bound_location = self.master_branch.get_bound_location()
429
if master_bound_location:
430
raise errors.CommitToDoubleBoundBranch(self.branch,
431
self.master_branch, master_bound_location)
433
# TODO: jam 20051230 We could automatically push local
434
# commits to the remote branch if they would fit.
435
# But for now, just require remote to be identical
438
# Make sure the local branch is identical to the master
439
master_rh = self.master_branch.revision_history()
440
local_rh = self.branch.revision_history()
441
if local_rh != master_rh:
442
raise errors.BoundBranchOutOfDate(self.branch,
445
# Now things are ready to change the master branch
447
self.bound_branch = self.branch
448
self.master_branch.lock_write()
449
self.master_locked = True
452
"""Cleanup any open locks, progress bars etc."""
453
cleanups = [self._cleanup_bound_branch,
454
self.work_tree.unlock,
456
found_exception = None
457
for cleanup in cleanups:
460
# we want every cleanup to run no matter what.
461
# so we have a catchall here, but we will raise the
462
# last encountered exception up the stack: and
463
# typically this will be useful enough.
466
if found_exception is not None:
467
# don't do a plan raise, because the last exception may have been
468
# trashed, e is our sure-to-work exception even though it loses the
469
# full traceback. XXX: RBC 20060421 perhaps we could check the
470
# exc_info and if its the same one do a plain raise otherwise
471
# 'raise e' as we do now.
474
def _cleanup_bound_branch(self):
475
"""Executed at the end of a try/finally to cleanup a bound branch.
477
If the branch wasn't bound, this is a no-op.
478
If it was, it resents self.branch to the local branch, instead
481
if not self.bound_branch:
483
if self.master_locked:
484
self.master_branch.unlock()
253
486
def _escape_commit_message(self):
254
487
"""Replace xml-incompatible control characters."""
488
# FIXME: RBC 20060419 this should be done by the revision
489
# serialiser not by commit. Then we can also add an unescaper
490
# in the deserializer and start roundtripping revision messages
491
# precisely. See repository_implementations/test_repository.py
255
493
# Python strings can include characters that can't be
256
494
# represented in well-formed XML; escape characters that
257
495
# aren't listed in the XML specification
258
496
# (http://www.w3.org/TR/REC-xml/#NT-Char).
259
if isinstance(self.message, unicode):
260
char_pattern = u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]'
262
# Use a regular 'str' as pattern to avoid having re.subn
263
# return 'unicode' results.
264
char_pattern = '[^x09\x0A\x0D\x20-\xFF]'
265
497
self.message, escape_count = re.subn(
498
u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
267
499
lambda match: match.group(0).encode('unicode_escape'),
365
551
None; inventory entries that are carried over untouched have their
366
552
revision set to their prior value.
554
# ESEPARATIONOFCONCERNS: this function is diffing and using the diff
555
# results to create a new inventory at the same time, which results
556
# in bugs like #46635. Any reason not to use/enhance Tree.changes_from?
368
558
mutter("Selecting files for commit with filter %s", self.specific_files)
369
self.new_inv = Inventory()
370
for path, new_ie in self.work_inv.iter_entries():
559
assert self.work_inv.root is not None
560
entries = self.work_inv.iter_entries()
561
if not self.builder.record_root_entry:
562
symbol_versioning.warn('CommitBuilders should support recording'
563
' the root entry as of bzr 0.10.', DeprecationWarning,
565
self.builder.new_inventory.add(self.basis_inv.root.copy())
567
self._emit_progress_update()
568
for path, new_ie in entries:
569
self._emit_progress_update()
371
570
file_id = new_ie.file_id
372
mutter('check %s {%s}', path, new_ie.file_id)
373
if self.specific_files:
374
if not is_inside_any(self.specific_files, path):
375
mutter('%s not selected for commit', path)
376
self._carry_entry(file_id)
571
# mutter('check %s {%s}', path, file_id)
572
if (not self.specific_files or
573
is_inside_or_parent_of_any(self.specific_files, path)):
574
# mutter('%s selected for commit', path)
578
# mutter('%s not selected for commit', path)
579
if self.basis_inv.has_id(file_id):
580
ie = self.basis_inv[file_id].copy()
582
# this entry is new and not being committed
379
# this is selected, ensure its parents are too.
380
parent_id = new_ie.parent_id
381
while parent_id != ROOT_ID:
382
if not self.new_inv.has_id(parent_id):
383
ie = self._select_entry(self.work_inv[parent_id])
384
mutter('%s selected for commit because of %s',
385
self.new_inv.id2path(parent_id), path)
387
ie = self.new_inv[parent_id]
388
if ie.revision is not None:
390
mutter('%s selected for commit because of %s',
391
self.new_inv.id2path(parent_id), path)
392
parent_id = ie.parent_id
393
mutter('%s selected for commit', path)
394
self._select_entry(new_ie)
396
def _select_entry(self, new_ie):
397
"""Make new_ie be considered for committing."""
403
def _carry_entry(self, file_id):
404
"""Carry the file unchanged from the basis revision."""
405
if self.basis_inv.has_id(file_id):
406
self.new_inv.add(self.basis_inv[file_id].copy())
584
self.builder.record_entry_contents(ie, self.parent_invs,
585
path, self.work_tree)
586
# describe the nature of the change that has occurred relative to
587
# the basis inventory.
588
if (self.basis_inv.has_id(ie.file_id)):
589
basis_ie = self.basis_inv[ie.file_id]
592
change = ie.describe_change(basis_ie, ie)
593
if change in (InventoryEntry.RENAMED,
594
InventoryEntry.MODIFIED_AND_RENAMED):
595
old_path = self.basis_inv.id2path(ie.file_id)
596
self.reporter.renamed(change, old_path, path)
598
self.reporter.snapshot_change(change, path)
600
if not self.specific_files:
603
# ignore removals that don't match filespec
604
for path, new_ie in self.basis_inv.iter_entries():
605
if new_ie.file_id in self.work_inv:
607
if is_inside_any(self.specific_files, path):
611
self.builder.record_entry_contents(ie, self.parent_invs, path,
614
def _emit_progress_update(self):
615
"""Emit an update to the progress bar."""
616
self.pb.update("Committing", self.pb_count, self.pb_total)
408
619
def _report_deletes(self):
409
for file_id in self.basis_inv:
410
if file_id not in self.new_inv:
411
self.reporter.deleted(self.basis_inv.id2path(file_id))
413
def _gen_revision_id(branch, when):
414
"""Return new revision-id."""
415
s = '%s-%s-' % (user_email(branch), compact_date(when))
416
s += hexlify(rand_bytes(8))
620
for path, ie in self.basis_inv.iter_entries():
621
if ie.file_id not in self.builder.new_inventory:
622
self.reporter.deleted(path)