/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-09-16 04:47:12 UTC
  • Revision ID: mbp@sourcefrog.net-20050916044711-e5a7bfc6ead3800b
- is_ancestor now works by looking at the Branch's stored ancestry

- some tests related to this

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
# before intset (r923) 2000 versions in 41.5s
 
25
# with intset (r926) 2000 versions in 93s !!!
 
26
# better to just use plain sets.
 
27
 
 
28
# making _extract build and return a list, rather than being a generator
 
29
# takes 37.94s
 
30
 
 
31
# with python -O, r923 does 2000 versions in 36.87s
 
32
 
 
33
# with optimizations to avoid mutating lists - 35.75!  I guess copying
 
34
# all the elements every time costs more than the small manipulations.
 
35
# a surprisingly small change.
 
36
 
 
37
# r931, which avoids using a generator for extract, does 36.98s
 
38
 
 
39
# with memoized inclusions, takes 41.49s; not very good
 
40
 
 
41
# with slots, takes 37.35s; without takes 39.16, a bit surprising
 
42
 
 
43
# with the delta calculation mixed in with the add method, rather than
 
44
# separated, takes 36.78s
 
45
 
 
46
# with delta folded in and mutation of the list, 36.13s
 
47
 
 
48
# with all this and simplification of add code, 33s
 
49
 
 
50
 
 
51
 
 
52
 
 
53
 
 
54
# TODO: Perhaps have copy method for Weave instances?
 
55
 
 
56
# XXX: If we do weaves this way, will a merge still behave the same
 
57
# way if it's done in a different order?  That's a pretty desirable
 
58
# property.
 
59
 
 
60
# TODO: Nothing here so far assumes the lines are really \n newlines,
 
61
# rather than being split up in some other way.  We could accomodate
 
62
# binaries, perhaps by naively splitting on \n or perhaps using
 
63
# something like a rolling checksum.
 
64
 
 
65
# TODO: End marker for each version so we can stop reading?
 
66
 
 
67
# TODO: Check that no insertion occurs inside a deletion that was
 
68
# active in the version of the insertion.
 
69
 
 
70
# TODO: In addition to the SHA-1 check, perhaps have some code that
 
71
# checks structural constraints of the weave: ie that insertions are
 
72
# properly nested, that there is no text outside of an insertion, that
 
73
# insertions or deletions are not repeated, etc.
 
74
 
 
75
# TODO: Parallel-extract that passes back each line along with a
 
76
# description of which revisions include it.  Nice for checking all
 
77
# shas in parallel.
 
78
 
 
79
# TODO: Using a single _extract routine and then processing the output
 
80
# is probably inefficient.  It's simple enough that we can afford to
 
81
# have slight specializations for different ways its used: annotate,
 
82
# basis for add, get, etc.
 
83
 
 
84
# TODO: Perhaps the API should work only in names to hide the integer
 
85
# indexes from the user?
 
86
 
 
87
 
 
88
 
 
89
import sha
 
90
 
 
91
from cStringIO import StringIO
 
92
 
 
93
from bzrlib.osutils import sha_strings
 
94
 
 
95
 
 
96
class WeaveError(Exception):
 
97
    """Exception in processing weave"""
 
98
 
 
99
 
 
100
class WeaveFormatError(WeaveError):
 
101
    """Weave invariant violated"""
 
102
    
 
103
 
 
104
class Weave(object):
 
