214
168
allow_pointless -- If true (default), commit even if nothing
215
169
has changed and no merges are recorded.
217
strict -- If true, don't allow a commit if the working tree
218
contains unknown files.
220
revprops -- Properties for new revision
221
:param local: Perform a local only commit.
223
171
mutter('preparing to commit')
225
if deprecated_passed(branch):
226
warnings.warn("Commit.commit (branch, ...): The branch parameter is "
227
"deprecated as of bzr 0.8. Please use working_tree= instead.",
228
DeprecationWarning, stacklevel=2)
230
self.work_tree = self.branch.bzrdir.open_workingtree()
231
elif working_tree is None:
232
raise BzrError("One of branch and working_tree must be passed into commit().")
234
self.work_tree = working_tree
235
self.branch = self.work_tree.branch
237
raise BzrError("The message keyword parameter is required for commit().")
239
self.bound_branch = None
241
self.master_branch = None
242
self.master_locked = False
174
self.weave_store = branch.weave_store
244
176
self.specific_files = specific_files
245
177
self.allow_pointless = allow_pointless
247
if reporter is None and self.reporter is None:
248
self.reporter = NullCommitReporter()
249
elif reporter is not None:
250
self.reporter = reporter
252
self.work_tree.lock_write()
253
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
179
if timestamp is None:
180
self.timestamp = time.time()
182
self.timestamp = long(timestamp)
185
self.rev_id = _gen_revision_id(self.branch, self.timestamp)
189
if committer is None:
190
self.committer = username(self.branch)
192
assert isinstance(committer, basestring), type(committer)
193
self.committer = committer
196
self.timezone = local_time_offset()
198
self.timezone = int(timezone)
200
assert isinstance(message, basestring), type(message)
201
self.message = message
202
self._escape_commit_message()
204
self.branch.lock_write()
255
# Cannot commit with conflicts present.
256
if len(self.work_tree.conflicts())>0:
257
raise ConflictsInTree
259
# setup the bound branch variables as needed.
260
self._check_bound_branch()
262
# check for out of date working trees
263
# if we are bound, then self.branch is the master branch and this
264
# test is thus all we need.
265
if self.work_tree.last_revision() != self.master_branch.last_revision():
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()
206
self.work_tree = self.branch.working_tree()
282
207
self.work_inv = self.work_tree.inventory
283
self.basis_tree = self.work_tree.basis_tree()
208
self.basis_tree = self.branch.basis_tree()
284
209
self.basis_inv = self.basis_tree.inventory
285
# one to finish, one for rev and inventory, and one for each
286
# inventory entry, and the same for the new inventory.
287
# note that this estimate is too long when we do a partial tree
288
# commit which excludes some new files from being considered.
289
# The estimate is corrected when we populate the new inv.
290
self.pb_total = len(self.work_inv) + 5
293
211
self._gather_parents()
294
212
if len(self.parents) > 1 and self.specific_files:
295
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
213
raise NotImplementedError('selected-file commit of merges is not supported yet')
297
214
self._check_parents_present()
298
self.builder = self.branch.get_commit_builder(self.parents,
299
self.config, timestamp, timezone, committer, revprops, rev_id)
301
216
self._remove_deleted()
302
self._populate_new_inv()
217
self.new_inv = Inventory()
218
self._store_entries()
303
219
self._report_deletes()
220
self._set_name_versions()
305
222
if not (self.allow_pointless
306
223
or len(self.parents) > 1
307
or self.builder.new_inventory != self.basis_inv):
224
or self.new_inv != self.basis_inv):
308
225
raise PointlessCommit()
310
self._emit_progress_update()
311
# TODO: Now the new inventory is known, check for conflicts and prompt the
312
# user for a commit message.
313
self.builder.finish_inventory()
314
self._emit_progress_update()
315
self.rev_id = self.builder.commit(self.message)
316
self._emit_progress_update()
317
# revision data is in the local branch now.
319
# upload revision data to the master.
320
# this will propagate merged revisions too if needed.
321
if self.bound_branch:
322
self.master_branch.repository.fetch(self.branch.repository,
323
revision_id=self.rev_id)
324
# now the master has the revision data
325
# 'commit' to the master first so a timeout here causes the local
326
# branch to be out of date
327
self.master_branch.append_revision(self.rev_id)
329
# and now do the commit locally.
227
self._record_inventory()
228
self._record_ancestry()
229
self._make_revision()
230
note('committed r%d {%s}', (self.branch.revno() + 1),
330
232
self.branch.append_revision(self.rev_id)
332
self.work_tree.set_pending_merges([])
333
self.work_tree.set_last_revision(self.rev_id)
334
# now the work tree is up to date with the branch
336
self.reporter.completed(self.branch.revno(), self.rev_id)
337
if self.config.post_commit() is not None:
338
hooks = self.config.post_commit().split(' ')
339
# this would be nicer with twisted.python.reflect.namedAny
341
result = eval(hook + '(branch, rev_id)',
342
{'branch':self.branch,
344
'rev_id':self.rev_id})
345
self._emit_progress_update()
233
self.branch.set_pending_merges([])
350
def _check_bound_branch(self):
351
"""Check to see if the local branch is bound.
353
If it is bound, then most of the commit will actually be
354
done using the remote branch as the target branch.
355
Only at the end will the local branch be updated.
357
if self.local and not self.branch.get_bound_location():
358
raise errors.LocalRequiresBoundBranch()
361
self.master_branch = self.branch.get_master_branch()
363
if not self.master_branch:
364
# make this branch the reference branch for out of date checks.
365
self.master_branch = self.branch
368
# If the master branch is bound, we must fail
369
master_bound_location = self.master_branch.get_bound_location()
370
if master_bound_location:
371
raise errors.CommitToDoubleBoundBranch(self.branch,
372
self.master_branch, master_bound_location)
374
# TODO: jam 20051230 We could automatically push local
375
# commits to the remote branch if they would fit.
376
# But for now, just require remote to be identical
379
# Make sure the local branch is identical to the master
380
master_rh = self.master_branch.revision_history()
381
local_rh = self.branch.revision_history()
382
if local_rh != master_rh:
383
raise errors.BoundBranchOutOfDate(self.branch,
386
# Now things are ready to change the master branch
388
self.bound_branch = self.branch
389
self.master_branch.lock_write()
390
self.master_locked = True
393
"""Cleanup any open locks, progress bars etc."""
394
cleanups = [self._cleanup_bound_branch,
395
self.work_tree.unlock,
397
found_exception = None
398
for cleanup in cleanups:
401
# we want every cleanup to run no matter what.
402
# so we have a catchall here, but we will raise the
403
# last encountered exception up the stack: and
404
# typically this will be useful enough.
407
if found_exception is not None:
408
# don't do a plan raise, because the last exception may have been
409
# trashed, e is our sure-to-work exception even though it loses the
410
# full traceback. XXX: RBC 20060421 perhaps we could check the
411
# exc_info and if its the same one do a plain raise otherwise
412
# 'raise e' as we do now.
415
def _cleanup_bound_branch(self):
416
"""Executed at the end of a try/finally to cleanup a bound branch.
418
If the branch wasn't bound, this is a no-op.
419
If it was, it resents self.branch to the local branch, instead
422
if not self.bound_branch:
424
if self.master_locked:
425
self.master_branch.unlock()
239
def _record_inventory(self):
240
"""Store the inventory for the new revision."""
241
inv_text = serializer_v5.write_inventory_to_string(self.new_inv)
242
self.inv_sha1 = sha_string(inv_text)
243
s = self.branch.control_weaves
244
s.add_text('inventory', self.rev_id,
245
split_lines(inv_text), self.parents)
427
247
def _escape_commit_message(self):
428
248
"""Replace xml-incompatible control characters."""
429
# FIXME: RBC 20060419 this should be done by the revision
430
# serialiser not by commit. Then we can also add an unescaper
431
# in the deserializer and start roundtripping revision messages
432
# precisely. See repository_implementations/test_repository.py
434
249
# Python strings can include characters that can't be
435
250
# represented in well-formed XML; escape characters that
436
251
# aren't listed in the XML specification
437
252
# (http://www.w3.org/TR/REC-xml/#NT-Char).
253
if isinstance(self.message, unicode):
254
char_pattern = u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]'
256
# Use a regular 'str' as pattern to avoid having re.subn
257
# return 'unicode' results.
258
char_pattern = '[^x09\x0A\x0D\x20-\xFF]'
438
259
self.message, escape_count = re.subn(
439
u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
440
261
lambda match: match.group(0).encode('unicode_escape'),
443
self.reporter.escaped(escape_count, self.message)
264
note("replaced %d control characters in message", escape_count)
266
def _record_ancestry(self):
267
"""Append merged revision ancestry to the ancestry file.
269
This should be the merged ancestry of all parents, plus the
271
s = self.branch.control_weaves
272
w = s.get_weave_or_empty('ancestry')
273
lines = self._make_ancestry(w)
274
w.add(self.rev_id, self.parents, lines)
275
s.put_weave('ancestry', w)
278
def _make_ancestry(self, ancestry_weave):
279
"""Return merged ancestry lines.
281
The lines are revision-ids followed by newlines."""
282
parent_ancestries = [ancestry_weave.get(p) for p in self.parents]
283
new_lines = merge_ancestry_lines(self.rev_id, parent_ancestries)
284
mutter('merged ancestry of {%s}:\n%s', self.rev_id, ''.join(new_lines))
445
288
def _gather_parents(self):
446
"""Record the parents of a merge for merge detection."""
447
# TODO: Make sure that this list doesn't contain duplicate
448
# entries and the order is preserved when doing this.
449
self.parents = self.work_tree.get_parent_ids()
450
self.parent_invs = []
451
for revision in self.parents:
452
if self.branch.repository.has_revision(revision):
453
inventory = self.branch.repository.get_inventory(revision)
454
self.parent_invs.append(inventory)
289
pending_merges = self.branch.pending_merges()
291
self.parent_trees = []
292
precursor_id = self.branch.last_revision()
294
self.parents.append(precursor_id)
295
self.parent_trees.append(self.basis_tree)
296
self.parents += pending_merges
297
self.parent_trees.extend(map(self.branch.revision_tree, pending_merges))
456
300
def _check_parents_present(self):
457
301
for parent_id in self.parents:
458
302
mutter('commit parent revision {%s}', parent_id)
459
if not self.branch.repository.has_revision(parent_id):
460
if parent_id == self.branch.last_revision():
461
warning("parent is missing %r", parent_id)
462
raise BzrCheckError("branch %s is missing revision {%s}"
463
% (self.branch, parent_id))
303
if not self.branch.has_revision(parent_id):
304
warning("can't commit a merge from an absent parent")
305
raise HistoryMissing(self.branch, 'revision', parent_id)
308
def _make_revision(self):
309
"""Record a new revision object for this commit."""
310
self.rev = Revision(timestamp=self.timestamp,
311
timezone=self.timezone,
312
committer=self.committer,
313
message=self.message,
314
inventory_sha1=self.inv_sha1,
315
revision_id=self.rev_id)
316
self.rev.parent_ids = self.parents
318
serializer_v5.write_revision(self.rev, rev_tmp)
320
self.branch.revision_store.add(rev_tmp, self.rev_id)
321
mutter('new revision_id is {%s}', self.rev_id)
465
324
def _remove_deleted(self):
466
325
"""Remove deleted files from the working inventories.
479
338
if specific and not is_inside_any(specific, path):
481
340
if not self.work_tree.has_filename(path):
482
self.reporter.missing(path)
483
deleted_ids.append((path, ie.file_id))
341
note('missing %s', path)
342
deleted_ids.append(ie.file_id)
485
deleted_ids.sort(reverse=True)
486
for path, file_id in deleted_ids:
344
for file_id in deleted_ids:
487
345
del self.work_inv[file_id]
488
self.work_tree._write_inventory(self.work_inv)
490
def _populate_new_inv(self):
491
"""Build revision inventory.
493
This creates a new empty inventory. Depending on
494
which files are selected for commit, and what is present in the
495
current tree, the new inventory is populated. inventory entries
496
which are candidates for modification have their revision set to
497
None; inventory entries that are carried over untouched have their
498
revision set to their prior value.
346
self.branch._write_inventory(self.work_inv)
349
def _find_file_parents(self, file_id):
350
"""Return the text versions and hashes for all file parents.
352
Returned as a map from text version to inventory entry.
354
This is a set containing the file versions in all parents
355
revisions containing the file. If the file is new, the set
358
for tree in self.parent_trees:
359
if file_id in tree.inventory:
360
ie = tree.inventory[file_id]
361
assert ie.kind == 'file'
362
assert ie.file_id == file_id
363
if ie.text_version in r:
364
assert r[ie.text_version] == ie
366
r[ie.text_version] = ie
370
def _set_name_versions(self):
371
"""Pass over inventory and mark new entry version as needed.
373
Files get a new name version when they are new, have a
374
different parent, or a different name from in the
375
basis inventory, or if the file is in a different place
376
to any of the parents."""
377
# XXX: Need to think more here about when the user has
378
# made a specific decision on a particular value -- c.f.
380
for path, ie in self.new_inv.iter_entries():
383
for parent_tree in self.parent_trees:
384
parent_inv = parent_tree.inventory
385
if file_id not in parent_inv:
387
parent_ie = parent_inv[file_id]
388
if parent_ie.parent_id != ie.parent_id:
391
elif parent_ie.name != ie.name:
394
elif old_version is None:
395
old_version = parent_ie.name_version
396
elif old_version != parent_ie.name_version:
400
pass # so far so good
401
if old_version is None:
402
mutter('new name_version for {%s}', file_id)
403
ie.name_version = self.rev_id
405
mutter('name_version for {%s} inherited as {%s}',
406
file_id, old_version)
407
ie.name_version = old_version
410
def _store_entries(self):
411
"""Build revision inventory and store modified files.
413
This is called with new_inv a new empty inventory. Depending on
414
which files are selected for commit, and which ones have
415
been modified or merged, new inventory entries are built
416
based on the working and parent inventories.
418
As a side-effect this stores new text versions for committed
419
files with text changes or merges.
421
Each entry can have one of several things happen:
423
carry_file -- carried from the previous version (if not
426
commit_nonfile -- no text to worry about
428
commit_old_text -- same text, may have moved
430
commit_file -- new text version
500
mutter("Selecting files for commit with filter %s", self.specific_files)
501
# iter_entries does not visit the ROOT_ID node so we need to call
502
# self._emit_progress_update once by hand.
503
self._emit_progress_update()
504
432
for path, new_ie in self.work_inv.iter_entries():
505
self._emit_progress_update()
506
433
file_id = new_ie.file_id
507
mutter('check %s {%s}', path, file_id)
508
if (not self.specific_files or
509
is_inside_or_parent_of_any(self.specific_files, path)):
510
mutter('%s selected for commit', path)
514
mutter('%s not selected for commit', path)
515
if self.basis_inv.has_id(file_id):
516
ie = self.basis_inv[file_id].copy()
518
# this entry is new and not being committed
521
self.builder.record_entry_contents(ie, self.parent_invs,
522
path, self.work_tree)
523
# describe the nature of the change that has occurred relative to
524
# the basis inventory.
525
if (self.basis_inv.has_id(ie.file_id)):
526
basis_ie = self.basis_inv[ie.file_id]
529
change = ie.describe_change(basis_ie, ie)
530
if change in (InventoryEntry.RENAMED,
531
InventoryEntry.MODIFIED_AND_RENAMED):
532
old_path = self.basis_inv.id2path(ie.file_id)
533
self.reporter.renamed(change, old_path, path)
535
self.reporter.snapshot_change(change, path)
537
def _emit_progress_update(self):
538
"""Emit an update to the progress bar."""
539
self.pb.update("Committing", self.pb_count, self.pb_total)
434
mutter('check %s {%s}', path, new_ie.file_id)
435
if self.specific_files:
436
if not is_inside_any(self.specific_files, path):
437
mutter('%s not selected for commit', path)
438
self._carry_file(file_id)
440
if new_ie.kind != 'file':
441
self._commit_nonfile(file_id)
444
file_parents = self._find_file_parents(file_id)
445
mutter('parents of %s are %r', path, file_parents)
446
if len(file_parents) == 1:
447
parent_ie = file_parents.values()[0]
448
wc_sha1 = self.work_tree.get_file_sha1(file_id)
449
if parent_ie.text_sha1 == wc_sha1:
450
# text not changed or merged
451
self._commit_old_text(file_id, parent_ie)
453
# file is either new, or a file merge; need to record
455
if len(file_parents) > 1:
456
note('merged %s', path)
457
elif len(file_parents) == 0:
458
note('added %s', path)
460
note('modified %s', path)
461
self._commit_file(new_ie, file_id, file_parents)
464
def _commit_nonfile(self, file_id):
465
self.new_inv.add(self.work_inv[file_id].copy())
468
def _carry_file(self, file_id):
469
"""Carry the file unchanged from the basis revision."""
470
if self.basis_inv.has_id(file_id):
471
self.new_inv.add(self.basis_inv[file_id].copy())
474
def _commit_old_text(self, file_id, parent_ie):
475
"""Keep the same text as last time, but possibly a different name."""
476
ie = self.work_inv[file_id].copy()
477
ie.text_version = parent_ie.text_version
478
ie.text_size = parent_ie.text_size
479
ie.text_sha1 = parent_ie.text_sha1
542
483
def _report_deletes(self):
543
for path, ie in self.basis_inv.iter_entries():
544
if ie.file_id not in self.builder.new_inventory:
545
self.reporter.deleted(path)
484
for file_id in self.basis_inv:
485
if file_id not in self.new_inv:
486
note('deleted %s', self.basis_inv.id2path(file_id))
489
def _commit_file(self, new_ie, file_id, file_parents):
490
mutter('store new text for {%s} in revision {%s}',
491
file_id, self.rev_id)
492
new_lines = self.work_tree.get_file(file_id).readlines()
493
self._add_text_to_weave(file_id, new_lines, file_parents)
494
new_ie.text_version = self.rev_id
495
new_ie.text_sha1 = sha_strings(new_lines)
496
new_ie.text_size = sum(map(len, new_lines))
497
self.new_inv.add(new_ie)
500
def _add_text_to_weave(self, file_id, new_lines, parents):
501
self.weave_store.add_text(file_id, self.rev_id, new_lines, parents)
504
def _gen_revision_id(branch, when):
505
"""Return new revision-id."""
506
s = '%s-%s-' % (user_email(branch), compact_date(when))
507
s += hexlify(rand_bytes(8))
513
def merge_ancestry_lines(rev_id, ancestries):
514
"""Return merged ancestry lines.
516
rev_id -- id of the new revision
518
ancestries -- a sequence of ancestries for parent revisions,
519
as newline-terminated line lists.
521
if len(ancestries) == 0:
522
return [rev_id + '\n']
523
seen = set(ancestries[0])
524
ancs = ancestries[0][:]
525
for parent_ancestry in ancestries[1:]:
526
for line in parent_ancestry:
527
assert line[-1] == '\n'