##// END OF EJS Templates
simplemerge: convert MergeInput to regular, non-attr.ib class...
Martin von Zweigbergk -
r49594:aed8ef33 default
parent child Browse files
Show More
@@ -1,552 +1,551 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 from .thirdparty import attr
23 22 from . import (
24 23 error,
25 24 mdiff,
26 25 pycompat,
27 26 )
28 27 from .utils import stringutil
29 28
30 29
31 30 def intersect(ra, rb):
32 31 """Given two ranges return the range where they intersect or None.
33 32
34 33 >>> intersect((0, 10), (0, 6))
35 34 (0, 6)
36 35 >>> intersect((0, 10), (5, 15))
37 36 (5, 10)
38 37 >>> intersect((0, 10), (10, 15))
39 38 >>> intersect((0, 9), (10, 15))
40 39 >>> intersect((0, 9), (7, 15))
41 40 (7, 9)
42 41 """
43 42 assert ra[0] <= ra[1]
44 43 assert rb[0] <= rb[1]
45 44
46 45 sa = max(ra[0], rb[0])
47 46 sb = min(ra[1], rb[1])
48 47 if sa < sb:
49 48 return sa, sb
50 49 else:
51 50 return None
52 51
53 52
54 53 def compare_range(a, astart, aend, b, bstart, bend):
55 54 """Compare a[astart:aend] == b[bstart:bend], without slicing."""
56 55 if (aend - astart) != (bend - bstart):
57 56 return False
58 57 for ia, ib in zip(
59 58 pycompat.xrange(astart, aend), pycompat.xrange(bstart, bend)
60 59 ):
61 60 if a[ia] != b[ib]:
62 61 return False
63 62 else:
64 63 return True
65 64
66 65
67 66 class Merge3Text(object):
68 67 """3-way merge of texts.
69 68
70 69 Given strings BASE, OTHER, THIS, tries to produce a combined text
71 70 incorporating the changes from both BASE->OTHER and BASE->THIS."""
72 71
73 72 def __init__(self, basetext, atext, btext, base=None, a=None, b=None):
74 73 self.basetext = basetext
75 74 self.atext = atext
76 75 self.btext = btext
77 76 if base is None:
78 77 base = mdiff.splitnewlines(basetext)
79 78 if a is None:
80 79 a = mdiff.splitnewlines(atext)
81 80 if b is None:
82 81 b = mdiff.splitnewlines(btext)
83 82 self.base = base
84 83 self.a = a
85 84 self.b = b
86 85
87 86 def merge_groups(self):
88 87 """Yield sequence of line groups. Each one is a tuple:
89 88
90 89 'unchanged', lines
91 90 Lines unchanged from base
92 91
93 92 'a', lines
94 93 Lines taken from a
95 94
96 95 'same', lines
97 96 Lines taken from a (and equal to b)
98 97
99 98 'b', lines
100 99 Lines taken from b
101 100
102 101 'conflict', (base_lines, a_lines, b_lines)
103 102 Lines from base were changed to either a or b and conflict.
104 103 """
105 104 for t in self.merge_regions():
106 105 what = t[0]
107 106 if what == b'unchanged':
108 107 yield what, self.base[t[1] : t[2]]
109 108 elif what == b'a' or what == b'same':
110 109 yield what, self.a[t[1] : t[2]]
111 110 elif what == b'b':
112 111 yield what, self.b[t[1] : t[2]]
113 112 elif what == b'conflict':
114 113 yield (
115 114 what,
116 115 (
117 116 self.base[t[1] : t[2]],
118 117 self.a[t[3] : t[4]],
119 118 self.b[t[5] : t[6]],
120 119 ),
121 120 )
122 121 else:
123 122 raise ValueError(what)
124 123
125 124 def merge_regions(self):
126 125 """Return sequences of matching and conflicting regions.
127 126
128 127 This returns tuples, where the first value says what kind we
129 128 have:
130 129
131 130 'unchanged', start, end
132 131 Take a region of base[start:end]
133 132
134 133 'same', astart, aend
135 134 b and a are different from base but give the same result
136 135
137 136 'a', start, end
138 137 Non-clashing insertion from a[start:end]
139 138
140 139 'conflict', zstart, zend, astart, aend, bstart, bend
141 140 Conflict between a and b, with z as common ancestor
142 141
143 142 Method is as follows:
144 143
145 144 The two sequences align only on regions which match the base
146 145 and both descendants. These are found by doing a two-way diff
147 146 of each one against the base, and then finding the
148 147 intersections between those regions. These "sync regions"
149 148 are by definition unchanged in both and easily dealt with.
150 149
151 150 The regions in between can be in any of three cases:
152 151 conflicted, or changed on only one side.
153 152 """
154 153
155 154 # section a[0:ia] has been disposed of, etc
156 155 iz = ia = ib = 0
157 156
158 157 for region in self.find_sync_regions():
159 158 zmatch, zend, amatch, aend, bmatch, bend = region
160 159 # print 'match base [%d:%d]' % (zmatch, zend)
161 160
162 161 matchlen = zend - zmatch
163 162 assert matchlen >= 0
164 163 assert matchlen == (aend - amatch)
165 164 assert matchlen == (bend - bmatch)
166 165
167 166 len_a = amatch - ia
168 167 len_b = bmatch - ib
169 168 len_base = zmatch - iz
170 169 assert len_a >= 0
171 170 assert len_b >= 0
172 171 assert len_base >= 0
173 172
174 173 # print 'unmatched a=%d, b=%d' % (len_a, len_b)
175 174
176 175 if len_a or len_b:
177 176 # try to avoid actually slicing the lists
178 177 equal_a = compare_range(
179 178 self.a, ia, amatch, self.base, iz, zmatch
180 179 )
181 180 equal_b = compare_range(
182 181 self.b, ib, bmatch, self.base, iz, zmatch
183 182 )
184 183 same = compare_range(self.a, ia, amatch, self.b, ib, bmatch)
185 184
186 185 if same:
187 186 yield b'same', ia, amatch
188 187 elif equal_a and not equal_b:
189 188 yield b'b', ib, bmatch
190 189 elif equal_b and not equal_a:
191 190 yield b'a', ia, amatch
192 191 elif not equal_a and not equal_b:
193 192 yield b'conflict', iz, zmatch, ia, amatch, ib, bmatch
194 193 else:
195 194 raise AssertionError(b"can't handle a=b=base but unmatched")
196 195
197 196 ia = amatch
198 197 ib = bmatch
199 198 iz = zmatch
200 199
201 200 # if the same part of the base was deleted on both sides
202 201 # that's OK, we can just skip it.
203 202
204 203 if matchlen > 0:
205 204 assert ia == amatch
206 205 assert ib == bmatch
207 206 assert iz == zmatch
208 207
209 208 yield b'unchanged', zmatch, zend
210 209 iz = zend
211 210 ia = aend
212 211 ib = bend
213 212
214 213 def find_sync_regions(self):
215 214 """Return a list of sync regions, where both descendants match the base.
216 215
217 216 Generates a list of (base1, base2, a1, a2, b1, b2). There is
218 217 always a zero-length sync region at the end of all the files.
219 218 """
220 219
221 220 ia = ib = 0
222 221 amatches = mdiff.get_matching_blocks(self.basetext, self.atext)
223 222 bmatches = mdiff.get_matching_blocks(self.basetext, self.btext)
224 223 len_a = len(amatches)
225 224 len_b = len(bmatches)
226 225
227 226 sl = []
228 227
229 228 while ia < len_a and ib < len_b:
230 229 abase, amatch, alen = amatches[ia]
231 230 bbase, bmatch, blen = bmatches[ib]
232 231
233 232 # there is an unconflicted block at i; how long does it
234 233 # extend? until whichever one ends earlier.
235 234 i = intersect((abase, abase + alen), (bbase, bbase + blen))
236 235 if i:
237 236 intbase = i[0]
238 237 intend = i[1]
239 238 intlen = intend - intbase
240 239
241 240 # found a match of base[i[0], i[1]]; this may be less than
242 241 # the region that matches in either one
243 242 assert intlen <= alen
244 243 assert intlen <= blen
245 244 assert abase <= intbase
246 245 assert bbase <= intbase
247 246
248 247 asub = amatch + (intbase - abase)
249 248 bsub = bmatch + (intbase - bbase)
250 249 aend = asub + intlen
251 250 bend = bsub + intlen
252 251
253 252 assert self.base[intbase:intend] == self.a[asub:aend], (
254 253 self.base[intbase:intend],
255 254 self.a[asub:aend],
256 255 )
257 256
258 257 assert self.base[intbase:intend] == self.b[bsub:bend]
259 258
260 259 sl.append((intbase, intend, asub, aend, bsub, bend))
261 260
262 261 # advance whichever one ends first in the base text
263 262 if (abase + alen) < (bbase + blen):
264 263 ia += 1
265 264 else:
266 265 ib += 1
267 266
268 267 intbase = len(self.base)
269 268 abase = len(self.a)
270 269 bbase = len(self.b)
271 270 sl.append((intbase, intbase, abase, abase, bbase, bbase))
272 271
273 272 return sl
274 273
275 274
276 275 def _verifytext(text, path, ui, quiet=False, allow_binary=False):
277 276 """verifies that text is non-binary (unless opts[text] is passed,
278 277 then we just warn)"""
279 278 if stringutil.binary(text):
280 279 msg = _(b"%s looks like a binary file.") % path
281 280 if not quiet:
282 281 ui.warn(_(b'warning: %s\n') % msg)
283 282 if not allow_binary:
284 283 raise error.Abort(msg)
285 284 return text
286 285
287 286
288 287 def _format_labels(*inputs):
289 288 pad = max(len(input.label) if input.label else 0 for input in inputs)
290 289 labels = []
291 290 for input in inputs:
292 291 if input.label:
293 292 if input.label_detail:
294 293 label = (
295 294 (input.label + b':').ljust(pad + 1)
296 295 + b' '
297 296 + input.label_detail
298 297 )
299 298 else:
300 299 label = input.label
301 300 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
302 301 labels.append(stringutil.ellipsis(label, 80 - 8))
303 302 else:
304 303 labels.append(None)
305 304 return labels
306 305
307 306
308 307 def _detect_newline(m3):
309 308 if len(m3.a) > 0:
310 309 if m3.a[0].endswith(b'\r\n'):
311 310 return b'\r\n'
312 311 elif m3.a[0].endswith(b'\r'):
313 312 return b'\r'
314 313 return b'\n'
315 314
316 315
317 316 def _minimize(a_lines, b_lines):
318 317 """Trim conflict regions of lines where A and B sides match.
319 318
320 319 Lines where both A and B have made the same changes at the beginning
321 320 or the end of each merge region are eliminated from the conflict
322 321 region and are instead considered the same.
323 322 """
324 323 alen = len(a_lines)
325 324 blen = len(b_lines)
326 325
327 326 # find matches at the front
328 327 ii = 0
329 328 while ii < alen and ii < blen and a_lines[ii] == b_lines[ii]:
330 329 ii += 1
331 330 startmatches = ii
332 331
333 332 # find matches at the end
334 333 ii = 0
335 334 while ii < alen and ii < blen and a_lines[-ii - 1] == b_lines[-ii - 1]:
336 335 ii += 1
337 336 endmatches = ii
338 337
339 338 lines_before = a_lines[:startmatches]
340 339 new_a_lines = a_lines[startmatches : alen - endmatches]
341 340 new_b_lines = b_lines[startmatches : blen - endmatches]
342 341 lines_after = a_lines[alen - endmatches :]
343 342 return lines_before, new_a_lines, new_b_lines, lines_after
344 343
345 344
346 345 def render_minimized(
347 346 m3,
348 347 name_a=None,
349 348 name_b=None,
350 349 start_marker=b'<<<<<<<',
351 350 mid_marker=b'=======',
352 351 end_marker=b'>>>>>>>',
353 352 ):
354 353 """Return merge in cvs-like form."""
355 354 newline = _detect_newline(m3)
356 355 conflicts = False
357 356 if name_a:
358 357 start_marker = start_marker + b' ' + name_a
359 358 if name_b:
360 359 end_marker = end_marker + b' ' + name_b
361 360 merge_groups = m3.merge_groups()
362 361 lines = []
363 362 for what, group_lines in merge_groups:
364 363 if what == b'conflict':
365 364 conflicts = True
366 365 base_lines, a_lines, b_lines = group_lines
367 366 minimized = _minimize(a_lines, b_lines)
368 367 lines_before, a_lines, b_lines, lines_after = minimized
369 368 lines.extend(lines_before)
370 369 lines.append(start_marker + newline)
371 370 lines.extend(a_lines)
372 371 lines.append(mid_marker + newline)
373 372 lines.extend(b_lines)
374 373 lines.append(end_marker + newline)
375 374 lines.extend(lines_after)
376 375 else:
377 376 lines.extend(group_lines)
378 377 return lines, conflicts
379 378
380 379
381 380 def render_merge3(m3, name_a, name_b, name_base):
382 381 """Render conflicts as 3-way conflict markers."""
383 382 newline = _detect_newline(m3)
384 383 conflicts = False
385 384 lines = []
386 385 for what, group_lines in m3.merge_groups():
387 386 if what == b'conflict':
388 387 base_lines, a_lines, b_lines = group_lines
389 388 conflicts = True
390 389 lines.append(b'<<<<<<< ' + name_a + newline)
391 390 lines.extend(a_lines)
392 391 lines.append(b'||||||| ' + name_base + newline)
393 392 lines.extend(base_lines)
394 393 lines.append(b'=======' + newline)
395 394 lines.extend(b_lines)
396 395 lines.append(b'>>>>>>> ' + name_b + newline)
397 396 else:
398 397 lines.extend(group_lines)
399 398 return lines, conflicts
400 399
401 400
402 401 def render_mergediff(m3, name_a, name_b, name_base):
403 402 """Render conflicts as conflict markers with one snapshot and one diff."""
404 403 newline = _detect_newline(m3)
405 404 lines = []
406 405 conflicts = False
407 406 for what, group_lines in m3.merge_groups():
408 407 if what == b'conflict':
409 408 base_lines, a_lines, b_lines = group_lines
410 409 base_text = b''.join(base_lines)
411 410 b_blocks = list(
412 411 mdiff.allblocks(
413 412 base_text,
414 413 b''.join(b_lines),
415 414 lines1=base_lines,
416 415 lines2=b_lines,
417 416 )
418 417 )
419 418 a_blocks = list(
420 419 mdiff.allblocks(
421 420 base_text,
422 421 b''.join(a_lines),
423 422 lines1=base_lines,
424 423 lines2=b_lines,
425 424 )
426 425 )
427 426
428 427 def matching_lines(blocks):
429 428 return sum(
430 429 block[1] - block[0]
431 430 for block, kind in blocks
432 431 if kind == b'='
433 432 )
434 433
435 434 def diff_lines(blocks, lines1, lines2):
436 435 for block, kind in blocks:
437 436 if kind == b'=':
438 437 for line in lines1[block[0] : block[1]]:
439 438 yield b' ' + line
440 439 else:
441 440 for line in lines1[block[0] : block[1]]:
442 441 yield b'-' + line
443 442 for line in lines2[block[2] : block[3]]:
444 443 yield b'+' + line
445 444
446 445 lines.append(b"<<<<<<<" + newline)
447 446 if matching_lines(a_blocks) < matching_lines(b_blocks):
448 447 lines.append(b"======= " + name_a + newline)
449 448 lines.extend(a_lines)
450 449 lines.append(b"------- " + name_base + newline)
451 450 lines.append(b"+++++++ " + name_b + newline)
452 451 lines.extend(diff_lines(b_blocks, base_lines, b_lines))
453 452 else:
454 453 lines.append(b"------- " + name_base + newline)
455 454 lines.append(b"+++++++ " + name_a + newline)
456 455 lines.extend(diff_lines(a_blocks, base_lines, a_lines))
457 456 lines.append(b"======= " + name_b + newline)
458 457 lines.extend(b_lines)
459 458 lines.append(b">>>>>>>" + newline)
460 459 conflicts = True
461 460 else:
462 461 lines.extend(group_lines)
463 462 return lines, conflicts
464 463
465 464
466 465 def _resolve(m3, sides):
467 466 lines = []
468 467 for what, group_lines in m3.merge_groups():
469 468 if what == b'conflict':
470 469 for side in sides:
471 470 lines.extend(group_lines[side])
472 471 else:
473 472 lines.extend(group_lines)
474 473 return lines
475 474
476 475
477 @attr.s
478 476 class MergeInput(object):
479 fctx = attr.ib()
480 label = attr.ib(default=None)
481 # If the "detail" part is set, then that is rendered after the label and
482 # separated by a ':'. The label is padded to make the ':' aligned among all
483 # merge inputs.
484 label_detail = attr.ib(default=None)
477 def __init__(self, fctx, label=None, label_detail=None):
478 self.fctx = fctx
479 self.label = label
480 # If the "detail" part is set, then that is rendered after the label and
481 # separated by a ':'. The label is padded to make the ':' aligned among
482 # all merge inputs.
483 self.label_detail = label_detail
485 484
486 485
487 486 def simplemerge(
488 487 ui,
489 488 local,
490 489 base,
491 490 other,
492 491 mode=b'merge',
493 492 quiet=False,
494 493 allow_binary=False,
495 494 print_result=False,
496 495 ):
497 496 """Performs the simplemerge algorithm.
498 497
499 498 The merged result is written into `localctx`.
500 499 """
501 500
502 501 def readctx(ctx):
503 502 # Merges were always run in the working copy before, which means
504 503 # they used decoded data, if the user defined any repository
505 504 # filters.
506 505 #
507 506 # Maintain that behavior today for BC, though perhaps in the future
508 507 # it'd be worth considering whether merging encoded data (what the
509 508 # repository usually sees) might be more useful.
510 509 return _verifytext(
511 510 ctx.decodeddata(),
512 511 ctx.path(),
513 512 ui,
514 513 quiet=quiet,
515 514 allow_binary=allow_binary,
516 515 )
517 516
518 517 try:
519 518 localtext = readctx(local.fctx)
520 519 basetext = readctx(base.fctx)
521 520 othertext = readctx(other.fctx)
522 521 except error.Abort:
523 522 return True
524 523
525 524 m3 = Merge3Text(basetext, localtext, othertext)
526 525 conflicts = False
527 526 if mode == b'union':
528 527 lines = _resolve(m3, (1, 2))
529 528 elif mode == b'local':
530 529 lines = _resolve(m3, (1,))
531 530 elif mode == b'other':
532 531 lines = _resolve(m3, (2,))
533 532 else:
534 533 if mode == b'mergediff':
535 534 labels = _format_labels(local, other, base)
536 535 lines, conflicts = render_mergediff(m3, *labels)
537 536 elif mode == b'merge3':
538 537 labels = _format_labels(local, other, base)
539 538 lines, conflicts = render_merge3(m3, *labels)
540 539 else:
541 540 labels = _format_labels(local, other)
542 541 lines, conflicts = render_minimized(m3, *labels)
543 542
544 543 mergedtext = b''.join(lines)
545 544 if print_result:
546 545 ui.fout.write(mergedtext)
547 546 else:
548 547 # local.fctx.flags() already has the merged flags (done in
549 548 # mergestate.resolve())
550 549 local.fctx.write(mergedtext, local.fctx.flags())
551 550
552 551 return conflicts
General Comments 0
You need to be logged in to leave comments. Login now