/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/versionedfile.py

Functional get_record_stream interface tests covering full interface.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006 Canonical Ltd
 
2
#
 
3
# Authors:
 
4
#   Johan Rydberg <jrydberg@gnu.org>
 
5
#
 
6
# This program is free software; you can redistribute it and/or modify
 
7
# it under the terms of the GNU General Public License as published by
 
8
# the Free Software Foundation; either version 2 of the License, or
 
9
# (at your option) any later version.
 
10
#
 
11
# This program is distributed in the hope that it will be useful,
 
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
14
# GNU General Public License for more details.
 
15
#
 
16
# You should have received a copy of the GNU General Public License
 
17
# along with this program; if not, write to the Free Software
 
18
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
19
 
 
20
"""Versioned text file storage api."""
 
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
 
 
39
from bzrlib.inter import InterObject
 
40
from bzrlib.symbol_versioning import *
 
41
from bzrlib.textmerge import TextMerge
 
42
 
 
43
 
 
44
class ContentFactory(object):
 
45
    """Abstract interface for insertion and retrieval from a VersionedFile.
 
46
    
 
47
    :ivar sha1: None, or the sha1 of the content fulltext.
 
48
    :ivar storage_kind: The native storage kind of this factory. One of
 
49
        'mpdiff', 'knit-annotated-ft', 'knit-annotated-delta', 'knit-ft',
 
50
        'knit-delta', 'fulltext', 'knit-annotated-ft-gz',
 
51
        'knit-annotated-delta-gz', 'knit-ft-gz', 'knit-delta-gz'.
 
52
    :ivar key: The key of this content. Each key is a tuple with a single
 
53
        string in it.
 
54
    :ivar parents: A tuple of parent keys for self.key. If the object has
 
55
        no parent information, None (as opposed to () for an empty list of
 
56
        parents).
 
57
        """
 
58
 
 
59
    def __init__(self):
 
60
        """Create a ContentFactory."""
 
61
        self.sha1 = None
 
62
        self.storage_kind = None
 
63
        self.key = None
 
64
        self.parents = None
 
65
 
 
66
 
 
67
class VersionedFile(object):
 
68
    """Versioned text file storage.
 
69
    
 
70
    A versioned file manages versions of line-based text files,
 
71
    keeping track of the originating version for each line.
 
72
 
 
73
    To clients the "lines" of the file are represented as a list of
 
74
    strings. These strings will typically have terminal newline
 
75
    characters, but this is not required.  In particular files commonly
 
76
    do not have a newline at the end of the file.
 
77
 
 
78
    Texts are identified by a version-id string.
 
79
    """
 
80
 
 
81
    @staticmethod
 
82
    def check_not_reserved_id(version_id):
 
83
        revision.check_not_reserved_id(version_id)
 
84
 
 
85
    def copy_to(self, name, transport):
 
86
        """Copy this versioned file to name on transport."""
 
87
        raise NotImplementedError(self.copy_to)
 
88
 
 
89
    def get_record_stream(self, versions, ordering, include_delta_closure):
 
90
        """Get a stream of records for versions.
 
91
 
 
92
        :param versions: The versions to include. Each version is a tuple
 
93
            (version,).
 
94
        :param ordering: Either 'unordered' or 'topological'. A topologically
 
95
            sorted stream has compression parents strictly before their
 
96
            children.
 
97
        :param include_delta_closure: If True then the closure across any
 
98
            compression parents will be included (in the opaque data).
 
99
        :return: An iterator of ContentFactory objects, each of which is only
 
100
            valid until the iterator is advanced.
 
101
        """
 
102
        raise NotImplementedError(self.get_record_stream)
 
103
 
 
104
    @deprecated_method(one_four)
 
105
    def has_ghost(self, version_id):
 
106
        """Returns whether version is present as a ghost."""
 
107
        raise NotImplementedError(self.has_ghost)
 
108
 
 
109
    def has_version(self, version_id):
 
110
        """Returns whether version is present."""
 
111
        raise NotImplementedError(self.has_version)
 
112
 
 
113
    def add_lines(self, version_id, parents, lines, parent_texts=None,
 
114
        left_matching_blocks=None, nostore_sha=None, random_id=False,
 
115
        check_content=True):
 
