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