105
    """weave - versioned text file storage.
 
106
    
 
107
    A Weave manages versions of line-based text files, keeping track
 
108
    of the originating version for each line.
 
109
 
 
110
    To clients the "lines" of the file are represented as a list of strings.
 
111
    These strings  will typically have terminal newline characters, but
 
112
    this is not required.  In particular files commonly do not have a newline
 
113
    at the end of the file.
 
114
 
 
115
    Texts can be identified in either of two ways:
 
116
 
 
117
    * a nonnegative index number.
 
118
 
 
119
    * a version-id string. (not implemented yet)
 
120
 
 
121
    Typically the index number will be valid only inside this weave and
 
122
    the version-id is used to reference it in the larger world.
 
123
 
 
124
    The weave is represented as a list mixing edit instructions and
 
125
    literal text.  Each entry in _weave can be either a string (or
 
126
    unicode), or a tuple.  If a string, it means that the given line
 
127
    should be output in the currently active revisions.
 
128
 
 
129
    If a tuple, it gives a processing instruction saying in which
 
130
    revisions the enclosed lines are active.  The tuple has the form
 
131
    (instruction, version).
 
132
 
 
133
    The instruction can be '{' or '}' for an insertion block, and '['
 
134
    and ']' for a deletion block respectively.  The version is the
 
135
    integer version index.  There is no replace operator, only deletes
 
136
    and inserts.  For '}', the end of an insertion, there is no
 
137
    version parameter because it always closes the most recently
 
138
    opened insertion.
 
139
 
 
140
    Constraints/notes:
 
141
 
 
142
    * A later version can delete lines that were introduced by any
 
143
      number of ancestor versions; this implies that deletion
 
144
      instructions can span insertion blocks without regard to the
 
145
      insertion block's nesting.
 
146
 
 
147
    * Similarly, deletions need not be properly nested with regard to
 
148
      each other, because they might have been generated by
 
149
      independent revisions.
 
150
 
 
151
    * Insertions are always made by inserting a new bracketed block
 
152
      into a single point in the previous weave.  This implies they
 
153
      can nest but not overlap, and the nesting must always have later
 
154
      insertions on the inside.
 
155
 
 
156
    * It doesn't seem very useful to have an active insertion
 
157
      inside an inactive insertion, but it might happen.
 
158
      
 
159
    * Therefore, all instructions are always"considered"; that
 
160
      is passed onto and off the stack.  An outer inactive block
 
161
      doesn't disable an inner block.
 
162
 
 
163
    * Lines are enabled if the most recent enclosing insertion is
 
164
      active and none of the enclosing deletions are active.
 
165
 
 
166
    * There is no point having a deletion directly inside its own
 
167
      insertion; you might as well just not write it.  And there
 
168
      should be no way to get an earlier version deleting a later
 
169
      version.
 
170
 
 
171
    _weave
 
172
        Text of the weave; list of control instruction tuples and strings.
 
173
 
 
174
    _parents
 
175
        List of parents, indexed by version number.
 
176
        It is only necessary to store the minimal set of parents for
 
177
        each version; the parent's parents are implied.
 
178
 
 
179
    _sha1s
 
180
        List of hex SHA-1 of each version.
 
181
 
 
182
    _names
 
183
        List of symbolic names for each version.  Each should be unique.
 
184
 
 
185
    _name_map
 
186
        For each name, the version number.
 
187
 
 
188
    _weave_name
 
189
        Descriptive name of this weave; typically the filename if known.
 
190
        Set by read_weave.
 
191
    """
 
192
 
 
193
    __slots__ = ['_weave', '_parents', '_sha1s', '_names', '_name_map',
 
194
                 '_weave_name']
 
195
    
 
196
    def __init__(self, weave_name=None):
 
197
        self._weave = []
 
198
        self._parents = []
 
199
        self._sha1s = []
 
200
        self._names = []
 
201
        self._name_map = {}
 
202
        self._weave_name = weave_name
 
203
 
 
204
 
 
205
    def __eq__(self, other):
 
206
        if not isinstance(other, Weave):
 
207
            return False
 
208
        return self._parents == other._parents \
 
209
               and self._weave == other._weave \
 
210
               and self._sha1s == other._sha1s 
 
211
 
 
212
    
 
213
    def __ne__(self, other):
 
214
        return not self.__eq__(other)
 
215
 
 
216
 
 
217
    def lookup(self, name):
 
218
        assert isinstance(name, basestring), type(name)
 
219
        try:
 
220
            return self._name_map[name]
 
221
        except KeyError:
 
222
            raise WeaveError("name %r not present in weave %r" %
 
223
                             (name, self._weave_name))
 
224
 
 
225
 
 
226
    def idx_to_name(self, version):
 
227
        return self._names[version]
 
228
 
 
229
 
 
230
    def _check_repeated_add(self, name, parents, text):
 
231
        """Check that a duplicated add is OK.
 
232
 
 
233
        If it is, return the (old) index; otherwise raise an exception.
 
234
        """
 
235
        idx = self.lookup(name)
 
236
        if sorted(self._parents[idx]) != sorted(parents):
 
237
            raise WeaveError("name \"%s\" already present in weave "
 
238
                             "with different parents" % name)
 
239
        new_sha1 = sha_strings(text)
 
240
        if new_sha1 != self._sha1s[idx]:
 
241
            raise WeaveError("name \"%s\" already present in weave "
 
242
                             "with different text" % name)            
 
243
        return idx
 
244
        
 
245
 
 
246
        
 
247
    def add(self, name, parents, text):
 
