/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/weave.py

  • Committer: Martin Pool
  • Date: 2005-07-11 04:28:44 UTC
  • Revision ID: mbp@sourcefrog.net-20050711042844-2a2dc06fedcfa77a
- weave stores only direct parents, and calculates and memoizes expansion as needed

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#! /usr/bin/python
 
2
 
 
3
# Copyright (C) 2005 Canonical Ltd
 
4
 
 
5
# This program is free software; you can redistribute it and/or modify
 
6
# it under the terms of the GNU General Public License as published by
 
7
# the Free Software Foundation; either version 2 of the License, or
 
8
# (at your option) any later version.
 
9
 
 
10
# This program is distributed in the hope that it will be useful,
 
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
13
# GNU General Public License for more details.
 
14
 
 
15
# You should have received a copy of the GNU General Public License
 
16
# along with this program; if not, write to the Free Software
 
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
18
 
 
19
# Author: Martin Pool <mbp@canonical.com>
 
20
 
 
21
 
 
22
"""Weave - storage of related text file versions"""
 
23
 
 
24
# TODO: Perhaps have copy method for Weave instances?
 
25
 
 
26
# XXX: If we do weaves this way, will a merge still behave the same
 
27
# way if it's done in a different order?  That's a pretty desirable
 
28
# property.
 
29
 
 
30
# TODO: How to write these to disk?  One option is cPickle, which
 
31
# would be fast but less friendly to C, and perhaps not portable.  Another is
 
32
 
 
33
# TODO: Nothing here so far assumes the lines are really \n newlines,
 
34
# rather than being split up in some other way.  We could accomodate
 
35
# binaries, perhaps by naively splitting on \n or perhaps using
 
36
# something like a rolling checksum.
 
37
 
 
38
# TODO: Perhaps track SHA-1 in the header for protection?  This would
 
39
# be redundant with it being stored in the inventory, but perhaps
 
40
# usefully so?
 
41
 
 
42
# TODO: Track version names as well as indexes. 
 
43
 
 
44
# TODO: Probably do transitive expansion when specifying parents?
 
45
 
 
46
# TODO: Separate out some code to read and write weaves.
 
47
 
 
48
# TODO: End marker for each version so we can stop reading?
 
49
 
 
50
# TODO: Check that no insertion occurs inside a deletion that was
 
51
# active in the version of the insertion.
 
52
 
 
53
# TODO: Perhaps a special slower check() method that verifies more
 
54
# nesting constraints and the MD5 of each version?
 
55
 
 
56
 
 
57
 
 
58
try:
 
59
    set
 
60
    frozenset
 
61
except NameError:
 
62
    from sets import Set, ImmutableSet
 
63
    set = Set
 
64
    frozenset = ImmutableSet
 
65
    del Set, ImmutableSet
 
66
 
 
67
 
 
68
class WeaveError(Exception):
 
69
    """Exception in processing weave"""
 
70
 
 
71
 
 
72
class WeaveFormatError(WeaveError):
 
73
    """Weave invariant violated"""
 
74
    
 
75
 
 
76
class Weave(object):
 
