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.
74
79
from bzrlib.branch import gen_file_id
75
80
from bzrlib.errors import (BzrError, PointlessCommit,
79
83
from bzrlib.revision import Revision
80
84
from bzrlib.trace import mutter, note, warning
81
85
from bzrlib.xml5 import serializer_v5
82
from bzrlib.inventory import Inventory, ROOT_ID
86
from bzrlib.inventory import Inventory
83
87
from bzrlib.weave import Weave
84
88
from bzrlib.weavefile import read_weave, write_weave_v5
85
89
from bzrlib.atomicfile import AtomicFile
99
103
class NullCommitReporter(object):
100
104
"""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):
105
def added(self, path):
108
def removed(self, path):
111
def renamed(self, old_path, new_path):
117
115
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)
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)
134
126
class Commit(object):
135
127
"""Task of committing a new revision.
267
258
lambda match: match.group(0).encode('unicode_escape'),
270
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))
272
283
def _gather_parents(self):
273
284
"""Record the parents of a merge for merge detection."""
274
285
pending_merges = self.branch.pending_merges()
275
286
self.parents = []
276
self.parent_invs = []
287
self.parent_trees = []
277
288
self.present_parents = []
278
289
precursor_id = self.branch.last_revision()
289
300
mutter('commit parent revision {%s}', parent_id)
290
301
if not self.branch.has_revision(parent_id):
291
302
if parent_id == self.branch.last_revision():
292
warning("parent is missing %r", parent_id)
303
warning("parent is pissing %r", parent_id)
293
304
raise HistoryMissing(self.branch, 'revision', parent_id)
295
306
mutter("commit will ghost revision %r", parent_id)
326
338
if specific and not is_inside_any(specific, path):
328
340
if not self.work_tree.has_filename(path):
329
self.reporter.missing(path)
330
deleted_ids.append((path, ie.file_id))
341
note('missing %s', path)
342
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]
344
for file_id in deleted_ids:
345
if file_id in self.work_inv:
346
del self.work_inv[file_id]
335
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
337
369
def _store_snapshot(self):
338
370
"""Pass over inventory and record a snapshot.
340
372
Entries get a new revision when they are modified in
341
373
any way, which includes a merge with a new set of
342
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,
344
386
# XXX: Need to think more here about when the user has
345
387
# made a specific decision on a particular value -- c.f.
347
389
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))
390
previous_entries = self._find_entry_parents(ie. file_id)
351
391
if ie.revision is None:
352
392
change = ie.snapshot(self.rev_id, path, previous_entries,
353
393
self.work_tree, self.weave_store)
355
395
change = "unchanged"
356
self.reporter.snapshot_change(change, path)
396
note("%s %s", change, path)
358
398
def _populate_new_inv(self):
359
399
"""Build revision inventory.
373
413
if self.specific_files:
374
414
if not is_inside_any(self.specific_files, path):
375
415
mutter('%s not selected for commit', path)
376
self._carry_entry(file_id)
416
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
418
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):
423
def _carry_file(self, file_id):
404
424
"""Carry the file unchanged from the basis revision."""
405
425
if self.basis_inv.has_id(file_id):
406
426
self.new_inv.add(self.basis_inv[file_id].copy())
408
428
def _report_deletes(self):
409
429
for file_id in self.basis_inv:
410
430
if file_id not in self.new_inv:
411
self.reporter.deleted(self.basis_inv.id2path(file_id))
431
note('deleted %s', self.basis_inv.id2path(file_id))
413
435
def _gen_revision_id(branch, when):
414
436
"""Return new revision-id."""
415
437
s = '%s-%s-' % (user_email(branch), compact_date(when))
416
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'