~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/versionedfile.py

  • Committer: Martin Pool
  • Date: 2006-03-10 06:29:53 UTC
  • mfrom: (1608 +trunk)
  • mto: This revision was merged to the branch mainline in revision 1611.
  • Revision ID: mbp@sourcefrog.net-20060310062953-bc1c7ade75c89a7a
[merge] bzr.dev; pycurl not updated for readv yet

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 by 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
# Remaing to do is to figure out if get_graph should return a simple
 
21
# map, or a graph object of some kind.
 
22
 
 
23
 
 
24
"""Versioned text file storage api."""
 
25
 
 
26
 
 
27
from copy import deepcopy
 
28
from unittest import TestSuite
 
29
 
 
30
 
 
31
import bzrlib.errors as errors
 
32
from bzrlib.inter import InterObject
 
33
from bzrlib.symbol_versioning import *
 
34
from bzrlib.transport.memory import MemoryTransport
 
35
from bzrlib.tsort import topo_sort
 
36
from bzrlib import ui
 
37
 
 
38
 
 
39
class VersionedFile(object):
 
40
    """Versioned text file storage.
 
41
    
 
42
    A versioned file manages versions of line-based text files,
 
43
    keeping track of the originating version for each line.
 
44
 
 
45
    To clients the "lines" of the file are represented as a list of
 
46
    strings. These strings will typically have terminal newline
 
47
    characters, but this is not required.  In particular files commonly
 
48
    do not have a newline at the end of the file.
 
49
 
 
50
    Texts are identified by a version-id string.
 
51
    """
 
52
 
 
53
    def __init__(self, access_mode):
 
54
        self.finished = False
 
55
        self._access_mode = access_mode
 
56
 
 
57
    def copy_to(self, name, transport):
 
58
        """Copy this versioned file to name on transport."""
 
59
        raise NotImplementedError(self.copy_to)
 
60
    
 
61
    @deprecated_method(zero_eight)
 
62
    def names(self):
 
63
        """Return a list of all the versions in this versioned file.
 
64
 
 
65
        Please use versionedfile.versions() now.
 
66
        """
 
67
        return self.versions()
 
68
 
 
69
    def versions(self):
 
70
        """Return a unsorted list of versions."""
 
71
        raise NotImplementedError(self.versions)
 
72
 
 
73
    def has_ghost(self, version_id):
 
74
        """Returns whether version is present as a ghost."""
 
75
        raise NotImplementedError(self.has_ghost)
 
76
 
 
77
    def has_version(self, version_id):
 
78
        """Returns whether version is present."""
 
79
        raise NotImplementedError(self.has_version)
 
80
 
 
81
    def add_lines(self, version_id, parents, lines):
 
82
        """Add a single text on top of the versioned file.
 
83
 
 
84
        Must raise RevisionAlreadyPresent if the new version is
 
85
        already present in file history.
 
86
 
 
87
        Must raise RevisionNotPresent if any of the given parents are
 
88
        not present in file history.
 
89
        """
 
90
        self._check_write_ok()
 
91
        return self._add_lines(version_id, parents, lines)
 
92
 
 
93
    def _add_lines(self, version_id, parents, lines):
 
94
        """Helper to do the class specific add_lines."""
 
95
        raise NotImplementedError(self.add_lines)
 
96
 
 
97
    def add_lines_with_ghosts(self, version_id, parents, lines):
 
98
        """Add lines to the versioned file, allowing ghosts to be present."""
 
99
        self._check_write_ok()
 
100
        return self._add_lines_with_ghosts(version_id, parents, lines)
 
101
 
 
102
    def _add_lines_with_ghosts(self, version_id, parents, lines):
 
103
        """Helper to do class specific add_lines_with_ghosts."""
 
104
        raise NotImplementedError(self.add_lines_with_ghosts)
 
105
 
 
106
    def check(self, progress_bar=None):
 
107
        """Check the versioned file for integrity."""
 
108
        raise NotImplementedError(self.check)
 
109
 
 
110
    def _check_write_ok(self):
 
111
        """Is the versioned file marked as 'finished' ? Raise if it is."""
 
112
        if self.finished:
 
113
            raise errors.OutSideTransaction()
 
114
        if self._access_mode != 'w':
 
