~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/versionedfile.py

Update test support, and remove deprecated functions pullable_revisions and get_intervening_revisions.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
 
1
# Copyright (C) 2005 by Canonical Ltd
2
2
#
3
3
# Authors:
4
4
#   Johan Rydberg <jrydberg@gnu.org>
7
7
# it under the terms of the GNU General Public License as published by
8
8
# the Free Software Foundation; either version 2 of the License, or
9
9
# (at your option) any later version.
10
 
#
 
10
 
11
11
# This program is distributed in the hope that it will be useful,
12
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
14
# GNU General Public License for more details.
15
 
#
 
15
 
16
16
# You should have received a copy of the GNU General Public License
17
17
# along with this program; if not, write to the Free Software
18
18
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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
29
from bzrlib.symbol_versioning import *
42
30
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
 
31
from bzrlib.transport.memory import MemoryTransport
 
32
from bzrlib.tsort import topo_sort
 
33
from bzrlib import ui
106
34
 
107
35
 
108
36
class VersionedFile(object):
119
47
    Texts are identified by a version-id string.
120
48
    """
121
49
 
122
 
    @staticmethod
123
 
    def check_not_reserved_id(version_id):
124
 
        revision.check_not_reserved_id(version_id)
 
50
    def __init__(self, access_mode):
 
51
        self.finished = False
 
52
        self._access_mode = access_mode
125
53
 
126
54
    def copy_to(self, name, transport):
127
55
        """Copy this versioned file to name on transport."""
128
56
        raise NotImplementedError(self.copy_to)
129
 
 
130
 
    def get_record_stream(self, versions, ordering, include_delta_closure):
131
 
        """Get a stream of records for versions.
132
 
 
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.
 
57
    
 
58
    @deprecated_method(zero_eight)
 
59
    def names(self):
 
60
        """Return a list of all the versions in this versioned file.
 
61
 
 
62
        Please use versionedfile.versions() now.
144
63
        """
145
 
        raise NotImplementedError(self.get_record_stream)
 
64
        return self.versions()
 
65
 
 
66
    def versions(self):
 
67
        """Return a unsorted list of versions."""
 
68
        raise NotImplementedError(self.versions)
 
69
 
 
70
    def has_ghost(self, version_id):
 
71
        """Returns whether version is present as a ghost."""
 
72
        raise NotImplementedError(self.has_ghost)
146
73
 
147
74
    def has_version(self, version_id):
148
75
        """Returns whether version is present."""
149
76
        raise NotImplementedError(self.has_version)
150
77
 
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):
 
78
    def add_delta(self, version_id, parents, delta_parent, sha1, noeol, delta):
 
79
        """Add a text to the versioned file via a pregenerated delta.
 
80
 
 
81
        :param version_id: The version id being added.
 
82
        :param parents: The parents of the version_id.
 
83
        :param delta_parent: The parent this delta was created against.
 
84
        :param sha1: The sha1 of the full text.
 
85
        :param delta: The delta instructions. See get_delta for details.
 
86
        """
 
87
        self._check_write_ok()
 
88
        if self.has_version(version_id):
 
89
            raise errors.RevisionAlreadyPresent(version_id, self)
 
90
        return self._add_delta(version_id, parents, delta_parent, sha1, noeol, delta)
 
91
 
 
92
    def _add_delta(self, version_id, parents, delta_parent, sha1, noeol, delta):
 
93
        """Class specific routine to add a delta.
 
94
 
 
95
        This generic version simply applies the delta to the delta_parent and
 
96
        then inserts it.
 
97
        """
 
98
        # strip annotation from delta
 
99
        new_delta = []
 
100
        for start, stop, delta_len, delta_lines in delta:
 
101
            new_delta.append((start, stop, delta_len, [text for origin, text in delta_lines]))
 
102
        if delta_parent is not None:
 
103
            parent_full = self.get_lines(delta_parent)
 
104
        else:
 
105
            parent_full = []
 
106
        new_full = self._apply_delta(parent_full, new_delta)
 
107
        # its impossible to have noeol on an empty file
 