248
        """Add a single text on top of the weave.
 
249
  
 
250
        Returns the index number of the newly added version.
 
251
 
 
252
        name
 
253
            Symbolic name for this version.
 
254
            (Typically the revision-id of the revision that added it.)
 
255
 
 
256
        parents
 
257
            List or set of direct parent version numbers.
 
258
            
 
259
        text
 
260
            Sequence of lines to be added in the new version."""
 
261
 
 
262
        assert isinstance(name, basestring)
 
263
        if name in self._name_map:
 
264
            return self._check_repeated_add(name, parents, text)
 
265
        
 
266
        self._check_versions(parents)
 
267
        ## self._check_lines(text)
 
268
        new_version = len(self._parents)
 
269
 
 
270
        sha1 = sha_strings(text)
 
271
 
 
272
        # if we abort after here the (in-memory) weave will be corrupt because only
 
273
        # some fields are updated
 
274
        self._parents.append(parents[:])
 
275
        self._sha1s.append(sha1)
 
276
        self._names.append(name)
 
277
        self._name_map[name] = new_version
 
278
 
 
279
            
 
280
        if not parents:
 
281
            # special case; adding with no parents revision; can do
 
282
            # this more quickly by just appending unconditionally.
 
283
            # even more specially, if we're adding an empty text we
 
284
            # need do nothing at all.
 
285
            if text:
 
286
                self._weave.append(('{', new_version))
 
287
                self._weave.extend(text)
 
288
                self._weave.append(('}', None))
 
289
        
 
290
            return new_version
 
291
 
 
292
        if len(parents) == 1:
 
293
            pv = list(parents)[0]
 
294
            if sha1 == self._sha1s[pv]:
 
295
                # special case: same as the single parent
 
296
                return new_version
 
297
            
 
298
 
 
299
        ancestors = self.inclusions(parents)
 
300
 
 
301
        l = self._weave
 
302
 
 
303
        # basis a list of (origin, lineno, line)
 
304
        basis_lineno = []
 
305
        basis_lines = []
 
306
        for origin, lineno, line in self._extract(ancestors):
 
307
            basis_lineno.append(lineno)
 
308
            basis_lines.append(line)
 
309
 
 
310
        # another small special case: a merge, producing the same text
 
311
        # as auto-merge
 
312
        if text == basis_lines:
 
313
            return new_version            
 
314
 
 
315
        # add a sentinal, because we can also match against the final line
 
316
        basis_lineno.append(len(self._weave))
 
317
 
 
318
        # XXX: which line of the weave should we really consider
 
319
        # matches the end of the file?  the current code says it's the
 
320
        # last line of the weave?
 
321
 
 
322
        #print 'basis_lines:', basis_lines
 
323
        #print 'new_lines:  ', lines
 
324
 
 
325
        from difflib import SequenceMatcher
 
326
        s = SequenceMatcher(None, basis_lines, text)
 
327
 
 
328
        # offset gives the number of lines that have been inserted
 
329
        # into the weave up to the current point; if the original edit instruction
 
330
        # says to change line A then we actually change (A+offset)
 
331
        offset = 0
 
332
 
 
333
        for tag, i1, i2, j1, j2 in s.get_opcodes():
 
334
            # i1,i2 are given in offsets within basis_lines; we need to map them
 
335
            # back to offsets within the entire weave
 
336
            #print 'raw match', tag, i1, i2, j1, j2
 
337
            if tag == 'equal':
 
338
                continue
 
339
 
 
340
            i1 = basis_lineno[i1]
 
341
            i2 = basis_lineno[i2]
 
342
 
 
343
            assert 0 <= j1 <= j2 <= len(text)
 
344
 
 
345
            #print tag, i1, i2, j1, j2
 
346
 
 
347
            # the deletion and insertion are handled separately.
 
348
            # first delete the region.
 
349
            if i1 != i2:
 
350
                self._weave.insert(i1+offset, ('[', new_version))
 
351
                self._weave.insert(i2+offset+1, (']', new_version))
 
352
                offset += 2
 
353
 
 
354
            if j1 != j2:
 
355
                # there may have been a deletion spanning up to
 
356
                # i2; we want to insert after this region to make sure
 
357
                # we don't destroy ourselves
 
358
                i = i2 + offset
 
359
                self._weave[i:i] = ([('{', new_version)] 
 
360
                                    + text[j1:j2] 
 
361
                                    + [('}', None)])
 
362
                offset += 2 + (j2 - j1)
 
363
 
 
364
        return new_version
 
365
 
 
366
 
 
367
    def inclusions(self, versions):
 
368
        """Return set of all ancestors of given version(s)."""
 
