~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/versionedfile.py

  • Committer: abentley
  • Date: 2006-04-20 23:47:53 UTC
  • mfrom: (1681 +trunk)
  • mto: This revision was merged to the branch mainline in revision 1683.
  • Revision ID: abentley@lappy-20060420234753-6a6874b76f09f86d
Merge bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
31
31
import bzrlib.errors as errors
32
32
from bzrlib.inter import InterObject
33
33
from bzrlib.symbol_versioning import *
 
34
from bzrlib.textmerge import TextMerge
34
35
from bzrlib.transport.memory import MemoryTransport
35
36
from bzrlib.tsort import topo_sort
36
37
from bzrlib import ui
78
79
        """Returns whether version is present."""
79
80
        raise NotImplementedError(self.has_version)
80
81
 
81
 
    def add_lines(self, version_id, parents, lines):
 
82
    def add_delta(self, version_id, parents, delta_parent, sha1, noeol, delta):
 
83
        """Add a text to the versioned file via a pregenerated delta.
 
84
 
 
85
        :param version_id: The version id being added.
 
86
        :param parents: The parents of the version_id.
 
87
        :param delta_parent: The parent this delta was created against.
 
88
        :param sha1: The sha1 of the full text.
 
89
        :param delta: The delta instructions. See get_delta for details.
 
90
        """
 
91
        self._check_write_ok()
 
92
        if self.has_version(version_id):
 
93
            raise errors.RevisionAlreadyPresent(version_id, self)
 
94
        return self._add_delta(version_id, parents, delta_parent, sha1, noeol, delta)
 
95
 
 
96
    def _add_delta(self, version_id, parents, delta_parent, sha1, noeol, delta):
 
97
        """Class specific routine to add a delta.
 
98
 
 
99
        This generic version simply applies the delta to the delta_parent and
 
100
        then inserts it.
 
101
        """
 
102
        # strip annotation from delta
 
103
        new_delta = []
 
104
        for start, stop, delta_len, delta_lines in delta:
 
105
            new_delta.append((start, stop, delta_len, [text for origin, text in delta_lines]))
 
106
        if delta_parent is not None:
 
107
            parent_full = self.get_lines(delta_parent)
 
108
        else:
 
109
            parent_full = []
 
110
        new_full = self._apply_delta(parent_full, new_delta)
 
111
        # its impossible to have noeol on an empty file
 
112
        if noeol and new_full[-1][-1] == '\n':
 
113
            new_full[-1] = new_full[-1][:-1]
 
114
        self.add_lines(version_id, parents, new_full)
 
115
 
 
116
    def add_lines(self, version_id, parents, lines, parent_texts=None):
82
117
        """Add a single text on top of the versioned file.
83
118
 
84
119
        Must raise RevisionAlreadyPresent if the new version is
86
121
 
87
122
        Must raise RevisionNotPresent if any of the given parents are
88
123
        not present in file history.
 
124
        :param parent_texts: An optional dictionary containing the opaque 
 
125
             representations of some or all of the parents of 
 
126
             version_id to allow delta optimisations. 
 
127
             VERY IMPORTANT: the texts must be those returned
 
128
             by add_lines or data corruption can be caused.
 
129
        :return: An opaque representation of the inserted version which can be
 
130
                 provided back to future add_lines calls in the parent_texts
 
131
                 dictionary.
89
132
        """
90
133
        self._check_write_ok()
91
 
        return self._add_lines(version_id, parents, lines)
 
134
        return self._add_lines(version_id, parents, lines, parent_texts)
92
135
 
93
 
    def _add_lines(self, version_id, parents, lines):
 
136
    def _add_lines(self, version_id, parents, lines, parent_texts):
94
137
        """Helper to do the class specific add_lines."""
95
138
        raise NotImplementedError(self.add_lines)
96
139
 
97
 
    def add_lines_with_ghosts(self, version_id, parents, lines):
98
 
        """Add lines to the versioned file, allowing ghosts to be present."""
 
140
    def add_lines_with_ghosts(self, version_id, parents, lines,
 
141
                              parent_texts=None):
 
142
        """Add lines to the versioned file, allowing ghosts to be present.
 
143
        
 
144
        This takes the same parameters as add_lines.
 
145
        """
99
146
        self._check_write_ok()
100
 
        return self._add_lines_with_ghosts(version_id, parents, lines)
 
147
        return self._add_lines_with_ghosts(version_id, parents, lines,
 
148
                                           parent_texts)