77
    """weave - versioned text file storage.
 
78
    
 
79
    A Weave manages versions of line-based text files, keeping track
 
80
    of the originating version for each line.
 
81
 
 
82
    To clients the "lines" of the file are represented as a list of strings.
 
83
    These strings  will typically have terminal newline characters, but
 
84
    this is not required.  In particular files commonly do not have a newline
 
85
    at the end of the file.
 
86
 
 
87
    Texts can be identified in either of two ways:
 
88
 
 
89
    * a nonnegative index number.
 
90
 
 
91
    * a version-id string.
 
92
 
 
93
    Typically the index number will be valid only inside this weave and
 
94
    the version-id is used to reference it in the larger world.
 
95
 
 
96
    The weave is represented as a list mixing edit instructions and
 
97
    literal text.  Each entry in _l can be either a string (or
 
98
    unicode), or a tuple.  If a string, it means that the given line
 
99
    should be output in the currently active revisions.
 
100
 
 
101
    If a tuple, it gives a processing instruction saying in which
 
102
    revisions the enclosed lines are active.  The tuple has the form
 
103
    (instruction, version).
 
104
 
 
105
    The instruction can be '{' or '}' for an insertion block, and '['
 
106
    and ']' for a deletion block respectively.  The version is the
 
107
    integer version index.  There is no replace operator, only deletes
 
108
    and inserts.
 
109
 
 
110
    Constraints/notes:
 
111
 
 
112
    * A later version can delete lines that were introduced by any
 
113
      number of ancestor versions; this implies that deletion
 
114
      instructions can span insertion blocks without regard to the
 
115
      insertion block's nesting.
 
116
 
 
117
    * Similarly, deletions need not be properly nested with regard to
 
118
      each other, because they might have been generated by
 
119
      independent revisions.
 
120
 
 
121
    * Insertions are always made by inserting a new bracketed block
 
122
      into a single point in the previous weave.  This implies they
 
123
      can nest but not overlap, and the nesting must always have later
 
124
      insertions on the inside.
 
125
 
 
126
    * It doesn't seem very useful to have an active insertion
 
127
      inside an inactive insertion, but it might happen.
 
128
      
 
129
    * Therefore, all instructions are always"considered"; that
 
130
      is passed onto and off the stack.  An outer inactive block
 
131
      doesn't disable an inner block.
 
132
 
 
133
    * Lines are enabled if the most recent enclosing insertion is
 
134
      active and none of the enclosing deletions are active.
 
135
 
 
136
    * There is no point having a deletion directly inside its own
 
137
      insertion; you might as well just not write it.  And there
 
138
      should be no way to get an earlier version deleting a later
 
139
      version.
 
140
 
 
141
    _l
 
142
        Text of the weave.
 
143
 
 
144
    _v
 
145
        List of parents, indexed by version number.
 
146
        It is only necessary to store the minimal set of parents for
 
147
        each version; the parent's parents are implied.
 
148
 
 
149
    _i
 
150
        Full set of inclusions for each revision.
 
151
 
 
152
    _sha1s
 
153
        List of hex SHA-1 of each version, or None if not recorded.
 
154
    """
 
155
    def __init__(self):
 
156
        self._l = []
 
157
        self._v = []
 
158
        self._sha1s = []
 
159
        self._i = {}
 
160
 
 
161
 
 
162
    def __eq__(self, other):
 
163
        if not isinstance(other, Weave):
 
164
            return False
 
165
        return self._v == other._v \
 
166
               and self._l == other._l
 
167
    
 
168
 
 
169
    def __ne__(self, other):
 
170
        return not self.__eq__(other)
 
171
 
 
172
        
 
173
    def add(self, parents, text):
 
174
        """Add a single text on top of the weave.
 
175
  
 
176
        Returns the index number of the newly added version.
 
177
 
 
178
        parents
 
179
            List or set of direct parent version numbers.
 
180
            
 
181
        text
 
182
            Sequence of lines to be added in the new version."""
 
183
        ## self._check_versions(parents)
 
184
        ## self._check_lines(text)
 
185
        idx = len(self._v)
 
186
 
 
187
        import sha
 
188
        s = sha.new()
 
189
        for l in text:
 
190
            s.update(l)
 
191
        sha1 = s.hexdigest()
 
192
        del s
 
193
 
 
194
        if parents:
 
195
            ancestors = self.inclusions(parents)
 
196
            delta = self._delta(ancestors, text)
 
197
 
 
198
            # offset gives the number of lines that have been inserted
 
199
            # into the weave up to the current point; if the original edit instruction
 
200
            # says to change line A then we actually change (A+offset)
 
201
            offset = 0
 
202
 
 
203
            for i1, i2, newlines in delta:
 
204
                assert 0 <= i1
 
205
                assert i1 <= i2
 
206
                assert i2 <= len(self._l)
 
207
 
 
208
                # the deletion and insertion are handled separately.
 
209
                # first delete the region.
 
210
                if i1 != i2:
 
211
                    self._l.insert(i1+offset, ('[', idx))
 
212
                    self._l.insert(i2+offset+1, (']', idx))
 
213
                    offset += 2
 
214
                    # is this OK???
 
215
 
 
216
                if newlines:
 
217
                    # there may have been a deletion spanning up to
 
218
                    # i2; we want to insert after this region to make sure
 
219
                    # we don't destroy ourselves
 
220
                    i = i2 + offset
 
221
                    self._l[i:i] = [('{', idx)] \
 
222
                                   + newlines \
 