116
        """Add a single text on top of the versioned file.
 
117
 
 
118
        Must raise RevisionAlreadyPresent if the new version is
 
119
        already present in file history.
 
120
 
 
121
        Must raise RevisionNotPresent if any of the given parents are
 
122
        not present in file history.
 
123
 
 
124
        :param lines: A list of lines. Each line must be a bytestring. And all
 
125
            of them except the last must be terminated with \n and contain no
 
126
            other \n's. The last line may either contain no \n's or a single
 
127
            terminated \n. If the lines list does meet this constraint the add
 
128
            routine may error or may succeed - but you will be unable to read
 
129
            the data back accurately. (Checking the lines have been split
 
130
            correctly is expensive and extremely unlikely to catch bugs so it
 
131
            is not done at runtime unless check_content is True.)
 
132
        :param parent_texts: An optional dictionary containing the opaque 
 
133
            representations of some or all of the parents of version_id to
 
134
            allow delta optimisations.  VERY IMPORTANT: the texts must be those
 
135
            returned by add_lines or data corruption can be caused.
 
136
        :param left_matching_blocks: a hint about which areas are common
 
137
            between the text and its left-hand-parent.  The format is
 
138
            the SequenceMatcher.get_matching_blocks format.
 
139
        :param nostore_sha: Raise ExistingContent and do not add the lines to
 
140
            the versioned file if the digest of the lines matches this.
 
141
        :param random_id: If True a random id has been selected rather than
 
142
            an id determined by some deterministic process such as a converter
 
143
            from a foreign VCS. When True the backend may choose not to check
 
144
            for uniqueness of the resulting key within the versioned file, so
 
145
            this should only be done when the result is expected to be unique
 
146
            anyway.
 
147
        :param check_content: If True, the lines supplied are verified to be
 
148
            bytestrings that are correctly formed lines.
 
149
        :return: The text sha1, the number of bytes in the text, and an opaque
 
150
                 representation of the inserted version which can be provided
 
151
                 back to future add_lines calls in the parent_texts dictionary.
 
152
        """
 
153
        self._check_write_ok()
 
154
        return self._add_lines(version_id, parents, lines, parent_texts,
 
155
            left_matching_blocks, nostore_sha, random_id, check_content)
 
156
 
 
157
    def _add_lines(self, version_id, parents, lines, parent_texts,
 
158
        left_matching_blocks, nostore_sha, random_id, check_content):
 
159
        """Helper to do the class specific add_lines."""
 
160
        raise NotImplementedError(self.add_lines)
 
161
 
 
162
    def add_lines_with_ghosts(self, version_id, parents, lines,
 
163
        parent_texts=None, nostore_sha=None, random_id=False,
 
164
        check_content=True, left_matching_blocks=None):
 
165
        """Add lines to the versioned file, allowing ghosts to be present.
 
166
        
 
167
        This takes the same parameters as add_lines and returns the same.
 
168
        """
 
169
        self._check_write_ok()
 
170
        return self._add_lines_with_ghosts(version_id, parents, lines,
 
171
            parent_texts, nostore_sha, random_id, check_content, left_matching_blocks)
 
172
 
 
173
    def _add_lines_with_ghosts(self, version_id, parents, lines, parent_texts,
 
174
        nostore_sha, random_id, check_content, left_matching_blocks):
 
175
        """Helper to do class specific add_lines_with_ghosts."""
 
176
        raise NotImplementedError(self.add_lines_with_ghosts)
 
177
 
 
178
    def check(self, progress_bar=None):
 
179
        """Check the versioned file for integrity."""
 
180
        raise NotImplementedError(self.check)
 
181
 
 
182
    def _check_lines_not_unicode(self, lines):
 
183
        """Check that lines being added to a versioned file are not unicode."""
 
184
        for line in lines:
 
185
            if line.__class__ is not str:
 
186
                raise errors.BzrBadParameterUnicode("lines")
 
187
 
 
188
    def _check_lines_are_lines(self, lines):
 
189
        """Check that the lines really are full lines without inline EOL."""
 
190
        for line in lines:
 
191
            if '\n' in line[:-1]:
 
192
                raise errors.BzrBadParameterContainsNewline("lines")
 
193
 
 
194
    @deprecated_method(one_four)
 
195
    def enable_cache(self):
 
196
        """Tell this versioned file that it should cache any data it reads.
 
197
        
 
198
        This is advisory, implementations do not have to support caching.
 
199
        """
 
200
        pass
 
201
    
 
202
    @deprecated_method(one_four)
 
203
    def clear_cache(self):
 
204
        """Remove any data cached in the versioned file object.
 
205
 
 
206
        This only needs to be supported if caches are supported
 
207
        """
 
208
        pass
 
209
 
 
210
    @deprecated_method(one_four)
 
211
    def clone_text(self, new_version_id, old_version_id, parents):
 
212
        """Add an identical text to old_version_id as new_version_id.
 
213
 
 
214
        Must raise RevisionNotPresent if the old version or any of the
 
215
        parents are not present in file history.
 
216
 
 
217
        Must raise RevisionAlreadyPresent if the new version is
 
218
        already present in file history."""
 
