/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

  • Committer: Robert Collins
  • Date: 2006-03-12 17:09:11 UTC
  • mto: (1615.1.2 bzr.mbp.integration)
  • mto: This revision was merged to the branch mainline in revision 1616.
  • Revision ID: robertc@robertcollins.net-20060312170911-306a47e0478ec183
Subclass SequenceMatcher to get a slightly faster (in our case) find_longest_match routine.

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