44
44
# TODO: Update hashcache before and after - or does the WorkingTree
47
# This code requires all merge parents to be present in the branch.
48
# We could relax this but for the sake of simplicity the constraint is
49
# here for now. It's not totally clear to me how we'd know which file
50
# need new text versions if some parents are absent. -- mbp 20050915
47
52
# TODO: Rather than mashing together the ancestry and storing it back,
48
53
# perhaps the weave should have single method which does it all in one
49
54
# go, avoiding a lot of redundant work.
72
71
from binascii import hexlify
73
72
from cStringIO import StringIO
75
from bzrlib.osutils import (local_time_offset,
76
rand_bytes, compact_date,
74
from bzrlib.osutils import (local_time_offset, username,
75
rand_bytes, compact_date, user_email,
77
76
kind_marker, is_inside_any, quotefn,
78
77
sha_string, sha_strings, sha_file, isdir, isfile,
80
79
from bzrlib.branch import gen_file_id
82
80
from bzrlib.errors import (BzrError, PointlessCommit,
87
import bzrlib.gpg as gpg
88
83
from bzrlib.revision import Revision
89
from bzrlib.testament import Testament
90
84
from bzrlib.trace import mutter, note, warning
91
85
from bzrlib.xml5 import serializer_v5
92
from bzrlib.inventory import Inventory, ROOT_ID
86
from bzrlib.inventory import Inventory
93
87
from bzrlib.weave import Weave
94
88
from bzrlib.weavefile import read_weave, write_weave_v5
95
89
from bzrlib.atomicfile import AtomicFile
109
103
class NullCommitReporter(object):
110
104
"""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):
105
def added(self, path):
108
def removed(self, path):
111
def renamed(self, old_path, new_path):
127
115
class ReportCommitToLog(NullCommitReporter):
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)
116
def added(self, path):
117
note('added %s', path)
119
def removed(self, path):
120
note('removed %s', path)
122
def renamed(self, old_path, new_path):
123
note('renamed %s => %s', old_path, new_path)
144
126
class Commit(object):
145
127
"""Task of committing a new revision.
204
175
self.rev_id = rev_id
205
176
self.specific_files = specific_files
206
177
self.allow_pointless = allow_pointless
207
self.revprops = revprops
209
if strict and branch.unknowns():
210
raise StrictCommitFailed()
212
179
if timestamp is None:
213
180
self.timestamp = time.time()
215
182
self.timestamp = long(timestamp)
217
if self.config is None:
218
self.config = bzrlib.config.BranchConfig(self.branch)
220
184
if rev_id is None:
221
self.rev_id = _gen_revision_id(self.config, self.timestamp)
185
self.rev_id = _gen_revision_id(self.branch, self.timestamp)
223
187
self.rev_id = rev_id
225
189
if committer is None:
226
self.committer = self.config.username()
190
self.committer = username(self.branch)
228
192
assert isinstance(committer, basestring), type(committer)
229
193
self.committer = committer
276
239
self.inv_sha1 = sha_string(inv_text)
277
240
s = self.branch.control_weaves
278
241
s.add_text('inventory', self.rev_id,
279
split_lines(inv_text), self.present_parents,
280
self.branch.get_transaction())
242
split_lines(inv_text), self.present_parents)
282
244
def _escape_commit_message(self):
283
245
"""Replace xml-incompatible control characters."""
296
258
lambda match: match.group(0).encode('unicode_escape'),
299
self.reporter.escaped(escape_count, self.message)
261
note("replaced %d control characters in message", escape_count)
263
def _record_ancestry(self):
264
"""Append merged revision ancestry to the ancestry file.
266
This should be the merged ancestry of all parents, plus the
268
s = self.branch.control_weaves
269
w = s.get_weave_or_empty('ancestry')
270
lines = self._make_ancestry(w)
271
w.add(self.rev_id, self.present_parents, lines)
272
s.put_weave('ancestry', w)
274
def _make_ancestry(self, ancestry_weave):
275
"""Return merged ancestry lines.
277
The lines are revision-ids followed by newlines."""
278
parent_ancestries = [ancestry_weave.get(p) for p in self.present_parents]
279
new_lines = merge_ancestry_lines(self.rev_id, parent_ancestries)
280
mutter('merged ancestry of {%s}:\n%s', self.rev_id, ''.join(new_lines))
301
283
def _gather_parents(self):
302
284
"""Record the parents of a merge for merge detection."""
303
285
pending_merges = self.branch.pending_merges()
304
286
self.parents = []
305
self.parent_invs = []
287
self.parent_trees = []
306
288
self.present_parents = []
307
289
precursor_id = self.branch.last_revision()
318
300
mutter('commit parent revision {%s}', parent_id)
319
301
if not self.branch.has_revision(parent_id):
320
302
if parent_id == self.branch.last_revision():
321
warning("parent is missing %r", parent_id)
303
warning("parent is pissing %r", parent_id)
322
304
raise HistoryMissing(self.branch, 'revision', parent_id)
324
306
mutter("commit will ghost revision %r", parent_id)
330
312
committer=self.committer,
331
313
message=self.message,
332
314
inventory_sha1=self.inv_sha1,
333
revision_id=self.rev_id,
334
properties=self.revprops)
315
revision_id=self.rev_id)
335
316
self.rev.parent_ids = self.parents
336
317
rev_tmp = StringIO()
337
318
serializer_v5.write_revision(self.rev, rev_tmp)
339
if self.config.signature_needed():
340
plaintext = Testament(self.rev, self.new_inv).as_short_text()
341
self.branch.store_revision_signature(gpg.GPGStrategy(self.config),
342
plaintext, self.rev_id)
343
320
self.branch.revision_store.add(rev_tmp, self.rev_id)
344
321
mutter('new revision_id is {%s}', self.rev_id)
346
324
def _remove_deleted(self):
347
325
"""Remove deleted files from the working inventories.
360
338
if specific and not is_inside_any(specific, path):
362
340
if not self.work_tree.has_filename(path):
363
self.reporter.missing(path)
364
deleted_ids.append((path, ie.file_id))
341
note('missing %s', path)
342
deleted_ids.append(ie.file_id)
366
deleted_ids.sort(reverse=True)
367
for path, file_id in deleted_ids:
368
del self.work_inv[file_id]
344
for file_id in deleted_ids:
345
if file_id in self.work_inv:
346
del self.work_inv[file_id]
369
347
self.branch._write_inventory(self.work_inv)
350
def _find_entry_parents(self, file_id):
351
"""Return the text versions and hashes for all file parents.
353
Returned as a map from text version to inventory entry.
355
This is a set containing the file versions in all parents
356
revisions containing the file. If the file is new, the set
359
for tree in self.parent_trees:
360
if file_id in tree.inventory:
361
ie = tree.inventory[file_id]
362
assert ie.file_id == file_id
364
assert r[ie.revision] == ie
371
369
def _store_snapshot(self):
372
370
"""Pass over inventory and record a snapshot.
374
372
Entries get a new revision when they are modified in
375
373
any way, which includes a merge with a new set of
376
parents that have the same entry.
374
parents that have the same entry. Currently we do not
375
check for that set being ancestors of each other - and
376
we should - only parallel children should count for this
377
test see find_entry_parents to correct this. FIXME <---
378
I.e. if we are merging in revision FOO, and our
379
copy of file id BAR is identical to FOO.BAR, we should
380
generate a new revision of BAR IF and only IF FOO is
381
neither a child of our current tip, nor an ancestor of
382
our tip. The presence of FOO in our store should not
383
affect this logic UNLESS we are doing a merge of FOO,
378
386
# XXX: Need to think more here about when the user has
379
387
# made a specific decision on a particular value -- c.f.
381
389
for path, ie in self.new_inv.iter_entries():
382
previous_entries = ie.find_previous_heads(
384
self.weave_store.get_weave_or_empty(ie.file_id,
385
self.branch.get_transaction()))
390
previous_entries = self._find_entry_parents(ie. file_id)
386
391
if ie.revision is None:
387
392
change = ie.snapshot(self.rev_id, path, previous_entries,
388
self.work_tree, self.weave_store,
389
self.branch.get_transaction())
393
self.work_tree, self.weave_store)
391
395
change = "unchanged"
392
self.reporter.snapshot_change(change, path)
396
note("%s %s", change, path)
394
398
def _populate_new_inv(self):
395
399
"""Build revision inventory.
409
413
if self.specific_files:
410
414
if not is_inside_any(self.specific_files, path):
411
415
mutter('%s not selected for commit', path)
412
self._carry_entry(file_id)
416
self._carry_file(file_id)
415
# this is selected, ensure its parents are too.
416
parent_id = new_ie.parent_id
417
while parent_id != ROOT_ID:
418
if not self.new_inv.has_id(parent_id):
419
ie = self._select_entry(self.work_inv[parent_id])
420
mutter('%s selected for commit because of %s',
421
self.new_inv.id2path(parent_id), path)
423
ie = self.new_inv[parent_id]
424
if ie.revision is not None:
426
mutter('%s selected for commit because of %s',
427
self.new_inv.id2path(parent_id), path)
428
parent_id = ie.parent_id
429
418
mutter('%s selected for commit', path)
430
self._select_entry(new_ie)
432
def _select_entry(self, new_ie):
433
"""Make new_ie be considered for committing."""
439
def _carry_entry(self, file_id):
423
def _carry_file(self, file_id):
440
424
"""Carry the file unchanged from the basis revision."""
441
425
if self.basis_inv.has_id(file_id):
442
426
self.new_inv.add(self.basis_inv[file_id].copy())
444
428
def _report_deletes(self):
445
429
for file_id in self.basis_inv:
446
430
if file_id not in self.new_inv:
447
self.reporter.deleted(self.basis_inv.id2path(file_id))
449
def _gen_revision_id(config, when):
431
note('deleted %s', self.basis_inv.id2path(file_id))
435
def _gen_revision_id(branch, when):
450
436
"""Return new revision-id."""
451
s = '%s-%s-' % (config.user_email(), compact_date(when))
437
s = '%s-%s-' % (user_email(branch), compact_date(when))
452
438
s += hexlify(rand_bytes(8))
444
def merge_ancestry_lines(rev_id, ancestries):
445
"""Return merged ancestry lines.
447
rev_id -- id of the new revision
449
ancestries -- a sequence of ancestries for parent revisions,
450
as newline-terminated line lists.
452
if len(ancestries) == 0:
453
return [rev_id + '\n']
454
seen = set(ancestries[0])
455
ancs = ancestries[0][:]
456
for parent_ancestry in ancestries[1:]:
457
for line in parent_ancestry:
458
assert line[-1] == '\n'