72
66
from binascii import hexlify
73
67
from cStringIO import StringIO
75
from bzrlib.atomicfile import AtomicFile
76
from bzrlib.osutils import (local_time_offset,
77
rand_bytes, compact_date,
69
from bzrlib.osutils import (local_time_offset, username,
70
rand_bytes, compact_date, user_email,
78
71
kind_marker, is_inside_any, quotefn,
79
sha_file, isdir, isfile,
72
sha_string, sha_strings, sha_file, isdir, isfile,
82
import bzrlib.errors as errors
74
from bzrlib.branch import gen_file_id
83
75
from bzrlib.errors import (BzrError, PointlessCommit,
88
78
from bzrlib.revision import Revision
89
from bzrlib.testament import Testament
90
79
from bzrlib.trace import mutter, note, warning
91
80
from bzrlib.xml5 import serializer_v5
92
from bzrlib.inventory import Inventory, ROOT_ID, InventoryEntry
93
from bzrlib.symbol_versioning import *
94
from bzrlib.workingtree import WorkingTree
97
@deprecated_function(zero_seven)
81
from bzrlib.inventory import Inventory
82
from bzrlib.weave import Weave
83
from bzrlib.weavefile import read_weave, write_weave_v5
84
from bzrlib.atomicfile import AtomicFile
98
87
def commit(*args, **kwargs):
99
88
"""Commit a new revision to a branch.
109
98
class NullCommitReporter(object):
110
99
"""I report on progress of a commit."""
112
def snapshot_change(self, change, path):
115
def completed(self, revno, rev_id):
118
def deleted(self, file_id):
121
def escaped(self, escape_count, message):
124
def missing(self, path):
127
def renamed(self, change, old_path, new_path):
100
def added(self, path):
103
def removed(self, path):
106
def renamed(self, old_path, new_path):
131
110
class ReportCommitToLog(NullCommitReporter):
133
# this may be more useful if 'note' was replaced by an overridable
134
# method on self, which would allow more trivial subclassing.
135
# alternative, a callable could be passed in, allowing really trivial
136
# reuse for some uis. RBC 20060511
138
def snapshot_change(self, change, path):
139
if change == 'unchanged':
141
note("%s %s", change, path)
143
def completed(self, revno, rev_id):
144
note('Committed revision %d.', revno)
146
def deleted(self, file_id):
147
note('deleted %s', file_id)
149
def escaped(self, escape_count, message):
150
note("replaced %d control characters in message", escape_count)
152
def missing(self, path):
153
note('missing %s', path)
155
def renamed(self, change, old_path, new_path):
156
note('%s %s => %s', change, old_path, new_path)
111
def added(self, path):
112
note('added %s', path)
114
def removed(self, path):
115
note('removed %s', path)
117
def renamed(self, old_path, new_path):
118
note('renamed %s => %s', old_path, new_path)
159
121
class Commit(object):
216
163
allow_pointless -- If true (default), commit even if nothing
217
164
has changed and no merges are recorded.
219
strict -- If true, don't allow a commit if the working tree
220
contains unknown files.
222
revprops -- Properties for new revision
223
:param local: Perform a local only commit.
225
166
mutter('preparing to commit')
227
if deprecated_passed(branch):
228
warn("Commit.commit (branch, ...): The branch parameter is "
229
"deprecated as of bzr 0.8. Please use working_tree= instead.",
230
DeprecationWarning, stacklevel=2)
232
self.work_tree = self.branch.bzrdir.open_workingtree()
233
elif working_tree is None:
234
raise BzrError("One of branch and working_tree must be passed into commit().")
236
self.work_tree = working_tree
237
self.branch = self.work_tree.branch
239
raise BzrError("The message keyword parameter is required for commit().")
241
self.weave_store = self.branch.repository.weave_store
242
self.bound_branch = None
244
self.master_branch = None
245
self.master_locked = False
169
self.weave_store = branch.weave_store
246
170
self.rev_id = rev_id
247
171
self.specific_files = specific_files
248
172
self.allow_pointless = allow_pointless
250
if revprops is not None:
251
self.revprops.update(revprops)
253
if reporter is None and self.reporter is None:
254
self.reporter = NullCommitReporter()
255
elif reporter is not None:
256
self.reporter = reporter
258
self.work_tree.lock_write()
259
self.pb = bzrlib.ui.ui_factory.nested_progress_bar()
174
if timestamp is None:
175
self.timestamp = time.time()
177
self.timestamp = long(timestamp)
180
self.rev_id = _gen_revision_id(self.branch, self.timestamp)
184
if committer is None:
185
self.committer = username(self.branch)
187
assert isinstance(committer, basestring), type(committer)
188
self.committer = committer
191
self.timezone = local_time_offset()
193
self.timezone = int(timezone)
195
assert isinstance(message, basestring), type(message)
196
self.message = message
197
self._escape_commit_message()
199
self.branch.lock_write()
261
# Cannot commit with conflicts present.
262
if len(self.work_tree.conflicts())>0:
263
raise ConflictsInTree
265
# setup the bound branch variables as needed.
266
self._check_bound_branch()
268
# 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)
275
# raise an exception as soon as we find a single unknown.
276
for unknown in self.work_tree.unknowns():
277
raise StrictCommitFailed()
279
if timestamp is None:
280
self.timestamp = time.time()
282
self.timestamp = long(timestamp)
284
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()
201
self.work_tree = self.branch.working_tree()
309
202
self.work_inv = self.work_tree.inventory
310
self.basis_tree = self.work_tree.basis_tree()
203
self.basis_tree = self.branch.basis_tree()
311
204
self.basis_inv = self.basis_tree.inventory
312
# one to finish, one for rev and inventory, and one for each
313
# inventory entry, and the same for the new inventory.
314
# note that this estimate is too long when we do a partial tree
315
# commit which excludes some new files from being considered.
316
# The estimate is corrected when we populate the new inv.
317
self.pb_total = len(self.basis_inv) + len(self.work_inv) + 3 - 1
320
206
self._gather_parents()
321
207
if len(self.parents) > 1 and self.specific_files:
322
raise NotImplementedError('selected-file commit of merges is not supported yet: files %r',
208
raise NotImplementedError('selected-file commit of merges is not supported yet')
324
209
self._check_parents_present()
326
211
self._remove_deleted()
333
218
or self.new_inv != self.basis_inv):
334
219
raise PointlessCommit()
336
self._emit_progress_update()
337
self.inv_sha1 = self.branch.repository.add_inventory(
342
self._emit_progress_update()
221
self._record_inventory()
222
self._record_ancestry()
343
223
self._make_revision()
344
# revision data is in the local branch now.
346
# upload revision data to the master.
347
# this will propogate merged revisions too if needed.
348
if self.bound_branch:
349
self.master_branch.repository.fetch(self.branch.repository,
350
revision_id=self.rev_id)
351
# now the master has the revision data
352
# 'commit' to the master first so a timeout here causes the local
353
# branch to be out of date
354
self.master_branch.append_revision(self.rev_id)
356
# and now do the commit locally.
224
note('committed r%d {%s}', (self.branch.revno() + 1),
357
226
self.branch.append_revision(self.rev_id)
359
self.work_tree.set_pending_merges([])
360
self.work_tree.set_last_revision(self.rev_id)
361
# now the work tree is up to date with the branch
363
self.reporter.completed(self.branch.revno(), self.rev_id)
364
if self.config.post_commit() is not None:
365
hooks = self.config.post_commit().split(' ')
366
# this would be nicer with twisted.python.reflect.namedAny
368
result = eval(hook + '(branch, rev_id)',
369
{'branch':self.branch,
371
'rev_id':self.rev_id})
372
self._emit_progress_update()
227
self.branch.set_pending_merges([])
376
def _check_bound_branch(self):
377
"""Check to see if the local branch is bound.
379
If it is bound, then most of the commit will actually be
380
done using the remote branch as the target branch.
381
Only at the end will the local branch be updated.
383
if self.local and not self.branch.get_bound_location():
384
raise errors.LocalRequiresBoundBranch()
387
self.master_branch = self.branch.get_master_branch()
389
if not self.master_branch:
390
# make this branch the reference branch for out of date checks.
391
self.master_branch = self.branch
394
# If the master branch is bound, we must fail
395
master_bound_location = self.master_branch.get_bound_location()
396
if master_bound_location:
397
raise errors.CommitToDoubleBoundBranch(self.branch,
398
self.master_branch, master_bound_location)
400
# TODO: jam 20051230 We could automatically push local
401
# commits to the remote branch if they would fit.
402
# But for now, just require remote to be identical
405
# Make sure the local branch is identical to the master
406
master_rh = self.master_branch.revision_history()
407
local_rh = self.branch.revision_history()
408
if local_rh != master_rh:
409
raise errors.BoundBranchOutOfDate(self.branch,
412
# Now things are ready to change the master branch
414
self.bound_branch = self.branch
415
self.master_branch.lock_write()
416
self.master_locked = True
418
#### # Check to see if we have any pending merges. If we do
419
#### # those need to be pushed into the master branch
420
#### pending_merges = self.work_tree.pending_merges()
421
#### if pending_merges:
422
#### for revision_id in pending_merges:
423
#### self.master_branch.repository.fetch(self.bound_branch.repository,
424
#### revision_id=revision_id)
427
"""Cleanup any open locks, progress bars etc."""
428
cleanups = [self._cleanup_bound_branch,
429
self.work_tree.unlock,
431
found_exception = None
432
for cleanup in cleanups:
435
# we want every cleanup to run no matter what.
436
# so we have a catchall here, but we will raise the
437
# last encountered exception up the stack: and
438
# typically this will be useful enough.
441
if found_exception is not None:
442
# dont do a plan raise, because the last exception may have been
443
# trashed, e is our sure-to-work exception even though it loses the
444
# full traceback. XXX: RBC 20060421 perhaps we could check the
445
# exc_info and if its the same one do a plain raise otherwise
446
# 'raise e' as we do now.
449
def _cleanup_bound_branch(self):
450
"""Executed at the end of a try/finally to cleanup a bound branch.
452
If the branch wasn't bound, this is a no-op.
453
If it was, it resents self.branch to the local branch, instead
456
if not self.bound_branch:
458
if self.master_locked:
459
self.master_branch.unlock()
231
def _record_inventory(self):
232
"""Store the inventory for the new revision."""
233
inv_text = serializer_v5.write_inventory_to_string(self.new_inv)
234
self.inv_sha1 = sha_string(inv_text)
235
s = self.branch.control_weaves
236
s.add_text('inventory', self.rev_id,
237
split_lines(inv_text), self.present_parents)
461
239
def _escape_commit_message(self):
462
240
"""Replace xml-incompatible control characters."""
463
# FIXME: RBC 20060419 this should be done by the revision
464
# serialiser not by commit. Then we can also add an unescaper
465
# in the deserializer and start roundtripping revision messages
466
# precisely. See repository_implementations/test_repository.py
468
241
# Python strings can include characters that can't be
469
242
# represented in well-formed XML; escape characters that
470
243
# aren't listed in the XML specification
471
244
# (http://www.w3.org/TR/REC-xml/#NT-Char).
245
if isinstance(self.message, unicode):
246
char_pattern = u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]'
248
# Use a regular 'str' as pattern to avoid having re.subn
249
# return 'unicode' results.
250
char_pattern = '[^x09\x0A\x0D\x20-\xFF]'
472
251
self.message, escape_count = re.subn(
473
u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
474
253
lambda match: match.group(0).encode('unicode_escape'),
477
self.reporter.escaped(escape_count, self.message)
256
note("replaced %d control characters in message", escape_count)
258
def _record_ancestry(self):
259
"""Append merged revision ancestry to the ancestry file.
261
This should be the merged ancestry of all parents, plus the
263
s = self.branch.control_weaves
264
w = s.get_weave_or_empty('ancestry')
265
lines = self._make_ancestry(w)
266
w.add(self.rev_id, self.present_parents, lines)
267
s.put_weave('ancestry', w)
269
def _make_ancestry(self, ancestry_weave):
270
"""Return merged ancestry lines.
272
The lines are revision-ids followed by newlines."""
273
parent_ancestries = [ancestry_weave.get(p) for p in self.present_parents]
274
new_lines = merge_ancestry_lines(self.rev_id, parent_ancestries)
275
mutter('merged ancestry of {%s}:\n%s', self.rev_id, ''.join(new_lines))
479
278
def _gather_parents(self):
480
279
"""Record the parents of a merge for merge detection."""
481
pending_merges = self.work_tree.pending_merges()
280
pending_merges = self.branch.pending_merges()
482
281
self.parents = []
483
282
self.parent_invs = []
484
283
self.present_parents = []
487
286
self.parents.append(precursor_id)
488
287
self.parents += pending_merges
489
288
for revision in self.parents:
490
if self.branch.repository.has_revision(revision):
491
inventory = self.branch.repository.get_inventory(revision)
492
self.parent_invs.append(inventory)
289
if self.branch.has_revision(revision):
290
self.parent_invs.append(self.branch.get_inventory(revision))
493
291
self.present_parents.append(revision)
495
293
def _check_parents_present(self):
496
294
for parent_id in self.parents:
497
295
mutter('commit parent revision {%s}', parent_id)
498
if not self.branch.repository.has_revision(parent_id):
296
if not self.branch.has_revision(parent_id):
499
297
if parent_id == self.branch.last_revision():
500
warning("parent is missing %r", parent_id)
298
warning("parent is pissing %r", parent_id)
501
299
raise HistoryMissing(self.branch, 'revision', parent_id)
503
301
mutter("commit will ghost revision %r", parent_id)
505
303
def _make_revision(self):
506
304
"""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)
305
self.rev = Revision(timestamp=self.timestamp,
306
timezone=self.timezone,
307
committer=self.committer,
308
message=self.message,
309
inventory_sha1=self.inv_sha1,
310
revision_id=self.rev_id)
311
self.rev.parent_ids = self.parents
313
serializer_v5.write_revision(self.rev, rev_tmp)
315
self.branch.revision_store.add(rev_tmp, self.rev_id)
316
mutter('new revision_id is {%s}', self.rev_id)
517
319
def _remove_deleted(self):
518
320
"""Remove deleted files from the working inventories.
531
333
if specific and not is_inside_any(specific, path):
533
335
if not self.work_tree.has_filename(path):
534
self.reporter.missing(path)
535
deleted_ids.append((path, ie.file_id))
336
note('missing %s', path)
337
deleted_ids.append(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)
339
for file_id in deleted_ids:
340
if file_id in self.work_inv:
341
del self.work_inv[file_id]
342
self.branch._write_inventory(self.work_inv)
345
def _find_entry_parents(self, file_id):
346
"""Return the text versions and hashes for all file parents.
348
Returned as a map from text version to inventory entry.
350
This is a map containing the file versions in all parents
351
revisions containing the file. If the file is new, the set
354
for inv in self.parent_invs:
357
assert ie.file_id == file_id
359
assert r[ie.revision] == ie
542
364
def _store_snapshot(self):
543
365
"""Pass over inventory and record a snapshot.
545
367
Entries get a new revision when they are modified in
546
368
any way, which includes a merge with a new set of
547
parents that have the same entry.
369
parents that have the same entry. Currently we do not
370
check for that set being ancestors of each other - and
371
we should - only parallel children should count for this
372
test see find_entry_parents to correct this. FIXME <---
373
I.e. if we are merging in revision FOO, and our
374
copy of file id BAR is identical to FOO.BAR, we should
375
generate a new revision of BAR IF and only IF FOO is
376
neither a child of our current tip, nor an ancestor of
377
our tip. The presence of FOO in our store should not
378
affect this logic UNLESS we are doing a merge of FOO,
549
381
# XXX: Need to think more here about when the user has
550
382
# 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
384
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())
385
previous_entries = self._find_entry_parents(ie. file_id)
562
386
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)
387
change = ie.snapshot(self.rev_id, path, previous_entries,
388
self.work_tree, self.weave_store)
391
note("%s %s", change, path)
582
393
def _populate_new_inv(self):
583
394
"""Build revision inventory.
590
401
revision set to their prior value.
592
403
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()
404
self.new_inv = Inventory()
597
405
for path, new_ie in self.work_inv.iter_entries():
598
self._emit_progress_update()
599
406
file_id = new_ie.file_id
600
407
mutter('check %s {%s}', path, new_ie.file_id)
601
408
if self.specific_files:
602
409
if not is_inside_any(self.specific_files, path):
603
410
mutter('%s not selected for commit', path)
604
self._carry_entry(file_id)
411
self._carry_file(file_id)
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
413
mutter('%s selected for commit', path)
622
self._select_entry(new_ie)
624
def _emit_progress_update(self):
625
"""Emit an update to the progress bar."""
626
self.pb.update("Committing", self.pb_count, self.pb_total)
629
def _select_entry(self, new_ie):
630
"""Make new_ie be considered for committing."""
636
def _carry_entry(self, file_id):
418
def _carry_file(self, file_id):
637
419
"""Carry the file unchanged from the basis revision."""
638
420
if self.basis_inv.has_id(file_id):
639
421
self.new_inv.add(self.basis_inv[file_id].copy())
641
# this entry is new and not being committed
644
423
def _report_deletes(self):
645
for path, ie in self.basis_inv.iter_entries():
646
if ie.file_id not in self.new_inv:
647
self.reporter.deleted(path)
649
def _gen_revision_id(config, when):
424
for file_id in self.basis_inv:
425
if file_id not in self.new_inv:
426
note('deleted %s', self.basis_inv.id2path(file_id))
430
def _gen_revision_id(branch, when):
650
431
"""Return new revision-id."""
651
s = '%s-%s-' % (config.user_email(), compact_date(when))
432
s = '%s-%s-' % (user_email(branch), compact_date(when))
652
433
s += hexlify(rand_bytes(8))
439
def merge_ancestry_lines(rev_id, ancestries):
440
"""Return merged ancestry lines.
442
rev_id -- id of the new revision
444
ancestries -- a sequence of ancestries for parent revisions,
445
as newline-terminated line lists.
447
if len(ancestries) == 0:
448
return [rev_id + '\n']
449
seen = set(ancestries[0])
450
ancs = ancestries[0][:]
451
for parent_ancestry in ancestries[1:]:
452
for line in parent_ancestry:
453
assert line[-1] == '\n'