##// END OF EJS Templates
simplemerge: make merge_groups() yield only 2-tuples...
Martin von Zweigbergk -
r49379:374bf34c default
parent child Browse files
Show More
@@ -1,550 +1,552 b''
1 1 # Copyright (C) 2004, 2005 Canonical Ltd
2 2 #
3 3 # This program is free software; you can redistribute it and/or modify
4 4 # it under the terms of the GNU General Public License as published by
5 5 # the Free Software Foundation; either version 2 of the License, or
6 6 # (at your option) any later version.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU General Public License
14 14 # along with this program; if not, see <http://www.gnu.org/licenses/>.
15 15
16 16 # mbp: "you know that thing where cvs gives you conflict markers?"
17 17 # s: "i hate that."
18 18
19 19 from __future__ import absolute_import
20 20
21 21 from .i18n import _
22 22 from . import (
23 23 error,
24 24 mdiff,
25 25 pycompat,
26 26 )
27 27 from .utils import stringutil
28 28
29 29
30 30 class CantReprocessAndShowBase(Exception):
31 31 pass
32 32
33 33
34 34 def intersect(ra, rb):
35 35 """Given two ranges return the range where they intersect or None.
36 36
37 37 >>> intersect((0, 10), (0, 6))
38 38 (0, 6)
39 39 >>> intersect((0, 10), (5, 15))
40 40 (5, 10)
41 41 >>> intersect((0, 10), (10, 15))
42 42 >>> intersect((0, 9), (10, 15))
43 43 >>> intersect((0, 9), (7, 15))
44 44 (7, 9)
45 45 """
46 46 assert ra[0] <= ra[1]
47 47 assert rb[0] <= rb[1]
48 48
49 49 sa = max(ra[0], rb[0])
50 50 sb = min(ra[1], rb[1])
51 51 if sa < sb:
52 52 return sa, sb
53 53 else:
54 54 return None
55 55
56 56
57 57 def compare_range(a, astart, aend, b, bstart, bend):
58 58 """Compare a[astart:aend] == b[bstart:bend], without slicing."""
59 59 if (aend - astart) != (bend - bstart):
60 60 return False
61 61 for ia, ib in zip(
62 62 pycompat.xrange(astart, aend), pycompat.xrange(bstart, bend)
63 63 ):
64 64 if a[ia] != b[ib]:
65 65 return False
66 66 else:
67 67 return True
68 68
69 69
70 70 class Merge3Text(object):
71 71 """3-way merge of texts.
72 72
73 73 Given strings BASE, OTHER, THIS, tries to produce a combined text
74 74 incorporating the changes from both BASE->OTHER and BASE->THIS."""
75 75
76 76 def __init__(self, basetext, atext, btext, base=None, a=None, b=None):
77 77 self.basetext = basetext
78 78 self.atext = atext
79 79 self.btext = btext
80 80 if base is None:
81 81 base = mdiff.splitnewlines(basetext)
82 82 if a is None:
83 83 a = mdiff.splitnewlines(atext)
84 84 if b is None:
85 85 b = mdiff.splitnewlines(btext)
86 86 self.base = base
87 87 self.a = a
88 88 self.b = b
89 89
90 90 def merge_lines(
91 91 self,
92 92 name_a=None,
93 93 name_b=None,
94 94 name_base=None,
95 95 start_marker=b'<<<<<<<',
96 96 mid_marker=b'=======',
97 97 end_marker=b'>>>>>>>',
98 98 base_marker=None,
99 99 minimize=False,
100 100 ):
101 101 """Return merge in cvs-like form."""
102 102 self.conflicts = False
103 103 newline = b'\n'
104 104 if len(self.a) > 0:
105 105 if self.a[0].endswith(b'\r\n'):
106 106 newline = b'\r\n'
107 107 elif self.a[0].endswith(b'\r'):
108 108 newline = b'\r'
109 109 if name_a and start_marker:
110 110 start_marker = start_marker + b' ' + name_a
111 111 if name_b and end_marker:
112 112 end_marker = end_marker + b' ' + name_b
113 113 if name_base and base_marker:
114 114 base_marker = base_marker + b' ' + name_base
115 115 merge_regions = self.merge_regions()
116 116 if minimize:
117 117 merge_regions = self.minimize(merge_regions)
118 118 for t in merge_regions:
119 119 what = t[0]
120 120 if what == b'unchanged':
121 121 for i in range(t[1], t[2]):
122 122 yield self.base[i]
123 123 elif what == b'a' or what == b'same':
124 124 for i in range(t[1], t[2]):
125 125 yield self.a[i]
126 126 elif what == b'b':
127 127 for i in range(t[1], t[2]):
128 128 yield self.b[i]
129 129 elif what == b'conflict':
130 130 self.conflicts = True
131 131 if start_marker is not None:
132 132 yield start_marker + newline
133 133 for i in range(t[3], t[4]):
134 134 yield self.a[i]
135 135 if base_marker is not None:
136 136 yield base_marker + newline
137 137 for i in range(t[1], t[2]):
138 138 yield self.base[i]
139 139 if mid_marker is not None:
140 140 yield mid_marker + newline
141 141 for i in range(t[5], t[6]):
142 142 yield self.b[i]
143 143 if end_marker is not None:
144 144 yield end_marker + newline
145 145 else:
146 146 raise ValueError(what)
147 147
148 148 def merge_groups(self):
149 149 """Yield sequence of line groups. Each one is a tuple:
150 150
151 151 'unchanged', lines
152 152 Lines unchanged from base
153 153
154 154 'a', lines
155 155 Lines taken from a
156 156
157 157 'same', lines
158 158 Lines taken from a (and equal to b)
159 159
160 160 'b', lines
161 161 Lines taken from b
162 162
163 'conflict', base_lines, a_lines, b_lines
163 'conflict', (base_lines, a_lines, b_lines)
164 164 Lines from base were changed to either a or b and conflict.
165 165 """
166 166 for t in self.merge_regions():
167 167 what = t[0]
168 168 if what == b'unchanged':
169 169 yield what, self.base[t[1] : t[2]]
170 170 elif what == b'a' or what == b'same':
171 171 yield what, self.a[t[1] : t[2]]
172 172 elif what == b'b':
173 173 yield what, self.b[t[1] : t[2]]
174 174 elif what == b'conflict':
175 175 yield (
176 176 what,
177 self.base[t[1] : t[2]],
178 self.a[t[3] : t[4]],
179 self.b[t[5] : t[6]],
177 (
178 self.base[t[1] : t[2]],
179 self.a[t[3] : t[4]],
180 self.b[t[5] : t[6]],
181 ),
180 182 )
181 183 else:
182 184 raise ValueError(what)
183 185
184 186 def merge_regions(self):
185 187 """Return sequences of matching and conflicting regions.
186 188
187 189 This returns tuples, where the first value says what kind we
188 190 have:
189 191
190 192 'unchanged', start, end
191 193 Take a region of base[start:end]
192 194
193 195 'same', astart, aend
194 196 b and a are different from base but give the same result
195 197
196 198 'a', start, end
197 199 Non-clashing insertion from a[start:end]
198 200
199 201 'conflict', zstart, zend, astart, aend, bstart, bend
200 202 Conflict between a and b, with z as common ancestor
201 203
202 204 Method is as follows:
203 205
204 206 The two sequences align only on regions which match the base
205 207 and both descendants. These are found by doing a two-way diff
206 208 of each one against the base, and then finding the
207 209 intersections between those regions. These "sync regions"
208 210 are by definition unchanged in both and easily dealt with.
209 211
210 212 The regions in between can be in any of three cases:
211 213 conflicted, or changed on only one side.
212 214 """
213 215
214 216 # section a[0:ia] has been disposed of, etc
215 217 iz = ia = ib = 0
216 218
217 219 for region in self.find_sync_regions():
218 220 zmatch, zend, amatch, aend, bmatch, bend = region
219 221 # print 'match base [%d:%d]' % (zmatch, zend)
220 222
221 223 matchlen = zend - zmatch
222 224 assert matchlen >= 0
223 225 assert matchlen == (aend - amatch)
224 226 assert matchlen == (bend - bmatch)
225 227
226 228 len_a = amatch - ia
227 229 len_b = bmatch - ib
228 230 len_base = zmatch - iz
229 231 assert len_a >= 0
230 232 assert len_b >= 0
231 233 assert len_base >= 0
232 234
233 235 # print 'unmatched a=%d, b=%d' % (len_a, len_b)
234 236
235 237 if len_a or len_b:
236 238 # try to avoid actually slicing the lists
237 239 equal_a = compare_range(
238 240 self.a, ia, amatch, self.base, iz, zmatch
239 241 )
240 242 equal_b = compare_range(
241 243 self.b, ib, bmatch, self.base, iz, zmatch
242 244 )
243 245 same = compare_range(self.a, ia, amatch, self.b, ib, bmatch)
244 246
245 247 if same:
246 248 yield b'same', ia, amatch
247 249 elif equal_a and not equal_b:
248 250 yield b'b', ib, bmatch
249 251 elif equal_b and not equal_a:
250 252 yield b'a', ia, amatch
251 253 elif not equal_a and not equal_b:
252 254 yield b'conflict', iz, zmatch, ia, amatch, ib, bmatch
253 255 else:
254 256 raise AssertionError(b"can't handle a=b=base but unmatched")
255 257
256 258 ia = amatch
257 259 ib = bmatch
258 260 iz = zmatch
259 261
260 262 # if the same part of the base was deleted on both sides
261 263 # that's OK, we can just skip it.
262 264
263 265 if matchlen > 0:
264 266 assert ia == amatch
265 267 assert ib == bmatch
266 268 assert iz == zmatch
267 269
268 270 yield b'unchanged', zmatch, zend
269 271 iz = zend
270 272 ia = aend
271 273 ib = bend
272 274
273 275 def minimize(self, merge_regions):
274 276 """Trim conflict regions of lines where A and B sides match.
275 277
276 278 Lines where both A and B have made the same changes at the beginning
277 279 or the end of each merge region are eliminated from the conflict
278 280 region and are instead considered the same.
279 281 """
280 282 for region in merge_regions:
281 283 if region[0] != b"conflict":
282 284 yield region
283 285 continue
284 286 # pytype thinks this tuple contains only 3 things, but
285 287 # that's clearly not true because this code successfully
286 288 # executes. It might be wise to rework merge_regions to be
287 289 # some kind of attrs type.
288 290 (
289 291 issue,
290 292 z1,
291 293 z2,
292 294 a1,
293 295 a2,
294 296 b1,
295 297 b2,
296 298 ) = region # pytype: disable=bad-unpacking
297 299 alen = a2 - a1
298 300 blen = b2 - b1
299 301
300 302 # find matches at the front
301 303 ii = 0
302 304 while (
303 305 ii < alen and ii < blen and self.a[a1 + ii] == self.b[b1 + ii]
304 306 ):
305 307 ii += 1
306 308 startmatches = ii
307 309
308 310 # find matches at the end
309 311 ii = 0
310 312 while (
311 313 ii < alen
312 314 and ii < blen
313 315 and self.a[a2 - ii - 1] == self.b[b2 - ii - 1]
314 316 ):
315 317 ii += 1
316 318 endmatches = ii
317 319
318 320 if startmatches > 0:
319 321 yield b'same', a1, a1 + startmatches
320 322
321 323 yield (
322 324 b'conflict',
323 325 z1,
324 326 z2,
325 327 a1 + startmatches,
326 328 a2 - endmatches,
327 329 b1 + startmatches,
328 330 b2 - endmatches,
329 331 )
330 332
331 333 if endmatches > 0:
332 334 yield b'same', a2 - endmatches, a2
333 335
334 336 def find_sync_regions(self):
335 337 """Return a list of sync regions, where both descendants match the base.
336 338
337 339 Generates a list of (base1, base2, a1, a2, b1, b2). There is
338 340 always a zero-length sync region at the end of all the files.
339 341 """
340 342
341 343 ia = ib = 0
342 344 amatches = mdiff.get_matching_blocks(self.basetext, self.atext)
343 345 bmatches = mdiff.get_matching_blocks(self.basetext, self.btext)
344 346 len_a = len(amatches)
345 347 len_b = len(bmatches)
346 348
347 349 sl = []
348 350
349 351 while ia < len_a and ib < len_b:
350 352 abase, amatch, alen = amatches[ia]
351 353 bbase, bmatch, blen = bmatches[ib]
352 354
353 355 # there is an unconflicted block at i; how long does it
354 356 # extend? until whichever one ends earlier.
355 357 i = intersect((abase, abase + alen), (bbase, bbase + blen))
356 358 if i:
357 359 intbase = i[0]
358 360 intend = i[1]
359 361 intlen = intend - intbase
360 362
361 363 # found a match of base[i[0], i[1]]; this may be less than
362 364 # the region that matches in either one
363 365 assert intlen <= alen
364 366 assert intlen <= blen
365 367 assert abase <= intbase
366 368 assert bbase <= intbase
367 369
368 370 asub = amatch + (intbase - abase)
369 371 bsub = bmatch + (intbase - bbase)
370 372 aend = asub + intlen
371 373 bend = bsub + intlen
372 374
373 375 assert self.base[intbase:intend] == self.a[asub:aend], (
374 376 self.base[intbase:intend],
375 377 self.a[asub:aend],
376 378 )
377 379
378 380 assert self.base[intbase:intend] == self.b[bsub:bend]
379 381
380 382 sl.append((intbase, intend, asub, aend, bsub, bend))
381 383
382 384 # advance whichever one ends first in the base text
383 385 if (abase + alen) < (bbase + blen):
384 386 ia += 1
385 387 else:
386 388 ib += 1
387 389
388 390 intbase = len(self.base)
389 391 abase = len(self.a)
390 392 bbase = len(self.b)
391 393 sl.append((intbase, intbase, abase, abase, bbase, bbase))
392 394
393 395 return sl
394 396
395 397
396 398 def _verifytext(text, path, ui, opts):
397 399 """verifies that text is non-binary (unless opts[text] is passed,
398 400 then we just warn)"""
399 401 if stringutil.binary(text):
400 402 msg = _(b"%s looks like a binary file.") % path
401 403 if not opts.get('quiet'):
402 404 ui.warn(_(b'warning: %s\n') % msg)
403 405 if not opts.get('text'):
404 406 raise error.Abort(msg)
405 407 return text
406 408
407 409
408 410 def _picklabels(defaults, overrides):
409 411 if len(overrides) > 3:
410 412 raise error.Abort(_(b"can only specify three labels."))
411 413 result = defaults[:]
412 414 for i, override in enumerate(overrides):
413 415 result[i] = override
414 416 return result
415 417
416 418
417 419 def _mergediff(m3, name_a, name_b, name_base):
418 420 lines = []
419 421 conflicts = False
420 for group in m3.merge_groups():
421 if group[0] == b'conflict':
422 base_lines, a_lines, b_lines = group[1:]
422 for what, group_lines in m3.merge_groups():
423 if what == b'conflict':
424 base_lines, a_lines, b_lines = group_lines
423 425 base_text = b''.join(base_lines)
424 426 b_blocks = list(
425 427 mdiff.allblocks(
426 428 base_text,
427 429 b''.join(b_lines),
428 430 lines1=base_lines,
429 431 lines2=b_lines,
430 432 )
431 433 )
432 434 a_blocks = list(
433 435 mdiff.allblocks(
434 436 base_text,
435 437 b''.join(a_lines),
436 438 lines1=base_lines,
437 439 lines2=b_lines,
438 440 )
439 441 )
440 442
441 443 def matching_lines(blocks):
442 444 return sum(
443 445 block[1] - block[0]
444 446 for block, kind in blocks
445 447 if kind == b'='
446 448 )
447 449
448 450 def diff_lines(blocks, lines1, lines2):
449 451 for block, kind in blocks:
450 452 if kind == b'=':
451 453 for line in lines1[block[0] : block[1]]:
452 454 yield b' ' + line
453 455 else:
454 456 for line in lines1[block[0] : block[1]]:
455 457 yield b'-' + line
456 458 for line in lines2[block[2] : block[3]]:
457 459 yield b'+' + line
458 460
459 461 lines.append(b"<<<<<<<\n")
460 462 if matching_lines(a_blocks) < matching_lines(b_blocks):
461 463 lines.append(b"======= %s\n" % name_a)
462 464 lines.extend(a_lines)
463 465 lines.append(b"------- %s\n" % name_base)
464 466 lines.append(b"+++++++ %s\n" % name_b)
465 467 lines.extend(diff_lines(b_blocks, base_lines, b_lines))
466 468 else:
467 469 lines.append(b"------- %s\n" % name_base)
468 470 lines.append(b"+++++++ %s\n" % name_a)
469 471 lines.extend(diff_lines(a_blocks, base_lines, a_lines))
470 472 lines.append(b"======= %s\n" % name_b)
471 473 lines.extend(b_lines)
472 474 lines.append(b">>>>>>>\n")
473 475 conflicts = True
474 476 else:
475 lines.extend(group[1])
477 lines.extend(group_lines)
476 478 return lines, conflicts
477 479
478 480
479 481 def _resolve(m3, sides):
480 482 lines = []
481 for group in m3.merge_groups():
482 if group[0] == b'conflict':
483 for what, group_lines in m3.merge_groups():
484 if what == b'conflict':
483 485 for side in sides:
484 lines.extend(group[side + 1])
486 lines.extend(group_lines[side])
485 487 else:
486 lines.extend(group[1])
488 lines.extend(group_lines)
487 489 return lines
488 490
489 491
490 492 def simplemerge(ui, localctx, basectx, otherctx, **opts):
491 493 """Performs the simplemerge algorithm.
492 494
493 495 The merged result is written into `localctx`.
494 496 """
495 497
496 498 def readctx(ctx):
497 499 # Merges were always run in the working copy before, which means
498 500 # they used decoded data, if the user defined any repository
499 501 # filters.
500 502 #
501 503 # Maintain that behavior today for BC, though perhaps in the future
502 504 # it'd be worth considering whether merging encoded data (what the
503 505 # repository usually sees) might be more useful.
504 506 return _verifytext(ctx.decodeddata(), ctx.path(), ui, opts)
505 507
506 508 try:
507 509 localtext = readctx(localctx)
508 510 basetext = readctx(basectx)
509 511 othertext = readctx(otherctx)
510 512 except error.Abort:
511 513 return 1
512 514
513 515 m3 = Merge3Text(basetext, localtext, othertext)
514 516 conflicts = False
515 517 mode = opts.get('mode', b'merge')
516 518 if mode == b'union':
517 519 lines = _resolve(m3, (1, 2))
518 520 elif mode == b'local':
519 521 lines = _resolve(m3, (1,))
520 522 elif mode == b'other':
521 523 lines = _resolve(m3, (2,))
522 524 else:
523 525 name_a, name_b, name_base = _picklabels(
524 526 [localctx.path(), otherctx.path(), None], opts.get('label', [])
525 527 )
526 528 if mode == b'mergediff':
527 529 lines, conflicts = _mergediff(m3, name_a, name_b, name_base)
528 530 else:
529 531 extrakwargs = {
530 532 'minimize': True,
531 533 }
532 534 if name_base is not None:
533 535 extrakwargs['base_marker'] = b'|||||||'
534 536 extrakwargs['name_base'] = name_base
535 537 extrakwargs['minimize'] = False
536 538 lines = list(
537 539 m3.merge_lines(name_a=name_a, name_b=name_b, **extrakwargs)
538 540 )
539 541 conflicts = m3.conflicts
540 542
541 543 mergedtext = b''.join(lines)
542 544 if opts.get('print'):
543 545 ui.fout.write(mergedtext)
544 546 else:
545 547 # localctx.flags() already has the merged flags (done in
546 548 # mergestate.resolve())
547 549 localctx.write(mergedtext, localctx.flags())
548 550
549 551 if conflicts:
550 552 return 1
@@ -1,386 +1,386 b''
1 1 # Copyright (C) 2004, 2005 Canonical Ltd
2 2 #
3 3 # This program is free software; you can redistribute it and/or modify
4 4 # it under the terms of the GNU General Public License as published by
5 5 # the Free Software Foundation; either version 2 of the License, or
6 6 # (at your option) any later version.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU General Public License
14 14 # along with this program; if not, see <http://www.gnu.org/licenses/>.
15 15
16 16 from __future__ import absolute_import
17 17
18 18 import unittest
19 19 from mercurial import (
20 20 error,
21 21 simplemerge,
22 22 util,
23 23 )
24 24
25 25 from mercurial.utils import stringutil
26 26
27 27 TestCase = unittest.TestCase
28 28 # bzr compatible interface, for the tests
29 29 class Merge3(simplemerge.Merge3Text):
30 30 """3-way merge of texts.
31 31
32 32 Given BASE, OTHER, THIS, tries to produce a combined text
33 33 incorporating the changes from both BASE->OTHER and BASE->THIS.
34 34 All three will typically be sequences of lines."""
35 35
36 36 def __init__(self, base, a, b):
37 37 basetext = b'\n'.join([i.strip(b'\n') for i in base] + [b''])
38 38 atext = b'\n'.join([i.strip(b'\n') for i in a] + [b''])
39 39 btext = b'\n'.join([i.strip(b'\n') for i in b] + [b''])
40 40 if (
41 41 stringutil.binary(basetext)
42 42 or stringutil.binary(atext)
43 43 or stringutil.binary(btext)
44 44 ):
45 45 raise error.Abort(b"don't know how to merge binary files")
46 46 simplemerge.Merge3Text.__init__(
47 47 self, basetext, atext, btext, base, a, b
48 48 )
49 49
50 50
51 51 CantReprocessAndShowBase = simplemerge.CantReprocessAndShowBase
52 52
53 53
54 54 def split_lines(t):
55 55 return util.stringio(t).readlines()
56 56
57 57
58 58 ############################################################
59 59 # test case data from the gnu diffutils manual
60 60 # common base
61 61 TZU = split_lines(
62 62 b""" The Nameless is the origin of Heaven and Earth;
63 63 The named is the mother of all things.
64 64
65 65 Therefore let there always be non-being,
66 66 so we may see their subtlety,
67 67 And let there always be being,
68 68 so we may see their outcome.
69 69 The two are the same,
70 70 But after they are produced,
71 71 they have different names.
72 72 They both may be called deep and profound.
73 73 Deeper and more profound,
74 74 The door of all subtleties!
75 75 """
76 76 )
77 77
78 78 LAO = split_lines(
79 79 b""" The Way that can be told of is not the eternal Way;
80 80 The name that can be named is not the eternal name.
81 81 The Nameless is the origin of Heaven and Earth;
82 82 The Named is the mother of all things.
83 83 Therefore let there always be non-being,
84 84 so we may see their subtlety,
85 85 And let there always be being,
86 86 so we may see their outcome.
87 87 The two are the same,
88 88 But after they are produced,
89 89 they have different names.
90 90 """
91 91 )
92 92
93 93
94 94 TAO = split_lines(
95 95 b""" The Way that can be told of is not the eternal Way;
96 96 The name that can be named is not the eternal name.
97 97 The Nameless is the origin of Heaven and Earth;
98 98 The named is the mother of all things.
99 99
100 100 Therefore let there always be non-being,
101 101 so we may see their subtlety,
102 102 And let there always be being,
103 103 so we may see their result.
104 104 The two are the same,
105 105 But after they are produced,
106 106 they have different names.
107 107
108 108 -- The Way of Lao-Tzu, tr. Wing-tsit Chan
109 109
110 110 """
111 111 )
112 112
113 113 MERGED_RESULT = split_lines(
114 114 b"""\
115 115 The Way that can be told of is not the eternal Way;
116 116 The name that can be named is not the eternal name.
117 117 The Nameless is the origin of Heaven and Earth;
118 118 The Named is the mother of all things.
119 119 Therefore let there always be non-being,
120 120 so we may see their subtlety,
121 121 And let there always be being,
122 122 so we may see their result.
123 123 The two are the same,
124 124 But after they are produced,
125 125 they have different names.\
126 126 \n<<<<<<< LAO\
127 127 \n=======
128 128
129 129 -- The Way of Lao-Tzu, tr. Wing-tsit Chan
130 130 \
131 131 \n>>>>>>> TAO
132 132 """
133 133 )
134 134
135 135
136 136 class TestMerge3(TestCase):
137 137 def log(self, msg):
138 138 pass
139 139
140 140 def test_no_changes(self):
141 141 """No conflicts because nothing changed"""
142 142 m3 = Merge3([b'aaa', b'bbb'], [b'aaa', b'bbb'], [b'aaa', b'bbb'])
143 143
144 144 self.assertEqual(
145 145 list(m3.find_sync_regions()),
146 146 [(0, 2, 0, 2, 0, 2), (2, 2, 2, 2, 2, 2)],
147 147 )
148 148
149 149 self.assertEqual(list(m3.merge_regions()), [(b'unchanged', 0, 2)])
150 150
151 151 self.assertEqual(
152 152 list(m3.merge_groups()), [(b'unchanged', [b'aaa', b'bbb'])]
153 153 )
154 154
155 155 def test_front_insert(self):
156 156 m3 = Merge3([b'zz'], [b'aaa', b'bbb', b'zz'], [b'zz'])
157 157
158 158 # todo: should use a sentinel at end as from get_matching_blocks
159 159 # to match without zz
160 160 self.assertEqual(
161 161 list(m3.find_sync_regions()),
162 162 [(0, 1, 2, 3, 0, 1), (1, 1, 3, 3, 1, 1)],
163 163 )
164 164
165 165 self.assertEqual(
166 166 list(m3.merge_regions()), [(b'a', 0, 2), (b'unchanged', 0, 1)]
167 167 )
168 168
169 169 self.assertEqual(
170 170 list(m3.merge_groups()),
171 171 [(b'a', [b'aaa', b'bbb']), (b'unchanged', [b'zz'])],
172 172 )
173 173
174 174 def test_null_insert(self):
175 175 m3 = Merge3([], [b'aaa', b'bbb'], [])
176 176 # todo: should use a sentinel at end as from get_matching_blocks
177 177 # to match without zz
178 178 self.assertEqual(list(m3.find_sync_regions()), [(0, 0, 2, 2, 0, 0)])
179 179
180 180 self.assertEqual(list(m3.merge_regions()), [(b'a', 0, 2)])
181 181
182 182 self.assertEqual(list(m3.merge_lines()), [b'aaa', b'bbb'])
183 183
184 184 def test_no_conflicts(self):
185 185 """No conflicts because only one side changed"""
186 186 m3 = Merge3(
187 187 [b'aaa', b'bbb'], [b'aaa', b'111', b'bbb'], [b'aaa', b'bbb']
188 188 )
189 189
190 190 self.assertEqual(
191 191 list(m3.find_sync_regions()),
192 192 [(0, 1, 0, 1, 0, 1), (1, 2, 2, 3, 1, 2), (2, 2, 3, 3, 2, 2)],
193 193 )
194 194
195 195 self.assertEqual(
196 196 list(m3.merge_regions()),
197 197 [(b'unchanged', 0, 1), (b'a', 1, 2), (b'unchanged', 1, 2)],
198 198 )
199 199
200 200 def test_append_a(self):
201 201 m3 = Merge3(
202 202 [b'aaa\n', b'bbb\n'],
203 203 [b'aaa\n', b'bbb\n', b'222\n'],
204 204 [b'aaa\n', b'bbb\n'],
205 205 )
206 206
207 207 self.assertEqual(b''.join(m3.merge_lines()), b'aaa\nbbb\n222\n')
208 208
209 209 def test_append_b(self):
210 210 m3 = Merge3(
211 211 [b'aaa\n', b'bbb\n'],
212 212 [b'aaa\n', b'bbb\n'],
213 213 [b'aaa\n', b'bbb\n', b'222\n'],
214 214 )
215 215
216 216 self.assertEqual(b''.join(m3.merge_lines()), b'aaa\nbbb\n222\n')
217 217
218 218 def test_append_agreement(self):
219 219 m3 = Merge3(
220 220 [b'aaa\n', b'bbb\n'],
221 221 [b'aaa\n', b'bbb\n', b'222\n'],
222 222 [b'aaa\n', b'bbb\n', b'222\n'],
223 223 )
224 224
225 225 self.assertEqual(b''.join(m3.merge_lines()), b'aaa\nbbb\n222\n')
226 226
227 227 def test_append_clash(self):
228 228 m3 = Merge3(
229 229 [b'aaa\n', b'bbb\n'],
230 230 [b'aaa\n', b'bbb\n', b'222\n'],
231 231 [b'aaa\n', b'bbb\n', b'333\n'],
232 232 )
233 233
234 234 ml = m3.merge_lines(
235 235 name_a=b'a',
236 236 name_b=b'b',
237 237 start_marker=b'<<',
238 238 mid_marker=b'--',
239 239 end_marker=b'>>',
240 240 )
241 241 self.assertEqual(
242 242 b''.join(ml),
243 243 b'aaa\n' b'bbb\n' b'<< a\n' b'222\n' b'--\n' b'333\n' b'>> b\n',
244 244 )
245 245
246 246 def test_insert_agreement(self):
247 247 m3 = Merge3(
248 248 [b'aaa\n', b'bbb\n'],
249 249 [b'aaa\n', b'222\n', b'bbb\n'],
250 250 [b'aaa\n', b'222\n', b'bbb\n'],
251 251 )
252 252
253 253 ml = m3.merge_lines(
254 254 name_a=b'a',
255 255 name_b=b'b',
256 256 start_marker=b'<<',
257 257 mid_marker=b'--',
258 258 end_marker=b'>>',
259 259 )
260 260 self.assertEqual(b''.join(ml), b'aaa\n222\nbbb\n')
261 261
262 262 def test_insert_clash(self):
263 263 """Both try to insert lines in the same place."""
264 264 m3 = Merge3(
265 265 [b'aaa\n', b'bbb\n'],
266 266 [b'aaa\n', b'111\n', b'bbb\n'],
267 267 [b'aaa\n', b'222\n', b'bbb\n'],
268 268 )
269 269
270 270 self.assertEqual(
271 271 list(m3.find_sync_regions()),
272 272 [(0, 1, 0, 1, 0, 1), (1, 2, 2, 3, 2, 3), (2, 2, 3, 3, 3, 3)],
273 273 )
274 274
275 275 self.assertEqual(
276 276 list(m3.merge_regions()),
277 277 [
278 278 (b'unchanged', 0, 1),
279 279 (b'conflict', 1, 1, 1, 2, 1, 2),
280 280 (b'unchanged', 1, 2),
281 281 ],
282 282 )
283 283
284 284 self.assertEqual(
285 285 list(m3.merge_groups()),
286 286 [
287 287 (b'unchanged', [b'aaa\n']),
288 (b'conflict', [], [b'111\n'], [b'222\n']),
288 (b'conflict', ([], [b'111\n'], [b'222\n'])),
289 289 (b'unchanged', [b'bbb\n']),
290 290 ],
291 291 )
292 292
293 293 ml = m3.merge_lines(
294 294 name_a=b'a',
295 295 name_b=b'b',
296 296 start_marker=b'<<',
297 297 mid_marker=b'--',
298 298 end_marker=b'>>',
299 299 )
300 300 self.assertEqual(
301 301 b''.join(ml),
302 302 b'''aaa
303 303 << a
304 304 111
305 305 --
306 306 222
307 307 >> b
308 308 bbb
309 309 ''',
310 310 )
311 311
312 312 def test_replace_clash(self):
313 313 """Both try to insert lines in the same place."""
314 314 m3 = Merge3(
315 315 [b'aaa', b'000', b'bbb'],
316 316 [b'aaa', b'111', b'bbb'],
317 317 [b'aaa', b'222', b'bbb'],
318 318 )
319 319
320 320 self.assertEqual(
321 321 list(m3.find_sync_regions()),
322 322 [(0, 1, 0, 1, 0, 1), (2, 3, 2, 3, 2, 3), (3, 3, 3, 3, 3, 3)],
323 323 )
324 324
325 325 def test_replace_multi(self):
326 326 """Replacement with regions of different size."""
327 327 m3 = Merge3(
328 328 [b'aaa', b'000', b'000', b'bbb'],
329 329 [b'aaa', b'111', b'111', b'111', b'bbb'],
330 330 [b'aaa', b'222', b'222', b'222', b'222', b'bbb'],
331 331 )
332 332
333 333 self.assertEqual(
334 334 list(m3.find_sync_regions()),
335 335 [(0, 1, 0, 1, 0, 1), (3, 4, 4, 5, 5, 6), (4, 4, 5, 5, 6, 6)],
336 336 )
337 337
338 338 def test_merge_poem(self):
339 339 """Test case from diff3 manual"""
340 340 m3 = Merge3(TZU, LAO, TAO)
341 341 ml = list(m3.merge_lines(b'LAO', b'TAO'))
342 342 self.log(b'merge result:')
343 343 self.log(b''.join(ml))
344 344 self.assertEqual(ml, MERGED_RESULT)
345 345
346 346 def test_binary(self):
347 347 with self.assertRaises(error.Abort):
348 348 Merge3([b'\x00'], [b'a'], [b'b'])
349 349
350 350 def test_dos_text(self):
351 351 base_text = b'a\r\n'
352 352 this_text = b'b\r\n'
353 353 other_text = b'c\r\n'
354 354 m3 = Merge3(
355 355 base_text.splitlines(True),
356 356 other_text.splitlines(True),
357 357 this_text.splitlines(True),
358 358 )
359 359 m_lines = m3.merge_lines(b'OTHER', b'THIS')
360 360 self.assertEqual(
361 361 b'<<<<<<< OTHER\r\nc\r\n=======\r\nb\r\n'
362 362 b'>>>>>>> THIS\r\n'.splitlines(True),
363 363 list(m_lines),
364 364 )
365 365
366 366 def test_mac_text(self):
367 367 base_text = b'a\r'
368 368 this_text = b'b\r'
369 369 other_text = b'c\r'
370 370 m3 = Merge3(
371 371 base_text.splitlines(True),
372 372 other_text.splitlines(True),
373 373 this_text.splitlines(True),
374 374 )
375 375 m_lines = m3.merge_lines(b'OTHER', b'THIS')
376 376 self.assertEqual(
377 377 b'<<<<<<< OTHER\rc\r=======\rb\r'
378 378 b'>>>>>>> THIS\r'.splitlines(True),
379 379 list(m_lines),
380 380 )
381 381
382 382
383 383 if __name__ == '__main__':
384 384 import silenttestrunner
385 385
386 386 silenttestrunner.main(__name__)
General Comments 0
You need to be logged in to leave comments. Login now