##// END OF EJS Templates
Import 3-way merge code from bzr...
Alexis S. L. Carvalho -
r4362:465b9ea0 default
parent child Browse files
Show More
@@ -0,0 +1,438 b''
1 # Copyright (C) 2004, 2005 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16
17
18 # mbp: "you know that thing where cvs gives you conflict markers?"
19 # s: "i hate that."
20
21
22 from bzrlib.errors import CantReprocessAndShowBase
23 import bzrlib.patiencediff
24 from bzrlib.textfile import check_text_lines
25
26
27 def intersect(ra, rb):
28 """Given two ranges return the range where they intersect or None.
29
30 >>> intersect((0, 10), (0, 6))
31 (0, 6)
32 >>> intersect((0, 10), (5, 15))
33 (5, 10)
34 >>> intersect((0, 10), (10, 15))
35 >>> intersect((0, 9), (10, 15))
36 >>> intersect((0, 9), (7, 15))
37 (7, 9)
38 """
39 assert ra[0] <= ra[1]
40 assert rb[0] <= rb[1]
41
42 sa = max(ra[0], rb[0])
43 sb = min(ra[1], rb[1])
44 if sa < sb:
45 return sa, sb
46 else:
47 return None
48
49
50 def compare_range(a, astart, aend, b, bstart, bend):
51 """Compare a[astart:aend] == b[bstart:bend], without slicing.
52 """
53 if (aend-astart) != (bend-bstart):
54 return False
55 for ia, ib in zip(xrange(astart, aend), xrange(bstart, bend)):
56 if a[ia] != b[ib]:
57 return False
58 else:
59 return True
60
61
62
63
64 class Merge3(object):
65 """3-way merge of texts.
66
67 Given BASE, OTHER, THIS, tries to produce a combined text
68 incorporating the changes from both BASE->OTHER and BASE->THIS.
69 All three will typically be sequences of lines."""
70 def __init__(self, base, a, b):
71 check_text_lines(base)
72 check_text_lines(a)
73 check_text_lines(b)
74 self.base = base
75 self.a = a
76 self.b = b
77
78
79
80 def merge_lines(self,
81 name_a=None,
82 name_b=None,
83 name_base=None,
84 start_marker='<<<<<<<',
85 mid_marker='=======',
86 end_marker='>>>>>>>',
87 base_marker=None,
88 reprocess=False):
89 """Return merge in cvs-like form.
90 """
91 newline = '\n'
92 if len(self.a) > 0:
93 if self.a[0].endswith('\r\n'):
94 newline = '\r\n'
95 elif self.a[0].endswith('\r'):
96 newline = '\r'
97 if base_marker and reprocess:
98 raise CantReprocessAndShowBase()
99 if name_a:
100 start_marker = start_marker + ' ' + name_a
101 if name_b:
102 end_marker = end_marker + ' ' + name_b
103 if name_base and base_marker:
104 base_marker = base_marker + ' ' + name_base
105 merge_regions = self.merge_regions()
106 if reprocess is True:
107 merge_regions = self.reprocess_merge_regions(merge_regions)
108 for t in merge_regions:
109 what = t[0]
110 if what == 'unchanged':
111 for i in range(t[1], t[2]):
112 yield self.base[i]
113 elif what == 'a' or what == 'same':
114 for i in range(t[1], t[2]):
115 yield self.a[i]
116 elif what == 'b':
117 for i in range(t[1], t[2]):
118 yield self.b[i]
119 elif what == 'conflict':
120 yield start_marker + newline
121 for i in range(t[3], t[4]):
122 yield self.a[i]
123 if base_marker is not None:
124 yield base_marker + newline
125 for i in range(t[1], t[2]):
126 yield self.base[i]
127 yield mid_marker + newline
128 for i in range(t[5], t[6]):
129 yield self.b[i]
130 yield end_marker + newline
131 else:
132 raise ValueError(what)
133
134
135
136
137
138 def merge_annotated(self):
139 """Return merge with conflicts, showing origin of lines.
140
141 Most useful for debugging merge.
142 """
143 for t in self.merge_regions():
144 what = t[0]
145 if what == 'unchanged':
146 for i in range(t[1], t[2]):
147 yield 'u | ' + self.base[i]
148 elif what == 'a' or what == 'same':
149 for i in range(t[1], t[2]):
150 yield what[0] + ' | ' + self.a[i]
151 elif what == 'b':
152 for i in range(t[1], t[2]):
153 yield 'b | ' + self.b[i]
154 elif what == 'conflict':
155 yield '<<<<\n'
156 for i in range(t[3], t[4]):
157 yield 'A | ' + self.a[i]
158 yield '----\n'
159 for i in range(t[5], t[6]):
160 yield 'B | ' + self.b[i]
161 yield '>>>>\n'
162 else:
163 raise ValueError(what)
164
165
166
167
168
169 def merge_groups(self):
170 """Yield sequence of line groups. Each one is a tuple:
171
172 'unchanged', lines
173 Lines unchanged from base
174
175 'a', lines
176 Lines taken from a
177
178 'same', lines
179 Lines taken from a (and equal to b)
180
181 'b', lines
182 Lines taken from b
183
184 'conflict', base_lines, a_lines, b_lines
185 Lines from base were changed to either a or b and conflict.
186 """
187 for t in self.merge_regions():
188 what = t[0]
189 if what == 'unchanged':
190 yield what, self.base[t[1]:t[2]]
191 elif what == 'a' or what == 'same':
192 yield what, self.a[t[1]:t[2]]
193 elif what == 'b':
194 yield what, self.b[t[1]:t[2]]
195 elif what == 'conflict':
196 yield (what,
197 self.base[t[1]:t[2]],
198 self.a[t[3]:t[4]],
199 self.b[t[5]:t[6]])
200 else:
201 raise ValueError(what)
202
203
204 def merge_regions(self):
205 """Return sequences of matching and conflicting regions.
206
207 This returns tuples, where the first value says what kind we
208 have:
209
210 'unchanged', start, end
211 Take a region of base[start:end]
212
213 'same', astart, aend
214 b and a are different from base but give the same result
215
216 'a', start, end
217 Non-clashing insertion from a[start:end]
218
219 Method is as follows:
220
221 The two sequences align only on regions which match the base
222 and both descendents. These are found by doing a two-way diff
223 of each one against the base, and then finding the
224 intersections between those regions. These "sync regions"
225 are by definition unchanged in both and easily dealt with.
226
227 The regions in between can be in any of three cases:
228 conflicted, or changed on only one side.
229 """
230
231 # section a[0:ia] has been disposed of, etc
232 iz = ia = ib = 0
233
234 for zmatch, zend, amatch, aend, bmatch, bend in self.find_sync_regions():
235 #print 'match base [%d:%d]' % (zmatch, zend)
236
237 matchlen = zend - zmatch
238 assert matchlen >= 0
239 assert matchlen == (aend - amatch)
240 assert matchlen == (bend - bmatch)
241
242 len_a = amatch - ia
243 len_b = bmatch - ib
244 len_base = zmatch - iz
245 assert len_a >= 0
246 assert len_b >= 0
247 assert len_base >= 0
248
249 #print 'unmatched a=%d, b=%d' % (len_a, len_b)
250
251 if len_a or len_b:
252 # try to avoid actually slicing the lists
253 equal_a = compare_range(self.a, ia, amatch,
254 self.base, iz, zmatch)
255 equal_b = compare_range(self.b, ib, bmatch,
256 self.base, iz, zmatch)
257 same = compare_range(self.a, ia, amatch,
258 self.b, ib, bmatch)
259
260 if same:
261 yield 'same', ia, amatch
262 elif equal_a and not equal_b:
263 yield 'b', ib, bmatch
264 elif equal_b and not equal_a:
265 yield 'a', ia, amatch
266 elif not equal_a and not equal_b:
267 yield 'conflict', iz, zmatch, ia, amatch, ib, bmatch
268 else:
269 raise AssertionError("can't handle a=b=base but unmatched")
270
271 ia = amatch
272 ib = bmatch
273 iz = zmatch
274
275 # if the same part of the base was deleted on both sides
276 # that's OK, we can just skip it.
277
278
279 if matchlen > 0:
280 assert ia == amatch
281 assert ib == bmatch
282 assert iz == zmatch
283
284 yield 'unchanged', zmatch, zend
285 iz = zend
286 ia = aend
287 ib = bend
288
289
290 def reprocess_merge_regions(self, merge_regions):
291 """Where there are conflict regions, remove the agreed lines.
292
293 Lines where both A and B have made the same changes are
294 eliminated.
295 """
296 for region in merge_regions:
297 if region[0] != "conflict":
298 yield region
299 continue
300 type, iz, zmatch, ia, amatch, ib, bmatch = region
301 a_region = self.a[ia:amatch]
302 b_region = self.b[ib:bmatch]
303 matches = bzrlib.patiencediff.PatienceSequenceMatcher(
304 None, a_region, b_region).get_matching_blocks()
305 next_a = ia
306 next_b = ib
307 for region_ia, region_ib, region_len in matches[:-1]:
308 region_ia += ia
309 region_ib += ib
310 reg = self.mismatch_region(next_a, region_ia, next_b,
311 region_ib)
312 if reg is not None:
313 yield reg
314 yield 'same', region_ia, region_len+region_ia
315 next_a = region_ia + region_len
316 next_b = region_ib + region_len
317 reg = self.mismatch_region(next_a, amatch, next_b, bmatch)
318 if reg is not None:
319 yield reg
320
321
322 @staticmethod
323 def mismatch_region(next_a, region_ia, next_b, region_ib):
324 if next_a < region_ia or next_b < region_ib:
325 return 'conflict', None, None, next_a, region_ia, next_b, region_ib
326
327
328 def find_sync_regions(self):
329 """Return a list of sync regions, where both descendents match the base.
330
331 Generates a list of (base1, base2, a1, a2, b1, b2). There is
332 always a zero-length sync region at the end of all the files.
333 """
334
335 ia = ib = 0
336 amatches = bzrlib.patiencediff.PatienceSequenceMatcher(
337 None, self.base, self.a).get_matching_blocks()
338 bmatches = bzrlib.patiencediff.PatienceSequenceMatcher(
339 None, self.base, self.b).get_matching_blocks()
340 len_a = len(amatches)
341 len_b = len(bmatches)
342
343 sl = []
344
345 while ia < len_a and ib < len_b:
346 abase, amatch, alen = amatches[ia]
347 bbase, bmatch, blen = bmatches[ib]
348
349 # there is an unconflicted block at i; how long does it
350 # extend? until whichever one ends earlier.
351 i = intersect((abase, abase+alen), (bbase, bbase+blen))
352 if i:
353 intbase = i[0]
354 intend = i[1]
355 intlen = intend - intbase
356
357 # found a match of base[i[0], i[1]]; this may be less than
358 # the region that matches in either one
359 assert intlen <= alen
360 assert intlen <= blen
361 assert abase <= intbase
362 assert bbase <= intbase
363
364 asub = amatch + (intbase - abase)
365 bsub = bmatch + (intbase - bbase)
366 aend = asub + intlen
367 bend = bsub + intlen
368
369 assert self.base[intbase:intend] == self.a[asub:aend], \
370 (self.base[intbase:intend], self.a[asub:aend])
371
372 assert self.base[intbase:intend] == self.b[bsub:bend]
373
374 sl.append((intbase, intend,
375 asub, aend,
376 bsub, bend))
377
378 # advance whichever one ends first in the base text
379 if (abase + alen) < (bbase + blen):
380 ia += 1
381 else:
382 ib += 1
383
384 intbase = len(self.base)
385 abase = len(self.a)
386 bbase = len(self.b)
387 sl.append((intbase, intbase, abase, abase, bbase, bbase))
388
389 return sl
390
391
392
393 def find_unconflicted(self):
394 """Return a list of ranges in base that are not conflicted."""
395 am = bzrlib.patiencediff.PatienceSequenceMatcher(
396 None, self.base, self.a).get_matching_blocks()
397 bm = bzrlib.patiencediff.PatienceSequenceMatcher(
398 None, self.base, self.b).get_matching_blocks()
399
400 unc = []
401
402 while am and bm:
403 # there is an unconflicted block at i; how long does it
404 # extend? until whichever one ends earlier.
405 a1 = am[0][0]
406 a2 = a1 + am[0][2]
407 b1 = bm[0][0]
408 b2 = b1 + bm[0][2]
409 i = intersect((a1, a2), (b1, b2))
410 if i:
411 unc.append(i)
412
413 if a2 < b2:
414 del am[0]
415 else:
416 del bm[0]
417
418 return unc
419
420
421 def main(argv):
422 # as for diff3 and meld the syntax is "MINE BASE OTHER"
423 a = file(argv[1], 'rt').readlines()
424 base = file(argv[2], 'rt').readlines()
425 b = file(argv[3], 'rt').readlines()
426
427 m3 = Merge3(base, a, b)
428
429 #for sr in m3.find_sync_regions():
430 # print sr
431
432 # sys.stdout.writelines(m3.merge_lines(name_a=argv[1], name_b=argv[3]))
433 sys.stdout.writelines(m3.merge_annotated())
434
435
436 if __name__ == '__main__':
437 import sys
438 sys.exit(main(sys.argv))
@@ -0,0 +1,385 b''
1 # Copyright (C) 2004, 2005 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16
17
18 from bzrlib.tests import TestCaseInTempDir, TestCase
19 from bzrlib.merge3 import Merge3
20 from bzrlib.errors import CantReprocessAndShowBase, BinaryFile
21
22 def split_lines(t):
23 from cStringIO import StringIO
24 return StringIO(t).readlines()
25
26 ############################################################
27 # test case data from the gnu diffutils manual
28 # common base
29 TZU = split_lines(""" The Nameless is the origin of Heaven and Earth;
30 The named is the mother of all things.
31
32 Therefore let there always be non-being,
33 so we may see their subtlety,
34 And let there always be being,
35 so we may see their outcome.
36 The two are the same,
37 But after they are produced,
38 they have different names.
39 They both may be called deep and profound.
40 Deeper and more profound,
41 The door of all subtleties!
42 """)
43
44 LAO = split_lines(""" The Way that can be told of is not the eternal Way;
45 The name that can be named is not the eternal name.
46 The Nameless is the origin of Heaven and Earth;
47 The Named is the mother of all things.
48 Therefore let there always be non-being,
49 so we may see their subtlety,
50 And let there always be being,
51 so we may see their outcome.
52 The two are the same,
53 But after they are produced,
54 they have different names.
55 """)
56
57
58 TAO = split_lines(""" The Way that can be told of is not the eternal Way;
59 The name that can be named is not the eternal name.
60 The Nameless is the origin of Heaven and Earth;
61 The named is the mother of all things.
62
63 Therefore let there always be non-being,
64 so we may see their subtlety,
65 And let there always be being,
66 so we may see their result.
67 The two are the same,
68 But after they are produced,
69 they have different names.
70
71 -- The Way of Lao-Tzu, tr. Wing-tsit Chan
72
73 """)
74
75 MERGED_RESULT = split_lines(""" The Way that can be told of is not the eternal Way;
76 The name that can be named is not the eternal name.
77 The Nameless is the origin of Heaven and Earth;
78 The Named is the mother of all things.
79 Therefore let there always be non-being,
80 so we may see their subtlety,
81 And let there always be being,
82 so we may see their result.
83 The two are the same,
84 But after they are produced,
85 they have different names.
86 <<<<<<< LAO
87 =======
88
89 -- The Way of Lao-Tzu, tr. Wing-tsit Chan
90
91 >>>>>>> TAO
92 """)
93
94 class TestMerge3(TestCase):
95
96 def test_no_changes(self):
97 """No conflicts because nothing changed"""
98 m3 = Merge3(['aaa', 'bbb'],
99 ['aaa', 'bbb'],
100 ['aaa', 'bbb'])
101
102 self.assertEquals(m3.find_unconflicted(),
103 [(0, 2)])
104
105 self.assertEquals(list(m3.find_sync_regions()),
106 [(0, 2,
107 0, 2,
108 0, 2),
109 (2,2, 2,2, 2,2)])
110
111 self.assertEquals(list(m3.merge_regions()),
112 [('unchanged', 0, 2)])
113
114 self.assertEquals(list(m3.merge_groups()),
115 [('unchanged', ['aaa', 'bbb'])])
116
117 def test_front_insert(self):
118 m3 = Merge3(['zz'],
119 ['aaa', 'bbb', 'zz'],
120 ['zz'])
121
122 # todo: should use a sentinal at end as from get_matching_blocks
123 # to match without zz
124 self.assertEquals(list(m3.find_sync_regions()),
125 [(0,1, 2,3, 0,1),
126 (1,1, 3,3, 1,1),])
127
128 self.assertEquals(list(m3.merge_regions()),
129 [('a', 0, 2),
130 ('unchanged', 0, 1)])
131
132 self.assertEquals(list(m3.merge_groups()),
133 [('a', ['aaa', 'bbb']),
134 ('unchanged', ['zz'])])
135
136 def test_null_insert(self):
137 m3 = Merge3([],
138 ['aaa', 'bbb'],
139 [])
140 # todo: should use a sentinal at end as from get_matching_blocks
141 # to match without zz
142 self.assertEquals(list(m3.find_sync_regions()),
143 [(0,0, 2,2, 0,0)])
144
145 self.assertEquals(list(m3.merge_regions()),
146 [('a', 0, 2)])
147
148 self.assertEquals(list(m3.merge_lines()),
149 ['aaa', 'bbb'])
150
151 def test_no_conflicts(self):
152 """No conflicts because only one side changed"""
153 m3 = Merge3(['aaa', 'bbb'],
154 ['aaa', '111', 'bbb'],
155 ['aaa', 'bbb'])
156
157 self.assertEquals(m3.find_unconflicted(),
158 [(0, 1), (1, 2)])
159
160 self.assertEquals(list(m3.find_sync_regions()),
161 [(0,1, 0,1, 0,1),
162 (1,2, 2,3, 1,2),
163 (2,2, 3,3, 2,2),])
164
165 self.assertEquals(list(m3.merge_regions()),
166 [('unchanged', 0, 1),
167 ('a', 1, 2),
168 ('unchanged', 1, 2),])
169
170 def test_append_a(self):
171 m3 = Merge3(['aaa\n', 'bbb\n'],
172 ['aaa\n', 'bbb\n', '222\n'],
173 ['aaa\n', 'bbb\n'])
174
175 self.assertEquals(''.join(m3.merge_lines()),
176 'aaa\nbbb\n222\n')
177
178 def test_append_b(self):
179 m3 = Merge3(['aaa\n', 'bbb\n'],
180 ['aaa\n', 'bbb\n'],
181 ['aaa\n', 'bbb\n', '222\n'])
182
183 self.assertEquals(''.join(m3.merge_lines()),
184 'aaa\nbbb\n222\n')
185
186 def test_append_agreement(self):
187 m3 = Merge3(['aaa\n', 'bbb\n'],
188 ['aaa\n', 'bbb\n', '222\n'],
189 ['aaa\n', 'bbb\n', '222\n'])
190
191 self.assertEquals(''.join(m3.merge_lines()),
192 'aaa\nbbb\n222\n')
193
194 def test_append_clash(self):
195 m3 = Merge3(['aaa\n', 'bbb\n'],
196 ['aaa\n', 'bbb\n', '222\n'],
197 ['aaa\n', 'bbb\n', '333\n'])
198
199 ml = m3.merge_lines(name_a='a',
200 name_b='b',
201 start_marker='<<',
202 mid_marker='--',
203 end_marker='>>')
204 self.assertEquals(''.join(ml),
205 '''\
206 aaa
207 bbb
208 << a
209 222
210 --
211 333
212 >> b
213 ''')
214
215 def test_insert_agreement(self):
216 m3 = Merge3(['aaa\n', 'bbb\n'],
217 ['aaa\n', '222\n', 'bbb\n'],
218 ['aaa\n', '222\n', 'bbb\n'])
219
220 ml = m3.merge_lines(name_a='a',
221 name_b='b',
222 start_marker='<<',
223 mid_marker='--',
224 end_marker='>>')
225 self.assertEquals(''.join(ml), 'aaa\n222\nbbb\n')
226
227
228 def test_insert_clash(self):
229 """Both try to insert lines in the same place."""
230 m3 = Merge3(['aaa\n', 'bbb\n'],
231 ['aaa\n', '111\n', 'bbb\n'],
232 ['aaa\n', '222\n', 'bbb\n'])
233
234 self.assertEquals(m3.find_unconflicted(),
235 [(0, 1), (1, 2)])
236
237 self.assertEquals(list(m3.find_sync_regions()),
238 [(0,1, 0,1, 0,1),
239 (1,2, 2,3, 2,3),
240 (2,2, 3,3, 3,3),])
241
242 self.assertEquals(list(m3.merge_regions()),
243 [('unchanged', 0,1),
244 ('conflict', 1,1, 1,2, 1,2),
245 ('unchanged', 1,2)])
246
247 self.assertEquals(list(m3.merge_groups()),
248 [('unchanged', ['aaa\n']),
249 ('conflict', [], ['111\n'], ['222\n']),
250 ('unchanged', ['bbb\n']),
251 ])
252
253 ml = m3.merge_lines(name_a='a',
254 name_b='b',
255 start_marker='<<',
256 mid_marker='--',
257 end_marker='>>')
258 self.assertEquals(''.join(ml),
259 '''aaa
260 << a
261 111
262 --
263 222
264 >> b
265 bbb
266 ''')
267
268 def test_replace_clash(self):
269 """Both try to insert lines in the same place."""
270 m3 = Merge3(['aaa', '000', 'bbb'],
271 ['aaa', '111', 'bbb'],
272 ['aaa', '222', 'bbb'])
273
274 self.assertEquals(m3.find_unconflicted(),
275 [(0, 1), (2, 3)])
276
277 self.assertEquals(list(m3.find_sync_regions()),
278 [(0,1, 0,1, 0,1),
279 (2,3, 2,3, 2,3),
280 (3,3, 3,3, 3,3),])
281
282 def test_replace_multi(self):
283 """Replacement with regions of different size."""
284 m3 = Merge3(['aaa', '000', '000', 'bbb'],
285 ['aaa', '111', '111', '111', 'bbb'],
286 ['aaa', '222', '222', '222', '222', 'bbb'])
287
288 self.assertEquals(m3.find_unconflicted(),
289 [(0, 1), (3, 4)])
290
291
292 self.assertEquals(list(m3.find_sync_regions()),
293 [(0,1, 0,1, 0,1),
294 (3,4, 4,5, 5,6),
295 (4,4, 5,5, 6,6),])
296
297 def test_merge_poem(self):
298 """Test case from diff3 manual"""
299 m3 = Merge3(TZU, LAO, TAO)
300 ml = list(m3.merge_lines('LAO', 'TAO'))
301 self.log('merge result:')
302 self.log(''.join(ml))
303 self.assertEquals(ml, MERGED_RESULT)
304
305 def test_minimal_conflicts_common(self):
306 """Reprocessing"""
307 base_text = ("a\n" * 20).splitlines(True)
308 this_text = ("a\n"*10+"b\n" * 10).splitlines(True)
309 other_text = ("a\n"*10+"c\n"+"b\n" * 8 + "c\n").splitlines(True)
310 m3 = Merge3(base_text, other_text, this_text)
311 m_lines = m3.merge_lines('OTHER', 'THIS', reprocess=True)
312 merged_text = "".join(list(m_lines))
313 optimal_text = ("a\n" * 10 + "<<<<<<< OTHER\nc\n"
314 + 8* "b\n" + "c\n=======\n"
315 + 10*"b\n" + ">>>>>>> THIS\n")
316 self.assertEqualDiff(optimal_text, merged_text)
317
318 def test_minimal_conflicts_unique(self):
319 def add_newline(s):
320 """Add a newline to each entry in the string"""
321 return [(x+'\n') for x in s]
322
323 base_text = add_newline("abcdefghijklm")
324 this_text = add_newline("abcdefghijklmNOPQRSTUVWXYZ")
325 other_text = add_newline("abcdefghijklm1OPQRSTUVWXY2")
326 m3 = Merge3(base_text, other_text, this_text)
327 m_lines = m3.merge_lines('OTHER', 'THIS', reprocess=True)
328 merged_text = "".join(list(m_lines))
329 optimal_text = ''.join(add_newline("abcdefghijklm")
330 + ["<<<<<<< OTHER\n1\n=======\nN\n>>>>>>> THIS\n"]
331 + add_newline('OPQRSTUVWXY')
332 + ["<<<<<<< OTHER\n2\n=======\nZ\n>>>>>>> THIS\n"]
333 )
334 self.assertEqualDiff(optimal_text, merged_text)
335
336 def test_minimal_conflicts_nonunique(self):
337 def add_newline(s):
338 """Add a newline to each entry in the string"""
339 return [(x+'\n') for x in s]
340
341 base_text = add_newline("abacddefgghij")
342 this_text = add_newline("abacddefgghijkalmontfprz")
343 other_text = add_newline("abacddefgghijknlmontfprd")
344 m3 = Merge3(base_text, other_text, this_text)
345 m_lines = m3.merge_lines('OTHER', 'THIS', reprocess=True)
346 merged_text = "".join(list(m_lines))
347 optimal_text = ''.join(add_newline("abacddefgghijk")
348 + ["<<<<<<< OTHER\nn\n=======\na\n>>>>>>> THIS\n"]
349 + add_newline('lmontfpr')
350 + ["<<<<<<< OTHER\nd\n=======\nz\n>>>>>>> THIS\n"]
351 )
352 self.assertEqualDiff(optimal_text, merged_text)
353
354 def test_reprocess_and_base(self):
355 """Reprocessing and showing base breaks correctly"""
356 base_text = ("a\n" * 20).splitlines(True)
357 this_text = ("a\n"*10+"b\n" * 10).splitlines(True)
358 other_text = ("a\n"*10+"c\n"+"b\n" * 8 + "c\n").splitlines(True)
359 m3 = Merge3(base_text, other_text, this_text)
360 m_lines = m3.merge_lines('OTHER', 'THIS', reprocess=True,
361 base_marker='|||||||')
362 self.assertRaises(CantReprocessAndShowBase, list, m_lines)
363
364 def test_binary(self):
365 self.assertRaises(BinaryFile, Merge3, ['\x00'], ['a'], ['b'])
366
367 def test_dos_text(self):
368 base_text = 'a\r\n'
369 this_text = 'b\r\n'
370 other_text = 'c\r\n'
371 m3 = Merge3(base_text.splitlines(True), other_text.splitlines(True),
372 this_text.splitlines(True))
373 m_lines = m3.merge_lines('OTHER', 'THIS')
374 self.assertEqual('<<<<<<< OTHER\r\nc\r\n=======\r\nb\r\n'
375 '>>>>>>> THIS\r\n'.splitlines(True), list(m_lines))
376
377 def test_mac_text(self):
378 base_text = 'a\r'
379 this_text = 'b\r'
380 other_text = 'c\r'
381 m3 = Merge3(base_text.splitlines(True), other_text.splitlines(True),
382 this_text.splitlines(True))
383 m_lines = m3.merge_lines('OTHER', 'THIS')
384 self.assertEqual('<<<<<<< OTHER\rc\r=======\rb\r'
385 '>>>>>>> THIS\r'.splitlines(True), list(m_lines))
General Comments 0
You need to be logged in to leave comments. Login now