219
        self._check_write_ok()
 
220
        return self.add_lines(new_version_id, parents,
 
221
            self.get_lines(old_version_id))
 
222
 
 
223
    def get_format_signature(self):
 
224
        """Get a text description of the data encoding in this file.
 
225
        
 
226
        :since: 0.90
 
227
        """
 
228
        raise NotImplementedError(self.get_format_signature)
 
229
 
 
230
    def make_mpdiffs(self, version_ids):
 
231
        """Create multiparent diffs for specified versions."""
 
232
        knit_versions = set()
 
233
        knit_versions.update(version_ids)
 
234
        parent_map = self.get_parent_map(version_ids)
 
235
        for version_id in version_ids:
 
236
            try:
 
237
                knit_versions.update(parent_map[version_id])
 
238
            except KeyError:
 
239
                raise RevisionNotPresent(version_id, self)
 
240
        # We need to filter out ghosts, because we can't diff against them.
 
241
        knit_versions = set(self.get_parent_map(knit_versions).keys())
 
242
        lines = dict(zip(knit_versions,
 
243
            self._get_lf_split_line_list(knit_versions)))
 
244
        diffs = []
 
245
        for version_id in version_ids:
 
246
            target = lines[version_id]
 
247
            try:
 
248
                parents = [lines[p] for p in parent_map[version_id] if p in
 
249
                    knit_versions]
 
250
            except KeyError:
 
251
                raise RevisionNotPresent(version_id, self)
 
252
            if len(parents) > 0:
 
253
                left_parent_blocks = self._extract_blocks(version_id,
 
254
                                                          parents[0], target)
 
255
            else:
 
256
                left_parent_blocks = None
 
257
            diffs.append(multiparent.MultiParent.from_lines(target, parents,
 
258
                         left_parent_blocks))
 
259
        return diffs
 
260
 
 
261
    def _extract_blocks(self, version_id, source, target):
 
262
        return None
 
263
 
 
264
    def add_mpdiffs(self, records):
 
265
        """Add mpdiffs to this VersionedFile.
 
266
 
 
267
        Records should be iterables of version, parents, expected_sha1,
 
268
        mpdiff. mpdiff should be a MultiParent instance.
 
269
        """
 
270
        # Does this need to call self._check_write_ok()? (IanC 20070919)
 
271
        vf_parents = {}
 
272
        mpvf = multiparent.MultiMemoryVersionedFile()
 
273
        versions = []
 
274
        for version, parent_ids, expected_sha1, mpdiff in records:
 
275
            versions.append(version)
 
276
            mpvf.add_diff(mpdiff, version, parent_ids)
 
277
        needed_parents = set()
 
278
        for version, parent_ids, expected_sha1, mpdiff in records:
 
279
            needed_parents.update(p for p in parent_ids
 
280
                                  if not mpvf.has_version(p))
 
281
        present_parents = set(self.get_parent_map(needed_parents).keys())
 
282
        for parent_id, lines in zip(present_parents,
 
283
                                 self._get_lf_split_line_list(present_parents)):
 
284
            mpvf.add_version(lines, parent_id, [])
 
285
        for (version, parent_ids, expected_sha1, mpdiff), lines in\
 
286
            zip(records, mpvf.get_line_list(versions)):
 
287
            if len(parent_ids) == 1:
 
288
                left_matching_blocks = list(mpdiff.get_matching_blocks(0,
 
289
                    mpvf.get_diff(parent_ids[0]).num_lines()))
 
290
            else:
 
291
                left_matching_blocks = None
 
292
            try:
 
293
                _, _, version_text = self.add_lines_with_ghosts(version,
 
294
                    parent_ids, lines, vf_parents,
 
295
                    left_matching_blocks=left_matching_blocks)
 
296
            except NotImplementedError:
 
297
                # The vf can't handle ghosts, so add lines normally, which will
 
298
                # (reasonably) fail if there are ghosts in the data.
 
299
                _, _, version_text = self.add_lines(version,
 
300
                    parent_ids, lines, vf_parents,
 
301
                    left_matching_blocks=left_matching_blocks)
 
302
            vf_parents[version] = version_text
 
303
        for (version, parent_ids, expected_sha1, mpdiff), sha1 in\
 
304
             zip(records, self.get_sha1s(versions)):
 
305
            if expected_sha1 != sha1:
 
306
                raise errors.VersionedFileInvalidChecksum(version)
 
307
 
 
308
    @deprecated_method(one_four)
 
309
    def get_sha1(self, version_id):
 
310
        """Get the stored sha1 sum for the given revision.
 
311
        
 
312
        :param version_id: The name of the version to lookup
 
313
        """
 