108
        if noeol and new_full[-1][-1] == '\n':
 
109
            new_full[-1] = new_full[-1][:-1]
 
110
        self.add_lines(version_id, parents, new_full)
 
111
 
 
112
    def add_lines(self, version_id, parents, lines, parent_texts=None):
163
113
        """Add a single text on top of the versioned file.
164
114
 
165
115
        Must raise RevisionAlreadyPresent if the new version is
167
117
 
168
118
        Must raise RevisionNotPresent if any of the given parents are
169
119
        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
120
        :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.
 
121
             representations of some or all of the parents of 
 
122
             version_id to allow delta optimisations. 
 
123
             VERY IMPORTANT: the texts must be those returned
 
124
             by add_lines or data corruption can be caused.
 
125
        :return: An opaque representation of the inserted version which can be
 
126
                 provided back to future add_lines calls in the parent_texts
 
127
                 dictionary.
199
128
        """
200
129
        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)
 
130
        return self._add_lines(version_id, parents, lines, parent_texts)
203
131
 
204
 
    def _add_lines(self, version_id, parents, lines, parent_texts,
205
 
        left_matching_blocks, nostore_sha, random_id, check_content):
 
132
    def _add_lines(self, version_id, parents, lines, parent_texts):
206
133
        """Helper to do the class specific add_lines."""
207
134
        raise NotImplementedError(self.add_lines)
208
135
 
209
136
    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):
 
137
                              parent_texts=None):
212
138
        """Add lines to the versioned file, allowing ghosts to be present.
213
139
        
214
 
        This takes the same parameters as add_lines and returns the same.
 
140
        This takes the same parameters as add_lines.
215
141
        """
216
142
        self._check_write_ok()
217
143
        return self._add_lines_with_ghosts(version_id, parents, lines,
218
 
            parent_texts, nostore_sha, random_id, check_content, left_matching_blocks)
 
144
                                           parent_texts)
219
145
 
220
 
    def _add_lines_with_ghosts(self, version_id, parents, lines, parent_texts,
221
 
        nostore_sha, random_id, check_content, left_matching_blocks):
 
146
    def _add_lines_with_ghosts(self, version_id, parents, lines, parent_texts):
222
147
        """Helper to do class specific add_lines_with_ghosts."""
223
148
        raise NotImplementedError(self.add_lines_with_ghosts)
224
149
 
238
163
            if '\n' in line[:-1]:
239
164
                raise errors.BzrBadParameterContainsNewline("lines")
240
165
 
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
 
 
 
166
    def _check_write_ok(self):
 
167
        """Is the versioned file marked as 'finished' ? Raise if it is."""
 
168
        if self.finished:
 
169
            raise errors.OutSideTransaction()
 
170
        if self._access_mode != 'w':
 
171
            raise errors.ReadOnlyObjectDirtiedError(self)
 
172
 
 
173
    def clear_cache(self):
 
174
        """Remove any data cached in the versioned file object."""
 
175
 
 
176
    def clone_text(self, new_version_id, old_version_id, parents):
 
177
        """Add an identical text to old_version_id as new_version_id.
 
178
 
 
179
        Must raise RevisionNotPresent if the old version or any of the
 
180
        parents are not present in file history.
 
181
 
 
182
        Must raise RevisionAlreadyPresent if the new version is
 
183
        already present in file history."""
 
184
        self._check_write_ok()
 
185
        return self._clone_text(new_version_id, old_version_id, parents)
 
186
 
 
187
    def _clone_text(self, new_version_id, old_version_id, parents):
 
188
        """Helper function to do the _clone_text work."""
 
189
        raise NotImplementedError(self.clone_text)
 
190
 
 
191
    def create_empty(self, name, transport, mode=None):
 
192
        """Create a new versioned file of this exact type.
 
193
 
 
194
        :param name: the file name
 
195
        :param transport: the transport
 
196
        :param mode: optional file mode.
 
197
        """
 
198
        raise NotImplementedError(self.create_empty)
 
199
 
 
200
    def fix_parents(self, version, new_parents):
 
