74
74
from bzrlib.branch import gen_file_id
75
75
from bzrlib.errors import (BzrError, PointlessCommit,
79
78
from bzrlib.revision import Revision
80
79
from bzrlib.trace import mutter, note, warning
81
80
from bzrlib.xml5 import serializer_v5
82
from bzrlib.inventory import Inventory, ROOT_ID
81
from bzrlib.inventory import Inventory
83
82
from bzrlib.weave import Weave
84
83
from bzrlib.weavefile import read_weave, write_weave_v5
85
84
from bzrlib.atomicfile import AtomicFile
99
98
class NullCommitReporter(object):
100
99
"""I report on progress of a commit."""
102
def snapshot_change(self, change, path):
105
def completed(self, revno, rev_id):
108
def deleted(self, file_id):
111
def escaped(self, escape_count, message):
114
def missing(self, path):
100
def added(self, path):
103
def removed(self, path):
106
def renamed(self, old_path, new_path):
117
110
class ReportCommitToLog(NullCommitReporter):
119
def snapshot_change(self, change, path):
120
note("%s %s", change, path)
122
def completed(self, revno, rev_id):
123
note('committed r%d {%s}', revno, rev_id)
125
def deleted(self, file_id):
126
note('deleted %s', file_id)
128
def escaped(self, escape_count, message):
129
note("replaced %d control characters in message", escape_count)
131
def missing(self, path):
132
note('missing %s', 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)
134
121
class Commit(object):
135
122
"""Task of committing a new revision.
267
253
lambda match: match.group(0).encode('unicode_escape'),
270
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))
272
278
def _gather_parents(self):
273
279
"""Record the parents of a merge for merge detection."""
274
280
pending_merges = self.branch.pending_merges()
275
281
self.parents = []
276
self.parent_invs = []
282
self.parent_trees = []
277
283
self.present_parents = []
278
284
precursor_id = self.branch.last_revision()
289
295
mutter('commit parent revision {%s}', parent_id)
290
296
if not self.branch.has_revision(parent_id):
291
297
if parent_id == self.branch.last_revision():
292
warning("parent is missing %r", parent_id)
298
warning("parent is pissing %r", parent_id)
293
299
raise HistoryMissing(self.branch, 'revision', parent_id)
295
301
mutter("commit will ghost revision %r", parent_id)
326
333
if specific and not is_inside_any(specific, path):
328
335
if not self.work_tree.has_filename(path):
329
self.reporter.missing(path)
330
deleted_ids.append((path, ie.file_id))
336
note('missing %s', path)
337
deleted_ids.append(ie.file_id)
332
deleted_ids.sort(reverse=True)
333
for path, file_id in deleted_ids:
334
del self.work_inv[file_id]
339
for file_id in deleted_ids:
340
if file_id in self.work_inv:
341
del self.work_inv[file_id]
335
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 set containing the file versions in all parents
351
revisions containing the file. If the file is new, the set
354
for tree in self.parent_trees:
355
if file_id in tree.inventory:
356
ie = tree.inventory[file_id]
357
assert ie.file_id == file_id
359
assert r[ie.revision] == ie
337
364
def _store_snapshot(self):
338
365
"""Pass over inventory and record a snapshot.
340
367
Entries get a new revision when they are modified in
341
368
any way, which includes a merge with a new set of
342
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,
344
381
# XXX: Need to think more here about when the user has
345
382
# made a specific decision on a particular value -- c.f.
347
384
for path, ie in self.new_inv.iter_entries():
348
previous_entries = ie.find_previous_heads(
350
self.weave_store.get_weave_or_empty(ie.file_id))
385
previous_entries = self._find_entry_parents(ie. file_id)
351
386
if ie.revision is None:
352
387
change = ie.snapshot(self.rev_id, path, previous_entries,
353
388
self.work_tree, self.weave_store)
355
390
change = "unchanged"
356
self.reporter.snapshot_change(change, path)
391
note("%s %s", change, path)
358
393
def _populate_new_inv(self):
359
394
"""Build revision inventory.
373
408
if self.specific_files:
374
409
if not is_inside_any(self.specific_files, path):
375
410
mutter('%s not selected for commit', path)
376
self._carry_entry(file_id)
411
self._carry_file(file_id)
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
413
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):
418
def _carry_file(self, file_id):
404
419
"""Carry the file unchanged from the basis revision."""
405
420
if self.basis_inv.has_id(file_id):
406
421
self.new_inv.add(self.basis_inv[file_id].copy())
408
423
def _report_deletes(self):
409
424
for file_id in self.basis_inv:
410
425
if file_id not in self.new_inv:
411
self.reporter.deleted(self.basis_inv.id2path(file_id))
426
note('deleted %s', self.basis_inv.id2path(file_id))
413
430
def _gen_revision_id(branch, when):
414
431
"""Return new revision-id."""
415
432
s = '%s-%s-' % (user_email(branch), compact_date(when))
416
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'