101
149
 
102
 
    def _add_lines_with_ghosts(self, version_id, parents, lines):
 
150
    def _add_lines_with_ghosts(self, version_id, parents, lines, parent_texts):
103
151
        """Helper to do class specific add_lines_with_ghosts."""
104
152
        raise NotImplementedError(self.add_lines_with_ghosts)
105
153
 
107
155
        """Check the versioned file for integrity."""
108
156
        raise NotImplementedError(self.check)
109
157
 
 
158
    def _check_lines_not_unicode(self, lines):
 
159
        """Check that lines being added to a versioned file are not unicode."""
 
160
        for line in lines:
 
161
            if line.__class__ is not str:
 
162
                raise errors.BzrBadParameterUnicode("lines")
 
163
 
 
164
    def _check_lines_are_lines(self, lines):
 
165
        """Check that the lines really are full lines without inline EOL."""
 
166
        for line in lines:
 
167
            if '\n' in line[:-1]:
 
168
                raise errors.BzrBadParameterContainsNewline("lines")
 
169
 
110
170
    def _check_write_ok(self):
111
171
        """Is the versioned file marked as 'finished' ? Raise if it is."""
112
172
        if self.finished:
156
216
        """Helper for fix_parents."""
157
217
        raise NotImplementedError(self.fix_parents)
158
218
 
 
219
    def get_delta(self, version):
 
220
        """Get a delta for constructing version from some other version.
 
221
        
 
222
        :return: (delta_parent, sha1, noeol, delta)
 
223
        Where delta_parent is a version id or None to indicate no parent.
 
224
        """
 
225
        raise NotImplementedError(self.get_delta)
 
226
 
 
227
    def get_deltas(self, versions):
 
228
        """Get multiple deltas at once for constructing versions.
 
229
        
 
230
        :return: dict(version_id:(delta_parent, sha1, noeol, delta))
 
231
        Where delta_parent is a version id or None to indicate no parent, and
 
232
        version_id is the version_id created by that delta.
 
233
        """
 
234
        result = {}
 
235
        for version in versions:
 
236
            result[version] = self.get_delta(version)
 
237
        return result
 
238
 
 
239
    def get_sha1(self, version_id):
 
240
        """Get the stored sha1 sum for the given revision.
 
241
        
 
242
        :param name: The name of the version to lookup
 
243
        """
 
244
        raise NotImplementedError(self.get_sha1)
 
245
 
159
246
    def get_suffixes(self):
160
247
        """Return the file suffixes associated with this versioned file."""
161
248
        raise NotImplementedError(self.get_suffixes)
256
343
    def annotate(self, version_id):
257
344
        return list(self.annotate_iter(version_id))
258
345
 
 
346
    def _apply_delta(self, lines, delta):
 
347
        """Apply delta to lines."""
 
348
        lines = list(lines)
 
349
        offset = 0
 
350
        for start, end, count, delta_lines in delta:
 
351
            lines[offset+start:offset+end] = delta_lines
 
352
            offset = offset + (start - end) + count
 
353
        return lines
 
354
 
259
355
    def join(self, other, pb=None, msg=None, version_ids=None,
260
356
             ignore_missing=False):
261
357
        """Integrate versions from other into this versioned file.
324
420
        base.
325
421
 
326
422
        Weave lines present in none of them are skipped entirely.
 
423
 
 
424
        Legend:
 
425
        killed-base Dead in base revision
 
426
        killed-both Killed in each revision
 
427
        killed-a    Killed in a
 
428
        killed-b    Killed in b
 
429
        unchanged   Alive in both a and b (possibly created in both)
 
430
        new-a       Created in a
 
431
        new-b       Created in b
 
432
        ghost-a     Killed in a, unborn in b    
 
433
        ghost-b     Killed in b, unborn in a
 
434
        irrelevant  Not in either revision
327
435
        """
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'):
 
436
        raise NotImplementedError(VersionedFile.plan_merge)
 
437
        
 
438
    def weave_merge(self, plan, a_marker=TextMerge.A_MARKER, 
 
439
                    b_marker=TextMerge.B_MARKER):
 
440
        return PlanWeaveMerge(plan, a_marker, b_marker).merge_lines()[0]
 
441
 
 
442
 
 
443
class PlanWeaveMerge(TextMerge):
 
444
    """Weave merge that takes a plan as its input.
 
445
    
 
446
    This exists so that VersionedFile.plan_merge is implementable.
 
447
    Most callers will want to use WeaveMerge instead.
 
448
    """
 
