~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/versionedfile.py

  • Committer: John Arbash Meinel
  • Date: 2006-08-23 22:16:27 UTC
  • mto: This revision was merged to the branch mainline in revision 1955.
  • Revision ID: john@arbash-meinel.com-20060823221627-fc64105bb12ae770
Ghozzy: Fix Bzr's support of Active FTP (aftp://)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
 
1
# Copyright (C) 2005, 2006 by Canonical Ltd
2
2
#
3
3
# Authors:
4
4
#   Johan Rydberg <jrydberg@gnu.org>
19
19
 
20
20
"""Versioned text file storage api."""
21
21
 
22
 
from bzrlib.lazy_import import lazy_import
23
 
lazy_import(globals(), """
24
 
 
25
 
from bzrlib import (
26
 
    errors,
27
 
    osutils,
28
 
    multiparent,
29
 
    tsort,
30
 
    revision,
31
 
    ui,
32
 
    )
33
 
from bzrlib.graph import Graph
34
 
from bzrlib.transport.memory import MemoryTransport
35
 
""")
36
 
 
37
 
from cStringIO import StringIO
38
 
 
 
22
 
 
23
from copy import deepcopy
 
24
from unittest import TestSuite
 
25
 
 
26
 
 
27
import bzrlib.errors as errors
39
28
from bzrlib.inter import InterObject
40
 
from bzrlib.registry import Registry
41
 
from bzrlib.symbol_versioning import *
42
29
from bzrlib.textmerge import TextMerge
43
 
 
44
 
 
45
 
adapter_registry = Registry()
46
 
adapter_registry.register_lazy(('knit-delta-gz', 'fulltext'), 'bzrlib.knit',
47
 
    'DeltaPlainToFullText')
48
 
adapter_registry.register_lazy(('knit-ft-gz', 'fulltext'), 'bzrlib.knit',
49
 
    'FTPlainToFullText')
50
 
adapter_registry.register_lazy(('knit-annotated-delta-gz', 'knit-delta-gz'),
51
 
    'bzrlib.knit', 'DeltaAnnotatedToUnannotated')
52
 
adapter_registry.register_lazy(('knit-annotated-delta-gz', 'fulltext'),
53
 
    'bzrlib.knit', 'DeltaAnnotatedToFullText')
54
 
adapter_registry.register_lazy(('knit-annotated-ft-gz', 'knit-ft-gz'),
55
 
    'bzrlib.knit', 'FTAnnotatedToUnannotated')
56
 
adapter_registry.register_lazy(('knit-annotated-ft-gz', 'fulltext'),
57
 
    'bzrlib.knit', 'FTAnnotatedToFullText')
58
 
 
59
 
 
60
 
class ContentFactory(object):
61
 
    """Abstract interface for insertion and retrieval from a VersionedFile.
62
 
    
63
 
    :ivar sha1: None, or the sha1 of the content fulltext.
64
 
    :ivar storage_kind: The native storage kind of this factory. One of
65
 
        'mpdiff', 'knit-annotated-ft', 'knit-annotated-delta', 'knit-ft',
66
 
        'knit-delta', 'fulltext', 'knit-annotated-ft-gz',
67
 
        'knit-annotated-delta-gz', 'knit-ft-gz', 'knit-delta-gz'.
68
 
    :ivar key: The key of this content. Each key is a tuple with a single
69
 
        string in it.
70
 
    :ivar parents: A tuple of parent keys for self.key. If the object has
71
 
        no parent information, None (as opposed to () for an empty list of
72
 
        parents).
73
 
        """
74
 
 
75
 
    def __init__(self):
76
 
        """Create a ContentFactory."""
77
 
        self.sha1 = None
78
 
        self.storage_kind = None
79
 
        self.key = None
80
 
        self.parents = None
81
 
 
82
 
 
83
 
class AbsentContentFactory(object):
84
 
    """A placeholder content factory for unavailable texts.
85
 
    
86
 
    :ivar sha1: None.
87
 
    :ivar storage_kind: 'absent'.
88
 
    :ivar key: The key of this content. Each key is a tuple with a single
89
 
        string in it.
90
 
    :ivar parents: None.
91
 
    """
92
 
 
93
 
    def __init__(self, key):
94
 
        """Create a ContentFactory."""
95
 
        self.sha1 = None
96
 
        self.storage_kind = 'absent'
97
 
        self.key = key
98
 
        self.parents = None
99
 
 
100
 
 
101
 
def filter_absent(record_stream):
102
 
    """Adapt a record stream to remove absent records."""
103
 
    for record in record_stream:
104
 
        if record.storage_kind != 'absent':
105
 
            yield record
 
30
from bzrlib.transport.memory import MemoryTransport
 
31
from bzrlib.tsort import topo_sort
 
32
from bzrlib import ui
 
33
from bzrlib.symbol_versioning import (deprecated_function,
 
34
        deprecated_method,
 
35
        zero_eight,
 
36
        )
106
37
 
107
38
 
108
39
class VersionedFile(object):
119
50
    Texts are identified by a version-id string.
120
51
    """
121
52
 
122
 
    @staticmethod
123
 
    def check_not_reserved_id(version_id):
124
 
        revision.check_not_reserved_id(version_id)
 
53
    def __init__(self, access_mode):
 
54
        self.finished = False
 
55
        self._access_mode = access_mode
125
56
 
126
57
    def copy_to(self, name, transport):
127
58
        """Copy this versioned file to name on transport."""
128
59
        raise NotImplementedError(self.copy_to)
129
60
 
130
 
    def get_record_stream(self, versions, ordering, include_delta_closure):
131
 
        """Get a stream of records for versions.
 