223
                                   + [('}', idx)]
 
224
                    offset += 2 + len(newlines)
 
225
 
 
226
            self._addversion(parents)
 
227
        else:
 
228
            # special case; adding with no parents revision; can do this
 
229
            # more quickly by just appending unconditionally
 
230
            self._l.append(('{', idx))
 
231
            self._l += text
 
232
            self._l.append(('}', idx))
 
233
 
 
234
            self._addversion(None)
 
235
 
 
236
        self._sha1s.append(sha1)
 
237
            
 
238
        return idx
 
239
 
 
240
 
 
241
    def _expand(self, version):
 
242
        if version in self._i:
 
243
            return self._i[version]
 
244
        else:
 
245
            i = set([version])
 
246
            for pv in self._v[version]:
 
247
                i.update(self._expand(pv))
 
248
            self._i[version] = i
 
249
            return i
 
250
 
 
251
 
 
252
    def inclusions(self, versions):
 
253
        """Expand out everything included by versions."""
 
254
        i = set(versions)
 
255
        try:
 
256
            for v in versions:
 
257
                i.update(self._expand(v))
 
258
        except IndexError:
 
259
            raise ValueError("version %d not present in weave" % v)
 
260
        return i
 
261
 
 
262
 
 
263
    def minimal_parents(self, version):
 
264
        """Find the minimal set of parents for the version."""
 
265
        included = self._v[version]
 
266
        if not included:
 
267
            return []
 
268
        
 
269
        li = list(included)
 
270
        li.sort()
 
271
        li.reverse()
 
272
 
 
273
        mininc = []
 
274
        gotit = set()
 
275
 
 
276
        for pv in li:
 
277
            if pv not in gotit:
 
278
                mininc.append(pv)
 
279
                gotit.update(self._v[pv])
 
280
 
 
281
        assert mininc[0] >= 0
 
282
        assert mininc[-1] < version
 
283
        return mininc
 
284
 
 
285
 
 
286
    def _addversion(self, parents):
 
287
        if parents:
 
288
            self._v.append(parents)
 
289
        else:
 
290
            self._v.append(frozenset())
 
291
 
 
292
 
 
293
    def _check_lines(self, text):
 
294
        if not isinstance(text, list):
 
295
            raise ValueError("text should be a list, not %s" % type(text))
 
296
 
 
297
        for l in text:
 
298
            if not isinstance(l, basestring):
 
299
                raise ValueError("text line should be a string or unicode, not %s"
 
300
                                 % type(l))
 
301
        
 
302
 
 
303
 
 
304
    def _check_versions(self, indexes):
 
305
        """Check everything in the sequence of indexes is valid"""
 
306
        for i in indexes:
 
307
            try:
 
308
                self._v[i]
 
309
            except IndexError:
 
310
                raise IndexError("invalid version number %r" % i)
 
311
 
 
312
    
 
313
    def annotate(self, index):
 
314
        return list(self.annotate_iter(index))
 
315
 
 
316
 
 
317
    def annotate_iter(self, version):
 
318
        """Yield list of (index-id, line) pairs for the specified version.
 
319
 
 
320
        The index indicates when the line originated in the weave."""
 
321
        included = self.inclusions([version])
 
322
        for origin, lineno, text in self._extract(self.inclusions(included)):
 
323
            yield origin, text
 
324
 
 
325
 
 
326
    def _extract(self, included):
 
327
        """Yield annotation of lines in included set.
 
328
 
 
329
        Yields a sequence of tuples (origin, lineno, text), where
 
330
        origin is the origin version, lineno the index in the weave,
 
331
        and text the text of the line.
 
332
 
 
333
        The set typically but not necessarily corresponds to a version.
 
334
        """
 
335
 
 
336
        istack = []
 
337
        dset = set()
 
338
 
 
339
        lineno = 0         # line of weave, 0-based
 
340
 
 
341
        isactive = False
 
342
 
 
343
        WFE = WeaveFormatError
 
344
 
 
345
        for l in self._l:
 
346
            if isinstance(l, tuple):
 
347
                c, v = l
 
348
                if c == '{':
 
349
                    assert v not in istack
 
350
                    istack.append(v)
 
351
                    if not dset:
 
352
                        isactive = (v in included)
 
353
                elif c == '}':
 
354
                    oldv = istack.pop()
 