201
        """Fix the parents list for version.
 
202
        
 
203
        This is done by appending a new version to the index
 
204
        with identical data except for the parents list.
 
205
        the parents list must be a superset of the current
 
206
        list.
 
207
        """
 
208
        self._check_write_ok()
 
209
        return self._fix_parents(version, new_parents)
 
210
 
 
211
    def _fix_parents(self, version, new_parents):
 
212
        """Helper for fix_parents."""
 
213
        raise NotImplementedError(self.fix_parents)
 
214
 
 
215
    def get_delta(self, version):
 
216
        """Get a delta for constructing version from some other version.
 
217
        
 
218
        :return: (delta_parent, sha1, noeol, delta)
 
219
        Where delta_parent is a version id or None to indicate no parent.
 
220
        """
 
221
        raise NotImplementedError(self.get_delta)
 
222
 
 
223
    def get_deltas(self, versions):
 
224
        """Get multiple deltas at once for constructing versions.
 
225
        
 
226
        :return: dict(version_id:(delta_parent, sha1, noeol, delta))
 
227
        Where delta_parent is a version id or None to indicate no parent, and
 
228
        version_id is the version_id created by that delta.
 
229
        """
 
230
        result = {}
 
231
        for version in versions:
 
232
            result[version] = self.get_delta(version)
 
233
        return result
 
234
 
 
235
    def get_sha1(self, version_id):
 
236
        """Get the stored sha1 sum for the given revision.
 
237
        
 
238
        :param name: The name of the version to lookup
 
239
        """
 
240
        raise NotImplementedError(self.get_sha1)
 
241
 
 
242
    def get_suffixes(self):
 
243
        """Return the file suffixes associated with this versioned file."""
 
244
        raise NotImplementedError(self.get_suffixes)
 
245
    
334
246
    def get_text(self, version_id):
335
247
        """Return version contents as a text string.
336
248
 
340
252
        return ''.join(self.get_lines(version_id))
341
253
    get_string = get_text
342
254
 
343
 
    def get_texts(self, version_ids):
344
 
        """Return the texts of listed versions as a list of strings.
345
 
 
346
 
        Raises RevisionNotPresent if version is not present in
347
 
        file history.
348
 
        """
349
 
        return [''.join(self.get_lines(v)) for v in version_ids]
350
 
 
351
255
    def get_lines(self, version_id):
352
256
        """Return version contents as a sequence of lines.
353
257
 
356
260
        """
357
261
        raise NotImplementedError(self.get_lines)
358
262
 
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):
 
263
    def get_ancestry(self, version_ids):
363
264
        """Return a list of all ancestors of given version(s). This
364
265
        will not include the null revision.
365
266
 
366
 
        This list will not be topologically sorted if topo_sorted=False is
367
 
        passed.
368
 
 
369
267
        Must raise RevisionNotPresent if any of the given versions are
370
268
        not present in file history."""
371
269
        if isinstance(version_ids, basestring):
383
281
        but are not explicitly marked.
384
282
        """
385
283
        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)
 
284
        
 
285
    def get_graph(self, version_ids=None):
 
286
        """Return a graph from the versioned file. 
 
287
        
 
288
        Ghosts are not listed or referenced in the graph.
 
289
        :param version_ids: Versions to select.
 
290
                            None means retreive all versions.
 
291
        """
 
292
        result = {}
 
293
        if version_ids is None:
 
294
            for version in self.versions():
 
295
                result[version] = self.get_parents(version)
 
296
        else:
 
297
            pending = set(version_ids)
 
298
            while pending:
 
299
                version = pending.pop()
 
300
                if version in result:
 
301
                    continue
 
302
                parents = self.get_parents(version)
 
303
                for parent in parents:
 
304
                    if parent in result:
 
305
                        continue
 
306
                    pending.add(parent)
 
307
                result[version] = parents
 
308
        return result
 
309
 
 
310
    def get_graph_with_ghosts(self):
 
311
        """Return a graph for the entire versioned file.
 
312
        
 
313
        Ghosts are referenced in parents list but are not
 
314
        explicitly listed.
 
315
        """
 