61
    @deprecated_method(zero_eight)
 
62
    def names(self):
 
63
        """Return a list of all the versions in this versioned file.
132
64
 
133
 
        :param versions: The versions to include. Each version is a tuple
134
 
            (version,).
135
 
        :param ordering: Either 'unordered' or 'topological'. A topologically
136
 
            sorted stream has compression parents strictly before their
137
 
            children.
138
 
        :param include_delta_closure: If True then the closure across any
139
 
            compression parents will be included (in the data content of the
140
 
            stream, not in the emitted records). This guarantees that
141
 
            'fulltext' can be used successfully on every record.
142
 
        :return: An iterator of ContentFactory objects, each of which is only
143
 
            valid until the iterator is advanced.
 
65
        Please use versionedfile.versions() now.
144
66
        """
145
 
        raise NotImplementedError(self.get_record_stream)
 
67
        return self.versions()
 
68
 
 
69
    def versions(self):
 
70
        """Return a unsorted list of versions."""
 
71
        raise NotImplementedError(self.versions)
 
72
 
 
73
    def has_ghost(self, version_id):
 
74
        """Returns whether version is present as a ghost."""
 
75
        raise NotImplementedError(self.has_ghost)
146
76
 
147
77
    def has_version(self, version_id):
148
78
        """Returns whether version is present."""
149
79
        raise NotImplementedError(self.has_version)
150
80
 
151
 
    def insert_record_stream(self, stream):
152
 
        """Insert a record stream into this versioned file.
153
 
 
154
 
        :param stream: A stream of records to insert. 
155
 
        :return: None
156
 
        :seealso VersionedFile.get_record_stream:
157
 
        """
158
 
        raise NotImplementedError
159
 
 
160
 
    def add_lines(self, version_id, parents, lines, parent_texts=None,
161
 
        left_matching_blocks=None, nostore_sha=None, random_id=False,
162
 
        check_content=True):
 
81
    def add_delta(self, version_id, parents, delta_parent, sha1, noeol, delta):
 
82
        """Add a text to the versioned file via a pregenerated delta.
 
83
 
 
84
        :param version_id: The version id being added.
 
85
        :param parents: The parents of the version_id.
 
86
        :param delta_parent: The parent this delta was created against.
 
87
        :param sha1: The sha1 of the full text.
 
88
        :param delta: The delta instructions. See get_delta for details.
 
89
        """
 
90
        self._check_write_ok()
 
91
        if self.has_version(version_id):
 
92
            raise errors.RevisionAlreadyPresent(version_id, self)
 
93
        return self._add_delta(version_id, parents, delta_parent, sha1, noeol, delta)
 
94
 
 
95
    def _add_delta(self, version_id, parents, delta_parent, sha1, noeol, delta):
 
96
        """Class specific routine to add a delta.
 
97
 
 
98
        This generic version simply applies the delta to the delta_parent and
 
99
        then inserts it.
 
100
        """
 
101
        # strip annotation from delta
 
102
        new_delta = []
 
103
        for start, stop, delta_len, delta_lines in delta:
 
104
            new_delta.append((start, stop, delta_len, [text for origin, text in delta_lines]))
 
105
        if delta_parent is not None:
 
106
            parent_full = self.get_lines(delta_parent)
 
107
        else:
 
108
            parent_full = []
 
109
        new_full = self._apply_delta(parent_full, new_delta)
 
110
        # its impossible to have noeol on an empty file
 
111
        if noeol and new_full[-1][-1] == '\n':
 
112
            new_full[-1] = new_full[-1][:-1]
 
113
        self.add_lines(version_id, parents, new_full)
 
114
 
 
115
    def add_lines(self, version_id, parents, lines, parent_texts=None):
163
116
        """Add a single text on top of the versioned file.
164
117
 
165
118
        Must raise RevisionAlreadyPresent if the new version is
167
120
 
168
121
        Must raise RevisionNotPresent if any of the given parents are
169
122
        not present in file history.
170
 
 
171
 
        :param lines: A list of lines. Each line must be a bytestring. And all
172
 
            of them except the last must be terminated with \n and contain no
173
 
            other \n's. The last line may either contain no \n's or a single
174
 
            terminated \n. If the lines list does meet this constraint the add
175
 
            routine may error or may succeed - but you will be unable to read
176
 
            the data back accurately. (Checking the lines have been split
177
 
            correctly is expensive and extremely unlikely to catch bugs so it
178
 
            is not done at runtime unless check_content is True.)
179
123
        :param parent_texts: An optional dictionary containing the opaque 
180
 
            representations of some or all of the parents of version_id to
181
 
            allow delta optimisations.  VERY IMPORTANT: the texts must be those
182
 
            returned by add_lines or data corruption can be caused.
183
 
        :param left_matching_blocks: a hint about which areas are common
184
 
            between the text and its left-hand-parent.  The format is
185
 
            the SequenceMatcher.get_matching_blocks format.
186
 
        :param nostore_sha: Raise ExistingContent and do not add the lines to
187
 
            the versioned file if the digest of the lines matches this.
188
 
        :param random_id: If True a random id has been selected rather than
189
 
            an id determined by some deterministic process such as a converter
190
 
            from a foreign VCS. When True the backend may choose not to check
191
 
            for uniqueness of the resulting key within the versioned file, so
192
 
            this should only be done when the result is expected to be unique
193
 
            anyway.
194
 
        :param check_content: If True, the lines supplied are verified to be
195
 
            bytestrings that are correctly formed lines.
196
 
        :return: The text sha1, the number of bytes in the text, and an opaque
197
 
                 representation of the inserted version which can be provided
198
 
                 back to future add_lines calls in the parent_texts dictionary.
 
124
             representations of some or all of the parents of 
 
125
             version_id to allow delta optimisations. 
 
126
             VERY IMPORTANT: the texts must be those returned
 
127
             by add_lines or data corruption can be caused.
 
128
        :return: An opaque representation of the inserted version which can be
 
129
                 provided back to future add_lines calls in the parent_texts
 
130
                 dictionary.
199
131
        """
200
132
        self._check_write_ok()
201
 
        return self._add_lines(version_id, parents, lines, parent_texts,
202
 
            left_matching_blocks, nostore_sha, random_id, check_content)
 
133
        return self._add_lines(version_id, parents, lines, parent_texts)
203
134
 
204
 
    def _add_lines(self, version_id, parents, lines, parent_texts,
205
 
        left_matching_blocks, nostore_sha, random_id, check_content):
 
135
    def _add_lines(self, version_id, parents, lines, parent_texts):
206
136
        """Helper to do the class specific add_lines."""
207
137
        raise NotImplementedError(self.add_lines)
208
138
 
209
139
    def add_lines_with_ghosts(self, version_id, parents, lines,
210
 
        parent_texts=None, nostore_sha=None, random_id=False,
211
 
        check_content=True, left_matching_blocks=None):
 
140
                              parent_texts=None):
212
141
        """Add lines to the versioned file, allowing ghosts to be present.
213
142
        
214
 
        This takes the same parameters as add_lines and returns the same.
 
143
        This takes the same parameters as add_lines.
215
144
        """
216
145
        self._check_write_ok()
217
146
        return self._add_lines_with_ghosts(version_id, parents, lines,
218
 
            parent_texts, nostore_sha, random_id, check_content, left_matching_blocks)
 
147
                                           parent_texts)
219
148
 
220
 
    def _add_lines_with_ghosts(self, version_id, parents, lines, parent_texts,
221
 
        nostore_sha, random_id, check_content, left_matching_blocks):
 
149
    def _add_lines_with_ghosts(self, version_id, parents, lines, parent_texts):
222
150
        """Helper to do class specific add_lines_with_ghosts."""
223
151
        raise NotImplementedError(self.add_lines_with_ghosts)
224
152
 
238
166
            if '\n' in line[:-1]:
239
167
                raise errors.BzrBadParameterContainsNewline("lines")
240
168
 
241
 
    def get_format_signature(self):
242
 
        """Get a text description of the data encoding in this file.
243
 
        
244
 
        :since: 0.90
245
 
        """
246
 
        raise NotImplementedError(self.get_format_signature)
247
 
 
248
 
    def make_mpdiffs(self, version_ids):
249
 
        """Create multiparent diffs for specified versions."""
250
 
        knit_versions = set()
251
 
        knit_versions.update(version_ids)
252
 
        parent_map = self.get_parent_map(version_ids)
253
 
        for version_id in version_ids:
254
 
            try:
255
 
                knit_versions.update(parent_map[version_id])
256
 
            except KeyError:
257
 
                raise RevisionNotPresent(version_id, self)
258
 
        # We need to filter out ghosts, because we can't diff against them.
259
 
        knit_versions = set(self.get_parent_map(knit_versions).keys())
260
 
        lines = dict(zip(knit_versions,
261
 
            self._get_lf_split_line_list(knit_versions)))
262
 
        diffs = []
263
 
        for version_id in version_ids:
264
 
            target = lines[version_id]
265
 
            try:
266
 
                parents = [lines[p] for p in parent_map[version_id] if p in
267
 
                    knit_versions]
268
 
            except KeyError:
269
 
                raise RevisionNotPresent(version_id, self)
270
 
            if len(parents) > 0:
271
 
                left_parent_blocks = self._extract_blocks(version_id,
272
 
                                                          parents[0], target)
273
 
            else:
274
 
                left_parent_blocks = None
275
 
            diffs.append(multiparent.MultiParent.from_lines(target, parents,
276
 
                         left_parent_blocks))
277
 
        return diffs
278
 
 
279
 
    def _extract_blocks(self, version_id, source, target):
280
 
        return None
281
 
 
282
 
    def add_mpdiffs(self, records):
283
 
        """Add mpdiffs to this VersionedFile.
284
 
 
285
 
        Records should be iterables of version, parents, expected_sha1,
286
 
        mpdiff. mpdiff should be a MultiParent instance.
287
 
        """
288
 
        # Does this need to call self._check_write_ok()? (IanC 20070919)
289
 
        vf_parents = {}
290
 
        mpvf = multiparent.MultiMemoryVersionedFile()
291
 
        versions = []
292
 
        for version, parent_ids, expected_sha1, mpdiff in records:
293
 
            versions.append(version)
294
 
            mpvf.add_diff(mpdiff, version, parent_ids)
295
 
        needed_parents = set()
296
 
        for version, parent_ids, expected_sha1, mpdiff in records:
297
 
            needed_parents.update(p for p in parent_ids
298
 
                                  if not mpvf.has_version(p))
299
 
        present_parents = set(self.get_parent_map(needed_parents).keys())
300
 
        for parent_id, lines in zip(present_parents,
301
 
                                 self._get_lf_split_line_list(present_parents)):
302
 
            mpvf.add_version(lines, parent_id, [])
303
 
        for (version, parent_ids, expected_sha1, mpdiff), lines in\
304
 
            zip(records, mpvf.get_line_list(versions)):
305
 
            if len(parent_ids) == 1:
306
 
                left_matching_blocks = list(mpdiff.get_matching_blocks(0,
307
 
                    mpvf.get_diff(parent_ids[0]).num_lines()))
308
 
            else:
309
 
                left_matching_blocks = None
310
 
            try:
311
 
                _, _, version_text = self.add_lines_with_ghosts(version,
312
 
                    parent_ids, lines, vf_parents,
313
 
                    left_matching_blocks=left_matching_blocks)
314
 
            except NotImplementedError:
315
 
                # The vf can't handle ghosts, so add lines normally, which will
316
 
                # (reasonably) fail if there are ghosts in the data.
317
 
                _, _, version_text = self.add_lines(version,
318
 
                    parent_ids, lines, vf_parents,
319
 
                    left_matching_blocks=left_matching_blocks)
320
 
            vf_parents[version] = version_text
321
 
        for (version, parent_ids, expected_sha1, mpdiff), sha1 in\
322
 
             zip(records, self.get_sha1s(versions)):
323
 
            if expected_sha1 != sha1:
324
 
                raise errors.VersionedFileInvalidChecksum(version)
325
 
 
326
 
    def get_sha1s(self, version_ids):
327
 
        """Get the stored sha1 sums for the given revisions.
328
 
 
329
 
        :param version_ids: The names of the versions to lookup
330
 
        :return: a list of sha1s in order according to the version_ids
331
 
        """
332
 
        raise NotImplementedError(self.get_sha1s)
333
 
 
 
169
    def _check_write_ok(self):
 
170
        """Is the versioned file marked as 'finished' ? Raise if it is."""
 
171
        if self.finished:
 
172
            raise errors.OutSideTransaction()
 
173
        if self._access_mode != 'w':
 
174
            raise errors.ReadOnlyObjectDirtiedError(self)
 
175
 
 
176
    def enable_cache(self):
 
177
        """Tell this versioned file that it should cache any data it reads.
 
178
        
 
179
        This is advisory, implementations do not have to support caching.
 
180
        """
 
181
        pass
 
182
    
 
183
    def clear_cache(self):
 
184
        """Remove any data cached in the versioned file object.
 
185
 
 
186
        This only needs to be supported if caches are supported
 
187
        """
 
188
        pass
 
189
 
 
190
    def clone_text(self, new_version_id, old_version_id, parents):
 
191
        """Add an identical text to old_version_id as new_version_id.
 
192
 
 
193
        Must raise RevisionNotPresent if the old version or any of the
 
194
        parents are not present in file history.
 
195
 
 
196
        Must raise RevisionAlreadyPresent if the new version is
 
197
        already present in file history."""
 
198
        self._check_write_ok()
 
199
        return self._clone_text(new_version_id, old_version_id, parents)
 
200
 
 
201
    def _clone_text(self, new_version_id, old_version_id, parents):
 
202
        """Helper function to do the _clone_text work."""
 
203
        raise NotImplementedError(self.clone_text)
 
204
 
 
205
    def create_empty(self, name, transport, mode=None):
 
206
        """Create a new versioned file of this exact type.
 
207
 
 
208
        :param name: the file name
 
209
        :param transport: the transport
 
210
        :param mode: optional file mode.
 
211
        """
 
212
        raise NotImplementedError(self.create_empty)
 
213
 
 
214
    def fix_parents(self, version, new_parents):
 
215
        """Fix the parents list for version.
 
216
        
 
217
        This is done by appending a new version to the index
 
218
        with identical data except for the parents list.
 
219
        the parents list must be a superset of the current
 
220
        list.
 
221
        """
 
222
        self._check_write_ok()
 
223
        return self._fix_parents(version, new_parents)
 
224
 
 
225
    def _fix_parents(self, version, new_parents):
 
226
        """Helper for fix_parents."""
 
227
        raise NotImplementedError(self.fix_parents)
 
228
 
 
229
    def get_delta(self, version):
 
230
        """Get a delta for constructing version from some other version.
 
231
        
 
232
        :return: (delta_parent, sha1, noeol, delta)
 
233
        Where delta_parent is a version id or None to indicate no parent.
 
234
        """
 
235
        raise NotImplementedError(self.get_delta)
 
236
 
 
237
    def get_deltas(self, versions):
 
238
        """Get multiple deltas at once for constructing versions.
 
239
        
 
240
        :return: dict(version_id:(delta_parent, sha1, noeol, delta))
 
241
        Where delta_parent is a version id or None to indicate no parent, and
 
242
        version_id is the version_id created by that delta.
 
243
        """
 
244
        result = {}
 
245
        for version in versions:
 
246
            result[version] = self.get_delta(version)
 
247
        return result
 
248
 
 
249
    def get_sha1(self, version_id):
 
250
        """Get the stored sha1 sum for the given revision.
 
251
        
 
252
        :param name: The name of the version to lookup
 
253
        """
 
254
        raise NotImplementedError(self.get_sha1)
 
255
 
 
256
    def get_suffixes(self):
 
257
        """Return the file suffixes associated with this versioned file."""
 
258
        raise NotImplementedError(self.get_suffixes)
 
259
    
334
260
    def get_text(self, version_id):
335
261
        """Return version contents as a text string.
336
262
 
356
282
        """
357
283
        raise NotImplementedError(self.get_lines)
358
284
 
359
 
    def _get_lf_split_line_list(self, version_ids):
360
 
        return [StringIO(t).readlines() for t in self.get_texts(version_ids)]
361
 
 
362
 
    def get_ancestry(self, version_ids, topo_sorted=True):
 
285
    def get_ancestry(self, version_ids):