369
        i = set(versions)
 
370
        v = max(versions)
 
371
        try:
 
372
            while v >= 0:
 
373
                if v in i:
 
374
                    # include all its parents
 
375
                    i.update(self._parents[v])
 
376
                v -= 1
 
377
            return i
 
378
        except IndexError:
 
379
            raise ValueError("version %d not present in weave" % v)
 
380
 
 
381
 
 
382
    def parents(self, version):
 
383
        return self._parents[version]
 
384
 
 
385
 
 
386
    def minimal_parents(self, version):
 
387
        """Find the minimal set of parents for the version."""
 
388
        included = self._parents[version]
 
389
        if not included:
 
390
            return []
 
391
        
 
392
        li = list(included)
 
393
        li.sort(reverse=True)
 
394
 
 
395
        mininc = []
 
396
        gotit = set()
 
397
 
 
398
        for pv in li:
 
399
            if pv not in gotit:
 
400
                mininc.append(pv)
 
401
                gotit.update(self.inclusions(pv))
 
402
 
 
403
        assert mininc[0] >= 0
 
404
        assert mininc[-1] < version
 
405
        return mininc
 
406
 
 
407
 
 
408
 
 
409
    def _check_lines(self, text):
 
410
        if not isinstance(text, list):
 
411
            raise ValueError("text should be a list, not %s" % type(text))
 
412
 
 
413
        for l in text:
 
414
            if not isinstance(l, basestring):
 
415
                raise ValueError("text line should be a string or unicode, not %s"
 
416
                                 % type(l))
 
417
        
 
418
 
 
419
 
 
420
    def _check_versions(self, indexes):
 
421
        """Check everything in the sequence of indexes is valid"""
 
422
        for i in indexes:
 
423
            try:
 
424
                self._parents[i]
 
425
            except IndexError:
 
426
                raise IndexError("invalid version number %r" % i)
 
427
 
 
428
    
 
429
    def annotate(self, index):
 
430
        return list(self.annotate_iter(index))
 
431
 
 
432
 
 
433
    def annotate_iter(self, version):
 
434
        """Yield list of (index-id, line) pairs for the specified version.
 
435
 
 
436
        The index indicates when the line originated in the weave."""
 
437
        for origin, lineno, text in self._extract([version]):
 
438
            yield origin, text
 
439
 
 
440
 
 
441
    def _walk(self):
 
442
        """Walk the weave.
 
443
 
 
444
        Yields sequence of
 
445
        (lineno, insert, deletes, text)
 
446
        for each literal line.
 
447
        """
 
448
        
 
449
        istack = []
 
450
        dset = set()
 
451
 
 
452
        lineno = 0         # line of weave, 0-based
 
453
 
 
454
        for l in self._weave:
 
455
            if isinstance(l, tuple):
 
456
                c, v = l
 
457
                isactive = None
 
458
                if c == '{':
 
459
                    istack.append(v)
 
460
                elif c == '}':
 
461
                    istack.pop()
 
462
                elif c == '[':
 
463
                    assert v not in dset
 
464
                    dset.add(v)
 
465
                elif c == ']':
 
466
                    dset.remove(v)
 
467
                else:
 
468
                    raise WeaveFormatError('unexpected instruction %r'
 
469
                                           % v)
 
470
            else:
 
471
                assert isinstance(l, basestring)
 
472
                assert istack
 
473
                yield lineno, istack[-1], dset, l
 
474
            lineno += 1
 
475
 
 
476
 
 
477
 
 
478
    def _extract(self, versions):
 
479
        """Yield annotation of lines in included set.
 
480
 
 
481
        Yields a sequence of tuples (origin, lineno, text), where
 
482
        origin is the origin version, lineno the index in the weave,
 
483
        and text the text of the line.
 
484
 
 
485
        The set typically but not necessarily corresponds to a version.
 
486
        """
 
487
        for i in versions:
 
488
            if not isinstance(i, int):
 
489
                raise ValueError(i)
 
490
            
 
491
        included = self.inclusions(versions)
 
492
 
 
493
        istack = []
 
494
        dset = set()
 
495
 
 
496
        lineno = 0         # line of weave, 0-based
 
497
 
 
498
        isactive = None
 
499
 
 
500
        result = []
 
501
 
 
502
        WFE = WeaveFormatError
 
503
 
 
504
        for l in self._weave:
 
505
            if isinstance(l, tuple):
 
506
                c, v = l
 
507
                isactive = None
 
508
                if c == '{':
 
509
                    assert v not in istack
 