355
                    assert oldv == v
 
356
                    isactive = (not dset) and (istack and istack[-1] in included)
 
357
                elif c == '[':
 
358
                    if v in included:
 
359
                        assert v not in dset
 
360
                        dset.add(v)
 
361
                        isactive = False
 
362
                else:
 
363
                    assert c == ']'
 
364
                    if v in included:
 
365
                        assert v in dset
 
366
                        dset.remove(v)
 
367
                        isactive = (not dset) and (istack and istack[-1] in included)
 
368
            else:
 
369
                assert isinstance(l, basestring)
 
370
                if isactive:
 
371
                    yield istack[-1], lineno, l
 
372
            lineno += 1
 
373
 
 
374
        if istack:
 
375
            raise WFE("unclosed insertion blocks at end of weave",
 
376
                                   istack)
 
377
        if dset:
 
378
            raise WFE("unclosed deletion blocks at end of weave",
 
379
                                   dset)
 
380
 
 
381
 
 
382
    def get_iter(self, version):
 
383
        """Yield lines for the specified version."""
 
384
        for origin, lineno, line in self._extract(self.inclusions([version])):
 
385
            yield line
 
386
 
 
387
 
 
388
    def get(self, index):
 
389
        return list(self.get_iter(index))
 
390
 
 
391
 
 
392
    def mash_iter(self, included):
 
393
        """Return composed version of multiple included versions."""
 
394
        included = frozenset(included)
 
395
        for origin, lineno, text in self._extract(self.inclusions(included)):
 
396
            yield text
 
397
 
 
398
 
 
399
    def dump(self, to_file):
 
400
        from pprint import pprint
 
401
        print >>to_file, "Weave._l = ",
 
402
        pprint(self._l, to_file)
 
403
        print >>to_file, "Weave._v = ",
 
404
        pprint(self._v, to_file)
 
405
 
 
406
 
 
407
 
 
408
    def numversions(self):
 
409
        l = len(self._v)
 
410
        assert l == len(self._sha1s)
 
411
        return l
 
412
 
 
413
 
 
414
    def check(self):
 
415
        # check no circular inclusions
 
416
        for version in range(self.numversions()):
 
417
            inclusions = list(self._v[version])
 
418
            if inclusions:
 
419
                inclusions.sort()
 
420
                if inclusions[-1] >= version:
 
421
                    raise WeaveFormatError("invalid included version %d for index %d"
 
422
                                           % (inclusions[-1], version))
 
423
 
 
424
        # try extracting all versions; this is a bit slow and parallel
 
425
        # extraction could be used
 
426
        import sha
 
427
        for version in range(self.numversions()):
 
428
            s = sha.new()
 
429
            for l in self.get_iter(version):
 
430
                s.update(l)
 
431
            hd = s.hexdigest()
 
432
            expected = self._sha1s[version]
 
433
            if hd != expected:
 
434
                raise WeaveError("mismatched sha1 for version %d; "
 
435
                                 "got %s, expected %s"
 
436
                                 % (version, hd, expected))
 
437
 
 
438
        # TODO: check insertions are properly nested, that there are
 
439
        # no lines outside of insertion blocks, that deletions are
 
440
        # properly paired, etc.
 
441
 
 
442
 
 
443
 
 
444
    def merge(self, merge_versions):
 
445
        """Automerge and mark conflicts between versions.
 
446
 
 
447
        This returns a sequence, each entry describing alternatives
 
448
        for a chunk of the file.  Each of the alternatives is given as
 
449
        a list of lines.
 
450
 
 
451
        If there is a chunk of the file where there's no diagreement,
 
452
        only one alternative is given.
 
453
        """
 
454
 
 
455
        # approach: find the included versions common to all the
 
456
        # merged versions
 
457
        raise NotImplementedError()
 
458
 
 
459
 
 
460
 
 
461
    def _delta(self, included, lines):
 
462
        """Return changes from basis to new revision.
 
463
 
 
464
        The old text for comparison is the union of included revisions.
 
465
 
 
466
        This is used in inserting a new text.
 
467
 
 
468
        Delta is returned as a sequence of
 
469
        (weave1, weave2, newlines).
 
470
 
 
471
        This indicates that weave1:weave2 of the old weave should be
 
472
        replaced by the sequence of lines in newlines.  Note that
 
473
        these line numbers are positions in the total weave and don't
 
474
        correspond to the lines in any extracted version, or even the
 
475
        extracted union of included versions.
 
476
 
 
477
        If line1=line2, this is a pure insert; if newlines=[] this is a
 
478
        pure delete.  (Similar to difflib.)
 
479
        """
 