363
286
        """Return a list of all ancestors of given version(s). This
364
287
        will not include the null revision.
365
288
 
366
 
        This list will not be topologically sorted if topo_sorted=False is
367
 
        passed.
368
 
 
369
289
        Must raise RevisionNotPresent if any of the given versions are
370
290
        not present in file history."""
371
291
        if isinstance(version_ids, basestring):
383
303
        but are not explicitly marked.
384
304
        """
385
305
        raise NotImplementedError(self.get_ancestry_with_ghosts)
386
 
    
387
 
    def get_parent_map(self, version_ids):
388
 
        """Get a map of the parents of version_ids.
389
 
 
390
 
        :param version_ids: The version ids to look up parents for.
391
 
        :return: A mapping from version id to parents.
392
 
        """
393
 
        raise NotImplementedError(self.get_parent_map)
 
306
        
 
307
    def get_graph(self, version_ids=None):
 
308
        """Return a graph from the versioned file. 
 
309
        
 
310
        Ghosts are not listed or referenced in the graph.
 
311
        :param version_ids: Versions to select.
 
312
                            None means retrieve all versions.
 
313
        """
 
314
        result = {}
 
315
        if version_ids is None:
 
316
            for version in self.versions():
 
317
                result[version] = self.get_parents(version)
 
318
        else:
 
319
            pending = set(version_ids)
 
320
            while pending:
 
321
                version = pending.pop()
 
322
                if version in result:
 
323
                    continue
 
324
                parents = self.get_parents(version)
 
325
                for parent in parents:
 
326
                    if parent in result:
 
327
                        continue
 
328
                    pending.add(parent)
 
329
                result[version] = parents
 
330
        return result
 
331
 
 
332
    def get_graph_with_ghosts(self):
 
333
        """Return a graph for the entire versioned file.
 
334
        
 
335
        Ghosts are referenced in parents list but are not
 
336
        explicitly listed.
 
337
        """
 
338
        raise NotImplementedError(self.get_graph_with_ghosts)
 
339
 
 
340
    @deprecated_method(zero_eight)
 
341
    def parent_names(self, version):
 
342
        """Return version names for parents of a version.
 
343
        
 
344
        See get_parents for the current api.
 
345
        """
 
346
        return self.get_parents(version)
 
347
 
 
348
    def get_parents(self, version_id):
 
349
        """Return version names for parents of a version.
 
350
 
 
351
        Must raise RevisionNotPresent if version is not present in
 
352
        file history.
 
353
        """
 
354
        raise NotImplementedError(self.get_parents)
394
355
 
395
356
    def get_parents_with_ghosts(self, version_id):
396
357
        """Return version names for parents of version_id.
401
362
        Ghosts that are known about will be included in the parent list,
402
363
        but are not explicitly marked.
403
364
        """
404
 
        try:
405
 
            return list(self.get_parent_map([version_id])[version_id])
406
 
        except KeyError:
407
 
            raise errors.RevisionNotPresent(version_id, self)
 
365
        raise NotImplementedError(self.get_parents_with_ghosts)
 
366
 
 
367
    def annotate_iter(self, version_id):
 
368
        """Yield list of (version-id, line) pairs for the specified
 
369
        version.
 
370
 
 
371
        Must raise RevisionNotPresent if any of the given versions are
 
372
        not present in file history.
 
373
        """
 
374
        raise NotImplementedError(self.annotate_iter)
408
375
 
409
376
    def annotate(self, version_id):
410
 
        """Return a list of (version-id, line) tuples for version_id.
411
 
 
412
 
        :raise RevisionNotPresent: If the given version is
413
 
        not present in file history.
414
 
        """
415
 
        raise NotImplementedError(self.annotate)
416
 
 
417
 
    @deprecated_method(one_five)
 
377
        return list(self.annotate_iter(version_id))
 
378
 
 
379
    def _apply_delta(self, lines, delta):
 
380
        """Apply delta to lines."""
 
381
        lines = list(lines)
 
382
        offset = 0
 
383
        for start, end, count, delta_lines in delta:
 
384
            lines[offset+start:offset+end] = delta_lines
 
385
            offset = offset + (start - end) + count
 
386
        return lines
 
387
 
418
388
    def join(self, other, pb=None, msg=None, version_ids=None,
419
389
             ignore_missing=False):
420
390
        """Integrate versions from other into this versioned file.
423
393
        incorporated into this versioned file.
424
394
 
425
395
        Must raise RevisionNotPresent if any of the specified versions
426
 
        are not present in the other file's history unless ignore_missing
427
 
        is supplied in which case they are silently skipped.
 
396
        are not present in the other files history unless ignore_missing
 
397
        is supplied when they are silently skipped.
428
398
        """
429
399
        self._check_write_ok()
430
400
        return InterVersionedFile.get(other, self).join(
433
403
            version_ids,
434
404
            ignore_missing)
435
405
 
436
 
    def iter_lines_added_or_present_in_versions(self, version_ids=None,
437
 
                                                pb=None):
 
406
    def iter_lines_added_or_present_in_versions(self, version_ids=None):
438
407
        """Iterate over the lines in the versioned file from version_ids.
439
408
 
440
 
        This may return lines from other versions. Each item the returned
441
 
        iterator yields is a tuple of a line and a text version that that line
442
 
        is present in (not introduced in).
443
 
 
444
 
        Ordering of results is in whatever order is most suitable for the
445
 
        underlying storage format.
446
 
 
447
 
        If a progress bar is supplied, it may be used to indicate progress.
448
 
        The caller is responsible for cleaning up progress bars (because this
449
 
        is an iterator).
 
409
        This may return lines from other versions, and does not return the
 
