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