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