/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: Michael Ellerman
  • Date: 2005-10-26 10:03:47 UTC
  • mfrom: (1185.16.116)
  • mto: (1185.16.126)
  • mto: This revision was merged to the branch mainline in revision 1488.
  • Revision ID: michael@ellerman.id.au-20051026100347-bb0b2bd42f7953f2
MergeĀ mainline.

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
 
 
25
# XXX: If we do weaves this way, will a merge still behave the same
 
26
# way if it's done in a different order?  That's a pretty desirable
 
27
# property.
 
28
 
 
29
# TODO: Nothing here so far assumes the lines are really \n newlines,
 
30
# rather than being split up in some other way.  We could accomodate
 
31
# binaries, perhaps by naively splitting on \n or perhaps using
 
32
# something like a rolling checksum.
 
33
 
 
34
# TODO: End marker for each version so we can stop reading?
 
35
 
 
36
# TODO: Check that no insertion occurs inside a deletion that was
 
37
# active in the version of the insertion.
 
38
 
 
39
# TODO: In addition to the SHA-1 check, perhaps have some code that
 
40
# checks structural constraints of the weave: ie that insertions are
 
41
# properly nested, that there is no text outside of an insertion, that
 
42
# insertions or deletions are not repeated, etc.
 
43
 
 
44
# TODO: Parallel-extract that passes back each line along with a
 
45
# description of which revisions include it.  Nice for checking all
 
46
# shas or calculating stats in parallel.
 
47
 
 
48
# TODO: Using a single _extract routine and then processing the output
 
49
# is probably inefficient.  It's simple enough that we can afford to
 
50
# have slight specializations for different ways its used: annotate,
 
51
# basis for add, get, etc.
 
52
 
 
53
# TODO: Probably the API should work only in names to hide the integer
 
54
# indexes from the user.
 
55
 
 
56
# TODO: Is there any potential performance win by having an add()
 
57
# variant that is passed a pre-cooked version of the single basis
 
58
# version?
 
59
 
 
60
# TODO: Reweave can possibly be made faster by remembering diffs
 
61
# where the basis and destination are unchanged.
 
62
 
 
63
# FIXME: Sometimes we will be given a parents list for a revision
 
64
# that includes some redundant parents (i.e. already a parent of 
 
65
# something in the list.)  We should eliminate them.  This can 
 
66
# be done fairly efficiently because the sequence numbers constrain
 
67
# the possible relationships.
 
68
 
 
69
 
 
70
import sha
 
71
from difflib import SequenceMatcher
 
72
 
 
73
from bzrlib.trace import mutter
 
74
from bzrlib.errors import WeaveError, WeaveFormatError, WeaveParentMismatch, \
 
75
        WeaveRevisionNotPresent, WeaveRevisionAlreadyPresent
 
76
 
 
77
 
 
78
class Weave(object):
 
79
    """weave - versioned text file storage.
 
80
    
 
81
    A Weave manages versions of line-based text files, keeping track
 
82
    of the originating version for each line.
 
83
 
 
84
    To clients the "lines" of the file are represented as a list of strings.
 
85
    These strings  will typically have terminal newline characters, but
 
86
    this is not required.  In particular files commonly do not have a newline
 
87
    at the end of the file.
 
88
 
 
89
    Texts can be identified in either of two ways:
 
90
 
 
91
    * a nonnegative index number.
 
92
 
 
93
    * a version-id string.
 
94
 
 
95
    Typically the index number will be valid only inside this weave and
 
96
    the version-id is used to reference it in the larger world.
 
97
 
 
98
    The weave is represented as a list mixing edit instructions and
 
99
    literal text.  Each entry in _weave can be either a string (or
 
100
    unicode), or a tuple.  If a string, it means that the given line
 
101
    should be output in the currently active revisions.
 
102
 
 
103
    If a tuple, it gives a processing instruction saying in which
 
104
    revisions the enclosed lines are active.  The tuple has the form
 
105
    (instruction, version).
 
106
 
 
107
    The instruction can be '{' or '}' for an insertion block, and '['
 
108
    and ']' for a deletion block respectively.  The version is the
 
109
    integer version index.  There is no replace operator, only deletes
 
110
    and inserts.  For '}', the end of an insertion, there is no
 
111
    version parameter because it always closes the most recently
 
112
    opened insertion.
 
113
 
 
114
    Constraints/notes:
 
115
 
 
116
    * A later version can delete lines that were introduced by any
 
117
      number of ancestor versions; this implies that deletion
 
118
      instructions can span insertion blocks without regard to the
 
119
      insertion block's nesting.
 
120
 
 
121
    * Similarly, deletions need not be properly nested with regard to
 
122
      each other, because they might have been generated by
 
123
      independent revisions.
 
124
 
 
125
    * Insertions are always made by inserting a new bracketed block
 
126
      into a single point in the previous weave.  This implies they
 
127
      can nest but not overlap, and the nesting must always have later
 
128
      insertions on the inside.
 
129
 
 
130
    * It doesn't seem very useful to have an active insertion
 
131
      inside an inactive insertion, but it might happen.
 
132
      
 
133
    * Therefore, all instructions are always"considered"; that
 
134
      is passed onto and off the stack.  An outer inactive block
 
135
      doesn't disable an inner block.
 
136
 
 
137
    * Lines are enabled if the most recent enclosing insertion is
 
138
      active and none of the enclosing deletions are active.
 
139
 
 
140
    * There is no point having a deletion directly inside its own
 
141
      insertion; you might as well just not write it.  And there
 
142
      should be no way to get an earlier version deleting a later
 
143
      version.
 
144
 
 
145
    _weave
 
146
        Text of the weave; list of control instruction tuples and strings.
 
147
 
 
148
    _parents
 
149
        List of parents, indexed by version number.
 
150
        It is only necessary to store the minimal set of parents for
 
151
        each version; the parent's parents are implied.
 
152
 
 
153
    _sha1s
 
154
        List of hex SHA-1 of each version.
 
155
 
 
156
    _names
 
157
        List of symbolic names for each version.  Each should be unique.
 
158
 
 
159
    _name_map
 
160
        For each name, the version number.
 
161
 
 
162
    _weave_name
 
163
        Descriptive name of this weave; typically the filename if known.
 
164
        Set by read_weave.
 
165
    """
 
