/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

More work on roundtrip push support.

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