449
 
 
450
    def __init__(self, plan, a_marker=TextMerge.A_MARKER,
 
451
                 b_marker=TextMerge.B_MARKER):
 
452
        TextMerge.__init__(self, a_marker, b_marker)
 
453
        self.plan = plan
 
454
 
 
455
    def _merge_struct(self):
367
456
        lines_a = []
368
457
        lines_b = []
369
458
        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':
 
459
 
 
460
        def outstanding_struct():
 
461
            if not lines_a and not lines_b:
 
462
                return
 
463
            elif ch_a and not ch_b:
 
464
                # one-sided change:
 
465
                yield(lines_a,)
 
466
            elif ch_b and not ch_a:
 
467
                yield (lines_b,)
 
468
            elif lines_a == lines_b:
 
469
                yield(lines_a,)
 
470
            else:
 
471
                yield (lines_a, lines_b)
 
472
       
 
473
        # We previously considered either 'unchanged' or 'killed-both' lines
 
474
        # to be possible places to resynchronize.  However, assuming agreement
 
475
        # on killed-both lines may be too agressive. -- mbp 20060324
 
476
        for state, line in self.plan:
 
477
            if state == 'unchanged':
377
478
                # 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[:]
 
479
                for struct in outstanding_struct():
 
480
                    yield struct
 
481
                lines_a = []
 
482
                lines_b = []
396
483
                ch_a = ch_b = False
397
484
                
398
485
            if state == 'unchanged':
399
486
                if line:
400
 
                    yield line
 
487
                    yield ([line],)
401
488
            elif state == 'killed-a':
402
489
                ch_a = True
403
490
                lines_b.append(line)
411
498
                ch_b = True
412
499
                lines_b.append(line)
413
500
            else:
414
 
                assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
415
 
                                 'killed-both'), \
416
 
                       state
 
501
                assert state in ('irrelevant', 'ghost-a', 'ghost-b', 
 
502
                                 'killed-base', 'killed-both'), state
 
503
        for struct in outstanding_struct():
 
504
            yield struct
 
505
 
 
506
 
 
507
class WeaveMerge(PlanWeaveMerge):
 
508
    """Weave merge that takes a VersionedFile and two versions as its input"""
 
509
 
 
510
    def __init__(self, versionedfile, ver_a, ver_b, 
 
511
        a_marker=PlanWeaveMerge.A_MARKER, b_marker=PlanWeaveMerge.B_MARKER):
 
512
        plan = versionedfile.plan_merge(ver_a, ver_b)
 
513
        PlanWeaveMerge.__init__(self, plan, a_marker, b_marker)
417
514
 
418
515
 
419
516
class InterVersionedFile(InterObject):
456
553
        graph = self.source.get_graph()
457
554
        order = topo_sort(graph.items())
458
555
        pb = ui.ui_factory.nested_progress_bar()
 
556
        parent_texts = {}
459
557
        try:
 
558
            # TODO for incremental cross-format work:
 
559
            # make a versioned file with the following content:
 
560
            # all revisions we have been asked to join
 
561
            # all their ancestors that are *not* in target already.
 
562
            # the immediate parents of the above two sets, with 
 
563
            # empty parent lists - these versions are in target already
 
564
            # and the incorrect version data will be ignored.
 
565
            # TODO: for all ancestors that are present in target already,
 
566
            # check them for consistent data, this requires moving sha1 from
 
567
            # 
 
568
            # TODO: remove parent texts when they are not relevant any more for 
 
569
            # memory pressure reduction. RBC 20060313
 
570
            # pb.update('Converting versioned data', 0, len(order))
 
571
            # deltas = self.source.get_deltas(order)
460
572
            for index, version in enumerate(order):
461
573
                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))
 
574
                parent_text = target.add_lines(version,
 
575
                                               self.source.get_parents(version),
 
576
                                               self.source.get_lines(version),
 
577
                                               parent_texts=parent_texts)
 
578
                parent_texts[version] = parent_text
 
579
                #delta_parent, sha1, noeol, delta = deltas[version]
 
580
                #target.add_delta(version,
 
581
                #                 self.source.get_parents(version),
 
582
                #                 delta_parent,
 
583
                #                 sha1,
 
584
                #                 noeol,
 
585
                #                 delta)
 
586
                #target.get_lines(version)
465
587
            
466
588
            # this should hit the native code path for target
467
589
            if target is not self.target: