/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-19 10:46:28 UTC
  • Revision ID: mbp@sourcefrog.net-20050919104627-4b7d6e820a7ecf3e
- draft patch to cache weave inclusions
  (doesn't seem to speed things very much)

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