316
        raise NotImplementedError(self.get_graph_with_ghosts)
 
317
 
 
318
    @deprecated_method(zero_eight)
 
319
    def parent_names(self, version):
 
320
        """Return version names for parents of a version.
 
321
        
 
322
        See get_parents for the current api.
 
323
        """
 
324
        return self.get_parents(version)
 
325
 
 
326
    def get_parents(self, version_id):
 
327
        """Return version names for parents of a version.
 
328
 
 
329
        Must raise RevisionNotPresent if version is not present in
 
330
        file history.
 
331
        """
 
332
        raise NotImplementedError(self.get_parents)
394
333
 
395
334
    def get_parents_with_ghosts(self, version_id):
396
335
        """Return version names for parents of version_id.
401
340
        Ghosts that are known about will be included in the parent list,
402
341
        but are not explicitly marked.
403
342
        """
404
 
        try:
405
 
            return list(self.get_parent_map([version_id])[version_id])
406
 
        except KeyError:
407
 
            raise errors.RevisionNotPresent(version_id, self)
 
343
        raise NotImplementedError(self.get_parents_with_ghosts)
 
344
 
 
345
    def annotate_iter(self, version_id):
 
346
        """Yield list of (version-id, line) pairs for the specified
 
347
        version.
 
348
 
 
349
        Must raise RevisionNotPresent if any of the given versions are
 
350
        not present in file history.
 
351
        """
 
352
        raise NotImplementedError(self.annotate_iter)
408
353
 
409
354
    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)
 
355
        return list(self.annotate_iter(version_id))
 
356
 
 
357
    def _apply_delta(self, lines, delta):
 
358
        """Apply delta to lines."""
 
359
        lines = list(lines)
 
360
        offset = 0
 
361
        for start, end, count, delta_lines in delta:
 
362
            lines[offset+start:offset+end] = delta_lines
 
363
            offset = offset + (start - end) + count
 
364
        return lines
 
365
 
418
366
    def join(self, other, pb=None, msg=None, version_ids=None,
419
367
             ignore_missing=False):
420
368
        """Integrate versions from other into this versioned file.
423
371
        incorporated into this versioned file.
424
372
 
425
373
        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.
 
374
        are not present in the other files history unless ignore_missing
 
375
        is supplied when they are silently skipped.
428
376
        """
429
377
        self._check_write_ok()
430
378
        return InterVersionedFile.get(other, self).join(
433
381
            version_ids,
434
382
            ignore_missing)
435
383
 
436
 
    def iter_lines_added_or_present_in_versions(self, version_ids=None,
437
 
                                                pb=None):
 
384
    def iter_lines_added_or_present_in_versions(self, version_ids=None):
438
385
        """Iterate over the lines in the versioned file from version_ids.
439
386
 
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).
 
387
        This may return lines from other versions, and does not return the
 
388
        specific version marker at this point. The api may be changed
 
389
        during development to include the version that the versioned file
 
390
        thinks is relevant, but given that such hints are just guesses,
 
391
        its better not to have it if we dont need it.
450
392
 
451
393
        NOTES: Lines are normalised: they will all have \n terminators.
452
394
               Lines are returned in arbitrary order.
453
 
 
454
 
        :return: An iterator over (line, version_id).
455
395
        """
456
396
        raise NotImplementedError(self.iter_lines_added_or_present_in_versions)
457
397
 
 
398
    def transaction_finished(self):
 
399
        """The transaction that this file was opened in has finished.
 
400
 
 
401
        This records self.finished = True and should cause all mutating
 
402
        operations to error.
 
403
        """
 
404
        self.finished = True
 
405
 
 
406
    @deprecated_method(zero_eight)
 
407
    def walk(self, version_ids=None):
 
408
        """Walk the versioned file as a weave-like structure, for
 
409
        versions relative to version_ids.  Yields sequence of (lineno,
 
410
        insert, deletes, text) for each relevant line.
 
411
 
 
412
        Must raise RevisionNotPresent if any of the specified versions
 
413
        are not present in the file history.
 
414
 
 
415
        :param version_ids: the version_ids to walk with respect to. If not
 
416
                            supplied the entire weave-like structure is walked.
 
417
 
 
418
        walk is deprecated in favour of iter_lines_added_or_present_in_versions
 
419
        """
 