115
            raise errors.ReadOnlyObjectDirtiedError(self)
 
116
 
 
117
    def clear_cache(self):
 
118
        """Remove any data cached in the versioned file object."""
 
119
 
 
120
    def clone_text(self, new_version_id, old_version_id, parents):
 
121
        """Add an identical text to old_version_id as new_version_id.
 
122
 
 
123
        Must raise RevisionNotPresent if the old version or any of the
 
124
        parents are not present in file history.
 
125
 
 
126
        Must raise RevisionAlreadyPresent if the new version is
 
127
        already present in file history."""
 
128
        self._check_write_ok()
 
129
        return self._clone_text(new_version_id, old_version_id, parents)
 
130
 
 
131
    def _clone_text(self, new_version_id, old_version_id, parents):
 
132
        """Helper function to do the _clone_text work."""
 
133
        raise NotImplementedError(self.clone_text)
 
134
 
 
135
    def create_empty(self, name, transport, mode=None):
 
136
        """Create a new versioned file of this exact type.
 
137
 
 
138
        :param name: the file name
 
139
        :param transport: the transport
 
140
        :param mode: optional file mode.
 
141
        """
 
142
        raise NotImplementedError(self.create_empty)
 
143
 
 
144
    def fix_parents(self, version, new_parents):
 
145
        """Fix the parents list for version.
 
146
        
 
147
        This is done by appending a new version to the index
 
148
        with identical data except for the parents list.
 
149
        the parents list must be a superset of the current
 
150
        list.
 
151
        """
 
152
        self._check_write_ok()
 
153
        return self._fix_parents(version, new_parents)
 
154
 
 
155
    def _fix_parents(self, version, new_parents):
 
156
        """Helper for fix_parents."""
 
157
        raise NotImplementedError(self.fix_parents)
 
158
 
 
159
    def get_suffixes(self):
 
160
        """Return the file suffixes associated with this versioned file."""
 
161
        raise NotImplementedError(self.get_suffixes)
 
162
    
 
163
    def get_text(self, version_id):
 
164
        """Return version contents as a text string.
 
165
 
 
166
        Raises RevisionNotPresent if version is not present in
 
167
        file history.
 
168
        """
 
169
        return ''.join(self.get_lines(version_id))
 
170
    get_string = get_text
 
171
 
 
172
    def get_lines(self, version_id):
 
173
        """Return version contents as a sequence of lines.
 
174
 
 
175
        Raises RevisionNotPresent if version is not present in
 
176
        file history.
 
177
        """
 
178
        raise NotImplementedError(self.get_lines)
 
179
 
 
180
    def get_ancestry(self, version_ids):
 
181
        """Return a list of all ancestors of given version(s). This
 
182
        will not include the null revision.
 
183
 
 
184
        Must raise RevisionNotPresent if any of the given versions are
 
185
        not present in file history."""
 
186
        if isinstance(version_ids, basestring):
 
187
            version_ids = [version_ids]
 
188
        raise NotImplementedError(self.get_ancestry)
 
189
        
 
190
    def get_ancestry_with_ghosts(self, version_ids):
 
191
        """Return a list of all ancestors of given version(s). This
 
192
        will not include the null revision.
 
193
 
 
194
        Must raise RevisionNotPresent if any of the given versions are
 
195
        not present in file history.
 
196
        
 
197
        Ghosts that are known about will be included in ancestry list,
 
198
        but are not explicitly marked.
 
199
        """
 
200
        raise NotImplementedError(self.get_ancestry_with_ghosts)
 
201
        
 
202
    def get_graph(self):
 
203
        """Return a graph for the entire versioned file.
 
204
        
 
205
        Ghosts are not listed or referenced in the graph.
 
206
        """
 
207
        result = {}
 
208
        for version in self.versions():
 
209
            result[version] = self.get_parents(version)
 
210
        return result
 
211
 
 
212
    def get_graph_with_ghosts(self):
 
213
        """Return a graph for the entire versioned file.
 
214
        
 
215
        Ghosts are referenced in parents list but are not
 
216
        explicitly listed.
 
217
        """
 
218
        raise NotImplementedError(self.get_graph_with_ghosts)
 
219
 
 
220
    @deprecated_method(zero_eight)
 
221
    def parent_names(self, version):
 
222
        """Return version names for parents of a version.
 
223
        
 
224
        See get_parents for the current api.
 
225
        """
 
226
        return self.get_parents(version)
 
227
 
 
228
    def get_parents(self, version_id):
 
229
        """Return version names for parents of a version.
 
230
 
 
231
        Must raise RevisionNotPresent if version is not present in
 
232
        file history.
 
233
        """
 
234
        raise NotImplementedError(self.get_parents)
 
235
 
 
236
    def get_parents_with_ghosts(self, version_id):
 
237
        """Return version names for parents of version_id.
 
238
 
 
239
        Will raise RevisionNotPresent if version_id is not present
 
240
        in the history.
 
241
 
 
242
        Ghosts that are known about will be included in the parent list,
 
243
        but are not explicitly marked.
 
244
        """
 
245
        raise NotImplementedError(self.get_parents_with_ghosts)
 
246
 
 
247
    def annotate_iter(self, version_id):
 
248
        """Yield list of (version-id, line) pairs for the specified
 
249
        version.
 
250
 
 
251
        Must raise RevisionNotPresent if any of the given versions are
 
252
        not present in file history.
 
253
        """
 
254
        raise NotImplementedError(self.annotate_iter)
 
255
 
 
256
    def annotate(self, version_id):
 
257
        return list(self.annotate_iter(version_id))
 
258
 
 
259
    def join(self, other, pb=None, msg=None, version_ids=None,
 
260
             ignore_missing=False):
 
261
        """Integrate versions from other into this versioned file.
 
262
 
 
263
        If version_ids is None all versions from other should be
 
264
        incorporated into this versioned file.
 
265
 
 
266
        Must raise RevisionNotPresent if any of the specified versions
 
267
        are not present in the other files history unless ignore_missing
 
268
        is supplied when they are silently skipped.
 
269
        """
 
270
        self._check_write_ok()
 
271
        return InterVersionedFile.get(other, self).join(
 
272
            pb,
 
273
            msg,
 
274
            version_ids,
 
275
            ignore_missing)
 
276
 
 
277
    def iter_lines_added_or_present_in_versions(self, version_ids=None):
 
278
        """Iterate over the lines in the versioned file from version_ids.
 
279
 
 
280
        This may return lines from other versions, and does not return the
 
281
        specific version marker at this point. The api may be changed
 
282
        during development to include the version that the versioned file
 
283
        thinks is relevant, but given that such hints are just guesses,
 
284
        its better not to have it if we dont need it.
 
285
 
 
286
        NOTES: Lines are normalised: they will all have \n terminators.
 
287
               Lines are returned in arbitrary order.
 
288
        """
 
289
        raise NotImplementedError(self.iter_lines_added_or_present_in_versions)
 
290
 
 
291
    def transaction_finished(self):
 
292
        """The transaction that this file was opened in has finished.
 
293
 
 
294
        This records self.finished = True and should cause all mutating
 
295
        operations to error.
 
296
        """
 
297
        self.finished = True
 
298
 
 
299
    @deprecated_method(zero_eight)
 
300
    def walk(self, version_ids=None):
 
301
        """Walk the versioned file as a weave-like structure, for
 
302
        versions relative to version_ids.  Yields sequence of (lineno,
 
303
        insert, deletes, text) for each relevant line.
 
304
 
 
305
        Must raise RevisionNotPresent if any of the specified versions
 
306
        are not present in the file history.
 
307
 
 
308
        :param version_ids: the version_ids to walk with respect to. If not
 
309
                            supplied the entire weave-like structure is walked.
 
310
 
 
311
        walk is deprecated in favour of iter_lines_added_or_present_in_versions
 
312
        """
 
313
        raise NotImplementedError(self.walk)
 
314
 
 
315
    @deprecated_method(zero_eight)
 
316
    def iter_names(self):
 
317
        """Walk the names list."""
 
318
        return iter(self.versions())
 
319
 
 
320
    def plan_merge(self, ver_a, ver_b):
 
321
        """Return pseudo-annotation indicating how the two versions merge.
 
322
 
 
323
        This is computed between versions a and b and their common
 
324
        base.
 
325
 
 
326
        Weave lines present in none of them are skipped entirely.
 
327
        """
 
328
        inc_a = set(self.get_ancestry([ver_a]))
 
329
        inc_b = set(self.get_ancestry([ver_b]))
 
330
        inc_c = inc_a & inc_b
 
331
 
 
332
        for lineno, insert, deleteset, line in self.walk([ver_a, ver_b]):
 
333
            if deleteset & inc_c:
 
334
                # killed in parent; can't be in either a or b
 
335
                # not relevant to our work
 
336
                yield 'killed-base', line
 
337
            elif insert in inc_c:
 
338
                # was inserted in base
 
339
                killed_a = bool(deleteset & inc_a)
 
340
                killed_b = bool(deleteset & inc_b)
 
341
                if killed_a and killed_b:
 
342
                    yield 'killed-both', line
 
343
                elif killed_a:
 
344
                    yield 'killed-a', line
 
345
                elif killed_b:
 
346
                    yield 'killed-b', line
 
347
                else:
 
348
                    yield 'unchanged', line
 
349
            elif insert in inc_a:
 
350
                if deleteset & inc_a:
 
351
                    yield 'ghost-a', line
 
352
                else:
 
353
                    # new in A; not in B
 
354
                    yield 'new-a', line
 
355
            elif insert in inc_b:
 
356
                if deleteset & inc_b:
 
357
                    yield 'ghost-b', line
 
358
                else:
 
359
                    yield 'new-b', line
 
360
            else:
 
361
                # not in either revision
 
362
                yield 'irrelevant', line
 
363
 
 
364
        yield 'unchanged', ''           # terminator
 
365
 
 
366
    def weave_merge(self, plan, a_marker='<<<<<<< \n', b_marker='>>>>>>> \n'):
 
367
        lines_a = []
 
368
        lines_b = []
 
369
        ch_a = ch_b = False
 
370
        # TODO: Return a structured form of the conflicts (e.g. 2-tuples for
 
371
        # conflicted regions), rather than just inserting the markers.
 
372
        # 
 
373
        # TODO: Show some version information (e.g. author, date) on 
 
374
        # conflicted regions.
 
375
        for state, line in plan:
 
376
            if state == 'unchanged' or state == 'killed-both':
 
377
                # resync and flush queued conflicts changes if any
 
378
                if not lines_a and not lines_b:
 
379
                    pass
 
380
                elif ch_a and not ch_b:
 
381
                    # one-sided change:                    
 
382
                    for l in lines_a: yield l
 
383
                elif ch_b and not ch_a:
 
384
                    for l in lines_b: yield l
 
385
                elif lines_a == lines_b:
 
386
                    for l in lines_a: yield l
 
387
                else:
 
388
                    yield a_marker
 
389
                    for l in lines_a: yield l
 
390
                    yield '=======\n'
 
391
                    for l in lines_b: yield l
 
392
                    yield b_marker
 
393
 
 
394
                del lines_a[:]
 
395
                del lines_b[:]
 
396
                ch_a = ch_b = False
 
397
                
 
398
            if state == 'unchanged':
 
399
                if line:
 
400
                    yield line
 
401
            elif state == 'killed-a':
 
402
                ch_a = True
 
403
                lines_b.append(line)
 
404
            elif state == 'killed-b':
 
405
                ch_b = True
 
406
                lines_a.append(line)
 
407
            elif state == 'new-a':
 
408
                ch_a = True
 
409
                lines_a.append(line)
 
410
            elif state == 'new-b':
 
411
                ch_b = True
 
412
                lines_b.append(line)
 
413
            else:
 
414
                assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
 
415
                                 'killed-both'), \
 
416
                       state
 
417
 
 
418
 
 
419
class InterVersionedFile(InterObject):
 
420
    """This class represents operations taking place between two versionedfiles..
 
421
 
 
422
    Its instances have methods like join, and contain
 
423
    references to the source and target versionedfiles these operations can be 
 
424
    carried out on.
 
425
 
 
426
    Often we will provide convenience methods on 'versionedfile' which carry out
 
427
    operations with another versionedfile - they will always forward to
 
428
    InterVersionedFile.get(other).method_name(parameters).
 
429
    """
 
430
 
 
431
    _optimisers = set()
 
432
    """The available optimised InterVersionedFile types."""
 
433
 
 
434
    def join(self, pb=None, msg=None, version_ids=None, ignore_missing=False):
 
435
        """Integrate versions from self.source into self.target.
 
436
 
 
437
        If version_ids is None all versions from source should be
 
438
        incorporated into this versioned file.
 
439
 
 
440
        Must raise RevisionNotPresent if any of the specified versions
 
441
        are not present in the other files history unless ignore_missing is 
 
442
        supplied when they are silently skipped.
 
443
        """
 
444
        # the default join: 
 
445
        # - if the target is empty, just add all the versions from 
 
446
        #   source to target, otherwise:
 
447
        # - make a temporary versioned file of type target
 
448
        # - insert the source content into it one at a time
 
449
        # - join them
 
450
        if not self.target.versions():
 
451
            target = self.target
 
452
        else:
 
453
            # Make a new target-format versioned file. 
 
454
            temp_source = self.target.create_empty("temp", MemoryTransport())
 
455
            target = temp_source
 
456
        graph = self.source.get_graph()
 
457
        order = topo_sort(graph.items())
 
458
        pb = ui.ui_factory.nested_progress_bar()
 
459
        try:
 
460
            for index, version in enumerate(order):
 
461
                pb.update('Converting versioned data', index, len(order))
 
462
                target.add_lines(version,
 
463
                                 self.source.get_parents(version),
 
464
                                 self.source.get_lines(version))
 
465
            
 
466
            # this should hit the native code path for target
 
467
            if target is not self.target:
 
468
                return self.target.join(temp_source,
 
469
                                        pb,
 
470
                                        msg,
 
471
                                        version_ids,
 
472
                                        ignore_missing)
 
473
        finally:
 
474
            pb.finished()
 
475
 
 
476
 
 
477
class InterVersionedFileTestProviderAdapter(object):
 
478
    """A tool to generate a suite testing multiple inter versioned-file classes.
 
479
 
 
480
    This is done by copying the test once for each interversionedfile provider
 
481
    and injecting the transport_server, transport_readonly_server,
 
482
    versionedfile_factory and versionedfile_factory_to classes into each copy.
 
483
    Each copy is also given a new id() to make it easy to identify.
 
484
    """
 
485
 
 
486
    def __init__(self, transport_server, transport_readonly_server, formats):
 
487
        self._transport_server = transport_server
 
488
        self._transport_readonly_server = transport_readonly_server
 
489
        self._formats = formats
 
490
    
 
491
    def adapt(self, test):
 
492
        result = TestSuite()
 
493
        for (interversionedfile_class,
 
494
             versionedfile_factory,
 
495
             versionedfile_factory_to) in self._formats:
 
496
            new_test = deepcopy(test)
 
497
            new_test.transport_server = self._transport_server
 
498
            new_test.transport_readonly_server = self._transport_readonly_server
 
499
            new_test.interversionedfile_class = interversionedfile_class
 
500
            new_test.versionedfile_factory = versionedfile_factory
 
501
            new_test.versionedfile_factory_to = versionedfile_factory_to
 
502
            def make_new_test_id():
 
503
                new_id = "%s(%s)" % (new_test.id(), interversionedfile_class.__name__)
 
504
                return lambda: new_id
 
505
            new_test.id = make_new_test_id()
 
506
            result.addTest(new_test)
 
507
        return result
 
508
 
 
509
    @staticmethod
 
510
    def default_test_list():
 
511
        """Generate the default list of interversionedfile permutations to test."""
 
512
        from bzrlib.weave import WeaveFile
 
513
        from bzrlib.knit import KnitVersionedFile
 
514
        result = []
 
515
        # test the fallback InterVersionedFile from weave to annotated knits
 
516
        result.append((InterVersionedFile, 
 
517
                       WeaveFile,
 
518
                       KnitVersionedFile))
 
519
        for optimiser in InterVersionedFile._optimisers:
 
520
            result.append((optimiser,
 
521
                           optimiser._matching_file_factory,
 
522
                           optimiser._matching_file_factory
 
523
                           ))
 
524
        # if there are specific combinations we want to use, we can add them 
 
525
        # here.
 
526
        return result