510
                    istack.append(v)
 
511
                elif c == '}':
 
512
                    istack.pop()
 
513
                elif c == '[':
 
514
                    if v in included:
 
515
                        assert v not in dset
 
516
                        dset.add(v)
 
517
                else:
 
518
                    assert c == ']'
 
519
                    if v in included:
 
520
                        assert v in dset
 
521
                        dset.remove(v)
 
522
            else:
 
523
                assert isinstance(l, basestring)
 
524
                if isactive is None:
 
525
                    isactive = (not dset) and istack and (istack[-1] in included)
 
526
                if isactive:
 
527
                    result.append((istack[-1], lineno, l))
 
528
            lineno += 1
 
529
 
 
530
        if istack:
 
531
            raise WFE("unclosed insertion blocks at end of weave",
 
532
                                   istack)
 
533
        if dset:
 
534
            raise WFE("unclosed deletion blocks at end of weave",
 
535
                                   dset)
 
536
 
 
537
        return result
 
538
    
 
539
 
 
540
 
 
541
    def get_iter(self, version):
 
542
        """Yield lines for the specified version."""
 
543
        for origin, lineno, line in self._extract([version]):
 
544
            yield line
 
545
 
 
546
 
 
547
    def get_text(self, version):
 
548
        assert isinstance(version, int)
 
549
        s = StringIO()
 
550
        s.writelines(self.get_iter(version))
 
551
        return s.getvalue()
 
552
 
 
553
 
 
554
    def get(self, index):
 
555
        return list(self.get_iter(index))
 
556
 
 
557
 
 
558
    def mash_iter(self, included):
 
559
        """Return composed version of multiple included versions."""
 
560
        for origin, lineno, text in self._extract(included):
 
561
            yield text
 
562
 
 
563
 
 
564
    def dump(self, to_file):
 
565
        from pprint import pprint
 
566
        print >>to_file, "Weave._weave = ",
 
567
        pprint(self._weave, to_file)
 
568
        print >>to_file, "Weave._parents = ",
 
569
        pprint(self._parents, to_file)
 
570
 
 
571
 
 
572
 
 
573
    def numversions(self):
 
574
        l = len(self._parents)
 
575
        assert l == len(self._sha1s)
 
576
        return l
 
577
 
 
578
 
 
579
    def __len__(self):
 
580
        return self.numversions()
 
581
 
 
582
 
 
583
    def check(self, progress_bar=None):
 
584
        # check no circular inclusions
 
585
        for version in range(self.numversions()):
 
586
            inclusions = list(self._parents[version])
 
587
            if inclusions:
 
588
                inclusions.sort()
 
589
                if inclusions[-1] >= version:
 
590
                    raise WeaveFormatError("invalid included version %d for index %d"
 
591
                                           % (inclusions[-1], version))
 
592
 
 
593
        # try extracting all versions; this is a bit slow and parallel
 
594
        # extraction could be used
 
595
        nv = self.numversions()
 
596
        for version in range(nv):
 
597
            if progress_bar:
 
598
                progress_bar.update('checking text', version, nv)
 
599
            s = sha.new()
 
600
            for l in self.get_iter(version):
 
601
                s.update(l)
 
602
            hd = s.hexdigest()
 
603
            expected = self._sha1s[version]
 
604
            if hd != expected:
 
605
                raise WeaveError("mismatched sha1 for version %d; "
 
606
                                 "got %s, expected %s"
 
607
                                 % (version, hd, expected))
 
608
 
 
609
        # TODO: check insertions are properly nested, that there are
 
610
        # no lines outside of insertion blocks, that deletions are
 
611
        # properly paired, etc.
 
612
 
 
613
 
 
614
 
 
615
    def merge(self, merge_versions):
 
616
        """Automerge and mark conflicts between versions.
 
617
 
 
618
        This returns a sequence, each entry describing alternatives
 
619
        for a chunk of the file.  Each of the alternatives is given as
 
620
        a list of lines.
 
621
 
 
622
        If there is a chunk of the file where there's no diagreement,
 
623
        only one alternative is given.
 
624
        """
 
625
 
 
626
        # approach: find the included versions common to all the
 
627
        # merged versions
 
628
        raise NotImplementedError()
 
629
 
 
630
 
 
631
 
 
632
    def _delta(self, included, lines):
 
