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