314
        return self.get_sha1s([version_id])[0]
 
315
 
 
316
    def get_sha1s(self, version_ids):
 
317
        """Get the stored sha1 sums for the given revisions.
 
318
 
 
319
        :param version_ids: The names of the versions to lookup
 
320
        :return: a list of sha1s in order according to the version_ids
 
321
        """
 
322
        raise NotImplementedError(self.get_sha1s)
 
323
 
 
324
    def get_text(self, version_id):
 
325
        """Return version contents as a text string.
 
326
 
 
327
        Raises RevisionNotPresent if version is not present in
 
328
        file history.
 
329
        """
 
330
        return ''.join(self.get_lines(version_id))
 
331
    get_string = get_text
 
332
 
 
333
    def get_texts(self, version_ids):
 
334
        """Return the texts of listed versions as a list of strings.
 
335
 
 
336
        Raises RevisionNotPresent if version is not present in
 
337
        file history.
 
338
        """
 
339
        return [''.join(self.get_lines(v)) for v in version_ids]
 
340
 
 
341
    def get_lines(self, version_id):
 
342
        """Return version contents as a sequence of lines.
 
343
 
 
344
        Raises RevisionNotPresent if version is not present in
 
345
        file history.
 
346
        """
 
347
        raise NotImplementedError(self.get_lines)
 
348
 
 
349
    def _get_lf_split_line_list(self, version_ids):
 
350
        return [StringIO(t).readlines() for t in self.get_texts(version_ids)]
 
351
 
 
352
    def get_ancestry(self, version_ids, topo_sorted=True):
 
353
        """Return a list of all ancestors of given version(s). This
 
354
        will not include the null revision.
 
355
 
 
356
        This list will not be topologically sorted if topo_sorted=False is
 
357
        passed.
 
358
 
 
359
        Must raise RevisionNotPresent if any of the given versions are
 
360
        not present in file history."""
 
361
        if isinstance(version_ids, basestring):
 
362
            version_ids = [version_ids]
 
363
        raise NotImplementedError(self.get_ancestry)
 
364
        
 
365
    def get_ancestry_with_ghosts(self, version_ids):
 
366
        """Return a list of all ancestors of given version(s). This
 
367
        will not include the null revision.
 
368
 
 
369
        Must raise RevisionNotPresent if any of the given versions are
 
370
        not present in file history.
 
371
        
 
372
        Ghosts that are known about will be included in ancestry list,
 
373
        but are not explicitly marked.
 
374
        """
 
375
        raise NotImplementedError(self.get_ancestry_with_ghosts)
 
376
    
 
377
    @deprecated_method(one_four)
 
378
    def get_graph(self, version_ids=None):
 
379
        """Return a graph from the versioned file. 
 
380
        
 
381
        Ghosts are not listed or referenced in the graph.
 
382
        :param version_ids: Versions to select.
 
383
                            None means retrieve all versions.
 
384
        """
 
385
        if version_ids is None:
 
386
            result = self.get_parent_map(self.versions())
 
387
        else:
 
388
            result = {}
 
389
            pending = set(version_ids)
 
390
            while pending:
 
391
                this_iteration = pending
 
392
                pending = set()
 
393
                parents = self.get_parent_map(this_iteration)
 
394
                for version, parents in parents.iteritems():
 
395
                    result[version] = parents
 
396
                    for parent in parents:
 
397
                        if parent in result:
 
398
                            continue
 
399
                        pending.add(parent)
 
400
        references = set()
 
401
        for parents in result.itervalues():
 
402
            references.update(parents)
 
403
        existing_parents = self.get_parent_map(references)
 
404
        for key, parents in result.iteritems():
 
405
            present_parents = [parent for parent in parents if parent in
 
406
                existing_parents]
 
407
            result[key] = tuple(present_parents)
 
408
        return result
 
409
 
 
410
    @deprecated_method(one_four)
 
411
    def get_graph_with_ghosts(self):
 
412
        """Return a graph for the entire versioned file.
 
413
        
 
414
        Ghosts are referenced in parents list but are not
 
415
        explicitly listed.
 
416
        """
 
417
        raise NotImplementedError(self.get_graph_with_ghosts)
 
418
 
 
419
    def get_parent_map(self, version_ids):
 
420
        """Get a map of the parents of version_ids.
 
421
 
 
422
        :param version_ids: The version ids to look up parents for.
 
423
        :return: A mapping from version id to parents.
 
424
        """
 
425
        raise NotImplementedError(self.get_parent_map)
 
426
 
 
427
    @deprecated_method(one_four)
 
428
    def get_parents(self, version_id):
 
