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
52
# TODO: Rather than mashing together the ancestry and storing it back,
53
# perhaps the weave should have single method which does it all in one
54
# go, avoiding a lot of redundant work.
56
# TODO: Perhaps give a warning if one of the revisions marked as
57
# merged is already in the ancestry, and then don't record it as a
60
# TODO: If the file is newly merged but unchanged from the version it
61
# merges from, then it should still be reported as newly added
62
# relative to the basis revision.
75
59
kind_marker, is_inside_any, quotefn,
76
60
sha_string, sha_strings, sha_file, isdir, isfile,
78
from bzrlib.branch import gen_file_id
79
from bzrlib.errors import (BzrError, PointlessCommit,
82
from bzrlib.revision import Revision
83
from bzrlib.trace import mutter, note, warning
62
from bzrlib.branch import gen_file_id, INVENTORY_FILEID, ANCESTRY_FILEID
63
from bzrlib.errors import BzrError, PointlessCommit
64
from bzrlib.revision import Revision, RevisionReference
65
from bzrlib.trace import mutter, note
84
66
from bzrlib.xml5 import serializer_v5
85
67
from bzrlib.inventory import Inventory
86
68
from bzrlib.weave import Weave
207
187
self.basis_inv = self.basis_tree.inventory
209
189
self._gather_parents()
210
if len(self.parents) > 1 and self.specific_files:
211
raise NotImplementedError('selected-file commit of merges is not supported yet')
212
self._check_parents_present()
214
191
self._remove_deleted()
215
192
self.new_inv = Inventory()
216
self._store_entries()
217
194
self._report_deletes()
218
self._set_name_versions()
220
196
if not (self.allow_pointless
221
or len(self.parents) > 1
197
or len(self.parents) != 1
222
198
or self.new_inv != self.basis_inv):
223
199
raise PointlessCommit()
238
214
"""Store the inventory for the new revision."""
239
215
inv_text = serializer_v5.write_inventory_to_string(self.new_inv)
240
216
self.inv_sha1 = sha_string(inv_text)
241
s = self.branch.control_weaves
242
s.add_text('inventory', self.rev_id,
243
split_lines(inv_text), self.parents)
217
self.weave_store.add_text(INVENTORY_FILEID, self.rev_id,
218
split_lines(inv_text), self.parents)
246
221
def _record_ancestry(self):
247
"""Append merged revision ancestry to the ancestry file.
249
This should be the merged ancestry of all parents, plus the
251
s = self.branch.control_weaves
252
w = s.get_weave_or_empty('ancestry')
253
lines = self._make_ancestry(w)
254
w.add(self.rev_id, self.parents, lines)
255
s.put_weave('ancestry', w)
258
def _make_ancestry(self, ancestry_weave):
259
"""Return merged ancestry lines.
261
The lines are revision-ids followed by newlines."""
262
parent_ancestries = [ancestry_weave.get(p) for p in self.parents]
263
new_lines = merge_ancestry_lines(self.rev_id, parent_ancestries)
264
mutter('merged ancestry of {%s}:\n%s', self.rev_id, ''.join(new_lines))
222
"""Append merged revision ancestry to the ancestry file."""
223
w = self.weave_store.get_weave_or_empty(ANCESTRY_FILEID)
225
lines = w.get(w.lookup(self.parents[0]))
228
lines.append(self.rev_id + '\n')
229
parent_idxs = map(w.lookup, self.parents)
230
w.add(self.rev_id, parent_idxs, lines)
231
self.weave_store.put_weave(ANCESTRY_FILEID, w)
268
234
def _gather_parents(self):
269
235
pending_merges = self.branch.pending_merges()
270
236
self.parents = []
271
self.parent_trees = []
272
237
precursor_id = self.branch.last_revision()
274
239
self.parents.append(precursor_id)
275
self.parent_trees.append(self.basis_tree)
276
240
self.parents += pending_merges
277
self.parent_trees.extend(map(self.branch.revision_tree, pending_merges))
280
def _check_parents_present(self):
281
for parent_id in self.parents:
282
mutter('commit parent revision {%s}', parent_id)
283
if not self.branch.has_revision(parent_id):
284
warning("can't commit a merge from an absent parent")
285
raise HistoryMissing(self.branch, 'revision', parent_id)
241
self.parent_trees = map(self.branch.revision_tree, self.parents)
288
244
def _make_revision(self):
289
245
"""Record a new revision object for this commit."""
290
246
self.rev = Revision(timestamp=self.timestamp,
293
249
message=self.message,
294
250
inventory_sha1=self.inv_sha1,
295
251
revision_id=self.rev_id)
296
self.rev.parent_ids = self.parents
252
self.rev.parents = map(RevisionReference, self.parents)
297
253
rev_tmp = StringIO()
298
254
serializer_v5.write_revision(self.rev, rev_tmp)
300
self.branch.revision_store.add(rev_tmp, self.rev_id, compressed=False)
256
self.branch.revision_store.add(rev_tmp, self.rev_id)
301
257
mutter('new revision_id is {%s}', self.rev_id)
341
297
assert ie.kind == 'file'
342
298
assert ie.file_id == file_id
343
299
if ie.text_version in r:
344
assert r[ie.text_version] == ie
346
r[ie.text_version] = ie
350
def _set_name_versions(self):
351
"""Pass over inventory and mark new entry version as needed.
353
Files get a new name version when they are new, have a
354
different parent, or a different name from in the
355
basis inventory, or if the file is in a different place
356
to any of the parents."""
357
# XXX: Need to think more here about when the user has
358
# made a specific decision on a particular value -- c.f.
360
for path, ie in self.new_inv.iter_entries():
363
for parent_tree in self.parent_trees:
364
parent_inv = parent_tree.inventory
365
if file_id not in parent_inv:
367
parent_ie = parent_inv[file_id]
368
if parent_ie.parent_id != ie.parent_id:
371
elif parent_ie.name != ie.name:
374
elif old_version is None:
375
old_version = parent_ie.name_version
376
elif old_version != parent_ie.name_version:
380
pass # so far so good
381
if old_version is None:
382
mutter('new name_version for {%s}', file_id)
383
ie.name_version = self.rev_id
385
mutter('name_version for {%s} inherited as {%s}',
386
file_id, old_version)
387
ie.name_version = old_version
390
def _store_entries(self):
391
"""Build revision inventory and store modified files.
393
This is called with new_inv a new empty inventory. Depending on
394
which files are selected for commit, and which ones have
395
been modified or merged, new inventory entries are built
396
based on the working and parent inventories.
398
As a side-effect this stores new text versions for committed
399
files with text changes or merges.
401
Each entry can have one of several things happen:
403
carry_file -- carried from the previous version (if not
406
commit_nonfile -- no text to worry about
408
commit_old_text -- same text, may have moved
410
commit_file -- new text version
300
assert r[ie.text_version] == ie.text_sha1
302
r[ie.text_version] = ie.text_sha1
306
def _store_files(self):
307
"""Store new texts of modified/added files.
309
This is called with new_inv set to a copy of the working
310
inventory, with deleted/removed files already cut out. So
311
this code only needs to deal with setting text versions, and
312
possibly recording new file texts."""
412
313
for path, new_ie in self.work_inv.iter_entries():
413
314
file_id = new_ie.file_id
414
315
mutter('check %s {%s}', path, new_ie.file_id)
420
321
if new_ie.kind != 'file':
421
322
self._commit_nonfile(file_id)
424
324
file_parents = self._find_file_parents(file_id)
325
wc_sha1 = self.work_tree.get_file_sha1(file_id)
326
if (len(file_parents) == 1
327
and file_parents.values()[0] == wc_sha1):
328
# not changed or merged
329
self._carry_file(file_id)
425
332
mutter('parents of %s are %r', path, file_parents)
426
if len(file_parents) == 1:
427
parent_ie = file_parents.values()[0]
428
wc_sha1 = self.work_tree.get_file_sha1(file_id)
429
if parent_ie.text_sha1 == wc_sha1:
430
# text not changed or merged
431
self._commit_old_text(file_id, parent_ie)
433
334
# file is either new, or a file merge; need to record
435
336
if len(file_parents) > 1:
448
349
def _carry_file(self, file_id):
449
"""Carry the file unchanged from the basis revision."""
350
"""Keep a file in the same state as in the basis."""
450
351
if self.basis_inv.has_id(file_id):
451
352
self.new_inv.add(self.basis_inv[file_id].copy())
454
def _commit_old_text(self, file_id, parent_ie):
455
"""Keep the same text as last time, but possibly a different name."""
456
ie = self.work_inv[file_id].copy()
457
ie.text_version = parent_ie.text_version
458
ie.text_size = parent_ie.text_size
459
ie.text_sha1 = parent_ie.text_sha1
463
355
def _report_deletes(self):
464
356
for file_id in self.basis_inv:
465
357
if file_id not in self.new_inv:
493
def merge_ancestry_lines(rev_id, ancestries):
494
"""Return merged ancestry lines.
496
rev_id -- id of the new revision
498
ancestries -- a sequence of ancestries for parent revisions,
499
as newline-terminated line lists.
501
if len(ancestries) == 0:
502
return [rev_id + '\n']
503
seen = set(ancestries[0])
504
ancs = ancestries[0][:]
505
for parent_ancestry in ancestries[1:]:
506
for line in parent_ancestry:
507
assert line[-1] == '\n'