633
        """Return changes from basis to new revision.
 
634
 
 
635
        The old text for comparison is the union of included revisions.
 
636
 
 
637
        This is used in inserting a new text.
 
638
 
 
639
        Delta is returned as a sequence of
 
640
        (weave1, weave2, newlines).
 
641
 
 
642
        This indicates that weave1:weave2 of the old weave should be
 
643
        replaced by the sequence of lines in newlines.  Note that
 
644
        these line numbers are positions in the total weave and don't
 
645
        correspond to the lines in any extracted version, or even the
 
646
        extracted union of included versions.
 
647
 
 
648
        If line1=line2, this is a pure insert; if newlines=[] this is a
 
649
        pure delete.  (Similar to difflib.)
 
650
        """
 
651
 
 
652
 
 
653
            
 
654
    def plan_merge(self, ver_a, ver_b):
 
655
        """Return pseudo-annotation indicating how the two versions merge.
 
656
 
 
657
        This is computed between versions a and b and their common
 
658
        base.
 
659
 
 
660
        Weave lines present in none of them are skipped entirely.
 
661
        """
 
662
        inc_a = self.inclusions([ver_a])
 
663
        inc_b = self.inclusions([ver_b])
 
664
        inc_c = inc_a & inc_b
 
665
 
 
666
        for lineno, insert, deleteset, line in self._walk():
 
667
            if deleteset & inc_c:
 
668
                # killed in parent; can't be in either a or b
 
669
                # not relevant to our work
 
670
                yield 'killed-base', line
 
671
            elif insert in inc_c:
 
672
                # was inserted in base
 
673
                killed_a = bool(deleteset & inc_a)
 
674
                killed_b = bool(deleteset & inc_b)
 
675
                if killed_a and killed_b:
 
676
                    yield 'killed-both', line
 
677
                elif killed_a:
 
678
                    yield 'killed-a', line
 
679
                elif killed_b:
 
680
                    yield 'killed-b', line
 
681
                else:
 
682
                    yield 'unchanged', line
 
683
            elif insert in inc_a:
 
684
                if deleteset & inc_a:
 
685
                    yield 'ghost-a', line
 
686
                else:
 
687
                    # new in A; not in B
 
688
                    yield 'new-a', line
 
689
            elif insert in inc_b:
 
690
                if deleteset & inc_b:
 
691
                    yield 'ghost-b', line
 
692
                else:
 
693
                    yield 'new-b', line
 
694
            else:
 
695
                # not in either revision
 
696
                yield 'irrelevant', line
 
697
 
 
698
        yield 'unchanged', ''           # terminator
 
699
 
 
700
 
 
701
 
 
702
    def weave_merge(self, plan):
 
703
        lines_a = []
 
704
        lines_b = []
 
705
        ch_a = ch_b = False
 
706
 
 
707
        for state, line in plan:
 
708
            if state == 'unchanged' or state == 'killed-both':
 
709
                # resync and flush queued conflicts changes if any
 
710
                if not lines_a and not lines_b:
 
711
                    pass
 
712
                elif ch_a and not ch_b:
 
713
                    # one-sided change:                    
 
714
                    for l in lines_a: yield l
 
715
                elif ch_b and not ch_a:
 
716
                    for l in lines_b: yield l
 
717
                elif lines_a == lines_b:
 
718
                    for l in lines_a: yield l
 
719
                else:
 
720
                    yield '<<<<\n'
 
721
                    for l in lines_a: yield l
 
722
                    yield '====\n'
 
723
                    for l in lines_b: yield l
 
724
                    yield '>>>>\n'
 
725
 
 
726
                del lines_a[:]
 
727
                del lines_b[:]
 
728
                ch_a = ch_b = False
 
729
                
 
730
            if state == 'unchanged':
 
731
                if line:
 
732
                    yield line
 
733
            elif state == 'killed-a':
 
734
                ch_a = True
 
735
                lines_b.append(line)
 
736
            elif state == 'killed-b':
 
737
                ch_b = True
 
738
                lines_a.append(line)
 
739
            elif state == 'new-a':
 
740
                ch_a = True
 
741
                lines_a.append(line)
 
742
            elif state == 'new-b':
 
743
                ch_b = True
 
744
                lines_b.append(line)
 
745
            else:
 
746
                assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
 
747
                                 'killed-both'), \
 
748
                       state
 
749
 
 
750
                
 
751
 
 
752
 
 
753
 
 
754
 
 
755
 
 
756
def weave_toc(w):
 
757
    """Show the weave's table-of-contents"""
 
758
    print '%6s %50s %10s %10s' % ('ver', 'name', 'sha1', 'parents')
 
759
    for i in (6, 50, 10, 10):
 
760
        print '-' * i,
 
761
    print
 