420
        raise NotImplementedError(self.walk)
 
421
 
 
422
    @deprecated_method(zero_eight)
 
423
    def iter_names(self):
 
424
        """Walk the names list."""
 
425
        return iter(self.versions())
 
426
 
458
427
    def plan_merge(self, ver_a, ver_b):
459
428
        """Return pseudo-annotation indicating how the two versions merge.
460
429
 
477
446
        """
478
447
        raise NotImplementedError(VersionedFile.plan_merge)
479
448
        
480
 
    def weave_merge(self, plan, a_marker=TextMerge.A_MARKER,
 
449
    def weave_merge(self, plan, a_marker=TextMerge.A_MARKER, 
481
450
                    b_marker=TextMerge.B_MARKER):
482
451
        return PlanWeaveMerge(plan, a_marker, b_marker).merge_lines()[0]
483
452
 
484
453
 
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
454
class PlanWeaveMerge(TextMerge):
637
455
    """Weave merge that takes a plan as its input.
638
456
    
665
483
       
666
484
        # We previously considered either 'unchanged' or 'killed-both' lines
667
485
        # to be possible places to resynchronize.  However, assuming agreement
668
 
        # on killed-both lines may be too aggressive. -- mbp 20060324
 
486
        # on killed-both lines may be too agressive. -- mbp 20060324
669
487
        for state, line in self.plan:
670
488
            if state == 'unchanged':
671
489
                # resync and flush queued conflicts changes if any
690
508
            elif state == 'new-b':
691
509
                ch_b = True
692
510
                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
511
            else:
700
 
                if state not in ('irrelevant', 'ghost-a', 'ghost-b',
701
 
                        'killed-base', 'killed-both'):
702
 
                    raise AssertionError(state)
 
512
                assert state in ('irrelevant', 'ghost-a', 'ghost-b', 
 
513
                                 'killed-base', 'killed-both'), state
703
514
        for struct in outstanding_struct():
704
515
            yield struct
705
516
 
706
517
 
707
518
class WeaveMerge(PlanWeaveMerge):
708
 
    """Weave merge that takes a VersionedFile and two versions as its input."""
 
519
    """Weave merge that takes a VersionedFile and two versions as its input"""
709
520
 
710
521
    def __init__(self, versionedfile, ver_a, ver_b, 
711
522
        a_marker=PlanWeaveMerge.A_MARKER, b_marker=PlanWeaveMerge.B_MARKER):
714
525
 
715
526
 
716
527
class InterVersionedFile(InterObject):
717
 
    """This class represents operations taking place between two VersionedFiles.
 
528
    """This class represents operations taking place between two versionedfiles..
718
529
 
719
530
    Its instances have methods like join, and contain
720
531
    references to the source and target versionedfiles these operations can be 
725
536
    InterVersionedFile.get(other).method_name(parameters).
726
537
    """
727
538
 
728
 
    _optimisers = []
 
539
    _optimisers = set()
729
540
    """The available optimised InterVersionedFile types."""
730
541
 
731
542
    def join(self, pb=None, msg=None, version_ids=None, ignore_missing=False):
735
546
        incorporated into this versioned file.
736
547
 
737
548
        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.
 
549
        are not present in the other files history unless ignore_missing is 
 
550
        supplied when they are silently skipped.
740
551
        """
741
 
        target = self.target
 
552
        # the default join: 
 
553
        # - if the target is empty, just add all the versions from 
 
554
        #   source to target, otherwise:
 
555
        # - make a temporary versioned file of type target
 
556
        # - insert the source content into it one at a time
 
557
        # - join them
 
558
        if not self.target.versions():
 
559
            target = self.target
 
560
        else:
 
561
            # Make a new target-format versioned file. 
 
562
            temp_source = self.target.create_empty("temp", MemoryTransport())
 
563
            target = temp_source
742
564
        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())
 
565
        graph = self.source.get_graph(version_ids)
 
566
        order = topo_sort(graph.items())
749
567
        pb = ui.ui_factory.nested_progress_bar()
750
568
        parent_texts = {}
751
569
        try:
762
580
            # TODO: remove parent texts when they are not relevant any more for 
763
581
            # memory pressure reduction. RBC 20060313
764
582
            # pb.update('Converting versioned data', 0, len(order))
765
 
            total = len(order)
 
583
            # deltas = self.source.get_deltas(order)
766
584
            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],
 
585
                pb.update('Converting versioned data', index, len(order))
 
586
                parent_text = target.add_lines(version,
 
587
                                               self.source.get_parents(version),
772
588
                                               self.source.get_lines(version),
773
589
                                               parent_texts=parent_texts)
774
590
                parent_texts[version] = parent_text
775
 
            return total
 
591
                #delta_parent, sha1, noeol, delta = deltas[version]
 
592
                #target.add_delta(version,
 
593
                #                 self.source.get_parents(version),
 
594
                #                 delta_parent,
 
595
                #                 sha1,
 
596
                #                 noeol,
 
597
                #                 delta)
 
598
                #target.get_lines(version)
 
599
            
 
600
            # this should hit the native code path for target
 
601
            if target is not self.target:
 
602
                return self.target.join(temp_source,
 
603
                                        pb,
 
604
                                        msg,
 
605
                                        version_ids,
 
606
                                        ignore_missing)
776
607
        finally:
777
608
            pb.finished()
778
609
 
802
633
                    else:
803
634
                        new_version_ids.add(version)
804
635
                return new_version_ids
 
636
 
 
637
 
 
638
class InterVersionedFileTestProviderAdapter(object):
 
639
    """A tool to generate a suite testing multiple inter versioned-file classes.
 
640
 
 
641
    This is done by copying the test once for each interversionedfile provider
 
642
    and injecting the transport_server, transport_readonly_server,
 
643
    versionedfile_factory and versionedfile_factory_to classes into each copy.
 
644
    Each copy is also given a new id() to make it easy to identify.
 
645
    """
 
646
 
 
647
    def __init__(self, transport_server, transport_readonly_server, formats):
 
648
        self._transport_server = transport_server
 
649
        self._transport_readonly_server = transport_readonly_server
 
650
        self._formats = formats
 
651
    
 
652
    def adapt(self, test):
 
653
        result = TestSuite()
 
654
        for (interversionedfile_class,
 
655
             versionedfile_factory,
 
656
             versionedfile_factory_to) in self._formats:
 
657
            new_test = deepcopy(test)
 
658
            new_test.transport_server = self._transport_server
 
659
            new_test.transport_readonly_server = self._transport_readonly_server
 
660
            new_test.interversionedfile_class = interversionedfile_class
 
661
            new_test.versionedfile_factory = versionedfile_factory
 
662
            new_test.versionedfile_factory_to = versionedfile_factory_to
 
663
            def make_new_test_id():
 
664
                new_id = "%s(%s)" % (new_test.id(), interversionedfile_class.__name__)
 
665
                return lambda: new_id
 
666
            new_test.id = make_new_test_id()
 
667
            result.addTest(new_test)
 
668
        return result
 
669
 
 
670
    @staticmethod
 
671
    def default_test_list():
 
672
        """Generate the default list of interversionedfile permutations to test."""
 
673
        from bzrlib.weave import WeaveFile
 
674
        from bzrlib.knit import KnitVersionedFile
 
675
        result = []
 
676
        # test the fallback InterVersionedFile from annotated knits to weave
 
677
        result.append((InterVersionedFile, 
 
678
                       KnitVersionedFile,
 
679
                       WeaveFile))
 
680
        for optimiser in InterVersionedFile._optimisers:
 
681
            result.append((optimiser,
 
682
                           optimiser._matching_file_from_factory,
 
683
                           optimiser._matching_file_to_factory
 
684
                           ))
 
685
        # if there are specific combinations we want to use, we can add them 
 
686
        # here.
 
687
        return result