166
 
 
167
    __slots__ = ['_weave', '_parents', '_sha1s', '_names', '_name_map',
 
168
                 '_weave_name']
 
169
    
 
170
    def __init__(self, weave_name=None):
 
171
        self._weave = []
 
172
        self._parents = []
 
173
        self._sha1s = []
 
174
        self._names = []
 
175
        self._name_map = {}
 
176
        self._weave_name = weave_name
 
177
 
 
178
 
 
179
    def copy(self):
 
180
        """Return a deep copy of self.
 
181
        
 
182
        The copy can be modified without affecting the original weave."""
 
183
        other = Weave()
 
184
        other._weave = self._weave[:]
 
185
        other._parents = self._parents[:]
 
186
        other._sha1s = self._sha1s[:]
 
187
        other._names = self._names[:]
 
188
        other._name_map = self._name_map.copy()
 
189
        other._weave_name = self._weave_name
 
190
        return other
 
191
 
 
192
    def __eq__(self, other):
 
193
        if not isinstance(other, Weave):
 
194
            return False
 
195
        return self._parents == other._parents \
 
196
               and self._weave == other._weave \
 
197
               and self._sha1s == other._sha1s 
 
198
 
 
199
    
 
200
    def __ne__(self, other):
 
201
        return not self.__eq__(other)
 
202
 
 
203
 
 
204
    def maybe_lookup(self, name_or_index):
 
205
        """Convert possible symbolic name to index, or pass through indexes."""
 
206
        if isinstance(name_or_index, (int, long)):
 
207
            return name_or_index
 
208
        else:
 
209
            return self.lookup(name_or_index)
 
210
 
 
211
        
 
212
    def lookup(self, name):
 
213
        """Convert symbolic version name to index."""
 
214
        try:
 
215
            return self._name_map[name]
 
216
        except KeyError:
 
217
            raise WeaveRevisionNotPresent(name, self)
 
218
 
 
219
    def names(self):
 
220
        return self._names[:]
 
221
 
 
222
    def iter_names(self):
 
223
        """Yield a list of all names in this weave."""
 
224
        return iter(self._names)
 
225
 
 
226
    def idx_to_name(self, version):
 
227
        return self._names[version]
 
228
 
 
229
    def _check_repeated_add(self, name, parents, text, sha1):
 
230
        """Check that a duplicated add is OK.
 
231
 
 
232
        If it is, return the (old) index; otherwise raise an exception.
 
233
        """
 
234
        idx = self.lookup(name)
 
235
        if sorted(self._parents[idx]) != sorted(parents) \
 
236
            or sha1 != self._sha1s[idx]:
 
237
            raise WeaveRevisionAlreadyPresent(name, self)
 
238
        return idx
 
239
        
 
240
    def add(self, name, parents, text, sha1=None):
 
241
        """Add a single text on top of the weave.
 
242
  
 
243
        Returns the index number of the newly added version.
 
244
 
 
245
        name
 
246
            Symbolic name for this version.
 
247
            (Typically the revision-id of the revision that added it.)
 
248
 
 
249
        parents
 
250
            List or set of direct parent version numbers.
 
251
            
 
252
        text
 
253
            Sequence of lines to be added in the new version.
 
254
 
 
255
        sha -- SHA-1 of the file, if known.  This is trusted to be
 
256
            correct if supplied.
 
257
        """
 
258
        from bzrlib.osutils import sha_strings
 
259
 
 
260
        assert isinstance(name, basestring)
 
261
        if sha1 is None:
 
262
            sha1 = sha_strings(text)
 
263
        if name in self._name_map:
 
264
            return self._check_repeated_add(name, parents, text, sha1)
 
265
 
 
266
        parents = map(self.maybe_lookup, parents)
 
267
        self._check_versions(parents)
 
268
        ## self._check_lines(text)
 
269
        new_version = len(self._parents)
 
270
 
 
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
        s = SequenceMatcher(None, basis_lines, text)
 
326
 
 
327
        # offset gives the number of lines that have been inserted
 
328
        # into the weave up to the current point; if the original edit instruction
 
329
        # says to change line A then we actually change (A+offset)
 
330
        offset = 0
 
331
 
 
332
        for tag, i1, i2, j1, j2 in s.get_opcodes():
 
333
            # i1,i2 are given in offsets within basis_lines; we need to map them
 
334
            # back to offsets within the entire weave
 
335
            #print 'raw match', tag, i1, i2, j1, j2
 
336
            if tag == 'equal':
 
337
                continue
 
338
 
 
339
            i1 = basis_lineno[i1]
 
340
            i2 = basis_lineno[i2]
 
341
 
 
342
            assert 0 <= j1 <= j2 <= len(text)
 
343
 
 
344
            #print tag, i1, i2, j1, j2
 
345
 
 
346
            # the deletion and insertion are handled separately.
 
347
            # first delete the region.
 
348
            if i1 != i2:
 
349
                self._weave.insert(i1+offset, ('[', new_version))
 
350
                self._weave.insert(i2+offset+1, (']', new_version))
 
351
                offset += 2
 
352
 
 
353
            if j1 != j2:
 
354
                # there may have been a deletion spanning up to
 
355
                # i2; we want to insert after this region to make sure
 
356
                # we don't destroy ourselves
 
357
                i = i2 + offset
 
358
                self._weave[i:i] = ([('{', new_version)] 
 
359
                                    + text[j1:j2] 
 
360
                                    + [('}', None)])
 
361
                offset += 2 + (j2 - j1)
 
362
 
 
363
        return new_version
 
364
 
 
365
    def add_identical(self, old_rev_id, new_rev_id, parents):
 
366
        """Add an identical text to old_rev_id as new_rev_id."""
 
367
        old_lines = self.get(self.lookup(old_rev_id))
 
368
        self.add(new_rev_id, parents, old_lines)
 
369
 
 
370
    def inclusions(self, versions):
 
371
        """Return set of all ancestors of given version(s)."""
 
372
        i = set(versions)
 
373
        for v in xrange(max(versions), 0, -1):
 
374
            if v in i:
 
375
                # include all its parents
 
376
                i.update(self._parents[v])
 
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 parent_names(self, version):
 
387
        """Return version names for parents of a version."""
 
388
        return map(self.idx_to_name, self._parents[self.lookup(version)])
 
389
 
 
390
 
 
391
    def minimal_parents(self, version):
 
392
        """Find the minimal set of parents for the version."""
 
393
        included = self._parents[version]
 
394
        if not included:
 
395
            return []
 
396
        
 
397
        li = list(included)
 
398
        li.sort(reverse=True)
 
399
 
 
400
        mininc = []
 
401
        gotit = set()
 
402
 
 
403
        for pv in li:
 
404
            if pv not in gotit:
 
405
                mininc.append(pv)
 
406
                gotit.update(self.inclusions(pv))
 
407
 
 
408
        assert mininc[0] >= 0
 
409
        assert mininc[-1] < version
 
410
        return mininc
 
411
 
 
412
 
 
413
 
 
414
    def _check_lines(self, text):
 
415
        if not isinstance(text, list):
 
416
            raise ValueError("text should be a list, not %s" % type(text))
 
417
 
 
418
        for l in text:
 
419
            if not isinstance(l, basestring):
 
420
                raise ValueError("text line should be a string or unicode, not %s"
 
421
                                 % type(l))
 
422
        
 
423
 
 
424
 
 
425
    def _check_versions(self, indexes):
 
426
        """Check everything in the sequence of indexes is valid"""
 
427
        for i in indexes:
 
428
            try:
 
429
                self._parents[i]
 
430
            except IndexError:
 
431
                raise IndexError("invalid version number %r" % i)
 
432
 
 
433
    
 
434
    def annotate(self, name_or_index):
 
435
        return list(self.annotate_iter(name_or_index))
 
436
 
 
437
 
 
438
    def annotate_iter(self, name_or_index):
 
439
        """Yield list of (index-id, line) pairs for the specified version.
 
440
 
 
441
        The index indicates when the line originated in the weave."""
 
442
        incls = [self.maybe_lookup(name_or_index)]
 
443
        for origin, lineno, text in self._extract(incls):
 
444
            yield origin, text
 
445
 
 
446
    def _walk(self):
 
447
        """Walk the weave.
 
448
 
 
449
        Yields sequence of
 
450
        (lineno, insert, deletes, text)
 
451
        for each literal line.
 
452
        """
 
453
        
 
454
        istack = []
 
455
        dset = set()
 
456
 
 
457
        lineno = 0         # line of weave, 0-based
 
458
 
 
459
        for l in self._weave:
 
460
            if isinstance(l, tuple):
 
461
                c, v = l
 
462
                isactive = None
 
463
                if c == '{':
 
464
                    istack.append(v)
 
465
                elif c == '}':
 
466
                    istack.pop()
 
467
                elif c == '[':
 
468
                    assert v not in dset
 
469
                    dset.add(v)
 
470
                elif c == ']':
 
471
                    dset.remove(v)
 
472
                else:
 
473
                    raise WeaveFormatError('unexpected instruction %r' % v)
 
474
            else:
 
475
                assert isinstance(l, basestring)
 
476
                assert istack
 
477
                yield lineno, istack[-1], dset, l
 
478
            lineno += 1
 
479
 
 
480
 
 
481
 
 
482
    def _extract(self, versions):
 
483
        """Yield annotation of lines in included set.
 
484
 
 
485
        Yields a sequence of tuples (origin, lineno, text), where
 
486
        origin is the origin version, lineno the index in the weave,
 
487
        and text the text of the line.
 
488
 
 
489
        The set typically but not necessarily corresponds to a version.
 
490
        """
 
491
        for i in versions:
 
492
            if not isinstance(i, int):
 
493
                raise ValueError(i)
 
494
            
 
495
        included = self.inclusions(versions)
 
496
 
 
497
        istack = []
 
498
        dset = set()
 
499
 
 
500
        lineno = 0         # line of weave, 0-based
 
501
 
 
502
        isactive = None
 
503
 
 
504
        result = []
 
505
 
 
506
        WFE = WeaveFormatError
 
507
 
 
508
        for l in self._weave:
 
509
            if isinstance(l, tuple):
 
510
                c, v = l
 
511
                isactive = None
 
512
                if c == '{':
 
513
                    assert v not in istack
 
514
                    istack.append(v)
 
515
                elif c == '}':
 
516
                    istack.pop()
 
517
                elif c == '[':
 
518
                    if v in included:
 
519
                        assert v not in dset
 
520
                        dset.add(v)
 
521
                else:
 
522
                    assert c == ']'
 
523
                    if v in included:
 
524
                        assert v in dset
 
525
                        dset.remove(v)
 
526
            else:
 
527
                assert isinstance(l, basestring)
 
528
                if isactive is None:
 
529
                    isactive = (not dset) and istack and (istack[-1] in included)
 
530
                if isactive:
 
531
                    result.append((istack[-1], lineno, l))
 
532
            lineno += 1
 
533
        if istack:
 
534
            raise WeaveFormatError("unclosed insertion blocks "
 
535
                    "at end of weave: %s" % istack)
 
536
        if dset:
 
537
            raise WeaveFormatError("unclosed deletion blocks at end of weave: %s"
 
538
                                   % dset)
 
539
        return result
 
540
 
 
541
 
 
542
    def get_iter(self, name_or_index):
 
543
        """Yield lines for the specified version."""
 
544
        incls = [self.maybe_lookup(name_or_index)]
 
545
        for origin, lineno, line in self._extract(incls):
 
546
            yield line
 
547
 
 
548
 
 
549
    def get_text(self, name_or_index):
 
550
        return ''.join(self.get_iter(name_or_index))
 
551
        assert isinstance(version, int)
 
552
 
 
553
 
 
554
    def get_lines(self, name_or_index):
 
555
        return list(self.get_iter(name_or_index))
 
556
 
 
557
 
 
558
    get = get_lines
 
559
 
 
560
 
 
561
    def mash_iter(self, included):
 
562
        """Return composed version of multiple included versions."""
 
563
        included = map(self.maybe_lookup, included)
 
564
        for origin, lineno, text in self._extract(included):
 
565
            yield text
 
566
 
 
567
 
 
568
    def dump(self, to_file):
 
569
        from pprint import pprint
 
570
        print >>to_file, "Weave._weave = ",
 
571
        pprint(self._weave, to_file)
 
572
        print >>to_file, "Weave._parents = ",
 
573
        pprint(self._parents, to_file)
 
574
 
 
575
 
 
576
 
 
577
    def numversions(self):
 
578
        l = len(self._parents)
 
579
        assert l == len(self._sha1s)
 
580
        return l
 
581
 
 
582
 
 
583
    def __len__(self):
 
584
        return self.numversions()
 
585
 
 
586
 
 
587
    def check(self, progress_bar=None):
 
588
        # check no circular inclusions
 
589
        for version in range(self.numversions()):
 
590
            inclusions = list(self._parents[version])
 
591
            if inclusions:
 
592
                inclusions.sort()
 
593
                if inclusions[-1] >= version:
 
594
                    raise WeaveFormatError("invalid included version %d for index %d"
 
595
                                           % (inclusions[-1], version))
 
596
 
 
597
        # try extracting all versions; this is a bit slow and parallel
 
598
        # extraction could be used
 
599
        nv = self.numversions()
 
600
        for version in range(nv):
 
601
            if progress_bar:
 
602
                progress_bar.update('checking text', version, nv)
 
603
            s = sha.new()
 
604
            for l in self.get_iter(version):
 
605
                s.update(l)
 
606
            hd = s.hexdigest()
 
607
            expected = self._sha1s[version]
 
608
            if hd != expected:
 
609
                raise WeaveError("mismatched sha1 for version %d; "
 
610
                                 "got %s, expected %s"
 
611
                                 % (version, hd, expected))
 
612
 
 
613
        # TODO: check insertions are properly nested, that there are
 
614
        # no lines outside of insertion blocks, that deletions are
 
615
        # properly paired, etc.
 
616
 
 
617
 
 
618
 
 
619
    def merge(self, merge_versions):
 
620
        """Automerge and mark conflicts between versions.
 
621
 
 
622
        This returns a sequence, each entry describing alternatives
 
623
        for a chunk of the file.  Each of the alternatives is given as
 
624
        a list of lines.
 
625
 
 
626
        If there is a chunk of the file where there's no diagreement,
 
627
        only one alternative is given.
 
628
        """
 
629
        # approach: find the included versions common to all the
 
630
        # merged versions
 
631
        raise NotImplementedError()
 
632
 
 
633
 
 
634
 
 
635
    def _delta(self, included, lines):
 
636
        """Return changes from basis to new revision.
 
637
 
 
638
        The old text for comparison is the union of included revisions.
 
639
 
 
640
        This is used in inserting a new text.
 
641
 
 
642
        Delta is returned as a sequence of
 
643
        (weave1, weave2, newlines).
 
644
 
 
645
        This indicates that weave1:weave2 of the old weave should be
 
646
        replaced by the sequence of lines in newlines.  Note that
 
647
        these line numbers are positions in the total weave and don't
 
648
        correspond to the lines in any extracted version, or even the
 
649
        extracted union of included versions.
 
650
 
 
651
        If line1=line2, this is a pure insert; if newlines=[] this is a
 
652
        pure delete.  (Similar to difflib.)
 
653
        """
 
654
        raise NotImplementedError()
 
655
 
 
656
            
 
657
    def plan_merge(self, ver_a, ver_b):
 
658
        """Return pseudo-annotation indicating how the two versions merge.
 
659
 
 
660
        This is computed between versions a and b and their common
 
661
        base.
 
662
 
 
663
        Weave lines present in none of them are skipped entirely.
 
664
        """
 
665
        inc_a = self.inclusions([ver_a])
 
666
        inc_b = self.inclusions([ver_b])
 
667
        inc_c = inc_a & inc_b
 
668
 
 
669
        for lineno, insert, deleteset, line in self._walk():
 
670
            if deleteset & inc_c:
 
671
                # killed in parent; can't be in either a or b
 
672
                # not relevant to our work
 
673
                yield 'killed-base', line
 
674
            elif insert in inc_c:
 
675
                # was inserted in base
 
676
                killed_a = bool(deleteset & inc_a)
 
677
                killed_b = bool(deleteset & inc_b)
 
678
                if killed_a and killed_b:
 
679
                    yield 'killed-both', line
 
680
                elif killed_a:
 
681
                    yield 'killed-a', line
 
682
                elif killed_b:
 
683
                    yield 'killed-b', line
 
684
                else:
 
685
                    yield 'unchanged', line
 
686
            elif insert in inc_a:
 
687
                if deleteset & inc_a:
 
688
                    yield 'ghost-a', line
 
689
                else:
 
690
                    # new in A; not in B
 
691
                    yield 'new-a', line
 
692
            elif insert in inc_b:
 
693
                if deleteset & inc_b:
 
694
                    yield 'ghost-b', line
 
695
                else:
 
696
                    yield 'new-b', line
 
697
            else:
 
698
                # not in either revision
 
699
                yield 'irrelevant', line
 
700
 
 
701
        yield 'unchanged', ''           # terminator
 
702
 
 
703
 
 
704
 
 
705
    def weave_merge(self, plan):
 
706
        lines_a = []
 
707
        lines_b = []
 
708
        ch_a = ch_b = False
 
709
 
 
710
        for state, line in plan:
 
711
            if state == 'unchanged' or state == 'killed-both':
 
712
                # resync and flush queued conflicts changes if any
 
713
                if not lines_a and not lines_b:
 
714
                    pass
 
715
                elif ch_a and not ch_b:
 
716
                    # one-sided change:                    
 
717
                    for l in lines_a: yield l
 
718
                elif ch_b and not ch_a:
 
719
                    for l in lines_b: yield l
 
720
                elif lines_a == lines_b:
 
721
                    for l in lines_a: yield l
 
722
                else:
 
723
                    yield '<<<<\n'
 
724
                    for l in lines_a: yield l
 
725
                    yield '====\n'
 
726
                    for l in lines_b: yield l
 
727
                    yield '>>>>\n'
 
728
 
 
729
                del lines_a[:]
 
730
                del lines_b[:]
 
731
                ch_a = ch_b = False
 
732
                
 
733
            if state == 'unchanged':
 
734
                if line:
 
735
                    yield line
 
736
            elif state == 'killed-a':
 
737
                ch_a = True
 
738
                lines_b.append(line)
 
739
            elif state == 'killed-b':
 
740
                ch_b = True
 
741
                lines_a.append(line)
 
742
            elif state == 'new-a':
 
743
                ch_a = True
 
744
                lines_a.append(line)
 
745
            elif state == 'new-b':
 
746
                ch_b = True
 
747
                lines_b.append(line)
 
748
            else:
 
749
                assert state in ('irrelevant', 'ghost-a', 'ghost-b', 'killed-base',
 
750
                                 'killed-both'), \
 
751
                       state
 
752
 
 
753
                
 
754
    def join(self, other):
 
755
        """Integrate versions from other into this weave.
 
756
 
 
757
        The resulting weave contains all the history of both weaves; 
 
758
        any version you could retrieve from either self or other can be 
 
759
        retrieved from self after this call.
 
760
 
 
761
        It is illegal for the two weaves to contain different values 
 
762
        or different parents for any version.  See also reweave().
 
763
        """
 
764
        if other.numversions() == 0:
 
765
            return          # nothing to update, easy
 
766
        # two loops so that we do not change ourselves before verifying it
 
767
        # will be ok
 
768
        # work through in index order to make sure we get all dependencies
 
769
        for other_idx, name in enumerate(other._names):
 
770
            if self._check_version_consistent(other, other_idx, name):
 
771
                continue
 
772
        for other_idx, name in enumerate(other._names):
 
773
            # TODO: If all the parents of the other version are already 
 
774
            # present then we can avoid some work by just taking the delta
 
775
            # and adjusting the offsets.
 
776
            new_parents = self._imported_parents(other, other_idx)
 
777
            lines = other.get_lines(other_idx)
 
778
            sha1 = other._sha1s[other_idx]
 
779
            self.add(name, new_parents, lines, sha1)
 
780
 
 
781
 
 
782
    def _imported_parents(self, other, other_idx):
 
783
        """Return list of parents in self corresponding to indexes in other."""
 
784
        new_parents = []
 
785
        for parent_idx in other._parents[other_idx]:
 
786
            parent_name = other._names[parent_idx]
 
787
            if parent_name not in self._names:
 
788
                # should not be possible
 
789
                raise WeaveError("missing parent {%s} of {%s} in %r" 
 
790
                                 % (parent_name, other._name_map[other_idx], self))
 
791
            new_parents.append(self._name_map[parent_name])
 
792
        return new_parents
 
793
 
 
794
    def _check_version_consistent(self, other, other_idx, name):
 
795
        """Check if a version in consistent in this and other.
 
796
 
 
797
        To be consistent it must have:
 
798
 
 
799
         * the same text
 
800
         * the same direct parents (by name, not index, and disregarding
 
801
           order)
 
802
        
 
803
        If present & correct return True;
 
804
        if not present in self return False; 
 
805
        if inconsistent raise error."""
 
806
        this_idx = self._name_map.get(name, -1)
 
807
        if this_idx != -1:
 
808
            if self._sha1s[this_idx] != other._sha1s[other_idx]:
 
809
                raise WeaveError("inconsistent texts for version {%s} "
 
810
                                 "when joining weaves"
 
811
                                 % (name))
 
812
            self_parents = self._parents[this_idx]
 
813
            other_parents = other._parents[other_idx]
 
814
            n1 = [self._names[i] for i in self_parents]
 
815
            n2 = [other._names[i] for i in other_parents]
 
816
            n1.sort()
 
817
            n2.sort()
 
818
            if n1 != n2:
 
819
                raise WeaveParentMismatch("inconsistent parents "
 
820
                    "for version {%s}: %s vs %s" % (name, n1, n2))
 
821
            else:
 
822
                return True         # ok!
 
823
        else:
 
824
            return False
 
825
 
 
826
    def reweave(self, other):
 
827
        """Reweave self with other."""
 
828
        new_weave = reweave(self, other)
 
829
        for attr in self.__slots__:
 
830
            setattr(self, attr, getattr(new_weave, attr))
 
831
 
 
832
 
 
833
def reweave(wa, wb):
 
834
    """Combine two weaves and return the result.
 
835
 
 
836
    This works even if a revision R has different parents in 
 
837
    wa and wb.  In the resulting weave all the parents are given.
 
838
 
 
839
    This is done by just building up a new weave, maintaining ordering 
 
840
    of the versions in the two inputs.  More efficient approaches
 
841
    might be possible but it should only be necessary to do 
 
842
    this operation rarely, when a new previously ghost version is 
 
843
    inserted.
 
844
    """
 
845
    wr = Weave()
 
846
    ia = ib = 0
 
847
    queue_a = range(wa.numversions())
 
848
    queue_b = range(wb.numversions())
 
849
    # first determine combined parents of all versions
 
850
    # map from version name -> all parent names
 
851
    combined_parents = _reweave_parent_graphs(wa, wb)
 
852
    mutter("combined parents: %r", combined_parents)
 
853
    order = _make_reweave_order(wa._names, wb._names, combined_parents)
 
854
    mutter("order to reweave: %r", order)
 
855
    for name in order:
 
856
        if name in wa._name_map:
 
857
            lines = wa.get_lines(name)
 
858
            if name in wb._name_map:
 
859
                assert lines == wb.get_lines(name)
 
860
        else:
 
861
            lines = wb.get_lines(name)
 
862
        wr.add(name, combined_parents[name], lines)
 
863
    return wr
 
864
 
 
865
 
 
866
def _reweave_parent_graphs(wa, wb):
 
867
    """Return combined parent ancestry for two weaves.
 
868
    
 
869
    Returned as a list of (version_name, set(parent_names))"""
 
870
    combined = {}
 
871
    for weave in [wa, wb]:
 
872
        for idx, name in enumerate(weave._names):
 
873
            p = combined.setdefault(name, set())
 
874
            p.update(map(weave.idx_to_name, weave._parents[idx]))
 
875
    return combined
 
876
 
 
877
 
 
878
def _make_reweave_order(wa_order, wb_order, combined_parents):
 
879
    """Return an order to reweave versions respecting parents."""
 
880
    done = set()
 
881
    result = []
 
882
    ia = ib = 0
 
883
    next_a = next_b = None
 
884
    len_a = len(wa_order)
 
885
    len_b = len(wb_order)
 
886
    while ia < len(wa_order) or ib < len(wb_order):
 
887
        if ia < len_a:
 
888
            next_a = wa_order[ia]
 
889
            if next_a in done:
 
890
                ia += 1
 
891
                continue
 
892
            if combined_parents[next_a].issubset(done):
 
893
                ia += 1
 
894
                result.append(next_a)
 
895
                done.add(next_a)
 
896
                continue
 
897
        if ib < len_b:
 
898
            next_b = wb_order[ib]
 
899
            if next_b in done:
 
900
                ib += 1
 
901
                continue
 
902
            elif combined_parents[next_b].issubset(done):
 
903
                ib += 1
 
904
                result.append(next_b)
 
905
                done.add(next_b)
 
906
                continue
 
907
        raise WeaveError("don't know how to reweave at {%s} and {%s}"
 
908
                         % (next_a, next_b))
 
909
    return result
 
910
 
 
911
 
 
912
def weave_toc(w):
 
913
    """Show the weave's table-of-contents"""
 
914
    print '%6s %50s %10s %10s' % ('ver', 'name', 'sha1', 'parents')
 
915
    for i in (6, 50, 10, 10):
 
916
        print '-' * i,
 
917
    print
 
918
    for i in range(w.numversions()):
 
919
        sha1 = w._sha1s[i]
 
920
        name = w._names[i]
 
921
        parent_str = ' '.join(map(str, w._parents[i]))
 
922
        print '%6d %-50.50s %10.10s %s' % (i, name, sha1, parent_str)
 
923
 
 
924
 
 
925
 
 
926
def weave_stats(weave_file, pb):
 
927
    from bzrlib.weavefile import read_weave
 
928
 
 
929
    wf = file(weave_file, 'rb')
 
930
    w = read_weave(wf)
 
931
    # FIXME: doesn't work on pipes
 
932
    weave_size = wf.tell()
 
933
 
 
934
    total = 0
 
935
    vers = len(w)
 
936
    for i in range(vers):
 
937
        pb.update('checking sizes', i, vers)
 
938
        for origin, lineno, line in w._extract([i]):
 
939
            total += len(line)
 
940
 
 
941
    pb.clear()
 
942
 
 
943
    print 'versions          %9d' % vers
 
944
    print 'weave file        %9d bytes' % weave_size
 
945
    print 'total contents    %9d bytes' % total
 
946
    print 'compression ratio %9.2fx' % (float(total) / float(weave_size))
 
947
    if vers:
 
948
        avg = total/vers
 
949
        print 'average size      %9d bytes' % avg
 
950
        print 'relative size     %9.2fx' % (float(weave_size) / float(avg))
 
951
 
 
952
 
 
953
def usage():
 
954
    print """bzr weave tool
 
955
 
 
956
Experimental tool for weave algorithm.
 
957
 
 
958
usage:
 
959
    weave init WEAVEFILE
 
960
        Create an empty weave file
 
961
    weave get WEAVEFILE VERSION
 
962
        Write out specified version.
 
963
    weave check WEAVEFILE
 
964
        Check consistency of all versions.
 
965
    weave toc WEAVEFILE
 
966
        Display table of contents.
 
967
    weave add WEAVEFILE NAME [BASE...] < NEWTEXT
 
968
        Add NEWTEXT, with specified parent versions.
 
969
    weave annotate WEAVEFILE VERSION
 
970
        Display origin of each line.
 
971
    weave mash WEAVEFILE VERSION...
 
972
        Display composite of all selected versions.
 
973
    weave merge WEAVEFILE VERSION1 VERSION2 > OUT
 
974
        Auto-merge two versions and display conflicts.
 
975
    weave diff WEAVEFILE VERSION1 VERSION2 
 
976
        Show differences between two versions.
 
977
 
 
978
example:
 
979
 
 
980
    % weave init foo.weave
 
981
    % vi foo.txt
 
982
    % weave add foo.weave ver0 < foo.txt
 
983
    added version 0
 
984
 
 
985
    (create updated version)
 
986
    % vi foo.txt
 
987
    % weave get foo.weave 0 | diff -u - foo.txt
 
988
    % weave add foo.weave ver1 0 < foo.txt
 
989
    added version 1
 
990
 
 
991
    % weave get foo.weave 0 > foo.txt       (create forked version)
 
992
    % vi foo.txt
 
993
    % weave add foo.weave ver2 0 < foo.txt
 
994
    added version 2
 
995
 
 
996
    % weave merge foo.weave 1 2 > foo.txt   (merge them)
 
997
    % vi foo.txt                            (resolve conflicts)
 
998
    % weave add foo.weave merged 1 2 < foo.txt     (commit merged version)     
 
999
    
 
1000
"""
 
1001
    
 
1002
 
 
1003
 
 
1004
def main(argv):
 
1005
    import sys
 
1006
    import os
 
1007
    try:
 
1008
        import bzrlib
 
1009
    except ImportError:
 
1010
        # in case we're run directly from the subdirectory
 
1011
        sys.path.append('..')
 
1012
        import bzrlib
 
1013
    from bzrlib.weavefile import write_weave, read_weave
 
1014
    from bzrlib.progress import ProgressBar
 
1015
 
 
1016
    try:
 
1017
        import psyco
 
1018
        psyco.full()
 
1019
    except ImportError:
 
1020
        pass
 
1021
 
 
1022
    if len(argv) < 2:
 
1023
        usage()
 
1024
        return 0
 
1025
 
 
1026
    cmd = argv[1]
 
1027
 
 
1028
    def readit():
 
1029
        return read_weave(file(argv[2], 'rb'))
 
1030
    
 
1031
    if cmd == 'help':
 
1032
        usage()
 
1033
    elif cmd == 'add':
 
1034
        w = readit()
 
1035
        # at the moment, based on everything in the file
 
1036
        name = argv[3]
 
1037
        parents = map(int, argv[4:])
 
1038
        lines = sys.stdin.readlines()
 
1039
        ver = w.add(name, parents, lines)
 
1040
        write_weave(w, file(argv[2], 'wb'))
 
1041
        print 'added version %r %d' % (name, ver)
 