410
        specific version marker at this point. The api may be changed
 
411
        during development to include the version that the versioned file
 
412
        thinks is relevant, but given that such hints are just guesses,
 
413
        its better not to have it if we don't need it.
450
414
 
451
415
        NOTES: Lines are normalised: they will all have \n terminators.
452
416
               Lines are returned in arbitrary order.
453
 
 
454
 
        :return: An iterator over (line, version_id).
455
417
        """
456
418
        raise NotImplementedError(self.iter_lines_added_or_present_in_versions)
457
419
 
 
420
    def transaction_finished(self):
 
421
        """The transaction that this file was opened in has finished.
 
422
 
 
423
        This records self.finished = True and should cause all mutating
 
424
        operations to error.
 
425
        """
 
426
        self.finished = True
 
427
 
 
428
    @deprecated_method(zero_eight)
 
429
    def walk(self, version_ids=None):
 
430
        """Walk the versioned file as a weave-like structure, for
 
431
        versions relative to version_ids.  Yields sequence of (lineno,
 
432
        insert, deletes, text) for each relevant line.
 
433
 
 
434
        Must raise RevisionNotPresent if any of the specified versions
 
435
        are not present in the file history.
 
436
 
 
437
        :param version_ids: the version_ids to walk with respect to. If not
 
438
                            supplied the entire weave-like structure is walked.
 
439
 
 
440
        walk is deprecated in favour of iter_lines_added_or_present_in_versions
 
441
        """
 
442
        raise NotImplementedError(self.walk)
 
443
 
 
444
    @deprecated_method(zero_eight)
 
445
    def iter_names(self):
 
446
        """Walk the names list."""
 
447
        return iter(self.versions())
 
448
 
458
449
    def plan_merge(self, ver_a, ver_b):
459
450
        """Return pseudo-annotation indicating how the two versions merge.
460
451
 
477
468
        """
478
469
        raise NotImplementedError(VersionedFile.plan_merge)
479
470
        
480
 
    def weave_merge(self, plan, a_marker=TextMerge.A_MARKER,
 
471
    def weave_merge(self, plan, a_marker=TextMerge.A_MARKER, 
481
472
                    b_marker=TextMerge.B_MARKER):
482
473
        return PlanWeaveMerge(plan, a_marker, b_marker).merge_lines()[0]
483
474
 
484
475
 
485
 
class RecordingVersionedFileDecorator(object):
486
 
    """A minimal versioned file that records calls made on it.
487
 
    
488
 
    Only enough methods have been added to support tests using it to date.
489
 
 
490
 
    :ivar calls: A list of the calls made; can be reset at any time by
491
 
        assigning [] to it.
492
 
    """
493
 
 
494
 
    def __init__(self, backing_vf):
495
 
        """Create a RecordingVersionedFileDecorator decorating backing_vf.
496
 
        
497
 
        :param backing_vf: The versioned file to answer all methods.
498
 
        """
499
 
        self._backing_vf = backing_vf
500
 
        self.calls = []
501
 
 
502
 
    def get_lines(self, version_ids):
503
 
        self.calls.append(("get_lines", version_ids))
504
 
        return self._backing_vf.get_lines(version_ids)
505
 
 
506
 
 
507
 
class _PlanMergeVersionedFile(object):
508
 
    """A VersionedFile for uncommitted and committed texts.
509
 
 
510
 
    It is intended to allow merges to be planned with working tree texts.
511
 
    It implements only the small part of the VersionedFile interface used by
512
 
    PlanMerge.  It falls back to multiple versionedfiles for data not stored in
513
 
    _PlanMergeVersionedFile itself.
514
 
    """
515
 
 
516
 
    def __init__(self, file_id, fallback_versionedfiles=None):
517
 
        """Constuctor
518
 
 
519
 
        :param file_id: Used when raising exceptions.
520
 
        :param fallback_versionedfiles: If supplied, the set of fallbacks to
521
 
            use.  Otherwise, _PlanMergeVersionedFile.fallback_versionedfiles
522
 
            can be appended to later.
523
 
        """
524
 
        self._file_id = file_id
525
 
        if fallback_versionedfiles is None:
526
 
            self.fallback_versionedfiles = []
527
 
        else:
528
 
            self.fallback_versionedfiles = fallback_versionedfiles
529
 
        self._parents = {}
530
 
        self._lines = {}
531
 
 
532
 
    def plan_merge(self, ver_a, ver_b, base=None):
533
 
        """See VersionedFile.plan_merge"""
534
 
        from bzrlib.merge import _PlanMerge
535
 
        if base is None:
536
 
            return _PlanMerge(ver_a, ver_b, self).plan_merge()
537
 
        old_plan = list(_PlanMerge(ver_a, base, self).plan_merge())
538
 
        new_plan = list(_PlanMerge(ver_a, ver_b, self).plan_merge())
539
 
        return _PlanMerge._subtract_plans(old_plan, new_plan)
540
 
 
541
 
    def plan_lca_merge(self, ver_a, ver_b, base=None):
542
 
        from bzrlib.merge import _PlanLCAMerge
543
 
        graph = self._get_graph()
544
 
        new_plan = _PlanLCAMerge(ver_a, ver_b, self, graph).plan_merge()
545
 
        if base is None:
546
 
            return new_plan
547
 
        old_plan = _PlanLCAMerge(ver_a, base, self, graph).plan_merge()
548
 
        return _PlanLCAMerge._subtract_plans(list(old_plan), list(new_plan))
549
 
 
550
 
    def add_lines(self, version_id, parents, lines):
551
 
        """See VersionedFile.add_lines
