/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 breezy/merge3.py

  • Committer: Jelmer Vernooij
  • Date: 2020-02-07 02:14:30 UTC
  • mto: This revision was merged to the branch mainline in revision 7492.
  • Revision ID: jelmer@jelmer.uk-20200207021430-m49iq3x4x8xlib6x
Drop python2 support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2010 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
from __future__ import absolute_import
 
18
 
 
19
# mbp: "you know that thing where cvs gives you conflict markers?"
 
20
# s: "i hate that."
 
21
 
 
22
import patiencediff
 
23
 
 
24
 
 
25
from . import (
 
26
    errors,
 
27
    textfile,
 
28
    )
 
29
 
 
30
 
 
31
class CantReprocessAndShowBase(errors.BzrError):
 
32
 
 
33
    _fmt = ("Can't reprocess and show base, because reprocessing obscures "
 
34
            "the relationship of conflicting lines to the base")
 
35
 
 
36
 
 
37
def intersect(ra, rb):
 
38
    """Given two ranges return the range where they intersect or None.
 
39
 
 
40
    >>> intersect((0, 10), (0, 6))
 
41
    (0, 6)
 
42
    >>> intersect((0, 10), (5, 15))
 
43
    (5, 10)
 
44
    >>> intersect((0, 10), (10, 15))
 
45
    >>> intersect((0, 9), (10, 15))
 
46
    >>> intersect((0, 9), (7, 15))
 
47
    (7, 9)
 
48
    """
 
49
    # preconditions: (ra[0] <= ra[1]) and (rb[0] <= rb[1])
 
50
 
 
51
    sa = max(ra[0], rb[0])
 
52
    sb = min(ra[1], rb[1])
 
53
    if sa < sb:
 
54
        return sa, sb
 
55
    else:
 
56
        return None
 
57
 
 
58
 
 
59
def compare_range(a, astart, aend, b, bstart, bend):
 
60
    """Compare a[astart:aend] == b[bstart:bend], without slicing.
 
61
    """
 
62
    if (aend - astart) != (bend - bstart):
 
63
        return False
 
64
    for ia, ib in zip(range(astart, aend), range(bstart, bend)):
 
65
        if a[ia] != b[ib]:
 
66
            return False
 
67
    else:
 
68
        return True
 
69
 
 
70
 
 
71
class Merge3(object):
 
72
    """3-way merge of texts.
 
73
 
 
74
    Given BASE, OTHER, THIS, tries to produce a combined text
 
75
    incorporating the changes from both BASE->OTHER and BASE->THIS.
 
76
    All three will typically be sequences of lines."""
 
77
 
 
78
    def __init__(self, base, a, b, is_cherrypick=False, allow_objects=False):
 
79
        """Constructor.
 
80
 
 
81
        :param base: lines in BASE
 
82
        :param a: lines in A
 
83
        :param b: lines in B
 
84
        :param is_cherrypick: flag indicating if this merge is a cherrypick.
 
85
            When cherrypicking b => a, matches with b and base do not conflict.
 
86
        :param allow_objects: if True, do not require that base, a and b are
 
87
            plain Python strs.  Also prevents BinaryFile from being raised.
 
88
            Lines can be any sequence of comparable and hashable Python
 
89
            objects.
 
90
        """
 
91
        if not allow_objects:
 
92
            textfile.check_text_lines(base)
 
93
            textfile.check_text_lines(a)
 
94
            textfile.check_text_lines(b)
 
95
        self.base = base
 
96
        self.a = a
 
97
        self.b = b
 
98
        self.is_cherrypick = is_cherrypick
 
99
 
 
100
    def merge_lines(self,
 
101
                    name_a=None,
 
102
                    name_b=None,
 
103
                    name_base=None,
 
104
                    start_marker=b'<<<<<<<',
 
105
                    mid_marker=b'=======',
 
106
                    end_marker=b'>>>>>>>',
 
107
                    base_marker=None,
 
108
                    reprocess=False):
 
109
        """Return merge in cvs-like form.
 
110
        """
 
111
        newline = b'\n'
 