429
        """Return version names for parents of a version.
 
430
 
 
431
        Must raise RevisionNotPresent if version is not present in
 
432
        file history.
 
433
        """
 
434
        try:
 
435
            all = self.get_parent_map([version_id])[version_id]
 
436
        except KeyError:
 
437
            raise errors.RevisionNotPresent(version_id, self)
 
438
        result = []
 
439
        parent_parents = self.get_parent_map(all)
 
440
        for version_id in all:
 
441
            if version_id in parent_parents:
 
442
                result.append(version_id)
 
443
        return result
 
444
 
 
445
    def get_parents_with_ghosts(self, version_id):
 
446
        """Return version names for parents of version_id.
 
447
 
 
448
        Will raise RevisionNotPresent if version_id is not present
 
449
        in the history.
 
450
 
 
451
        Ghosts that are known about will be included in the parent list,
 
452
        but are not explicitly marked.
 
453
        """
 
454
        try:
 
455
            return list(self.get_parent_map([version_id])[version_id])
 
456
        except KeyError:
 
457
            raise errors.RevisionNotPresent(version_id, self)
 
458
 
 
459
    @deprecated_method(one_four)
 
460
    def annotate_iter(self, version_id):
 
461
        """Yield list of (version-id, line) pairs for the specified
 
462
        version.
 
463
 
 
464
        Must raise RevisionNotPresent if the given version is
 
465
        not present in file history.
 
466
        """
 
467
        return iter(self.annotate(version_id))
 
468
 
 
469
    def annotate(self, version_id):
 
470
        """Return a list of (version-id, line) tuples for version_id.
 
471
 
 
472
        :raise RevisionNotPresent: If the given version is
 
473
        not present in file history.
 
474
        """
 
475
        raise NotImplementedError(self.annotate)
 
476
 
 
477
    def join(self, other, pb=None, msg=None, version_ids=None,
 
478
             ignore_missing=False):
 
479
        """Integrate versions from other into this versioned file.
 
480
 
 
481
        If version_ids is None all versions from other should be
 
482
        incorporated into this versioned file.
 
483
 
 
484
        Must raise RevisionNotPresent if any of the specified versions
 
485
        are not present in the other file's history unless ignore_missing
 
486
        is supplied in which case they are silently skipped.
 
487
        """
 
488
        self._check_write_ok()
 
489
        return InterVersionedFile.get(other, self).join(
 
490
            pb,
 
491
            msg,
 
492
            version_ids,
 
493
            ignore_missing)
 
494
 
 
495
    def iter_lines_added_or_present_in_versions(self, version_ids=None,
 
496
                                                pb=None):
 
497
        """Iterate over the lines in the versioned file from version_ids.
 
498
 
 
499
        This may return lines from other versions. Each item the returned
 
500
        iterator yields is a tuple of a line and a text version that that line
 
501
        is present in (not introduced in).
 
502
 
 
503
        Ordering of results is in whatever order is most suitable for the
 
504
        underlying storage format.
 
505
 
 
506
        If a progress bar is supplied, it may be used to indicate progress.
 
507
        The caller is responsible for cleaning up progress bars (because this
 
508
        is an iterator).
 
509
 
 
510
        NOTES: Lines are normalised: they will all have \n terminators.
 
511
               Lines are returned in arbitrary order.
 
512
 
 
513
        :return: An iterator over (line, version_id).
 
514
        """
 
515
        raise NotImplementedError(self.iter_lines_added_or_present_in_versions)
 
516
 
 
517
    @deprecated_method(one_four)
 
518
    def iter_parents(self, version_ids):
 
519
        """Iterate through the parents for many version ids.
 
520
 
 
521
        :param version_ids: An iterable yielding version_ids.
 
522
        :return: An iterator that yields (version_id, parents). Requested 
 
523
            version_ids not present in the versioned file are simply skipped.
 
524
            The order is undefined, allowing for different optimisations in
 
525
            the underlying implementation.
 
526
        """
 
527
        return self.get_parent_map(version_ids).iteritems()
 
528
 
 
529
    def plan_merge(self, ver_a, ver_b):
 
530
        """Return pseudo-annotation indicating how the two versions merge.
 
531
 
 
532
        This is computed between versions a and b and their common
 
533
        base.
 
534
 
 
535
        Weave lines present in none of them are skipped entirely.
 
536
 
 
537
        Legend:
 
538
        killed-base Dead in base revision
 
539
        killed-both Killed in each revision
 
540
        killed-a    Killed in a
 
541
        killed-b    Killed in b
 
542
        unchanged   Alive in both a and b (possibly created in both)
 
543
        new-a       Created in a
 
544
        new-b       Created in b
 
545
        ghost-a     Killed in a, unborn in b    
 
546
        ghost-b     Killed in b, unborn in a
 
547
        irrelevant  Not in either revision
 
548
        """
 