480
        # basis a list of (origin, lineno, line)
 
481
        basis_lineno = []
 
482
        basis_lines = []
 
483
        for origin, lineno, line in self._extract(self.inclusions(included)):
 
484
            basis_lineno.append(lineno)
 
485
            basis_lines.append(line)
 
486
 
 
487
        # add a sentinal, because we can also match against the final line
 
488
        basis_lineno.append(len(self._l))
 
489
 
 
490
        # XXX: which line of the weave should we really consider
 
491
        # matches the end of the file?  the current code says it's the
 
492
        # last line of the weave?
 
493
 
 
494
        from difflib import SequenceMatcher
 
495
        s = SequenceMatcher(None, basis_lines, lines)
 
496
 
 
497
        # TODO: Perhaps return line numbers from composed weave as well?
 
498
 
 
499
        for tag, i1, i2, j1, j2 in s.get_opcodes():
 
500
            ##print tag, i1, i2, j1, j2
 
501
 
 
502
            if tag == 'equal':
 
503
                continue
 
504
 
 
505
            # i1,i2 are given in offsets within basis_lines; we need to map them
 
506
            # back to offsets within the entire weave
 
507
            real_i1 = basis_lineno[i1]
 
508
            real_i2 = basis_lineno[i2]
 
509
 
 
510
            assert 0 <= j1
 
511
            assert j1 <= j2
 
512
            assert j2 <= len(lines)
 
513
 
 
514
            yield real_i1, real_i2, lines[j1:j2]
 
515
 
 
516
 
 
517
 
 
518
def weave_info(filename, out):
 
519
    """Show some text information about the weave."""
 
520
    from weavefile import read_weave
 
521
    wf = file(filename, 'rb')
 
522
    w = read_weave(wf)
 
523
    # FIXME: doesn't work on pipes
 
524
    weave_size = wf.tell()
 
525
    print >>out, "weave file size %d bytes" % weave_size
 
526
    print >>out, "weave contains %d versions" % len(w._v)
 
527
 
 
528
    total = 0
 
529
    print '%6s %6s %8s %40s %20s' % ('ver', 'lines', 'bytes', 'sha1', 'parents')
 
530
    for i in (6, 6, 8, 40, 20):
 
531
        print '-' * i,
 
532
    print
 
533
    for i in range(len(w._v)):
 
534
        text = w.get(i)
 
535
        lines = len(text)
 
536
        bytes = sum((len(a) for a in text))
 
537
        sha1 = w._sha1s[i]
 
538
        print '%6d %6d %8d %40s' % (i, lines, bytes, sha1),
 
539
        for pv in w._v[i]:
 
540
            print pv,
 
541
        print
 
542
        total += bytes
 
543
 
 
544
    print >>out, "versions total %d bytes" % total
 
545
    print >>out, "compression ratio %.3f" % (float(total)/float(weave_size))
 
546
 
 
547
 
 
548
def usage():
 
549
    print """bzr weave tool
 
550
 
 
551
Experimental tool for weave algorithm.
 
552
 
 
553
usage:
 
554
    weave init WEAVEFILE
 
555
        Create an empty weave file
 
556
    weave get WEAVEFILE VERSION
 
557
        Write out specified version.
 
558
    weave check WEAVEFILE
 
559
        Check consistency of all versions.
 
560
    weave info WEAVEFILE
 
561
        Display table of contents.
 
562
    weave add WEAVEFILE [BASE...] < NEWTEXT
 
563
        Add NEWTEXT, with specified parent versions.
 
564
    weave annotate WEAVEFILE VERSION
 
565
        Display origin of each line.
 
566
    weave mash WEAVEFILE VERSION...
 
567
        Display composite of all selected versions.
 
568
    weave merge WEAVEFILE VERSION1 VERSION2 > OUT
 
569
        Auto-merge two versions and display conflicts.
 
570
 
 
571
example:
 
572
 
 
573
    % weave init foo.weave
 
574
    % vi foo.txt
 
575
    % weave add foo.weave < foo.txt
 
576
    added version 0
 
577
 
 
578
    (create updated version)
 
579
    % vi foo.txt
 
580
    % weave get foo.weave 0 | diff -u - foo.txt
 
581
    % weave add foo.weave 0 < foo.txt
 
582
    added version 1
 
583
 
 
584
    % weave get foo.weave 0 > foo.txt       (create forked version)
 
585
    % vi foo.txt
 
586
    % weave add foo.weave 0 < foo.txt
 
587
    added version 2
 
588
 
 
589
    % weave merge foo.weave 1 2 > foo.txt   (merge them)
 
590
    % vi foo.txt                            (resolve conflicts)
 
591
    % weave add foo.weave 1 2 < foo.txt     (commit merged version)     
 
592
    
 
593
"""
 