552
 
 
553
 
        Lines are added locally, not fallback versionedfiles.  Also, ghosts are
554
 
        permitted.  Only reserved ids are permitted.
555
 
        """
556
 
        if not revision.is_reserved_id(version_id):
557
 
            raise ValueError('Only reserved ids may be used')
558
 
        if parents is None:
559
 
            raise ValueError('Parents may not be None')
560
 
        if lines is None:
561
 
            raise ValueError('Lines may not be None')
562
 
        self._parents[version_id] = tuple(parents)
563
 
        self._lines[version_id] = lines
564
 
 
565
 
    def get_lines(self, version_id):
566
 
        """See VersionedFile.get_ancestry"""
567
 
        lines = self._lines.get(version_id)
568
 
        if lines is not None:
569
 
            return lines
570
 
        for versionedfile in self.fallback_versionedfiles:
571
 
            try:
572
 
                return versionedfile.get_lines(version_id)
573
 
            except errors.RevisionNotPresent:
574
 
                continue
575
 
        else:
576
 
            raise errors.RevisionNotPresent(version_id, self._file_id)
577
 
 
578
 
    def get_ancestry(self, version_id, topo_sorted=False):
579
 
        """See VersionedFile.get_ancestry.
580
 
 
581
 
        Note that this implementation assumes that if a VersionedFile can
582
 
        answer get_ancestry at all, it can give an authoritative answer.  In
583
 
        fact, ghosts can invalidate this assumption.  But it's good enough
584
 
        99% of the time, and far cheaper/simpler.
585
 
 
586
 
        Also note that the results of this version are never topologically
587
 
        sorted, and are a set.
588
 
        """
589
 
        if topo_sorted:
590
 
            raise ValueError('This implementation does not provide sorting')
591
 
        parents = self._parents.get(version_id)
592
 
        if parents is None:
593
 
            for vf in self.fallback_versionedfiles:
594
 
                try:
595
 
                    return vf.get_ancestry(version_id, topo_sorted=False)
596
 
                except errors.RevisionNotPresent:
597
 
                    continue
598
 
            else:
599
 
                raise errors.RevisionNotPresent(version_id, self._file_id)
600
 
        ancestry = set([version_id])
601
 
        for parent in parents:
602
 
            ancestry.update(self.get_ancestry(parent, topo_sorted=False))
603
 
        return ancestry
604
 
 
605
 
    def get_parent_map(self, version_ids):
606
 
        """See VersionedFile.get_parent_map"""
607
 
        result = {}
608
 
        pending = set(version_ids)
609
 
        for key in version_ids:
610
 
            try:
611
 
                result[key] = self._parents[key]
612
 
            except KeyError:
613
 
                pass
614
 
        pending = pending - set(result.keys())
615
 
        for versionedfile in self.fallback_versionedfiles:
616
 
            parents = versionedfile.get_parent_map(pending)
617
 
            result.update(parents)
618
 
            pending = pending - set(parents.keys())
619
 
            if not pending:
620
 
                return result
621
 
        return result
622
 
 
623
 
    def _get_graph(self):
624
 
        from bzrlib.graph import (
625
 
            DictParentsProvider,
626
 
            Graph,
627
 
            _StackedParentsProvider,
628
 
            )
629
 
        from bzrlib.repofmt.knitrepo import _KnitParentsProvider
630
 
        parent_providers = [DictParentsProvider(self._parents)]
631
 
        for vf in self.fallback_versionedfiles:
632
 
            parent_providers.append(_KnitParentsProvider(vf))
633
 
        return Graph(_StackedParentsProvider(parent_providers))
634
 
 
635
 
 
636
476
class PlanWeaveMerge(TextMerge):
637
477
    """Weave merge that takes a plan as its input.
638
478
    
690
530
            elif state == 'new-b':
691
531
                ch_b = True
692
532
                lines_b.append(line)
693
 
            elif state == 'conflicted-a':
694
 
                ch_b = ch_a = True
695
 
                lines_a.append(line)
696
 
            elif state == 'conflicted-b':
697
 
                ch_b = ch_a = True
698
 
                lines_b.append(line)
699
533
            else:
700
 
                if state not in ('irrelevant', 'ghost-a', 'ghost-b',
701
 
                        'killed-base', 'killed-both'):
702
 
                    raise AssertionError(state)
 
534
                assert state in ('irrelevant', 'ghost-a', 'ghost-b', 
 
535
                                 'killed-base', 'killed-both'), state
703
536
        for struct in outstanding_struct():
704
537
            yield struct
705
538
 
706
539
 
707
540
class WeaveMerge(PlanWeaveMerge):
708
 
    """Weave merge that takes a VersionedFile and two versions as its input."""
 
541
    """Weave merge that takes a VersionedFile and two versions as its input"""
709
542
 
710
543
    def __init__(self, versionedfile, ver_a, ver_b, 
711
544
        a_marker=PlanWeaveMerge.A_MARKER, b_marker=PlanWeaveMerge.B_MARKER):
714
547
 
715
548
 
716
549
class InterVersionedFile(InterObject):
717
 
    """This class represents operations taking place between two VersionedFiles.
 
550
    """This class represents operations taking place between two versionedfiles..
718
551
 
719
552
    Its instances have methods like join, and contain
720
553
    references to the source and target versionedfiles these operations can be 
725
558
    InterVersionedFile.get(other).method_name(parameters).
