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