762
    for i in range(w.numversions()):
 
763
        sha1 = w._sha1s[i]
 
764
        name = w._names[i]
 
765
        parent_str = ' '.join(map(str, w._parents[i]))
 
766
        print '%6d %-50.50s %10.10s %s' % (i, name, sha1, parent_str)
 
767
 
 
768
 
 
769
 
 
770
def weave_stats(weave_file):
 
771
    from bzrlib.progress import ProgressBar
 
772
    from bzrlib.weavefile import read_weave
 
773
 
 
774
    pb = ProgressBar()
 
775
 
 
776
    wf = file(weave_file, 'rb')
 
777
    w = read_weave(wf)
 
778
    # FIXME: doesn't work on pipes
 
779
    weave_size = wf.tell()
 
780
 
 
781
    total = 0
 
782
    vers = len(w)
 
783
    for i in range(vers):
 
784
        pb.update('checking sizes', i, vers)
 
785
        for line in w.get_iter(i):
 
786
            total += len(line)
 
787
 
 
788
    pb.clear()
 
789
 
 
790
    print 'versions          %9d' % vers
 
791
    print 'weave file        %9d bytes' % weave_size
 
792
    print 'total contents    %9d bytes' % total
 
793
    print 'compression ratio %9.2fx' % (float(total) / float(weave_size))
 
794
    if vers:
 
795
        avg = total/vers
 
796
        print 'average size      %9d bytes' % avg
 
797
        print 'relative size     %9.2fx' % (float(weave_size) / float(avg))
 
798
 
 
799
 
 
800
def usage():
 
801
    print """bzr weave tool
 
802
 
 
803
Experimental tool for weave algorithm.
 
804
 
 
805
usage:
 
806
    weave init WEAVEFILE
 
807
        Create an empty weave file
 
808
    weave get WEAVEFILE VERSION
 
809
        Write out specified version.
 
810
    weave check WEAVEFILE
 
811
        Check consistency of all versions.
 
812
    weave toc WEAVEFILE
 
813
        Display table of contents.
 
814
    weave add WEAVEFILE NAME [BASE...] < NEWTEXT
 
815
        Add NEWTEXT, with specified parent versions.
 
816
    weave annotate WEAVEFILE VERSION
 
817
        Display origin of each line.
 
818
    weave mash WEAVEFILE VERSION...
 
819
        Display composite of all selected versions.
 
820
    weave merge WEAVEFILE VERSION1 VERSION2 > OUT
 
821
        Auto-merge two versions and display conflicts.
 
822
 
 
823
example:
 
824
 
 
825
    % weave init foo.weave
 
826
    % vi foo.txt
 
827
    % weave add foo.weave ver0 < foo.txt
 
828
    added version 0
 
829
 
 
830
    (create updated version)
 
831
    % vi foo.txt
 
832
    % weave get foo.weave 0 | diff -u - foo.txt
 
833
    % weave add foo.weave ver1 0 < foo.txt
 
834
    added version 1
 
835
 
 
836
    % weave get foo.weave 0 > foo.txt       (create forked version)
 
837
    % vi foo.txt
 
838
    % weave add foo.weave ver2 0 < foo.txt
 
839
    added version 2
 
840
 
 
841
    % weave merge foo.weave 1 2 > foo.txt   (merge them)
 
842
    % vi foo.txt                            (resolve conflicts)
 
843
    % weave add foo.weave merged 1 2 < foo.txt     (commit merged version)     
 
844
    
 
845
"""
 
846
    
 
847
 
 
848
 
 
849
def main(argv):
 
850
    import sys
 
851
    import os
 
852
    from weavefile import write_weave, read_weave
 
853
    from bzrlib.progress import ProgressBar
 
854
 
 
855
    try:
 
856
        import psyco
 
857
        psyco.full()
 
858
    except ImportError:
 
859
        pass
 
860
 
 
861
    if len(argv) < 2:
 
862
        usage()
 
863
        return 0
 
864
 
 
865
    cmd = argv[1]
 
866
 
 
867
    def readit():
 
868
        return read_weave(file(argv[2], 'rb'))
 
869
    
 
870
    if cmd == 'help':
 
871
        usage()
 
872
    elif cmd == 'add':
 
873
        w = readit()
 
874
        # at the moment, based on everything in the file
 
875
        name = argv[3]
 
876
        parents = map(int, argv[4:])
 
877
        lines = sys.stdin.readlines()
 
878
        ver = w.add(name, parents, lines)
 
879
        write_weave(w, file(argv[2], 'wb'))
 