549
        raise NotImplementedError(VersionedFile.plan_merge)
 
550
        
 
551
    def weave_merge(self, plan, a_marker=TextMerge.A_MARKER,
 
552
                    b_marker=TextMerge.B_MARKER):
 
553
        return PlanWeaveMerge(plan, a_marker, b_marker).merge_lines()[0]
 
554
 
 
555
 
 
556
class _PlanMergeVersionedFile(object):
 
557
    """A VersionedFile for uncommitted and committed texts.
 
558
 
 
559
    It is intended to allow merges to be planned with working tree texts.
 
560
    It implements only the small part of the VersionedFile interface used by
 
561
    PlanMerge.  It falls back to multiple versionedfiles for data not stored in
 
562
    _PlanMergeVersionedFile itself.
 
563
    """
 
564
 
 
565
    def __init__(self, file_id, fallback_versionedfiles=None):
 
566
        """Constuctor
 
567
 
 
568
        :param file_id: Used when raising exceptions.
 
569
        :param fallback_versionedfiles: If supplied, the set of fallbacks to
 
570
            use.  Otherwise, _PlanMergeVersionedFile.fallback_versionedfiles
 
571
            can be appended to later.
 
572
        """
 
573
        self._file_id = file_id
 
574
        if fallback_versionedfiles is None:
 
575
            self.fallback_versionedfiles = []
 
576
        else:
 
577
            self.fallback_versionedfiles = fallback_versionedfiles
 
578
        self._parents = {}
 
579
        self._lines = {}
 
580
 
 
581
    def plan_merge(self, ver_a, ver_b, base=None):
 
582
        """See VersionedFile.plan_merge"""
 
583
        from bzrlib.merge import _PlanMerge
 
584
        if base is None:
 
585
            return _PlanMerge(ver_a, ver_b, self).plan_merge()
 
586
        old_plan = list(_PlanMerge(ver_a, base, self).plan_merge())
 
587
        new_plan = list(_PlanMerge(ver_a, ver_b, self).plan_merge())
 
588
        return _PlanMerge._subtract_plans(old_plan, new_plan)
 
589
 
 
590
    def plan_lca_merge(self, ver_a, ver_b, base=None):
 
591
        from bzrlib.merge import _PlanLCAMerge
 
592
        graph = self._get_graph()
 
593
        new_plan = _PlanLCAMerge(ver_a, ver_b, self, graph).plan_merge()
 
594
        if base is None:
 
595
            return new_plan
 
596
        old_plan = _PlanLCAMerge(ver_a, base, self, graph).plan_merge()
 
597
        return _PlanLCAMerge._subtract_plans(list(old_plan), list(new_plan))
 
598
 
 
599
    def add_lines(self, version_id, parents, lines):
 
600
        """See VersionedFile.add_lines
 
601
 
 
602
        Lines are added locally, not fallback versionedfiles.  Also, ghosts are
 
603
        permitted.  Only reserved ids are permitted.
 
604
        """
 
605
        if not revision.is_reserved_id(version_id):
 
606
            raise ValueError('Only reserved ids may be used')
 
607
        if parents is None:
 
608
            raise ValueError('Parents may not be None')
 
609
        if lines is None:
 
610
            raise ValueError('Lines may not be None')
 
611
        self._parents[version_id] = tuple(parents)
 
612
        self._lines[version_id] = lines
 
613
 
 
614
    def get_lines(self, version_id):
 
615
        """See VersionedFile.get_ancestry"""
 
616
        lines = self._lines.get(version_id)
 
617
        if lines is not None:
 
618
            return lines
 
619
        for versionedfile in self.fallback_versionedfiles:
 
620
            try:
 
621
                return versionedfile.get_lines(version_id)
 
622
            except errors.RevisionNotPresent:
 
623
                continue
 
624
        else:
 
625
            raise errors.RevisionNotPresent(version_id, self._file_id)
 
626
 
 
627
    def get_ancestry(self, version_id, topo_sorted=False):
 
628
        """See VersionedFile.get_ancestry.
 
629
 
 
630
        Note that this implementation assumes that if a VersionedFile can
 
631
        answer get_ancestry at all, it can give an authoritative answer.  In
 
632
        fact, ghosts can invalidate this assumption.  But it's good enough
 
633
        99% of the time, and far cheaper/simpler.
 
634
 
 
635
        Also note that the results of this version are never topologically
 
636
        sorted, and are a set.
 
637
        """
 
