20
17
"""Versioned text file storage api."""
19
from __future__ import absolute_import
22
21
from copy import copy
23
from cStringIO import StringIO
26
25
from zlib import adler32
28
from bzrlib.lazy_import import lazy_import
27
from ..lazy_import import lazy_import
29
28
lazy_import(globals(), """
35
32
graph as _mod_graph,
45
from bzrlib.graph import DictParentsProvider, Graph, StackedParentsProvider
46
from bzrlib.transport.memory import MemoryTransport
39
from breezy.bzr import (
48
from bzrlib.registry import Registry
49
from bzrlib.symbol_versioning import *
50
from bzrlib.textmerge import TextMerge
51
from bzrlib import bencode
48
from ..registry import Registry
49
from ..sixish import (
55
from ..textmerge import TextMerge
54
58
adapter_registry = Registry()
55
adapter_registry.register_lazy(('knit-delta-gz', 'fulltext'), 'bzrlib.knit',
56
'DeltaPlainToFullText')
57
adapter_registry.register_lazy(('knit-ft-gz', 'fulltext'), 'bzrlib.knit',
59
59
adapter_registry.register_lazy(('knit-annotated-delta-gz', 'knit-delta-gz'),
60
'bzrlib.knit', 'DeltaAnnotatedToUnannotated')
61
adapter_registry.register_lazy(('knit-annotated-delta-gz', 'fulltext'),
62
'bzrlib.knit', 'DeltaAnnotatedToFullText')
60
'breezy.bzr.knit', 'DeltaAnnotatedToUnannotated')
63
61
adapter_registry.register_lazy(('knit-annotated-ft-gz', 'knit-ft-gz'),
64
'bzrlib.knit', 'FTAnnotatedToUnannotated')
65
adapter_registry.register_lazy(('knit-annotated-ft-gz', 'fulltext'),
66
'bzrlib.knit', 'FTAnnotatedToFullText')
67
# adapter_registry.register_lazy(('knit-annotated-ft-gz', 'chunked'),
68
# 'bzrlib.knit', 'FTAnnotatedToChunked')
62
'breezy.bzr.knit', 'FTAnnotatedToUnannotated')
63
for target_storage_kind in ('fulltext', 'chunked', 'lines'):
64
adapter_registry.register_lazy(('knit-delta-gz', target_storage_kind), 'breezy.bzr.knit',
65
'DeltaPlainToFullText')
66
adapter_registry.register_lazy(('knit-ft-gz', target_storage_kind), 'breezy.bzr.knit',
68
adapter_registry.register_lazy(('knit-annotated-ft-gz', target_storage_kind),
69
'breezy.bzr.knit', 'FTAnnotatedToFullText')
70
adapter_registry.register_lazy(('knit-annotated-delta-gz', target_storage_kind),
71
'breezy.bzr.knit', 'DeltaAnnotatedToFullText')
74
class UnavailableRepresentation(errors.InternalBzrError):
76
_fmt = ("The encoding '%(wanted)s' is not available for key %(key)s which "
77
"is encoded as '%(native)s'.")
79
def __init__(self, key, wanted, native):
80
errors.InternalBzrError.__init__(self)
86
class ExistingContent(errors.BzrError):
88
_fmt = "The content being inserted is already present."
71
91
class ContentFactory(object):
72
92
"""Abstract interface for insertion and retrieval from a VersionedFile.
74
94
:ivar sha1: None, or the sha1 of the content fulltext.
95
:ivar size: None, or the size of the content fulltext.
75
96
:ivar storage_kind: The native storage kind of this factory. One of
76
97
'mpdiff', 'knit-annotated-ft', 'knit-annotated-delta', 'knit-ft',
77
98
'knit-delta', 'fulltext', 'knit-annotated-ft-gz',
106
129
:ivar parents: A tuple of parent keys for self.key. If the object has
107
130
no parent information, None (as opposed to () for an empty list of
132
:ivar chunks_are_lines: Whether chunks are lines.
111
def __init__(self, key, parents, sha1, chunks):
135
def __init__(self, key, parents, sha1, chunks, chunks_are_lines=None):
112
136
"""Create a ContentFactory."""
138
self.size = sum(map(len, chunks))
114
139
self.storage_kind = 'chunked'
116
141
self.parents = parents
117
142
self._chunks = chunks
143
self._chunks_are_lines = chunks_are_lines
119
145
def get_bytes_as(self, storage_kind):
120
146
if storage_kind == 'chunked':
121
147
return self._chunks
122
148
elif storage_kind == 'fulltext':
123
return ''.join(self._chunks)
124
raise errors.UnavailableRepresentation(self.key, storage_kind,
149
return b''.join(self._chunks)
150
elif storage_kind == 'lines':
151
if self._chunks_are_lines:
153
return list(osutils.chunks_to_lines(self._chunks))
154
raise UnavailableRepresentation(self.key, storage_kind,
157
def iter_bytes_as(self, storage_kind):
158
if storage_kind == 'chunked':
159
return iter(self._chunks)
160
elif storage_kind == 'lines':
161
if self._chunks_are_lines:
162
return iter(self._chunks)
163
return iter(osutils.chunks_to_lines(self._chunks))
164
raise UnavailableRepresentation(self.key, storage_kind,
128
167
class FulltextContentFactory(ContentFactory):
129
168
"""Static data content factory.
154
196
return self._text
155
197
elif storage_kind == 'chunked':
156
198
return [self._text]
157
raise errors.UnavailableRepresentation(self.key, storage_kind,
199
elif storage_kind == 'lines':
200
return osutils.split_lines(self._text)
201
raise UnavailableRepresentation(self.key, storage_kind,
204
def iter_bytes_as(self, storage_kind):
205
if storage_kind == 'chunked':
206
return iter([self._text])
207
elif storage_kind == 'lines':
208
return iter(osutils.split_lines(self._text))
209
raise UnavailableRepresentation(self.key, storage_kind,
213
class FileContentFactory(ContentFactory):
214
"""File-based content factory.
217
def __init__(self, key, parents, fileobj, sha1=None, size=None):
219
self.parents = parents
221
self.storage_kind = 'file'
225
def get_bytes_as(self, storage_kind):
227
if storage_kind == 'fulltext':
228
return self.file.read()
229
elif storage_kind == 'chunked':
230
return list(osutils.file_iterator(self.file))
231
elif storage_kind == 'lines':
232
return list(self.file.readlines())
233
raise UnavailableRepresentation(self.key, storage_kind,
236
def iter_bytes_as(self, storage_kind):
238
if storage_kind == 'chunked':
239
return osutils.file_iterator(self.file)
240
elif storage_kind == 'lines':
242
raise UnavailableRepresentation(self.key, storage_kind,
161
246
class AbsentContentFactory(ContentFactory):
301
class _MPDiffGenerator(object):
302
"""Pull out the functionality for generating mp_diffs."""
304
def __init__(self, vf, keys):
306
# This is the order the keys were requested in
307
self.ordered_keys = tuple(keys)
308
# keys + their parents, what we need to compute the diffs
309
self.needed_keys = ()
310
# Map from key: mp_diff
312
# Map from key: parents_needed (may have ghosts)
314
# Parents that aren't present
315
self.ghost_parents = ()
316
# Map from parent_key => number of children for this text
318
# Content chunks that are cached while we still need them
321
def _find_needed_keys(self):
322
"""Find the set of keys we need to request.
324
This includes all the original keys passed in, and the non-ghost
325
parents of those keys.
327
:return: (needed_keys, refcounts)
328
needed_keys is the set of all texts we need to extract
329
refcounts is a dict of {key: num_children} letting us know when we
330
no longer need to cache a given parent text
332
# All the keys and their parents
333
needed_keys = set(self.ordered_keys)
334
parent_map = self.vf.get_parent_map(needed_keys)
335
self.parent_map = parent_map
336
# TODO: Should we be using a different construct here? I think this
337
# uses difference_update internally, and we expect the result to
339
missing_keys = needed_keys.difference(parent_map)
341
raise errors.RevisionNotPresent(list(missing_keys)[0], self.vf)
342
# Parents that might be missing. They are allowed to be ghosts, but we
343
# should check for them
345
setdefault = refcounts.setdefault
347
for child_key, parent_keys in viewitems(parent_map):
349
# parent_keys may be None if a given VersionedFile claims to
350
# not support graph operations.
352
just_parents.update(parent_keys)
353
needed_keys.update(parent_keys)
354
for p in parent_keys:
355
refcounts[p] = setdefault(p, 0) + 1
356
just_parents.difference_update(parent_map)
357
# Remove any parents that are actually ghosts from the needed set
358
self.present_parents = set(self.vf.get_parent_map(just_parents))
359
self.ghost_parents = just_parents.difference(self.present_parents)
360
needed_keys.difference_update(self.ghost_parents)
361
self.needed_keys = needed_keys
362
self.refcounts = refcounts
363
return needed_keys, refcounts
365
def _compute_diff(self, key, parent_lines, lines):
366
"""Compute a single mp_diff, and store it in self._diffs"""
367
if len(parent_lines) > 0:
368
# XXX: _extract_blocks is not usefully defined anywhere...
369
# It was meant to extract the left-parent diff without
370
# having to recompute it for Knit content (pack-0.92,
371
# etc). That seems to have regressed somewhere
372
left_parent_blocks = self.vf._extract_blocks(key,
373
parent_lines[0], lines)
375
left_parent_blocks = None
376
diff = multiparent.MultiParent.from_lines(lines,
377
parent_lines, left_parent_blocks)
378
self.diffs[key] = diff
380
def _process_one_record(self, key, this_chunks):
382
if key in self.parent_map:
383
# This record should be ready to diff, since we requested
384
# content in 'topological' order
385
parent_keys = self.parent_map.pop(key)
386
# If a VersionedFile claims 'no-graph' support, then it may return
387
# None for any parent request, so we replace it with an empty tuple
388
if parent_keys is None:
391
for p in parent_keys:
392
# Alternatively we could check p not in self.needed_keys, but
393
# ghost_parents should be tiny versus huge
394
if p in self.ghost_parents:
396
refcount = self.refcounts[p]
397
if refcount == 1: # Last child reference
398
self.refcounts.pop(p)
399
parent_chunks = self.chunks.pop(p)
401
self.refcounts[p] = refcount - 1
402
parent_chunks = self.chunks[p]
403
p_lines = osutils.chunks_to_lines(parent_chunks)
404
# TODO: Should we cache the line form? We did the
405
# computation to get it, but storing it this way will
406
# be less memory efficient...
407
parent_lines.append(p_lines)
409
lines = osutils.chunks_to_lines(this_chunks)
410
# Since we needed the lines, we'll go ahead and cache them this way
412
self._compute_diff(key, parent_lines, lines)
414
# Is this content required for any more children?
415
if key in self.refcounts:
416
self.chunks[key] = this_chunks
418
def _extract_diffs(self):
419
needed_keys, refcounts = self._find_needed_keys()
420
for record in self.vf.get_record_stream(needed_keys,
421
'topological', True):
422
if record.storage_kind == 'absent':
423
raise errors.RevisionNotPresent(record.key, self.vf)
424
self._process_one_record(record.key,
425
record.get_bytes_as('chunked'))
427
def compute_diffs(self):
428
self._extract_diffs()
429
dpop = self.diffs.pop
430
return [dpop(k) for k in self.ordered_keys]
209
433
class VersionedFile(object):
210
434
"""Versioned text file storage.
301
525
self._check_write_ok()
302
526
return self._add_lines(version_id, parents, lines, parent_texts,
303
left_matching_blocks, nostore_sha, random_id, check_content)
527
left_matching_blocks, nostore_sha, random_id, check_content)
305
529
def _add_lines(self, version_id, parents, lines, parent_texts,
306
left_matching_blocks, nostore_sha, random_id, check_content):
530
left_matching_blocks, nostore_sha, random_id, check_content):
307
531
"""Helper to do the class specific add_lines."""
308
532
raise NotImplementedError(self.add_lines)
310
534
def add_lines_with_ghosts(self, version_id, parents, lines,
311
parent_texts=None, nostore_sha=None, random_id=False,
312
check_content=True, left_matching_blocks=None):
535
parent_texts=None, nostore_sha=None, random_id=False,
536
check_content=True, left_matching_blocks=None):
313
537
"""Add lines to the versioned file, allowing ghosts to be present.
315
539
This takes the same parameters as add_lines and returns the same.
317
541
self._check_write_ok()
318
542
return self._add_lines_with_ghosts(version_id, parents, lines,
319
parent_texts, nostore_sha, random_id, check_content, left_matching_blocks)
543
parent_texts, nostore_sha, random_id, check_content, left_matching_blocks)
321
545
def _add_lines_with_ghosts(self, version_id, parents, lines, parent_texts,
322
nostore_sha, random_id, check_content, left_matching_blocks):
546
nostore_sha, random_id, check_content, left_matching_blocks):
323
547
"""Helper to do class specific add_lines_with_ghosts."""
324
548
raise NotImplementedError(self.add_lines_with_ghosts)
401
629
for version, parent_ids, expected_sha1, mpdiff in records:
402
630
needed_parents.update(p for p in parent_ids
403
631
if not mpvf.has_version(p))
404
present_parents = set(self.get_parent_map(needed_parents).keys())
632
present_parents = set(self.get_parent_map(needed_parents))
405
633
for parent_id, lines in zip(present_parents,
406
self._get_lf_split_line_list(present_parents)):
634
self._get_lf_split_line_list(present_parents)):
407
635
mpvf.add_version(lines, parent_id, [])
408
for (version, parent_ids, expected_sha1, mpdiff), lines in\
409
zip(records, mpvf.get_line_list(versions)):
636
for (version, parent_ids, expected_sha1, mpdiff), lines in zip(
637
records, mpvf.get_line_list(versions)):
410
638
if len(parent_ids) == 1:
411
639
left_matching_blocks = list(mpdiff.get_matching_blocks(0,
412
mpvf.get_diff(parent_ids[0]).num_lines()))
640
mpvf.get_diff(parent_ids[0]).num_lines()))
414
642
left_matching_blocks = None
416
644
_, _, version_text = self.add_lines_with_ghosts(version,
417
parent_ids, lines, vf_parents,
418
left_matching_blocks=left_matching_blocks)
645
parent_ids, lines, vf_parents,
646
left_matching_blocks=left_matching_blocks)
419
647
except NotImplementedError:
420
648
# The vf can't handle ghosts, so add lines normally, which will
421
649
# (reasonably) fail if there are ghosts in the data.
422
650
_, _, version_text = self.add_lines(version,
423
parent_ids, lines, vf_parents,
424
left_matching_blocks=left_matching_blocks)
651
parent_ids, lines, vf_parents,
652
left_matching_blocks=left_matching_blocks)
425
653
vf_parents[version] = version_text
426
654
sha1s = self.get_sha1s(versions)
427
655
for version, parent_ids, expected_sha1, mpdiff in records:
454
682
raise NotImplementedError(self.get_lines)
456
684
def _get_lf_split_line_list(self, version_ids):
457
return [StringIO(t).readlines() for t in self.get_texts(version_ids)]
685
return [BytesIO(t).readlines() for t in self.get_texts(version_ids)]
459
def get_ancestry(self, version_ids, topo_sorted=True):
687
def get_ancestry(self, version_ids):
460
688
"""Return a list of all ancestors of given version(s). This
461
689
will not include the null revision.
463
This list will not be topologically sorted if topo_sorted=False is
466
691
Must raise RevisionNotPresent if any of the given versions are
467
692
not present in file history."""
468
if isinstance(version_ids, basestring):
469
version_ids = [version_ids]
470
693
raise NotImplementedError(self.get_ancestry)
472
695
def get_ancestry_with_ghosts(self, version_ids):
580
803
def add_lines(self, key, parents, lines, parent_texts=None,
581
left_matching_blocks=None, nostore_sha=None, random_id=False,
804
left_matching_blocks=None, nostore_sha=None, random_id=False,
583
806
self.calls.append(("add_lines", key, parents, lines, parent_texts,
584
left_matching_blocks, nostore_sha, random_id, check_content))
807
left_matching_blocks, nostore_sha, random_id, check_content))
585
808
return self._backing_vf.add_lines(key, parents, lines, parent_texts,
586
left_matching_blocks, nostore_sha, random_id, check_content)
809
left_matching_blocks, nostore_sha, random_id, check_content)
811
def add_content(self, factory, parent_texts=None,
812
left_matching_blocks=None, nostore_sha=None, random_id=False,
814
self.calls.append(("add_content", factory, parent_texts,
815
left_matching_blocks, nostore_sha, random_id, check_content))
816
return self._backing_vf.add_content(
817
factory, parent_texts, left_matching_blocks, nostore_sha,
818
random_id, check_content)
589
821
self._backing_vf.check()
744
976
This mapper is for use with a transport based backend.
747
_safe = "abcdefghijklmnopqrstuvwxyz0123456789-_@,."
979
_safe = bytearray(b"abcdefghijklmnopqrstuvwxyz0123456789-_@,.")
749
981
def _escape(self, prefix):
750
982
"""Turn a key element into a filesystem safe string.
752
This is similar to a plain urllib.quote, except
984
This is similar to a plain urlutils.quote, except
753
985
it uses specific safe characters, so that it doesn't
754
986
have to translate a lot of valid file ids.
756
988
# @ does not get escaped. This is because it is a valid
757
989
# filesystem character we use all the time, and it looks
758
990
# a lot better than seeing %40 all the time.
759
r = [((c in self._safe) and c or ('%%%02x' % ord(c)))
991
r = [(c in self._safe) and chr(c) or ('%%%02x' % c)
992
for c in bytearray(prefix)]
993
return ''.join(r).encode('ascii')
763
995
def _unescape(self, basename):
764
996
"""Escaped names are easily unescaped by urlutils."""
765
return urllib.unquote(basename)
997
return urlutils.unquote(basename)
768
1000
def make_versioned_files_factory(versioned_file_factory, mapper):
790
1022
The keyspace is expressed via simple tuples. Any instance of VersionedFiles
791
1023
may have a different length key-size, but that size will be constant for
792
all texts added to or retrieved from it. For instance, bzrlib uses
1024
all texts added to or retrieved from it. For instance, breezy uses
793
1025
instances with a key-size of 2 for storing user files in a repository, with
794
1026
the first element the fileid, and the second the version of that file.
796
1028
The use of tuples allows a single code base to support several different
797
1029
uses with only the mapping logic changing from instance to instance.
1031
:ivar _immediate_fallback_vfs: For subclasses that support stacking,
1032
this is a list of other VersionedFiles immediately underneath this
1033
one. They may in turn each have further fallbacks.
800
1036
def add_lines(self, key, parents, lines, parent_texts=None,
801
left_matching_blocks=None, nostore_sha=None, random_id=False,
1037
left_matching_blocks=None, nostore_sha=None, random_id=False,
1038
check_content=True):
803
1039
"""Add a text to the store.
805
1041
:param key: The key tuple of the text to add. If the last element is
837
1073
raise NotImplementedError(self.add_lines)
839
def _add_text(self, key, parents, text, nostore_sha=None, random_id=False):
840
"""Add a text to the store.
842
This is a private function for use by CommitBuilder.
1075
def add_content(self, factory, parent_texts=None,
1076
left_matching_blocks=None, nostore_sha=None, random_id=False,
1077
check_content=True):
1078
"""Add a text to the store from a chunk iterable.
844
1080
:param key: The key tuple of the text to add. If the last element is
845
1081
None, a CHK string will be generated during the addition.
846
1082
:param parents: The parents key tuples of the text to add.
847
:param text: A string containing the text to be committed.
1083
:param chunk_iter: An iterable over bytestrings.
1084
:param parent_texts: An optional dictionary containing the opaque
1085
representations of some or all of the parents of version_id to
1086
allow delta optimisations. VERY IMPORTANT: the texts must be those
1087
returned by add_lines or data corruption can be caused.
1088
:param left_matching_blocks: a hint about which areas are common
1089
between the text and its left-hand-parent. The format is
1090
the SequenceMatcher.get_matching_blocks format.
848
1091
:param nostore_sha: Raise ExistingContent and do not add the lines to
849
1092
the versioned file if the digest of the lines matches this.
850
1093
:param random_id: If True a random id has been selected rather than
884
1122
if not mpvf.has_version(p))
885
1123
# It seems likely that adding all the present parents as fulltexts can
886
1124
# easily exhaust memory.
887
chunks_to_lines = osutils.chunks_to_lines
888
1125
for record in self.get_record_stream(needed_parents, 'unordered',
890
1127
if record.storage_kind == 'absent':
892
mpvf.add_version(chunks_to_lines(record.get_bytes_as('chunked')),
894
for (key, parent_keys, expected_sha1, mpdiff), lines in\
895
zip(records, mpvf.get_line_list(versions)):
1129
mpvf.add_version(record.get_bytes_as('lines'), record.key, [])
1130
for (key, parent_keys, expected_sha1, mpdiff), lines in zip(
1131
records, mpvf.get_line_list(versions)):
896
1132
if len(parent_keys) == 1:
897
1133
left_matching_blocks = list(mpdiff.get_matching_blocks(0,
898
mpvf.get_diff(parent_keys[0]).num_lines()))
1134
mpvf.get_diff(parent_keys[0]).num_lines()))
900
1136
left_matching_blocks = None
901
1137
version_sha1, _, version_text = self.add_lines(key,
902
parent_keys, lines, vf_parents,
903
left_matching_blocks=left_matching_blocks)
1138
parent_keys, lines, vf_parents,
1139
left_matching_blocks=left_matching_blocks)
904
1140
if version_sha1 != expected_sha1:
905
1141
raise errors.VersionedFileInvalidChecksum(version)
906
1142
vf_parents[key] = version_text
1048
1284
def make_mpdiffs(self, keys):
1049
1285
"""Create multiparent diffs for specified keys."""
1050
keys_order = tuple(keys)
1051
keys = frozenset(keys)
1052
knit_keys = set(keys)
1053
parent_map = self.get_parent_map(keys)
1054
for parent_keys in parent_map.itervalues():
1056
knit_keys.update(parent_keys)
1057
missing_keys = keys - set(parent_map)
1059
raise errors.RevisionNotPresent(list(missing_keys)[0], self)
1060
# We need to filter out ghosts, because we can't diff against them.
1061
maybe_ghosts = knit_keys - keys
1062
ghosts = maybe_ghosts - set(self.get_parent_map(maybe_ghosts))
1063
knit_keys.difference_update(ghosts)
1065
chunks_to_lines = osutils.chunks_to_lines
1066
for record in self.get_record_stream(knit_keys, 'topological', True):
1067
lines[record.key] = chunks_to_lines(record.get_bytes_as('chunked'))
1068
# line_block_dict = {}
1069
# for parent, blocks in record.extract_line_blocks():
1070
# line_blocks[parent] = blocks
1071
# line_blocks[record.key] = line_block_dict
1073
for key in keys_order:
1075
parents = parent_map[key] or []
1076
# Note that filtering knit_keys can lead to a parent difference
1077
# between the creation and the application of the mpdiff.
1078
parent_lines = [lines[p] for p in parents if p in knit_keys]
1079
if len(parent_lines) > 0:
1080
left_parent_blocks = self._extract_blocks(key, parent_lines[0],
1083
left_parent_blocks = None
1084
diffs.append(multiparent.MultiParent.from_lines(target,
1085
parent_lines, left_parent_blocks))
1286
generator = _MPDiffGenerator(self, keys)
1287
return generator.compute_diffs()
1289
def get_annotator(self):
1290
return annotate.Annotator(self)
1088
1292
missing_keys = index._missing_keys_from_parent_map
1090
1294
def _extract_blocks(self, version_id, source, target):
1297
def _transitive_fallbacks(self):
1298
"""Return the whole stack of fallback versionedfiles.
1300
This VersionedFiles may have a list of fallbacks, but it doesn't
1301
necessarily know about the whole stack going down, and it can't know
1302
at open time because they may change after the objects are opened.
1305
for a_vfs in self._immediate_fallback_vfs:
1306
all_fallbacks.append(a_vfs)
1307
all_fallbacks.extend(a_vfs._transitive_fallbacks())
1308
return all_fallbacks
1094
1311
class ThunkedVersionedFiles(VersionedFiles):
1095
1312
"""Storage for many versioned files thunked onto a 'VersionedFile' class.
1108
1325
self._mapper = mapper
1109
1326
self._is_locked = is_locked
1328
def add_content(self, factory, parent_texts=None,
1329
left_matching_blocks=None, nostore_sha=None, random_id=False):
1330
"""See VersionedFiles.add_content()."""
1331
lines = factory.get_bytes_as('lines')
1332
return self.add_lines(
1333
factory.key, factory.parents, lines,
1334
parent_texts=parent_texts,
1335
left_matching_blocks=left_matching_blocks,
1336
nostore_sha=nostore_sha,
1337
random_id=random_id,
1111
1340
def add_lines(self, key, parents, lines, parent_texts=None,
1112
left_matching_blocks=None, nostore_sha=None, random_id=False,
1113
check_content=True):
1341
left_matching_blocks=None, nostore_sha=None, random_id=False,
1342
check_content=True):
1114
1343
"""See VersionedFiles.add_lines()."""
1115
1344
path = self._mapper.map(key)
1116
1345
version_id = key[-1]
1121
1350
return vf.add_lines_with_ghosts(version_id, parents, lines,
1122
parent_texts=parent_texts,
1123
left_matching_blocks=left_matching_blocks,
1124
nostore_sha=nostore_sha, random_id=random_id,
1125
check_content=check_content)
1351
parent_texts=parent_texts,
1352
left_matching_blocks=left_matching_blocks,
1353
nostore_sha=nostore_sha, random_id=random_id,
1354
check_content=check_content)
1126
1355
except NotImplementedError:
1127
1356
return vf.add_lines(version_id, parents, lines,
1128
parent_texts=parent_texts,
1129
left_matching_blocks=left_matching_blocks,
1130
nostore_sha=nostore_sha, random_id=random_id,
1131
check_content=check_content)
1357
parent_texts=parent_texts,
1358
left_matching_blocks=left_matching_blocks,
1359
nostore_sha=nostore_sha, random_id=random_id,
1360
check_content=check_content)
1132
1361
except errors.NoSuchFile:
1133
1362
# parent directory may be missing, try again.
1134
1363
self._transport.mkdir(osutils.dirname(path))
1136
1365
return vf.add_lines_with_ghosts(version_id, parents, lines,
1137
parent_texts=parent_texts,
1138
left_matching_blocks=left_matching_blocks,
1139
nostore_sha=nostore_sha, random_id=random_id,
1140
check_content=check_content)
1366
parent_texts=parent_texts,
1367
left_matching_blocks=left_matching_blocks,
1368
nostore_sha=nostore_sha, random_id=random_id,
1369
check_content=check_content)
1141
1370
except NotImplementedError:
1142
1371
return vf.add_lines(version_id, parents, lines,
1143
parent_texts=parent_texts,
1144
left_matching_blocks=left_matching_blocks,
1145
nostore_sha=nostore_sha, random_id=random_id,
1146
check_content=check_content)
1372
parent_texts=parent_texts,
1373
left_matching_blocks=left_matching_blocks,
1374
nostore_sha=nostore_sha, random_id=random_id,
1375
check_content=check_content)
1148
1377
def annotate(self, key):
1149
1378
"""Return a list of (version-key, line) tuples for the text of key.
1540
class VersionedFilesWithFallbacks(VersionedFiles):
1542
def without_fallbacks(self):
1543
"""Return a clone of this object without any fallbacks configured."""
1544
raise NotImplementedError(self.without_fallbacks)
1546
def add_fallback_versioned_files(self, a_versioned_files):
1547
"""Add a source of texts for texts not present in this knit.
1549
:param a_versioned_files: A VersionedFiles object.
1551
raise NotImplementedError(self.add_fallback_versioned_files)
1553
def get_known_graph_ancestry(self, keys):
1554
"""Get a KnownGraph instance with the ancestry of keys."""
1555
parent_map, missing_keys = self._index.find_ancestry(keys)
1556
for fallback in self._transitive_fallbacks():
1557
if not missing_keys:
1559
(f_parent_map, f_missing_keys) = fallback._index.find_ancestry(
1561
parent_map.update(f_parent_map)
1562
missing_keys = f_missing_keys
1563
kg = _mod_graph.KnownGraph(parent_map)
1314
1567
class _PlanMergeVersionedFile(VersionedFiles):
1315
1568
"""A VersionedFile for uncommitted and committed texts.
1337
1590
# line data for locally held keys.
1338
1591
self._lines = {}
1339
1592
# key lookup providers
1340
self._providers = [DictParentsProvider(self._parents)]
1593
self._providers = [_mod_graph.DictParentsProvider(self._parents)]
1342
1595
def plan_merge(self, ver_a, ver_b, base=None):
1343
1596
"""See VersionedFile.plan_merge"""
1344
from bzrlib.merge import _PlanMerge
1597
from ..merge import _PlanMerge
1345
1598
if base is None:
1346
1599
return _PlanMerge(ver_a, ver_b, self, (self._file_id,)).plan_merge()
1347
old_plan = list(_PlanMerge(ver_a, base, self, (self._file_id,)).plan_merge())
1348
new_plan = list(_PlanMerge(ver_a, ver_b, self, (self._file_id,)).plan_merge())
1600
old_plan = list(_PlanMerge(ver_a, base, self,
1601
(self._file_id,)).plan_merge())
1602
new_plan = list(_PlanMerge(ver_a, ver_b, self,
1603
(self._file_id,)).plan_merge())
1349
1604
return _PlanMerge._subtract_plans(old_plan, new_plan)
1351
1606
def plan_lca_merge(self, ver_a, ver_b, base=None):
1352
from bzrlib.merge import _PlanLCAMerge
1354
new_plan = _PlanLCAMerge(ver_a, ver_b, self, (self._file_id,), graph).plan_merge()
1607
from ..merge import _PlanLCAMerge
1608
graph = _mod_graph.Graph(self)
1609
new_plan = _PlanLCAMerge(
1610
ver_a, ver_b, self, (self._file_id,), graph).plan_merge()
1355
1611
if base is None:
1356
1612
return new_plan
1357
old_plan = _PlanLCAMerge(ver_a, base, self, (self._file_id,), graph).plan_merge()
1613
old_plan = _PlanLCAMerge(
1614
ver_a, base, self, (self._file_id,), graph).plan_merge()
1358
1615
return _PlanLCAMerge._subtract_plans(list(old_plan), list(new_plan))
1617
def add_content(self, factory):
1618
return self.add_lines(
1619
factory.key, factory.parents, factory.get_bytes_as('lines'))
1360
1621
def add_lines(self, key, parents, lines):
1361
1622
"""See VersionedFiles.add_lines
1363
1624
Lines are added locally, not to fallback versionedfiles. Also, ghosts
1364
1625
are permitted. Only reserved ids are permitted.
1366
if type(key) is not tuple:
1627
if not isinstance(key, tuple):
1367
1628
raise TypeError(key)
1368
1629
if not revision.is_reserved_id(key[-1]):
1369
1630
raise ValueError('Only reserved ids may be used')
1891
class NoDupeAddLinesDecorator(object):
1892
"""Decorator for a VersionedFiles that skips doing an add_lines if the key
1896
def __init__(self, store):
1899
def add_lines(self, key, parents, lines, parent_texts=None,
1900
left_matching_blocks=None, nostore_sha=None, random_id=False,
1901
check_content=True):
1902
"""See VersionedFiles.add_lines.
1904
This implementation may return None as the third element of the return
1905
value when the original store wouldn't.
1908
raise NotImplementedError(
1909
"NoDupeAddLinesDecorator.add_lines does not implement the "
1910
"nostore_sha behaviour.")
1912
sha1 = osutils.sha_strings(lines)
1913
key = (b"sha1:" + sha1,)
1916
if key in self._store.get_parent_map([key]):
1917
# This key has already been inserted, so don't do it again.
1919
sha1 = osutils.sha_strings(lines)
1920
return sha1, sum(map(len, lines)), None
1921
return self._store.add_lines(key, parents, lines,
1922
parent_texts=parent_texts,
1923
left_matching_blocks=left_matching_blocks,
1924
nostore_sha=nostore_sha, random_id=random_id,
1925
check_content=check_content)
1927
def __getattr__(self, name):
1928
return getattr(self._store, name)
1627
1931
def network_bytes_to_kind_and_offset(network_bytes):
1628
1932
"""Strip of a record kind from the front of network_bytes.
1630
1934
:param network_bytes: The bytes of a record.
1631
1935
:return: A tuple (storage_kind, offset_of_remaining_bytes)
1633
line_end = network_bytes.find('\n')
1634
storage_kind = network_bytes[:line_end]
1937
line_end = network_bytes.find(b'\n')
1938
storage_kind = network_bytes[:line_end].decode('ascii')
1635
1939
return storage_kind, line_end + 1
1664
1968
for bytes in self._bytes_iterator:
1665
1969
storage_kind, line_end = network_bytes_to_kind_and_offset(bytes)
1666
1970
for record in self._kind_factory[storage_kind](
1667
storage_kind, bytes, line_end):
1971
storage_kind, bytes, line_end):
1671
1975
def fulltext_network_to_record(kind, bytes, line_end):
1672
1976
"""Convert a network fulltext record to record."""
1673
meta_len, = struct.unpack('!L', bytes[line_end:line_end+4])
1674
record_meta = bytes[line_end+4:line_end+4+meta_len]
1977
meta_len, = struct.unpack('!L', bytes[line_end:line_end + 4])
1978
record_meta = bytes[line_end + 4:line_end + 4 + meta_len]
1675
1979
key, parents = bencode.bdecode_as_tuple(record_meta)
1676
if parents == 'nil':
1980
if parents == b'nil':
1678
fulltext = bytes[line_end+4+meta_len:]
1982
fulltext = bytes[line_end + 4 + meta_len:]
1679
1983
return [FulltextContentFactory(key, parents, None, fulltext)]
1720
2024
for prefix in sorted(per_prefix_map):
1721
2025
present_keys.extend(reversed(tsort.topo_sort(per_prefix_map[prefix])))
1722
2026
return present_keys
2029
class _KeyRefs(object):
2031
def __init__(self, track_new_keys=False):
2032
# dict mapping 'key' to 'set of keys referring to that key'
2035
# set remembering all new keys
2036
self.new_keys = set()
2038
self.new_keys = None
2044
self.new_keys.clear()
2046
def add_references(self, key, refs):
2047
# Record the new references
2048
for referenced in refs:
2050
needed_by = self.refs[referenced]
2052
needed_by = self.refs[referenced] = set()
2054
# Discard references satisfied by the new key
2057
def get_new_keys(self):
2058
return self.new_keys
2060
def get_unsatisfied_refs(self):
2061
return self.refs.keys()
2063
def _satisfy_refs_for_key(self, key):
2067
# No keys depended on this key. That's ok.
2070
def add_key(self, key):
2071
# satisfy refs for key, and remember that we've seen this key.
2072
self._satisfy_refs_for_key(key)
2073
if self.new_keys is not None:
2074
self.new_keys.add(key)
2076
def satisfy_refs_for_keys(self, keys):
2078
self._satisfy_refs_for_key(key)
2080
def get_referrers(self):
2081
return set(itertools.chain.from_iterable(viewvalues(self.refs)))