726
559
    """
727
560
 
728
 
    _optimisers = []
 
561
    _optimisers = set()
729
562
    """The available optimised InterVersionedFile types."""
730
563
 
731
564
    def join(self, pb=None, msg=None, version_ids=None, ignore_missing=False):
735
568
        incorporated into this versioned file.
736
569
 
737
570
        Must raise RevisionNotPresent if any of the specified versions
738
 
        are not present in the other file's history unless ignore_missing is 
739
 
        supplied in which case they are silently skipped.
 
571
        are not present in the other files history unless ignore_missing is 
 
572
        supplied when they are silently skipped.
740
573
        """
741
 
        target = self.target
 
574
        # the default join: 
 
575
        # - if the target is empty, just add all the versions from 
 
576
        #   source to target, otherwise:
 
577
        # - make a temporary versioned file of type target
 
578
        # - insert the source content into it one at a time
 
579
        # - join them
 
580
        if not self.target.versions():
 
581
            target = self.target
 
582
        else:
 
583
            # Make a new target-format versioned file. 
 
584
            temp_source = self.target.create_empty("temp", MemoryTransport())
 
585
            target = temp_source
742
586
        version_ids = self._get_source_version_ids(version_ids, ignore_missing)
743
 
        graph = Graph(self.source)
744
 
        search = graph._make_breadth_first_searcher(version_ids)
745
 
        transitive_ids = set()
746
 
        map(transitive_ids.update, list(search))
747
 
        parent_map = self.source.get_parent_map(transitive_ids)
748
 
        order = tsort.topo_sort(parent_map.items())
 
587
        graph = self.source.get_graph(version_ids)
 
588
        order = topo_sort(graph.items())
749
589
        pb = ui.ui_factory.nested_progress_bar()
750
590
        parent_texts = {}
751
591
        try:
762
602
            # TODO: remove parent texts when they are not relevant any more for 
763
603
            # memory pressure reduction. RBC 20060313
764
604
            # pb.update('Converting versioned data', 0, len(order))
765
 
            total = len(order)
 
605
            # deltas = self.source.get_deltas(order)
766
606
            for index, version in enumerate(order):
767
 
                pb.update('Converting versioned data', index, total)
768
 
                if version in target:
769
 
                    continue
770
 
                _, _, parent_text = target.add_lines(version,
771
 
                                               parent_map[version],
 
607
                pb.update('Converting versioned data', index, len(order))
 
608
                parent_text = target.add_lines(version,
 
609
                                               self.source.get_parents(version),
772
610
                                               self.source.get_lines(version),
773
611
                                               parent_texts=parent_texts)
774
612
                parent_texts[version] = parent_text
775
 
            return total
 
613
                #delta_parent, sha1, noeol, delta = deltas[version]
 
614
                #target.add_delta(version,
 
615
                #                 self.source.get_parents(version),
 
616
                #                 delta_parent,
 
617
                #                 sha1,
 
618
                #                 noeol,
 
619
                #                 delta)
 
620
                #target.get_lines(version)
 
621
            
 
622
            # this should hit the native code path for target
 
623
            if target is not self.target:
 
624
                return self.target.join(temp_source,
 
625
                                        pb,
 
626
                                        msg,
 
627
                                        version_ids,
 
628
                                        ignore_missing)
776
629
        finally:
777
630
            pb.finished()
778
631
 
802
655
                    else:
803
656
                        new_version_ids.add(version)
804
657
                return new_version_ids
 
658
 
 
659
 
 
660
class InterVersionedFileTestProviderAdapter(object):
 
661
    """A tool to generate a suite testing multiple inter versioned-file classes.
 
662
 
 
663
    This is done by copying the test once for each InterVersionedFile provider
 
664
    and injecting the transport_server, transport_readonly_server,
 
665
    versionedfile_factory and versionedfile_factory_to classes into each copy.
 
666
    Each copy is also given a new id() to make it easy to identify.
 
667
    """
 
668
 
 
669
    def __init__(self, transport_server, transport_readonly_server, formats):
 
670
        self._transport_server = transport_server
 
671
        self._transport_readonly_server = transport_readonly_server
 
672
        self._formats = formats
 
673
    
 
674
    def adapt(self, test):
 
675
        result = TestSuite()
 
676
        for (interversionedfile_class,
 
677
             versionedfile_factory,
 
678
             versionedfile_factory_to) in self._formats:
 
679
            new_test = deepcopy(test)
 
680
            new_test.transport_server = self._transport_server
 
681
            new_test.transport_readonly_server = self._transport_readonly_server
 
682
            new_test.interversionedfile_class = interversionedfile_class
 
683
            new_test.versionedfile_factory = versionedfile_factory
 
684
            new_test.versionedfile_factory_to = versionedfile_factory_to
 
685
            def make_new_test_id():
 
686
                new_id = "%s(%s)" % (new_test.id(), interversionedfile_class.__name__)
 
687
                return lambda: new_id
 
688
            new_test.id = make_new_test_id()
 
689
            result.addTest(new_test)
 
690
        return result
 
691
 
 
692
    @staticmethod
 
693
    def default_test_list():
 
694
        """Generate the default list of interversionedfile permutations to test."""
 
695
        from bzrlib.weave import WeaveFile
 
696
        from bzrlib.knit import KnitVersionedFile
 
697
        result = []
 
698
        # test the fallback InterVersionedFile from annotated knits to weave
 
699
        result.append((InterVersionedFile, 
 
700
                       KnitVersionedFile,
 
701
                       WeaveFile))
 
702
        for optimiser in InterVersionedFile._optimisers:
 
703
            result.append((optimiser,
 
704
                           optimiser._matching_file_from_factory,
 
705
                           optimiser._matching_file_to_factory
 
706
                           ))
 
707
        # if there are specific combinations we want to use, we can add them 
 
708
        # here.
 
709
        return result