112
        if len(self.a) > 0:
 
113
            if self.a[0].endswith(b'\r\n'):
 
114
                newline = b'\r\n'
 
115
            elif self.a[0].endswith(b'\r'):
 
116
                newline = b'\r'
 
117
        if base_marker and reprocess:
 
118
            raise CantReprocessAndShowBase()
 
119
        if name_a:
 
120
            start_marker = start_marker + b' ' + name_a
 
121
        if name_b:
 
122
            end_marker = end_marker + b' ' + name_b
 
123
        if name_base and base_marker:
 
124
            base_marker = base_marker + b' ' + name_base
 
125
        merge_regions = self.merge_regions()
 
126
        if reprocess is True:
 
127
            merge_regions = self.reprocess_merge_regions(merge_regions)
 
128
        for t in merge_regions:
 
129
            what = t[0]
 
130
            if what == 'unchanged':
 
131
                for i in range(t[1], t[2]):
 
132
                    yield self.base[i]
 
133
            elif what == 'a' or what == 'same':
 
134
                for i in range(t[1], t[2]):
 
135
                    yield self.a[i]
 
136
            elif what == 'b':
 
137
                for i in range(t[1], t[2]):
 
138
                    yield self.b[i]
 
139
            elif what == 'conflict':
 
140
                yield start_marker + newline
 
141
                for i in range(t[3], t[4]):
 
142
                    yield self.a[i]
 
143
                if base_marker is not None:
 
144
                    yield base_marker + newline
 
145
                    for i in range(t[1], t[2]):
 
146
                        yield self.base[i]
 
147
                yield mid_marker + newline
 
148
                for i in range(t[5], t[6]):
 
149
                    yield self.b[i]
 
150
                yield end_marker + newline
 
151
            else:
 
152
                raise ValueError(what)
 
153
 
 
154
    def merge_annotated(self):
 
155
        """Return merge with conflicts, showing origin of lines.
 
156
 
 
157
        Most useful for debugging merge.
 
158
        """
 
159
        for t in self.merge_regions():
 
160
            what = t[0]
 
161
            if what == 'unchanged':
 
162
                for i in range(t[1], t[2]):
 
163
                    yield 'u | ' + self.base[i]
 
164
            elif what == 'a' or what == 'same':
 
165
                for i in range(t[1], t[2]):
 
166
                    yield what[0] + ' | ' + self.a[i]
 
167
            elif what == 'b':
 
168
                for i in range(t[1], t[2]):
 
169
                    yield 'b | ' + self.b[i]
 
170
            elif what == 'conflict':
 
171
                yield '<<<<\n'
 
172
                for i in range(t[3], t[4]):
 
173
                    yield 'A | ' + self.a[i]
 
174
                yield '----\n'
 
175
                for i in range(t[5], t[6]):
 
176
                    yield 'B | ' + self.b[i]
 
177
                yield '>>>>\n'
 
178
            else:
 
179
                raise ValueError(what)
 
180
 
 
181
    def merge_groups(self):
 
182
        """Yield sequence of line groups.  Each one is a tuple:
 
183
 
 
184
        'unchanged', lines
 
185
             Lines unchanged from base
 
186
 
 
187
        'a', lines
 
188
             Lines taken from a
 
189
 
 
190
        'same', lines
 
191
             Lines taken from a (and equal to b)
 
192
 
 
193
        'b', lines
 
194
             Lines taken from b
 
195
 
 
196
        'conflict', base_lines, a_lines, b_lines
 
197
             Lines from base were changed to either a or b and conflict.
 
198
        """
 
199
        for t in self.merge_regions():
 
200
            what = t[0]
 
201
            if what == 'unchanged':
 
202
                yield what, self.base[t[1]:t[2]]
 
203
            elif what == 'a' or what == 'same':
 
204
                yield what, self.a[t[1]:t[2]]
 
205
            elif what == 'b':
 
206
                yield what, self.b[t[1]:t[2]]
 
207
            elif what == 'conflict':
 
208
                yield (what,
 
209
                       self.base[t[1]:t[2]],
 
210
                       self.a[t[3]:t[4]],
 
211
                       self.b[t[5]:t[6]])
 
212
            else:
 
213
                raise ValueError(what)
 
214
 
 
215
    def merge_regions(self):
 
216
        """Return sequences of matching and conflicting regions.
 
217
 
 
218
        This returns tuples, where the first value says what kind we
 
219
        have:
 
220
 
 
221
        'unchanged', start, end
 
222
             Take a region of base[start:end]
 
223
 
 
224
        'same', astart, aend
 
225
             b and a are different from base but give the same result
 
226
 
 
227
        'a', start, end
 
228
             Non-clashing insertion from a[start:end]
 
229
 
 
230
        Method is as follows:
 
231
 
 
232
        The two sequences align only on regions which match the base
 
233
        and both descendents.  These are found by doing a two-way diff
 
234
        of each one against the base, and then finding the
 
235
        intersections between those regions.  These "sync regions"
 
236
        are by definition unchanged in both and easily dealt with.
 
237
 
 
238
        The regions in between can be in any of three cases:
 
239
        conflicted, or changed on only one side.
 
240
        """
 
241
 
 
242
        # section a[0:ia] has been disposed of, etc
 
243
        iz = ia = ib = 0
 
244
 
 
245
        for zmatch, zend, amatch, aend, bmatch, bend in self.find_sync_regions():
 
246
            matchlen = zend - zmatch
 
247
            # invariants:
 
248
            #   matchlen >= 0
 
249
            #   matchlen == (aend - amatch)
 
250
            #   matchlen == (bend - bmatch)
 
251
            len_a = amatch - ia
 
252
            len_b = bmatch - ib
 
253
            # invariants:
 
254
            # assert len_a >= 0
 
255
            # assert len_b >= 0
 
256
 
 
257
            # print 'unmatched a=%d, b=%d' % (len_a, len_b)
 
258
 
 
259
            if len_a or len_b:
 
260
                # try to avoid actually slicing the lists
 
261
                same = compare_range(self.a, ia, amatch,
 
262
                                     self.b, ib, bmatch)
 
263
 
 
264
                if same:
 
265
                    yield 'same', ia, amatch
 
266
                else:
 
267
                    equal_a = compare_range(self.a, ia, amatch,
 
268
                                            self.base, iz, zmatch)
 
269
                    equal_b = compare_range(self.b, ib, bmatch,
 
270
                                            self.base, iz, zmatch)
 
271
                    if equal_a and not equal_b:
 
272
                        yield 'b', ib, bmatch
 
273
                    elif equal_b and not equal_a:
 
274
                        yield 'a', ia, amatch
 
275
                    elif not equal_a and not equal_b:
 
276
                        if self.is_cherrypick:
 
277
                            for node in self._refine_cherrypick_conflict(
 
278
                                    iz, zmatch, ia, amatch,
 
279
                                    ib, bmatch):
 
280
                                yield node
 
281
                        else:
 
282
                            yield ('conflict', iz, zmatch, ia, amatch, ib,
 
283
                                   bmatch)
 
284
                    else:
 
285
                        raise AssertionError(
 
286
                            "can't handle a=b=base but unmatched")
 
287
 
 
288
                ia = amatch
 
289
                ib = bmatch
 
290
            iz = zmatch
 
291
 
 
292
            # if the same part of the base was deleted on both sides
 
293
            # that's OK, we can just skip it.
 
294
 
 
295
            if matchlen > 0:
 
296
                # invariants:
 
297
                # assert ia == amatch
 
298
                # assert ib == bmatch
 
299
                # assert iz == zmatch
 
300
 
 
301
                yield 'unchanged', zmatch, zend
 
302
                iz = zend
 
303
                ia = aend
 
304
                ib = bend
 
305
 
 
306
    def _refine_cherrypick_conflict(self, zstart, zend, astart, aend, bstart,
 
307
                                    bend):
 
308
        """When cherrypicking b => a, ignore matches with b and base."""
 
309
        # Do not emit regions which match, only regions which do not match
 
310
        matches = patiencediff.PatienceSequenceMatcher(
 
311
            None, self.base[zstart:zend], self.b[bstart:bend]
 
312
            ).get_matching_blocks()
 
313
        last_base_idx = 0
 
314
        last_b_idx = 0
 
315
        last_b_idx = 0
 
316
        yielded_a = False
 
317
        for base_idx, b_idx, match_len in matches:
 
318
            conflict_b_len = b_idx - last_b_idx
 
319
            if conflict_b_len == 0:
 
320
                # There are no lines in b which conflict, so skip it
 
321
                pass
 
322
            else:
 
323
                if yielded_a:
 
324
                    yield ('conflict',
 
325
                           zstart + last_base_idx, zstart + base_idx,
 
326
                           aend, aend, bstart + last_b_idx, bstart + b_idx)
 
327
                else:
 
328
                    # The first conflict gets the a-range
 
329
                    yielded_a = True
 
330
                    yield ('conflict', zstart + last_base_idx, zstart +
 
331
                           base_idx,
 
332
                           astart, aend, bstart + last_b_idx, bstart + b_idx)
 
333
            last_base_idx = base_idx + match_len
 
334
            last_b_idx = b_idx + match_len
 
335
        if last_base_idx != zend - zstart or last_b_idx != bend - bstart:
 
336
            if yielded_a:
 
337
                yield ('conflict', zstart + last_base_idx, zstart + base_idx,
 
338
                       aend, aend, bstart + last_b_idx, bstart + b_idx)
 
339
            else:
 
340
                # The first conflict gets the a-range
 
341
                yielded_a = True
 
342
                yield ('conflict', zstart + last_base_idx, zstart + base_idx,
 
343
                       astart, aend, bstart + last_b_idx, bstart + b_idx)
 
344
        if not yielded_a:
 
345
            yield ('conflict', zstart, zend, astart, aend, bstart, bend)
 
346
 
 
347
    def reprocess_merge_regions(self, merge_regions):
 
348
        """Where there are conflict regions, remove the agreed lines.
 
349
 
 
350
        Lines where both A and B have made the same changes are
 
351
        eliminated.
 
352
        """
 
353
        for region in merge_regions:
 
354
            if region[0] != "conflict":
 
355
                yield region
 
356
                continue
 
357
            type, iz, zmatch, ia, amatch, ib, bmatch = region
 
358
            a_region = self.a[ia:amatch]
 
359
            b_region = self.b[ib:bmatch]
 
360
            matches = patiencediff.PatienceSequenceMatcher(
 
361
                None, a_region, b_region).get_matching_blocks()
 
362
            next_a = ia
 
363
            next_b = ib
 
364
            for region_ia, region_ib, region_len in matches[:-1]:
 
365
                region_ia += ia
 
366
                region_ib += ib
 
367
                reg = self.mismatch_region(next_a, region_ia, next_b,
 
368
                                           region_ib)
 
369
                if reg is not None:
 
370
                    yield reg
 
371
                yield 'same', region_ia, region_len + region_ia
 
372
                next_a = region_ia + region_len
 
373
                next_b = region_ib + region_len
 
374
            reg = self.mismatch_region(next_a, amatch, next_b, bmatch)
 
375
            if reg is not None:
 
376
                yield reg
 
377
 
 
378
    @staticmethod
 
379
    def mismatch_region(next_a, region_ia, next_b, region_ib):
 
380
        if next_a < region_ia or next_b < region_ib:
 
381
            return 'conflict', None, None, next_a, region_ia, next_b, region_ib
 
382
 
 
383
    def find_sync_regions(self):
 
384
        """Return a list of sync regions, where both descendents match the base.
 
385
 
 
386
        Generates a list of (base1, base2, a1, a2, b1, b2).  There is
 
387
        always a zero-length sync region at the end of all the files.
 
388
        """
 
389
 
 
390
        ia = ib = 0
 
391
        amatches = patiencediff.PatienceSequenceMatcher(
 
392
            None, self.base, self.a).get_matching_blocks()
 