638
        if topo_sorted:
 
639
            raise ValueError('This implementation does not provide sorting')
 
640
        parents = self._parents.get(version_id)
 
641
        if parents is None:
 
642
            for vf in self.fallback_versionedfiles:
 
643
                try:
 
644
                    return vf.get_ancestry(version_id, topo_sorted=False)
 
645
                except errors.RevisionNotPresent:
 
646
                    continue
 
647
            else:
 
648
                raise errors.RevisionNotPresent(version_id, self._file_id)
 
649
        ancestry = set([version_id])
 
650
        for parent in parents:
 
651
            ancestry.update(self.get_ancestry(parent, topo_sorted=False))
 
652
        return ancestry
 
653
 
 
654
    def get_parent_map(self, version_ids):
 
655
        """See VersionedFile.get_parent_map"""
 
656
        result = {}
 
657
        pending = set(version_ids)
 
658
        for key in version_ids:
 
659
            try:
 
660
                result[key] = self._parents[key]
 
661
            except KeyError:
 
662
                pass
 
663
        pending = pending - set(result.keys())
 
664
        for versionedfile in self.fallback_versionedfiles:
 
665
            parents = versionedfile.get_parent_map(pending)
 
666
            result.update(parents)
 
667
            pending = pending - set(parents.keys())
 
668
            if not pending:
 
669
                return result
 
670
        return result
 
671
 
 
672
    def _get_graph(self):
 
673
        from bzrlib.graph import (
 
674
            DictParentsProvider,
 
675
            Graph,
 
676
            _StackedParentsProvider,
 
677
            )
 
678
        from bzrlib.repofmt.knitrepo import _KnitParentsProvider
 
679
        parent_providers = [DictParentsProvider(self._parents)]
 
680
        for vf in self.fallback_versionedfiles:
 
681
            parent_providers.append(_KnitParentsProvider(vf))
 
682
        return Graph(_StackedParentsProvider(parent_providers))
 
683
 
 
684
 
 
685
class PlanWeaveMerge(TextMerge):
 
686
    """Weave merge that takes a plan as its input.
 
687
    
 
688
    This exists so that VersionedFile.plan_merge is implementable.
 
689
    Most callers will want to use WeaveMerge instead.
 
690
    """
 
691
 
 
692
    def __init__(self, plan, a_marker=TextMerge.A_MARKER,
 
693
                 b_marker=TextMerge.B_MARKER):
 
694
        TextMerge.__init__(self, a_marker, b_marker)
 
695
        self.plan = plan
 
696
 
 
697
    def _merge_struct(self):
 
698
        lines_a = []
 
699
        lines_b = []
 
700
        ch_a = ch_b = False
 
701
 
 
702
        def outstanding_struct():
 
703
            if not lines_a and not lines_b:
 
704
                return
 
705
            elif ch_a and not ch_b:
 
706
                # one-sided change:
 
707
                yield(lines_a,)
 
708
            elif ch_b and not ch_a:
 
709
                yield (lines_b,)
 
710
            elif lines_a == lines_b:
 
711
                yield(lines_a,)
 
712
            else:
 
713
                yield (lines_a, lines_b)
 
714
       
 
715
        # We previously considered either 'unchanged' or 'killed-both' lines
 
716
        # to be possible places to resynchronize.  However, assuming agreement
 
717
        # on killed-both lines may be too aggressive. -- mbp 20060324
 
718
        for state, line in self.plan:
 
719
            if state == 'unchanged':
 
720
                # resync and flush queued conflicts changes if any
 
721
                for struct in outstanding_struct():
 
722
                    yield struct
 
723
                lines_a = []
 
724
                lines_b = []
 
725
                ch_a = ch_b = False
 
726
                
 
727
            if state == 'unchanged':
 
728
                if line:
 
729
                    yield ([line],)
 
730
            elif state == 'killed-a':
 
731
                ch_a = True
 
732
                lines_b.append(line)
 
733
            elif state == 'killed-b':
 
734
                ch_b = True
 
735
                lines_a.append(line)
 
736
            elif state == 'new-a':
 
737
                ch_a = True
 
738
                lines_a.append(line)
 
739
            elif state == 'new-b':
 
740
                ch_b = True
 
741
                lines_b.append(line)
 
742
            elif state == 'conflicted-a':
 
743
                ch_b = ch_a = True
 
744
                lines_a.append(line)
 
745
            elif state == 'conflicted-b':
 
746
                ch_b = ch_a = True
 
747
                lines_b.append(line)
 
748
            else:
 
749
                assert state in ('irrelevant', 'ghost-a', 'ghost-b', 
 
750
                                 'killed-base', 'killed-both'), state
 
