220
199
return self._parents == other._parents \
221
200
and self._weave == other._weave \
222
201
and self._sha1s == other._sha1s
224
204
def __ne__(self, other):
225
205
return not self.__eq__(other)
227
def _idx_to_name(self, version):
228
return self._names[version]
230
def _lookup(self, name):
231
"""Convert symbolic version name to index."""
232
self.check_not_reserved_id(name)
208
def lookup(self, name):
234
210
return self._name_map[name]
236
raise RevisionNotPresent(name, self._weave_name)
239
"""See VersionedFile.versions."""
240
return self._names[:]
242
def has_version(self, version_id):
243
"""See VersionedFile.has_version."""
244
return (version_id in self._name_map)
246
__contains__ = has_version
248
def get_delta(self, version_id):
249
"""See VersionedFile.get_delta."""
250
return self.get_deltas([version_id])[version_id]
252
def get_deltas(self, version_ids):
253
"""See VersionedFile.get_deltas."""
254
version_ids = self.get_ancestry(version_ids)
255
for version_id in version_ids:
256
if not self.has_version(version_id):
257
raise RevisionNotPresent(version_id, self)
258
# try extracting all versions; parallel extraction is used
259
nv = self.num_versions()
265
last_parent_lines = {}
267
parent_inclusions = {}
272
# its simplest to generate a full set of prepared variables.
274
name = self._names[i]
275
sha1s[name] = self.get_sha1(name)
276
parents_list = self.get_parents(name)
278
parent = parents_list[0]
279
parents[name] = parent
280
parent_inclusions[name] = inclusions[parent]
283
parent_inclusions[name] = set()
284
# we want to emit start, finish, replacement_length, replacement_lines tuples.
285
diff_hunks[name] = []
286
current_hunks[name] = [0, 0, 0, []] # #start, finish, repl_length, repl_tuples
287
parent_linenums[name] = 0
289
parent_noeols[name] = False
290
last_parent_lines[name] = None
291
new_inc = set([name])
292
for p in self._parents[i]:
293
new_inc.update(inclusions[self._idx_to_name(p)])
294
# debug only, known good so far.
295
#assert set(new_inc) == set(self.get_ancestry(name)), \
296
# 'failed %s != %s' % (set(new_inc), set(self.get_ancestry(name)))
297
inclusions[name] = new_inc
299
nlines = len(self._weave)
301
for lineno, inserted, deletes, line in self._walk_internal():
302
# a line is active in a version if:
303
# insert is in the versions inclusions
305
# deleteset & the versions inclusions is an empty set.
306
# so - if we have a included by mapping - version is included by
307
# children, we get a list of children to examine for deletes affect
308
# ing them, which is less than the entire set of children.
309
for version_id in version_ids:
310
# The active inclusion must be an ancestor,
311
# and no ancestors must have deleted this line,
312
# because we don't support resurrection.
313
parent_inclusion = parent_inclusions[version_id]
314
inclusion = inclusions[version_id]
315
parent_active = inserted in parent_inclusion and not (deletes & parent_inclusion)
316
version_active = inserted in inclusion and not (deletes & inclusion)
317
if not parent_active and not version_active:
318
# unrelated line of ancestry
320
elif parent_active and version_active:
322
parent_linenum = parent_linenums[version_id]
323
if current_hunks[version_id] != [parent_linenum, parent_linenum, 0, []]:
324
diff_hunks[version_id].append(tuple(current_hunks[version_id]))
326
current_hunks[version_id] = [parent_linenum, parent_linenum, 0, []]
327
parent_linenums[version_id] = parent_linenum
330
noeols[version_id] = True
333
elif parent_active and not version_active:
335
current_hunks[version_id][1] += 1
336
parent_linenums[version_id] += 1
337
last_parent_lines[version_id] = line
338
elif not parent_active and version_active:
340
# noeol only occurs at the end of a file because we
341
# diff linewise. We want to show noeol changes as a
342
# empty diff unless the actual eol-less content changed.
345
if last_parent_lines[version_id][-1] != '\n':
346
parent_noeols[version_id] = True
347
except (TypeError, IndexError):
350
if theline[-1] != '\n':
351
noeols[version_id] = True
355
parent_should_go = False
357
if parent_noeols[version_id] == noeols[version_id]:
358
# no noeol toggle, so trust the weaves statement
359
# that this line is changed.
361
if parent_noeols[version_id]:
362
theline = theline + '\n'
363
elif parent_noeols[version_id]:
364
# parent has no eol, we do:
365
# our line is new, report as such..
367
elif noeols[version_id]:
368
# append a eol so that it looks like
370
theline = theline + '\n'
371
if parents[version_id] is not None:
372
#if last_parent_lines[version_id] is not None:
373
parent_should_go = True
374
if last_parent_lines[version_id] != theline:
377
#parent_should_go = False
379
current_hunks[version_id][2] += 1
380
current_hunks[version_id][3].append((inserted, theline))
382
# last hunk last parent line is not eaten
383
current_hunks[version_id][1] -= 1
384
if current_hunks[version_id][1] < 0:
385
current_hunks[version_id][1] = 0
386
# import pdb;pdb.set_trace()
387
# assert current_hunks[version_id][1] >= 0
391
version = self._idx_to_name(i)
392
if current_hunks[version] != [0, 0, 0, []]:
393
diff_hunks[version].append(tuple(current_hunks[version]))
395
for version_id in version_ids:
396
result[version_id] = (
400
diff_hunks[version_id],
404
def get_parents(self, version_id):
405
"""See VersionedFile.get_parent."""
406
return map(self._idx_to_name, self._parents[self._lookup(version_id)])
408
def _check_repeated_add(self, name, parents, text, sha1):
409
"""Check that a duplicated add is OK.
411
If it is, return the (old) index; otherwise raise an exception.
413
idx = self._lookup(name)
414
if sorted(self._parents[idx]) != sorted(parents) \
415
or sha1 != self._sha1s[idx]:
416
raise RevisionAlreadyPresent(name, self._weave_name)
419
def _add_lines(self, version_id, parents, lines, parent_texts,
420
left_matching_blocks=None):
421
"""See VersionedFile.add_lines."""
422
return self._add(version_id, lines, map(self._lookup, parents))
424
def _add(self, version_id, lines, parents, sha1=None):
212
raise WeaveError("name %s not present in weave" % name)
215
def add(self, name, parents, text):
425
216
"""Add a single text on top of the weave.
427
218
Returns the index number of the newly added version.
430
221
Symbolic name for this version.
431
222
(Typically the revision-id of the revision that added it.)
434
225
List or set of direct parent version numbers.
437
Sequence of lines to be added in the new version.
440
assert isinstance(version_id, basestring)
441
self._check_lines_not_unicode(lines)
442
self._check_lines_are_lines(lines)
444
sha1 = sha_strings(lines)
445
if version_id in self._name_map:
446
return self._check_repeated_add(version_id, parents, lines, sha1)
228
Sequence of lines to be added in the new version."""
230
assert isinstance(name, basestring)
231
if name in self._name_map:
232
raise WeaveError("name %r already present in weave" % name)
448
234
self._check_versions(parents)
449
## self._check_lines(lines)
235
## self._check_lines(text)
450
236
new_version = len(self._parents)
452
243
# if we abort after here the (in-memory) weave will be corrupt because only
453
244
# some fields are updated
454
# XXX: FIXME implement a succeed-or-fail of the rest of this routine.
455
# - Robert Collins 20060226
456
245
self._parents.append(parents[:])
457
246
self._sha1s.append(sha1)
458
self._names.append(version_id)
459
self._name_map[version_id] = new_version
247
self._names.append(name)
248
self._name_map[name] = new_version
627
419
lineno = 0 # line of weave, 0-based
629
421
for l in self._weave:
630
if l.__class__ == tuple:
422
if isinstance(l, tuple):
634
istack.append(self._names[v])
638
assert self._names[v] not in dset
639
dset.add(self._names[v])
641
dset.remove(self._names[v])
643
raise WeaveFormatError('unexpected instruction %r' % v)
435
raise WeaveFormatError('unexpected instruction %r'
645
assert l.__class__ in (str, unicode)
438
assert isinstance(l, basestring)
647
yield lineno, istack[-1], frozenset(dset), l
440
yield lineno, istack[-1], dset, l
445
def _extract(self, versions):
446
"""Yield annotation of lines in included set.
448
Yields a sequence of tuples (origin, lineno, text), where
449
origin is the origin version, lineno the index in the weave,
450
and text the text of the line.
452
The set typically but not necessarily corresponds to a version.
454
included = self.inclusions(versions)
459
lineno = 0 # line of weave, 0-based
465
WFE = WeaveFormatError
467
for l in self._weave:
468
if isinstance(l, tuple):
472
assert v not in istack
486
assert isinstance(l, basestring)
488
isactive = (not dset) and istack and (istack[-1] in included)
490
result.append((istack[-1], lineno, l))
651
raise WeaveFormatError("unclosed insertion blocks "
652
"at end of weave: %s" % istack)
494
raise WFE("unclosed insertion blocks at end of weave",
654
raise WeaveFormatError("unclosed deletion blocks at end of weave: %s"
497
raise WFE("unclosed deletion blocks at end of weave",
504
def get_iter(self, version):
505
"""Yield lines for the specified version."""
506
for origin, lineno, line in self._extract([version]):
510
def get(self, index):
511
return list(self.get_iter(index))
514
def mash_iter(self, included):
515
"""Return composed version of multiple included versions."""
516
for origin, lineno, text in self._extract(included):
520
def dump(self, to_file):
521
from pprint import pprint
522
print >>to_file, "Weave._weave = ",
523
pprint(self._weave, to_file)
524
print >>to_file, "Weave._parents = ",
525
pprint(self._parents, to_file)
529
def numversions(self):
530
l = len(self._parents)
531
assert l == len(self._sha1s)
536
return self.numversions()
539
def check(self, progress_bar=None):
540
# check no circular inclusions
541
for version in range(self.numversions()):
542
inclusions = list(self._parents[version])
545
if inclusions[-1] >= version:
546
raise WeaveFormatError("invalid included version %d for index %d"
547
% (inclusions[-1], version))
549
# try extracting all versions; this is a bit slow and parallel
550
# extraction could be used
551
nv = self.numversions()
552
for version in range(nv):
554
progress_bar.update('checking text', version, nv)
556
for l in self.get_iter(version):
559
expected = self._sha1s[version]
561
raise WeaveError("mismatched sha1 for version %d; "
562
"got %s, expected %s"
563
% (version, hd, expected))
565
# TODO: check insertions are properly nested, that there are
566
# no lines outside of insertion blocks, that deletions are
567
# properly paired, etc.
571
def merge(self, merge_versions):
572
"""Automerge and mark conflicts between versions.
574
This returns a sequence, each entry describing alternatives
575
for a chunk of the file. Each of the alternatives is given as
578
If there is a chunk of the file where there's no diagreement,
579
only one alternative is given.
582
# approach: find the included versions common to all the
584
raise NotImplementedError()
588
def _delta(self, included, lines):
589
"""Return changes from basis to new revision.
591
The old text for comparison is the union of included revisions.
593
This is used in inserting a new text.
595
Delta is returned as a sequence of
596
(weave1, weave2, newlines).
598
This indicates that weave1:weave2 of the old weave should be
599
replaced by the sequence of lines in newlines. Note that
600
these line numbers are positions in the total weave and don't
601
correspond to the lines in any extracted version, or even the
602
extracted union of included versions.
604
If line1=line2, this is a pure insert; if newlines=[] this is a
605
pure delete. (Similar to difflib.)
657
610
def plan_merge(self, ver_a, ver_b):
658
611
"""Return pseudo-annotation indicating how the two versions merge.
701
654
yield 'unchanged', '' # terminator
703
def _extract(self, versions):
704
"""Yield annotation of lines in included set.
706
Yields a sequence of tuples (origin, lineno, text), where
707
origin is the origin version, lineno the index in the weave,
708
and text the text of the line.
710
The set typically but not necessarily corresponds to a version.
713
if not isinstance(i, int):
716
included = self._inclusions(versions)
722
lineno = 0 # line of weave, 0-based
728
WFE = WeaveFormatError
731
# 449 0 4474.6820 2356.5590 bzrlib.weave:556(_extract)
732
# +285282 0 1676.8040 1676.8040 +<isinstance>
733
# 1.6 seconds in 'isinstance'.
734
# changing the first isinstance:
735
# 449 0 2814.2660 1577.1760 bzrlib.weave:556(_extract)
736
# +140414 0 762.8050 762.8050 +<isinstance>
737
# note that the inline time actually dropped (less function calls)
738
# and total processing time was halved.
739
# we're still spending ~1/4 of the method in isinstance though.
740
# so lets hard code the acceptable string classes we expect:
741
# 449 0 1202.9420 786.2930 bzrlib.weave:556(_extract)
742
# +71352 0 377.5560 377.5560 +<method 'append' of 'list'
744
# yay, down to ~1/4 the initial extract time, and our inline time
745
# has shrunk again, with isinstance no longer dominating.
746
# tweaking the stack inclusion test to use a set gives:
747
# 449 0 1122.8030 713.0080 bzrlib.weave:556(_extract)
748
# +71352 0 354.9980 354.9980 +<method 'append' of 'list'
750
# - a 5% win, or possibly just noise. However with large istacks that
751
# 'in' test could dominate, so I'm leaving this change in place -
752
# when its fast enough to consider profiling big datasets we can review.
757
for l in self._weave:
758
if l.__class__ == tuple:
766
iset.remove(istack.pop())
658
def weave_merge(self, plan):
663
for state, line in plan:
664
if state == 'unchanged' or state == 'killed-both':
665
# resync and flush queued conflicts changes if any
666
if not lines_a and not lines_b:
668
elif ch_a and not ch_b:
670
for l in lines_a: yield l
671
elif ch_b and not ch_a:
672
for l in lines_b: yield l
673
elif lines_a == lines_b:
674
for l in lines_a: yield l
777
assert l.__class__ in (str, unicode)
779
isactive = (not dset) and istack and (istack[-1] in included)
781
result.append((istack[-1], lineno, l))
784
raise WeaveFormatError("unclosed insertion blocks "
785
"at end of weave: %s" % istack)
787
raise WeaveFormatError("unclosed deletion blocks at end of weave: %s"
791
def _maybe_lookup(self, name_or_index):
792
"""Convert possible symbolic name to index, or pass through indexes.
796
if isinstance(name_or_index, (int, long)):
799
return self._lookup(name_or_index)
801
def get_lines(self, version_id):
802
"""See VersionedFile.get_lines()."""
803
int_index = self._maybe_lookup(version_id)
804
result = [line for (origin, lineno, line) in self._extract([int_index])]
805
expected_sha1 = self._sha1s[int_index]
806
measured_sha1 = sha_strings(result)
807
if measured_sha1 != expected_sha1:
808
raise errors.WeaveInvalidChecksum(
809
'file %s, revision %s, expected: %s, measured %s'
810
% (self._weave_name, version_id,
811
expected_sha1, measured_sha1))
814
def get_sha1(self, version_id):
815
"""See VersionedFile.get_sha1()."""
816
return self._sha1s[self._lookup(version_id)]
818
def get_sha1s(self, version_ids):
819
"""See VersionedFile.get_sha1s()."""
820
return [self._sha1s[self._lookup(v)] for v in version_ids]
822
def num_versions(self):
823
"""How many versions are in this weave?"""
824
l = len(self._parents)
825
assert l == len(self._sha1s)
828
__len__ = num_versions
830
def check(self, progress_bar=None):
831
# TODO evaluate performance hit of using string sets in this routine.
832
# TODO: check no circular inclusions
833
# TODO: create a nested progress bar
834
for version in range(self.num_versions()):
835
inclusions = list(self._parents[version])
838
if inclusions[-1] >= version:
839
raise WeaveFormatError("invalid included version %d for index %d"
840
% (inclusions[-1], version))
842
# try extracting all versions; parallel extraction is used
843
nv = self.num_versions()
848
# For creating the ancestry, IntSet is much faster (3.7s vs 0.17s)
849
# The problem is that set membership is much more expensive
850
name = self._idx_to_name(i)
851
sha1s[name] = sha.new()
853
new_inc = set([name])
854
for p in self._parents[i]:
855
new_inc.update(inclusions[self._idx_to_name(p)])
857
assert set(new_inc) == set(self.get_ancestry(name)), \
858
'failed %s != %s' % (set(new_inc), set(self.get_ancestry(name)))
859
inclusions[name] = new_inc
861
nlines = len(self._weave)
863
update_text = 'checking weave'
865
short_name = os.path.basename(self._weave_name)
866
update_text = 'checking %s' % (short_name,)
867
update_text = update_text[:25]
869
for lineno, insert, deleteset, line in self._walk_internal():
871
progress_bar.update(update_text, lineno, nlines)
873
for name, name_inclusions in inclusions.items():
874
# The active inclusion must be an ancestor,
875
# and no ancestors must have deleted this line,
876
# because we don't support resurrection.
877
if (insert in name_inclusions) and not (deleteset & name_inclusions):
878
sha1s[name].update(line)
881
version = self._idx_to_name(i)
882
hd = sha1s[version].hexdigest()
883
expected = self._sha1s[i]
885
raise errors.WeaveInvalidChecksum(
886
"mismatched sha1 for version %s: "
887
"got %s, expected %s"
888
% (version, hd, expected))
890
# TODO: check insertions are properly nested, that there are
891
# no lines outside of insertion blocks, that deletions are
892
# properly paired, etc.
894
def _join(self, other, pb, msg, version_ids, ignore_missing):
895
"""Worker routine for join()."""
896
if not other.versions():
897
return # nothing to update, easy
900
# versions is never none, InterWeave checks this.
903
# two loops so that we do not change ourselves before verifying it
905
# work through in index order to make sure we get all dependencies
908
# get the selected versions only that are in other.versions.
909
version_ids = set(other.versions()).intersection(set(version_ids))
910
# pull in the referenced graph.
911
version_ids = other.get_ancestry(version_ids)
912
pending_graph = [(version, other.get_parents(version)) for
913
version in version_ids]
914
for name in topo_sort(pending_graph):
915
other_idx = other._name_map[name]
916
# returns True if we have it, False if we need it.
917
if not self._check_version_consistent(other, other_idx, name):
918
names_to_join.append((other_idx, name))
927
for other_idx, name in names_to_join:
928
# TODO: If all the parents of the other version are already
929
# present then we can avoid some work by just taking the delta
930
# and adjusting the offsets.
931
new_parents = self._imported_parents(other, other_idx)
932
sha1 = other._sha1s[other_idx]
937
pb.update(msg, merged, len(names_to_join))
939
lines = other.get_lines(other_idx)
940
self._add(name, lines, new_parents, sha1)
942
mutter("merged = %d, processed = %d, file_id=%s; deltat=%d"%(
943
merged, processed, self._weave_name, time.time()-time0))
945
def _imported_parents(self, other, other_idx):
946
"""Return list of parents in self corresponding to indexes in other."""
948
for parent_idx in other._parents[other_idx]:
949
parent_name = other._names[parent_idx]
950
if parent_name not in self._name_map:
951
# should not be possible
952
raise WeaveError("missing parent {%s} of {%s} in %r"
953
% (parent_name, other._name_map[other_idx], self))
954
new_parents.append(self._name_map[parent_name])
957
def _check_version_consistent(self, other, other_idx, name):
958
"""Check if a version in consistent in this and other.
960
To be consistent it must have:
963
* the same direct parents (by name, not index, and disregarding
966
If present & correct return True;
967
if not present in self return False;
968
if inconsistent raise error."""
969
this_idx = self._name_map.get(name, -1)
971
if self._sha1s[this_idx] != other._sha1s[other_idx]:
972
raise errors.WeaveTextDiffers(name, self, other)
973
self_parents = self._parents[this_idx]
974
other_parents = other._parents[other_idx]
975
n1 = set([self._names[i] for i in self_parents])
976
n2 = set([other._names[i] for i in other_parents])
977
if not self._compatible_parents(n1, n2):
978
raise WeaveParentMismatch("inconsistent parents "
979
"for version {%s}: %s vs %s" % (name, n1, n2))
985
def _reweave(self, other, pb, msg):
986
"""Reweave self with other - internal helper for join().
988
:param other: The other weave to merge
989
:param pb: An optional progress bar, indicating how far done we are
990
:param msg: An optional message for the progress
992
new_weave = _reweave(self, other, pb=pb, msg=msg)
993
self._copy_weave_content(new_weave)
995
def _copy_weave_content(self, otherweave):
996
"""adsorb the content from otherweave."""
997
for attr in self.__slots__:
998
if attr != '_weave_name':
999
setattr(self, attr, copy(getattr(otherweave, attr)))
1002
class WeaveFile(Weave):
1003
"""A WeaveFile represents a Weave on disk and writes on change."""
1005
WEAVE_SUFFIX = '.weave'
1007
def __init__(self, name, transport, filemode=None, create=False, access_mode='w'):
1008
"""Create a WeaveFile.
1010
:param create: If not True, only open an existing knit.
1012
super(WeaveFile, self).__init__(name, access_mode)
1013
self._transport = transport
1014
self._filemode = filemode
1016
_read_weave_v5(self._transport.get(name + WeaveFile.WEAVE_SUFFIX), self)
1017
except errors.NoSuchFile:
1023
def _add_lines(self, version_id, parents, lines, parent_texts,
1024
left_matching_blocks=None):
1025
"""Add a version and save the weave."""
1026
self.check_not_reserved_id(version_id)
1027
result = super(WeaveFile, self)._add_lines(version_id, parents, lines,
1032
def _clone_text(self, new_version_id, old_version_id, parents):
1033
"""See VersionedFile.clone_text."""
1034
super(WeaveFile, self)._clone_text(new_version_id, old_version_id, parents)
1037
def copy_to(self, name, transport):
1038
"""See VersionedFile.copy_to()."""
1039
# as we are all in memory always, just serialise to the new place.
1041
write_weave_v5(self, sio)
1043
transport.put_file(name + WeaveFile.WEAVE_SUFFIX, sio, self._filemode)
1045
def create_empty(self, name, transport, filemode=None):
1046
return WeaveFile(name, transport, filemode, create=True)
1049
"""Save the weave."""
1050
self._check_write_ok()
1052
write_weave_v5(self, sio)
1054
self._transport.put_file(self._weave_name + WeaveFile.WEAVE_SUFFIX,
1060
"""See VersionedFile.get_suffixes()."""
1061
return [WeaveFile.WEAVE_SUFFIX]
1063
def join(self, other, pb=None, msg=None, version_ids=None,
1064
ignore_missing=False):
1065
"""Join other into self and save."""
1066
super(WeaveFile, self).join(other, pb, msg, version_ids, ignore_missing)
1070
def _reweave(wa, wb, pb=None, msg=None):
1071
"""Combine two weaves and return the result.
1073
This works even if a revision R has different parents in
1074
wa and wb. In the resulting weave all the parents are given.
1076
This is done by just building up a new weave, maintaining ordering
1077
of the versions in the two inputs. More efficient approaches
1078
might be possible but it should only be necessary to do
1079
this operation rarely, when a new previously ghost version is
1082
:param pb: An optional progress bar, indicating how far done we are
1083
:param msg: An optional message for the progress
1087
queue_a = range(wa.num_versions())
1088
queue_b = range(wb.num_versions())
1089
# first determine combined parents of all versions
1090
# map from version name -> all parent names
1091
combined_parents = _reweave_parent_graphs(wa, wb)
1092
mutter("combined parents: %r", combined_parents)
1093
order = topo_sort(combined_parents.iteritems())
1094
mutter("order to reweave: %r", order)
1099
for idx, name in enumerate(order):
1101
pb.update(msg, idx, len(order))
1102
if name in wa._name_map:
1103
lines = wa.get_lines(name)
1104
if name in wb._name_map:
1105
lines_b = wb.get_lines(name)
1106
if lines != lines_b:
1107
mutter('Weaves differ on content. rev_id {%s}', name)
1108
mutter('weaves: %s, %s', wa._weave_name, wb._weave_name)
1110
lines = list(difflib.unified_diff(lines, lines_b,
1111
wa._weave_name, wb._weave_name))
1112
mutter('lines:\n%s', ''.join(lines))
1113
raise errors.WeaveTextDiffers(name, wa, wb)
1115
lines = wb.get_lines(name)
1116
wr._add(name, lines, [wr._lookup(i) for i in combined_parents[name]])
1119
def _reweave_parent_graphs(wa, wb):
1120
"""Return combined parent ancestry for two weaves.
1122
Returned as a list of (version_name, set(parent_names))"""
1124
for weave in [wa, wb]:
1125
for idx, name in enumerate(weave._names):
1126
p = combined.setdefault(name, set())
1127
p.update(map(weave._idx_to_name, weave._parents[idx]))
677
for l in lines_a: yield l
679
for l in lines_b: yield l
686
if state == 'unchanged':
689
elif state == 'killed-a':
692
elif state == 'killed-b':
695
elif state == 'new-a':
698
elif state == 'new-b':
702
assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
1131
712
def weave_toc(w):