393
        bmatches = patiencediff.PatienceSequenceMatcher(
 
394
            None, self.base, self.b).get_matching_blocks()
 
395
        len_a = len(amatches)
 
396
        len_b = len(bmatches)
 
397
 
 
398
        sl = []
 
399
 
 
400
        while ia < len_a and ib < len_b:
 
401
            abase, amatch, alen = amatches[ia]
 
402
            bbase, bmatch, blen = bmatches[ib]
 
403
 
 
404
            # there is an unconflicted block at i; how long does it
 
405
            # extend?  until whichever one ends earlier.
 
406
            i = intersect((abase, abase + alen), (bbase, bbase + blen))
 
407
            if i:
 
408
                intbase = i[0]
 
409
                intend = i[1]
 
410
                intlen = intend - intbase
 
411
 
 
412
                # found a match of base[i[0], i[1]]; this may be less than
 
413
                # the region that matches in either one
 
414
                # assert intlen <= alen
 
415
                # assert intlen <= blen
 
416
                # assert abase <= intbase
 
417
                # assert bbase <= intbase
 
418
 
 
419
                asub = amatch + (intbase - abase)
 
420
                bsub = bmatch + (intbase - bbase)
 
421
                aend = asub + intlen
 
422
                bend = bsub + intlen
 
423
 
 
424
                # assert self.base[intbase:intend] == self.a[asub:aend], \
 
425
                #       (self.base[intbase:intend], self.a[asub:aend])
 
426
                # assert self.base[intbase:intend] == self.b[bsub:bend]
 
427
 
 
428
                sl.append((intbase, intend,
 
429
                           asub, aend,
 
430
                           bsub, bend))
 
431
            # advance whichever one ends first in the base text
 
432
            if (abase + alen) < (bbase + blen):
 
433
                ia += 1
 
434
            else:
 
435
                ib += 1
 
436
 
 
437
        intbase = len(self.base)
 
438
        abase = len(self.a)
 
439
        bbase = len(self.b)
 
440
        sl.append((intbase, intbase, abase, abase, bbase, bbase))
 
441
 
 
442
        return sl
 
443
 
 
444
    def find_unconflicted(self):
 
445
        """Return a list of ranges in base that are not conflicted."""
 
446
        am = patiencediff.PatienceSequenceMatcher(
 
447
            None, self.base, self.a).get_matching_blocks()
 
448
        bm = patiencediff.PatienceSequenceMatcher(
 
449
            None, self.base, self.b).get_matching_blocks()
 
450
 
 
451
        unc = []
 
452
 
 
453
        while am and bm:
 
454
            # there is an unconflicted block at i; how long does it
 
455
            # extend?  until whichever one ends earlier.
 
456
            a1 = am[0][0]
 
457
            a2 = a1 + am[0][2]
 
458
            b1 = bm[0][0]
 
459
            b2 = b1 + bm[0][2]
 
460
            i = intersect((a1, a2), (b1, b2))
 
461
            if i:
 
462
                unc.append(i)
 
463
 
 
464
            if a2 < b2:
 
465
                del am[0]
 
466
            else:
 
467
                del bm[0]
 
468
 
 
469
        return unc
 
470
 
 
471
 
 
472
def main(argv):
 
473
    # as for diff3 and meld the syntax is "MINE BASE OTHER"
 
474
    with open(argv[1], 'rt') as f:
 
475
        a = f.readlines()
 
476
    with open(argv[2], 'rt') as f:
 
477
        base = f.readlines()
 
478
    with open(argv[3], 'rt') as f:
 
479
        b = f.readlines()
 
480
 
 
481
    m3 = Merge3(base, a, b)
 
482
 
 
483
    # for sr in m3.find_sync_regions():
 
484
    #    print sr
 
485
 
 
486
    # sys.stdout.writelines(m3.merge_lines(name_a=argv[1], name_b=argv[3]))
 
487
    sys.stdout.writelines(m3.merge_annotated())
 
488
 
 
489
 
 
490
if __name__ == '__main__':
 
491
    import sys
 
492
    sys.exit(main(sys.argv))