880
        print 'added version %r %d' % (name, ver)
 
881
    elif cmd == 'init':
 
882
        fn = argv[2]
 
883
        if os.path.exists(fn):
 
884
            raise IOError("file exists")
 
885
        w = Weave()
 
886
        write_weave(w, file(fn, 'wb'))
 
887
    elif cmd == 'get': # get one version
 
888
        w = readit()
 
889
        sys.stdout.writelines(w.get_iter(int(argv[3])))
 
890
        
 
891
    elif cmd == 'mash': # get composite
 
892
        w = readit()
 
893
        sys.stdout.writelines(w.mash_iter(map(int, argv[3:])))
 
894
 
 
895
    elif cmd == 'annotate':
 
896
        w = readit()
 
897
        # newline is added to all lines regardless; too hard to get
 
898
        # reasonable formatting otherwise
 
899
        lasto = None
 
900
        for origin, text in w.annotate(int(argv[3])):
 
901
            text = text.rstrip('\r\n')
 
902
            if origin == lasto:
 
903
                print '      | %s' % (text)
 
904
            else:
 
905
                print '%5d | %s' % (origin, text)
 
906
                lasto = origin
 
907
                
 
908
    elif cmd == 'toc':
 
909
        weave_toc(readit())
 
910
 
 
911
    elif cmd == 'stats':
 
912
        weave_stats(argv[2])
 
913
        
 
914
    elif cmd == 'check':
 
915
        w = readit()
 
916
        pb = ProgressBar()
 
917
        w.check(pb)
 
918
        pb.clear()
 
919
        print '%d versions ok' % w.numversions()
 
920
 
 
921
    elif cmd == 'inclusions':
 
922
        w = readit()
 
923
        print ' '.join(map(str, w.inclusions([int(argv[3])])))
 
924
 
 
925
    elif cmd == 'parents':
 
926
        w = readit()
 
927
        print ' '.join(map(str, w._parents[int(argv[3])]))
 
928
 
 
929
    elif cmd == 'plan-merge':
 
930
        w = readit()
 
931
        for state, line in w.plan_merge(int(argv[3]), int(argv[4])):
 
932
            if line:
 
933
                print '%14s | %s' % (state, line),
 
934
 
 
935
    elif cmd == 'merge':
 
936
        w = readit()
 
937
        p = w.plan_merge(int(argv[3]), int(argv[4]))
 
938
        sys.stdout.writelines(w.weave_merge(p))
 
939
            
 
940
    elif cmd == 'mash-merge':
 
941
        if len(argv) != 5:
 
942
            usage()
 
943
            return 1
 
944
 
 
945
        w = readit()
 
946
        v1, v2 = map(int, argv[3:5])
 
947
 
 
948
        basis = w.inclusions([v1]).intersection(w.inclusions([v2]))
 
949
 
 
950
        base_lines = list(w.mash_iter(basis))
 
951
        a_lines = list(w.get(v1))
 
952
        b_lines = list(w.get(v2))
 
953
 
 
954
        from bzrlib.merge3 import Merge3
 
955
        m3 = Merge3(base_lines, a_lines, b_lines)
 
956
 
 
957
        name_a = 'version %d' % v1
 
958
        name_b = 'version %d' % v2
 
959
        sys.stdout.writelines(m3.merge_lines(name_a=name_a, name_b=name_b))
 
960
    else:
 
961
        raise ValueError('unknown command %r' % cmd)
 
962
    
 
963
 
 
964
 
 
965
def profile_main(argv): 
 
966
    import tempfile, hotshot, hotshot.stats
 
967
 
 
968
    prof_f = tempfile.NamedTemporaryFile()
 
969
 
 
970
    prof = hotshot.Profile(prof_f.name)
 
971
 
 
972
    ret = prof.runcall(main, argv)
 
973
    prof.close()
 
974
 
 
975
    stats = hotshot.stats.load(prof_f.name)
 
976
    #stats.strip_dirs()
 
977
    stats.sort_stats('cumulative')
 
978
    ## XXX: Might like to write to stderr or the trace file instead but
 
979
    ## print_stats seems hardcoded to stdout
 
980
    stats.print_stats(20)
 
981
            
 
982
    return ret
 
983
 
 
984
 
 
985
if __name__ == '__main__':
 
986
    import sys
 
987
    if '--profile' in sys.argv:
 
988
        args = sys.argv[:]
 
989
        args.remove('--profile')
 
990
        sys.exit(profile_main(args))
 
991
    else:
 
992
        sys.exit(main(sys.argv))
 
993