751
        for struct in outstanding_struct():
 
752
            yield struct
 
753
 
 
754
 
 
755
class WeaveMerge(PlanWeaveMerge):
 
756
    """Weave merge that takes a VersionedFile and two versions as its input."""
 
757
 
 
758
    def __init__(self, versionedfile, ver_a, ver_b, 
 
759
        a_marker=PlanWeaveMerge.A_MARKER, b_marker=PlanWeaveMerge.B_MARKER):
 
760
        plan = versionedfile.plan_merge(ver_a, ver_b)
 
761
        PlanWeaveMerge.__init__(self, plan, a_marker, b_marker)
 
762
 
 
763
 
 
764
class InterVersionedFile(InterObject):
 
765
    """This class represents operations taking place between two VersionedFiles.
 
766
 
 
767
    Its instances have methods like join, and contain
 
768
    references to the source and target versionedfiles these operations can be 
 
769
    carried out on.
 
770
 
 
771
    Often we will provide convenience methods on 'versionedfile' which carry out
 
772
    operations with another versionedfile - they will always forward to
 
773
    InterVersionedFile.get(other).method_name(parameters).
 
774
    """
 
775
 
 
776
    _optimisers = []
 
777
    """The available optimised InterVersionedFile types."""
 
778
 
 
779
    def join(self, pb=None, msg=None, version_ids=None, ignore_missing=False):
 
780
        """Integrate versions from self.source into self.target.
 
781
 
 
782
        If version_ids is None all versions from source should be
 
783
        incorporated into this versioned file.
 
784
 
 
785
        Must raise RevisionNotPresent if any of the specified versions
 
786
        are not present in the other file's history unless ignore_missing is 
 
787
        supplied in which case they are silently skipped.
 
788
        """
 
789
        target = self.target
 
790
        version_ids = self._get_source_version_ids(version_ids, ignore_missing)
 
791
        graph = Graph(self.source)
 
792
        search = graph._make_breadth_first_searcher(version_ids)
 
793
        transitive_ids = set()
 
794
        map(transitive_ids.update, list(search))
 
795
        parent_map = self.source.get_parent_map(transitive_ids)
 
796
        order = tsort.topo_sort(parent_map.items())
 
797
        pb = ui.ui_factory.nested_progress_bar()
 
798
        parent_texts = {}
 
799
        try:
 
800
            # TODO for incremental cross-format work:
 
801
            # make a versioned file with the following content:
 
802
            # all revisions we have been asked to join
 
803
            # all their ancestors that are *not* in target already.
 
804
            # the immediate parents of the above two sets, with 
 
805
            # empty parent lists - these versions are in target already
 
806
            # and the incorrect version data will be ignored.
 
807
            # TODO: for all ancestors that are present in target already,
 
808
            # check them for consistent data, this requires moving sha1 from
 
809
            # 
 
810
            # TODO: remove parent texts when they are not relevant any more for 
 
811
            # memory pressure reduction. RBC 20060313
 
812
            # pb.update('Converting versioned data', 0, len(order))
 
813
            total = len(order)
 
814
            for index, version in enumerate(order):
 
815
                pb.update('Converting versioned data', index, total)
 
816
                if version in target:
 
817
                    continue
 
818
                _, _, parent_text = target.add_lines(version,
 
819
                                               parent_map[version],
 
820
                                               self.source.get_lines(version),
 
821
                                               parent_texts=parent_texts)
 
822
                parent_texts[version] = parent_text
 
823
            return total
 
824
        finally:
 
825
            pb.finished()
 
826
 
 
827
    def _get_source_version_ids(self, version_ids, ignore_missing):
 
828
        """Determine the version ids to be used from self.source.
 
829
 
 
830
        :param version_ids: The caller-supplied version ids to check. (None 
 
831
                            for all). If None is in version_ids, it is stripped.
 
832
        :param ignore_missing: if True, remove missing ids from the version 
 
833
                               list. If False, raise RevisionNotPresent on
 
834
                               a missing version id.
 
835
        :return: A set of version ids.
 
836
        """
 
837
        if version_ids is None:
 
838
            # None cannot be in source.versions
 
839
            return set(self.source.versions())
 
840
        else:
 
841
            if ignore_missing:
 
842
                return set(self.source.versions()).intersection(set(version_ids))
 
843
            else:
 
844
                new_version_ids = set()
 
845
                for version in version_ids:
 
846
                    if version is None:
 
847
                        continue
 
848
                    if not self.source.has_version(version):
 
849
                        raise errors.RevisionNotPresent(version, str(self.source))
 
850
                    else:
 
851
                        new_version_ids.add(version)
 
852
                return new_version_ids