1042
    elif cmd == 'init':
 
1043
        fn = argv[2]
 
1044
        if os.path.exists(fn):
 
1045
            raise IOError("file exists")
 
1046
        w = Weave()
 
1047
        write_weave(w, file(fn, 'wb'))
 
1048
    elif cmd == 'get': # get one version
 
1049
        w = readit()
 
1050
        sys.stdout.writelines(w.get_iter(int(argv[3])))
 
1051
        
 
1052
    elif cmd == 'mash': # get composite
 
1053
        w = readit()
 
1054
        sys.stdout.writelines(w.mash_iter(map(int, argv[3:])))
 
1055
 
 
1056
    elif cmd == 'diff':
 
1057
        from difflib import unified_diff
 
1058
        w = readit()
 
1059
        fn = argv[2]
 
1060
        v1, v2 = map(int, argv[3:5])
 
1061
        lines1 = w.get(v1)
 
1062
        lines2 = w.get(v2)
 
1063
        diff_gen = unified_diff(lines1, lines2,
 
1064
                                '%s version %d' % (fn, v1),
 
1065
                                '%s version %d' % (fn, v2))
 
1066
        sys.stdout.writelines(diff_gen)
 
1067
            
 
1068
    elif cmd == 'annotate':
 
1069
        w = readit()
 
1070
        # newline is added to all lines regardless; too hard to get
 
1071
        # reasonable formatting otherwise
 
1072
        lasto = None
 
1073
        for origin, text in w.annotate(int(argv[3])):
 
1074
            text = text.rstrip('\r\n')
 
1075
            if origin == lasto:
 
1076
                print '      | %s' % (text)
 
1077
            else:
 
1078
                print '%5d | %s' % (origin, text)
 
1079
                lasto = origin
 
1080
                
 
1081
    elif cmd == 'toc':
 
1082
        weave_toc(readit())
 
1083
 
 
1084
    elif cmd == 'stats':
 
1085
        weave_stats(argv[2], ProgressBar())
 
1086
        
 
1087
    elif cmd == 'check':
 
1088
        w = readit()
 
1089
        pb = ProgressBar()
 
1090
        w.check(pb)
 
1091
        pb.clear()
 
1092
        print '%d versions ok' % w.numversions()
 
1093
 
 
1094
    elif cmd == 'inclusions':
 
1095
        w = readit()
 
1096
        print ' '.join(map(str, w.inclusions([int(argv[3])])))
 
1097
 
 
1098
    elif cmd == 'parents':
 
1099
        w = readit()
 
1100
        print ' '.join(map(str, w._parents[int(argv[3])]))
 
1101
 
 
1102
    elif cmd == 'plan-merge':
 
1103
        w = readit()
 
1104
        for state, line in w.plan_merge(int(argv[3]), int(argv[4])):
 
1105
            if line:
 
1106
                print '%14s | %s' % (state, line),
 
1107
 
 
1108
    elif cmd == 'merge':
 
1109
        w = readit()
 
1110
        p = w.plan_merge(int(argv[3]), int(argv[4]))
 
1111
        sys.stdout.writelines(w.weave_merge(p))
 
1112
            
 
1113
    elif cmd == 'mash-merge':
 
1114
        if len(argv) != 5:
 
1115
            usage()
 
1116
            return 1
 
1117
 
 
1118
        w = readit()
 
1119
        v1, v2 = map(int, argv[3:5])
 
1120
 
 
1121
        basis = w.inclusions([v1]).intersection(w.inclusions([v2]))
 
1122
 
 
1123
        base_lines = list(w.mash_iter(basis))
 
1124
        a_lines = list(w.get(v1))
 
1125
        b_lines = list(w.get(v2))
 
1126
 
 
1127
        from bzrlib.merge3 import Merge3
 
1128
        m3 = Merge3(base_lines, a_lines, b_lines)
 
1129
 
 
1130
        name_a = 'version %d' % v1
 
1131
        name_b = 'version %d' % v2
 
1132
        sys.stdout.writelines(m3.merge_lines(name_a=name_a, name_b=name_b))
 
1133
    else:
 
1134
        raise ValueError('unknown command %r' % cmd)
 
1135
    
 
1136
 
 
1137
 
 
1138
def profile_main(argv): 
 
1139
    import tempfile, hotshot, hotshot.stats
 
1140
 
 
1141
    prof_f = tempfile.NamedTemporaryFile()
 
1142
 
 
1143
    prof = hotshot.Profile(prof_f.name)
 
1144
 
 
1145
    ret = prof.runcall(main, argv)
 
1146
    prof.close()
 
1147
 
 
1148
    stats = hotshot.stats.load(prof_f.name)
 
1149
    #stats.strip_dirs()
 
1150
    stats.sort_stats('cumulative')
 
1151
    ## XXX: Might like to write to stderr or the trace file instead but
 
1152
    ## print_stats seems hardcoded to stdout
 
1153
    stats.print_stats(20)
 
1154
            
 
1155
    return ret
 
1156
 
 
1157
 
 
1158
if __name__ == '__main__':
 
1159
    import sys
 
1160
    if '--profile' in sys.argv:
 
1161
        args = sys.argv[:]
 
1162
        args.remove('--profile')
 
1163
        sys.exit(profile_main(args))
 
1164
    else:
 
1165
        sys.exit(main(sys.argv))
 
1166