594
    
 
595
 
 
596
 
 
597
def main(argv):
 
598
    import sys
 
599
    import os
 
600
    from weavefile import write_weave, read_weave
 
601
    cmd = argv[1]
 
602
 
 
603
    def readit():
 
604
        return read_weave(file(argv[2], 'rb'))
 
605
    
 
606
    if cmd == 'help':
 
607
        usage()
 
608
    elif cmd == 'add':
 
609
        w = readit()
 
610
        # at the moment, based on everything in the file
 
611
        parents = map(int, argv[3:])
 
612
        lines = sys.stdin.readlines()
 
613
        ver = w.add(parents, lines)
 
614
        write_weave(w, file(argv[2], 'wb'))
 
615
        print 'added version %d' % ver
 
616
    elif cmd == 'init':
 
617
        fn = argv[2]
 
618
        if os.path.exists(fn):
 
619
            raise IOError("file exists")
 
620
        w = Weave()
 
621
        write_weave(w, file(fn, 'wb'))
 
622
    elif cmd == 'get': # get one version
 
623
        w = readit()
 
624
        sys.stdout.writelines(w.get_iter(int(argv[3])))
 
625
        
 
626
    elif cmd == 'mash': # get composite
 
627
        w = readit()
 
628
        sys.stdout.writelines(w.mash_iter(map(int, argv[3:])))
 
629
 
 
630
    elif cmd == 'annotate':
 
631
        w = readit()
 
632
        # newline is added to all lines regardless; too hard to get
 
633
        # reasonable formatting otherwise
 
634
        lasto = None
 
635
        for origin, text in w.annotate(int(argv[3])):
 
636
            text = text.rstrip('\r\n')
 
637
            if origin == lasto:
 
638
                print '      | %s' % (text)
 
639
            else:
 
640
                print '%5d | %s' % (origin, text)
 
641
                lasto = origin
 
642
                
 
643
    elif cmd == 'info':
 
644
        weave_info(argv[2], sys.stdout)
 
645
        
 
646
    elif cmd == 'check':
 
647
        w = readit()
 
648
        w.check()
 
649
 
 
650
    elif cmd == 'inclusions':
 
651
        w = readit()
 
652
        print ' '.join(map(str, w.inclusions([int(argv[3])])))
 
653
 
 
654
    elif cmd == 'parents':
 
655
        w = readit()
 
656
        print ' '.join(map(str, w._v[int(argv[3])]))
 
657
 
 
658
    elif cmd == 'merge':
 
659
        if len(argv) != 5:
 
660
            usage()
 
661
            return 1
 
662
 
 
663
        w = readit()
 
664
        v1, v2 = map(int, argv[3:5])
 
665
 
 
666
        basis = w.inclusions([v1]).intersection(w.inclusions([v2]))
 
667
 
 
668
        base_lines = list(w.mash_iter(basis))
 
669
        a_lines = list(w.get(v1))
 
670
        b_lines = list(w.get(v2))
 
671
 
 
672
        from bzrlib.merge3 import Merge3
 
673
        m3 = Merge3(base_lines, a_lines, b_lines)
 
674
 
 
675
        name_a = 'version %d' % v1
 
676
        name_b = 'version %d' % v2
 
677
        sys.stdout.writelines(m3.merge_lines(name_a=name_a, name_b=name_b))
 
678
    else:
 
679
        raise ValueError('unknown command %r' % cmd)
 
680
    
 
681
 
 
682
if __name__ == '__main__':
 
683
    import sys
 
684
    sys.exit(main(sys.argv))