58
72
from binascii import hexlify
59
73
from cStringIO import StringIO
61
from bzrlib.osutils import (local_time_offset, username,
62
rand_bytes, compact_date, user_email,
75
from bzrlib.osutils import (local_time_offset,
76
rand_bytes, compact_date,
63
77
kind_marker, is_inside_any, quotefn,
64
78
sha_string, sha_strings, sha_file, isdir, isfile,
66
from bzrlib.branch import gen_file_id, INVENTORY_FILEID, ANCESTRY_FILEID
80
from bzrlib.branch import gen_file_id
67
82
from bzrlib.errors import (BzrError, PointlessCommit,
70
from bzrlib.revision import Revision, RevisionReference
87
import bzrlib.gpg as gpg
88
from bzrlib.revision import Revision
89
from bzrlib.testament import Testament
71
90
from bzrlib.trace import mutter, note, warning
72
91
from bzrlib.xml5 import serializer_v5
73
from bzrlib.inventory import Inventory
92
from bzrlib.inventory import Inventory, ROOT_ID
74
93
from bzrlib.weave import Weave
75
94
from bzrlib.weavefile import read_weave, write_weave_v5
76
95
from bzrlib.atomicfile import AtomicFile
90
109
class NullCommitReporter(object):
91
110
"""I report on progress of a commit."""
92
def added(self, path):
95
def removed(self, path):
98
def renamed(self, old_path, new_path):
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):
102
127
class ReportCommitToLog(NullCommitReporter):
103
def added(self, path):
104
note('added %s', path)
106
def removed(self, path):
107
note('removed %s', path)
109
def renamed(self, old_path, new_path):
110
note('renamed %s => %s', old_path, new_path)
129
def snapshot_change(self, change, path):
130
note("%s %s", change, path)
132
def completed(self, revno, rev_id):
133
note('committed r%d {%s}', revno, rev_id)
135
def deleted(self, file_id):
136
note('deleted %s', file_id)
138
def escaped(self, escape_count, message):
139
note("replaced %d control characters in message", escape_count)
141
def missing(self, path):
142
note('missing %s', path)
113
144
class Commit(object):
114
145
"""Task of committing a new revision.
207
263
or self.new_inv != self.basis_inv):
208
264
raise PointlessCommit()
266
if len(list(self.work_tree.iter_conflicts()))>0:
267
raise ConflictsInTree
210
269
self._record_inventory()
211
self._record_ancestry()
212
270
self._make_revision()
213
note('committed r%d {%s}', (self.branch.revno() + 1),
215
271
self.branch.append_revision(self.rev_id)
216
272
self.branch.set_pending_merges([])
273
self.reporter.completed(self.branch.revno()+1, self.rev_id)
274
if self.config.post_commit() is not None:
275
hooks = self.config.post_commit().split(' ')
276
# this would be nicer with twisted.python.reflect.namedAny
278
result = eval(hook + '(branch, rev_id)',
279
{'branch':self.branch,
281
'rev_id':self.rev_id})
218
283
self.branch.unlock()
222
285
def _record_inventory(self):
223
286
"""Store the inventory for the new revision."""
224
287
inv_text = serializer_v5.write_inventory_to_string(self.new_inv)
225
288
self.inv_sha1 = sha_string(inv_text)
226
self.weave_store.add_text(INVENTORY_FILEID, self.rev_id,
227
split_lines(inv_text), self.parents)
230
def _record_ancestry(self):
231
"""Append merged revision ancestry to the ancestry file.
233
This should be the merged ancestry of all parents, plus the
235
w = self.weave_store.get_weave_or_empty(ANCESTRY_FILEID)
236
lines = self._merge_ancestry_lines(w)
237
w.add(self.rev_id, self.parents, lines)
238
self.weave_store.put_weave(ANCESTRY_FILEID, w)
241
def _merge_ancestry_lines(self, ancestry_weave):
242
"""Return merged ancestry lines.
244
The lines are revision-ids followed by newlines."""
247
for parent_id in self.parents:
248
for line in ancestry_weave.get(parent_id):
249
assert line[-1] == '\n'
253
r = self.rev_id + '\n'
256
mutter('merged ancestry of {%s}:\n%s', self.rev_id, ''.join(ancs))
289
s = self.branch.control_weaves
290
s.add_text('inventory', self.rev_id,
291
split_lines(inv_text), self.present_parents,
292
self.branch.get_transaction())
294
def _escape_commit_message(self):
295
"""Replace xml-incompatible control characters."""
296
# Python strings can include characters that can't be
297
# represented in well-formed XML; escape characters that
298
# aren't listed in the XML specification
299
# (http://www.w3.org/TR/REC-xml/#NT-Char).
300
if isinstance(self.message, unicode):
301
char_pattern = u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]'
303
# Use a regular 'str' as pattern to avoid having re.subn
304
# return 'unicode' results.
305
char_pattern = '[^x09\x0A\x0D\x20-\xFF]'
306
self.message, escape_count = re.subn(
308
lambda match: match.group(0).encode('unicode_escape'),
311
self.reporter.escaped(escape_count, self.message)
260
313
def _gather_parents(self):
314
"""Record the parents of a merge for merge detection."""
261
315
pending_merges = self.branch.pending_merges()
262
316
self.parents = []
263
self.parent_trees = []
317
self.parent_invs = []
318
self.present_parents = []
264
319
precursor_id = self.branch.last_revision()
266
321
self.parents.append(precursor_id)
267
self.parent_trees.append(self.basis_tree)
268
322
self.parents += pending_merges
269
self.parent_trees.extend(map(self.branch.revision_tree, pending_merges))
323
for revision in self.parents:
324
if self.branch.has_revision(revision):
325
self.parent_invs.append(self.branch.get_inventory(revision))
326
self.present_parents.append(revision)
272
328
def _check_parents_present(self):
273
329
for parent_id in self.parents:
274
330
mutter('commit parent revision {%s}', parent_id)
275
331
if not self.branch.has_revision(parent_id):
276
warning("can't commit a merge from an absent parent")
277
raise HistoryMissing(self.branch, 'revision', parent_id)
332
if parent_id == self.branch.last_revision():
333
warning("parent is missing %r", parent_id)
334
raise HistoryMissing(self.branch, 'revision', parent_id)
336
mutter("commit will ghost revision %r", parent_id)
280
338
def _make_revision(self):
281
339
"""Record a new revision object for this commit."""
310
372
if specific and not is_inside_any(specific, path):
312
374
if not self.work_tree.has_filename(path):
313
note('missing %s', path)
314
deleted_ids.append(ie.file_id)
375
self.reporter.missing(path)
376
deleted_ids.append((path, ie.file_id))
316
for file_id in deleted_ids:
378
deleted_ids.sort(reverse=True)
379
for path, file_id in deleted_ids:
317
380
del self.work_inv[file_id]
318
381
self.branch._write_inventory(self.work_inv)
321
def _find_file_parents(self, file_id):
322
"""Return the text versions and hashes for all file parents.
324
Returned as a map from text version to inventory entry.
326
This is a set containing the file versions in all parents
327
revisions containing the file. If the file is new, the set
330
for tree in self.parent_trees:
331
if file_id in tree.inventory:
332
ie = tree.inventory[file_id]
333
assert ie.kind == 'file'
334
assert ie.file_id == file_id
335
if ie.text_version in r:
336
assert r[ie.text_version] == ie
338
r[ie.text_version] = ie
342
def _store_entries(self):
343
"""Build revision inventory and store modified files.
345
This is called with new_inv a new empty inventory. Depending on
346
which files are selected for commit, and which ones have
347
been modified or merged, new inventory entries are built
348
based on the working and parent inventories.
350
As a side-effect this stores new text versions for committed
351
files with text changes or merges.
353
Each entry can have one of several things happen:
355
carry_file -- carried from the previous version (if not
358
commit_nonfile -- no text to worry about
360
commit_old_text -- same text, may have moved
362
commit_file -- new text version
383
def _store_snapshot(self):
384
"""Pass over inventory and record a snapshot.
386
Entries get a new revision when they are modified in
387
any way, which includes a merge with a new set of
388
parents that have the same entry.
390
# XXX: Need to think more here about when the user has
391
# made a specific decision on a particular value -- c.f.
393
for path, ie in self.new_inv.iter_entries():
394
previous_entries = ie.find_previous_heads(
396
self.weave_store.get_weave_or_empty(ie.file_id,
397
self.branch.get_transaction()))
398
if ie.revision is None:
399
change = ie.snapshot(self.rev_id, path, previous_entries,
400
self.work_tree, self.weave_store,
401
self.branch.get_transaction())
404
self.reporter.snapshot_change(change, path)
406
def _populate_new_inv(self):
407
"""Build revision inventory.
409
This creates a new empty inventory. Depending on
410
which files are selected for commit, and what is present in the
411
current tree, the new inventory is populated. inventory entries
412
which are candidates for modification have their revision set to
413
None; inventory entries that are carried over untouched have their
414
revision set to their prior value.
416
mutter("Selecting files for commit with filter %s", self.specific_files)
417
self.new_inv = Inventory()
364
418
for path, new_ie in self.work_inv.iter_entries():
365
419
file_id = new_ie.file_id
366
420
mutter('check %s {%s}', path, new_ie.file_id)
367
421
if self.specific_files:
368
422
if not is_inside_any(self.specific_files, path):
369
423
mutter('%s not selected for commit', path)
370
self._carry_file(file_id)
372
if new_ie.kind != 'file':
373
self._commit_nonfile(file_id)
376
file_parents = self._find_file_parents(file_id)
377
if len(file_parents) == 1:
378
parent_ie = file_parents.values()[0]
379
wc_sha1 = self.work_tree.get_file_sha1(file_id)
380
if parent_ie.text_sha1 == wc_sha1:
381
# text not changed or merged
382
self._commit_old_text(file_id, parent_ie)
385
mutter('parents of %s are %r', path, file_parents)
387
# file is either new, or a file merge; need to record
389
if len(file_parents) > 1:
390
note('merged %s', path)
391
elif len(file_parents) == 0:
392
note('added %s', path)
394
note('modified %s', path)
395
self._commit_file(new_ie, file_id, file_parents)
398
def _commit_nonfile(self, file_id):
399
self.new_inv.add(self.work_inv[file_id].copy())
402
def _carry_file(self, file_id):
424
self._carry_entry(file_id)
427
# this is selected, ensure its parents are too.
428
parent_id = new_ie.parent_id
429
while parent_id != ROOT_ID:
430
if not self.new_inv.has_id(parent_id):
431
ie = self._select_entry(self.work_inv[parent_id])
432
mutter('%s selected for commit because of %s',
433
self.new_inv.id2path(parent_id), path)
435
ie = self.new_inv[parent_id]
436
if ie.revision is not None:
438
mutter('%s selected for commit because of %s',
439
self.new_inv.id2path(parent_id), path)
440
parent_id = ie.parent_id
441
mutter('%s selected for commit', path)
442
self._select_entry(new_ie)
444
def _select_entry(self, new_ie):
445
"""Make new_ie be considered for committing."""
451
def _carry_entry(self, file_id):
403
452
"""Carry the file unchanged from the basis revision."""
404
453
if self.basis_inv.has_id(file_id):
405
454
self.new_inv.add(self.basis_inv[file_id].copy())
408
def _commit_old_text(self, file_id, parent_ie):
409
"""Keep the same text as last time, but possibly a different name."""
410
ie = self.work_inv[file_id].copy()
411
ie.text_version = parent_ie.text_version
412
ie.text_size = parent_ie.text_size
413
ie.text_sha1 = parent_ie.text_sha1
417
456
def _report_deletes(self):
418
457
for file_id in self.basis_inv:
419
458
if file_id not in self.new_inv:
420
note('deleted %s', self.basis_inv.id2path(file_id))
423
def _commit_file(self, new_ie, file_id, file_parents):
424
mutter('store new text for {%s} in revision {%s}',
425
file_id, self.rev_id)
426
new_lines = self.work_tree.get_file(file_id).readlines()
427
self._add_text_to_weave(file_id, new_lines, file_parents)
428
new_ie.text_version = self.rev_id
429
new_ie.text_sha1 = sha_strings(new_lines)
430
new_ie.text_size = sum(map(len, new_lines))
431
self.new_inv.add(new_ie)
434
def _add_text_to_weave(self, file_id, new_lines, parents):
435
if file_id.startswith('__'):
436
raise ValueError('illegal file-id %r for text file' % file_id)
437
self.weave_store.add_text(file_id, self.rev_id, new_lines, parents)
440
def _gen_revision_id(branch, when):
459
self.reporter.deleted(self.basis_inv.id2path(file_id))
461
def _gen_revision_id(config, when):
441
462
"""Return new revision-id."""
442
s = '%s-%s-' % (user_email(branch), compact_date(when))
463
s = '%s-%s-' % (config.user_email(), compact_date(when))
443
464
s += hexlify(rand_bytes(8))