##// END OF EJS Templates
simplemerge: take over formatting of label from `filemerge`...
Martin von Zweigbergk -
r49433:3c8cc987 default
parent child Browse files
Show More
@@ -1,1264 +1,1255
1 1 # filemerge.py - file-level merge handling for Mercurial
2 2 #
3 3 # Copyright 2006, 2007, 2008 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import contextlib
11 11 import os
12 12 import re
13 13 import shutil
14 14
15 15 from .i18n import _
16 16 from .node import (
17 17 hex,
18 18 short,
19 19 )
20 20 from .pycompat import (
21 21 getattr,
22 22 open,
23 23 )
24 24
25 25 from . import (
26 26 encoding,
27 27 error,
28 28 formatter,
29 29 match,
30 30 pycompat,
31 31 registrar,
32 32 scmutil,
33 33 simplemerge,
34 34 tagmerge,
35 35 templatekw,
36 36 templater,
37 37 templateutil,
38 38 util,
39 39 )
40 40
41 41 from .utils import (
42 42 procutil,
43 stringutil,
44 43 )
45 44
46 45
47 46 def _toolstr(ui, tool, part, *args):
48 47 return ui.config(b"merge-tools", tool + b"." + part, *args)
49 48
50 49
51 50 def _toolbool(ui, tool, part, *args):
52 51 return ui.configbool(b"merge-tools", tool + b"." + part, *args)
53 52
54 53
55 54 def _toollist(ui, tool, part):
56 55 return ui.configlist(b"merge-tools", tool + b"." + part)
57 56
58 57
59 58 internals = {}
60 59 # Merge tools to document.
61 60 internalsdoc = {}
62 61
63 62 internaltool = registrar.internalmerge()
64 63
65 64 # internal tool merge types
66 65 nomerge = internaltool.nomerge
67 66 mergeonly = internaltool.mergeonly # just the full merge, no premerge
68 67 fullmerge = internaltool.fullmerge # both premerge and merge
69 68
70 69 # IMPORTANT: keep the last line of this prompt very short ("What do you want to
71 70 # do?") because of issue6158, ideally to <40 English characters (to allow other
72 71 # languages that may take more columns to still have a chance to fit in an
73 72 # 80-column screen).
74 73 _localchangedotherdeletedmsg = _(
75 74 b"file '%(fd)s' was deleted in other%(o)s but was modified in local%(l)s.\n"
76 75 b"You can use (c)hanged version, (d)elete, or leave (u)nresolved.\n"
77 76 b"What do you want to do?"
78 77 b"$$ &Changed $$ &Delete $$ &Unresolved"
79 78 )
80 79
81 80 _otherchangedlocaldeletedmsg = _(
82 81 b"file '%(fd)s' was deleted in local%(l)s but was modified in other%(o)s.\n"
83 82 b"You can use (c)hanged version, leave (d)eleted, or leave (u)nresolved.\n"
84 83 b"What do you want to do?"
85 84 b"$$ &Changed $$ &Deleted $$ &Unresolved"
86 85 )
87 86
88 87
89 88 class absentfilectx(object):
90 89 """Represents a file that's ostensibly in a context but is actually not
91 90 present in it.
92 91
93 92 This is here because it's very specific to the filemerge code for now --
94 93 other code is likely going to break with the values this returns."""
95 94
96 95 def __init__(self, ctx, f):
97 96 self._ctx = ctx
98 97 self._f = f
99 98
100 99 def __bytes__(self):
101 100 return b'absent file %s@%s' % (self._f, self._ctx)
102 101
103 102 def path(self):
104 103 return self._f
105 104
106 105 def size(self):
107 106 return None
108 107
109 108 def data(self):
110 109 return None
111 110
112 111 def filenode(self):
113 112 return self._ctx.repo().nullid
114 113
115 114 _customcmp = True
116 115
117 116 def cmp(self, fctx):
118 117 """compare with other file context
119 118
120 119 returns True if different from fctx.
121 120 """
122 121 return not (
123 122 fctx.isabsent()
124 123 and fctx.changectx() == self.changectx()
125 124 and fctx.path() == self.path()
126 125 )
127 126
128 127 def flags(self):
129 128 return b''
130 129
131 130 def changectx(self):
132 131 return self._ctx
133 132
134 133 def isbinary(self):
135 134 return False
136 135
137 136 def isabsent(self):
138 137 return True
139 138
140 139
141 140 def _findtool(ui, tool):
142 141 if tool in internals:
143 142 return tool
144 143 cmd = _toolstr(ui, tool, b"executable", tool)
145 144 if cmd.startswith(b'python:'):
146 145 return cmd
147 146 return findexternaltool(ui, tool)
148 147
149 148
150 149 def _quotetoolpath(cmd):
151 150 if cmd.startswith(b'python:'):
152 151 return cmd
153 152 return procutil.shellquote(cmd)
154 153
155 154
156 155 def findexternaltool(ui, tool):
157 156 for kn in (b"regkey", b"regkeyalt"):
158 157 k = _toolstr(ui, tool, kn)
159 158 if not k:
160 159 continue
161 160 p = util.lookupreg(k, _toolstr(ui, tool, b"regname"))
162 161 if p:
163 162 p = procutil.findexe(p + _toolstr(ui, tool, b"regappend", b""))
164 163 if p:
165 164 return p
166 165 exe = _toolstr(ui, tool, b"executable", tool)
167 166 return procutil.findexe(util.expandpath(exe))
168 167
169 168
170 169 def _picktool(repo, ui, path, binary, symlink, changedelete):
171 170 strictcheck = ui.configbool(b'merge', b'strict-capability-check')
172 171
173 172 def hascapability(tool, capability, strict=False):
174 173 if tool in internals:
175 174 return strict and internals[tool].capabilities.get(capability)
176 175 return _toolbool(ui, tool, capability)
177 176
178 177 def supportscd(tool):
179 178 return tool in internals and internals[tool].mergetype == nomerge
180 179
181 180 def check(tool, pat, symlink, binary, changedelete):
182 181 tmsg = tool
183 182 if pat:
184 183 tmsg = _(b"%s (for pattern %s)") % (tool, pat)
185 184 if not _findtool(ui, tool):
186 185 if pat: # explicitly requested tool deserves a warning
187 186 ui.warn(_(b"couldn't find merge tool %s\n") % tmsg)
188 187 else: # configured but non-existing tools are more silent
189 188 ui.note(_(b"couldn't find merge tool %s\n") % tmsg)
190 189 elif symlink and not hascapability(tool, b"symlink", strictcheck):
191 190 ui.warn(_(b"tool %s can't handle symlinks\n") % tmsg)
192 191 elif binary and not hascapability(tool, b"binary", strictcheck):
193 192 ui.warn(_(b"tool %s can't handle binary\n") % tmsg)
194 193 elif changedelete and not supportscd(tool):
195 194 # the nomerge tools are the only tools that support change/delete
196 195 # conflicts
197 196 pass
198 197 elif not procutil.gui() and _toolbool(ui, tool, b"gui"):
199 198 ui.warn(_(b"tool %s requires a GUI\n") % tmsg)
200 199 else:
201 200 return True
202 201 return False
203 202
204 203 # internal config: ui.forcemerge
205 204 # forcemerge comes from command line arguments, highest priority
206 205 force = ui.config(b'ui', b'forcemerge')
207 206 if force:
208 207 toolpath = _findtool(ui, force)
209 208 if changedelete and not supportscd(toolpath):
210 209 return b":prompt", None
211 210 else:
212 211 if toolpath:
213 212 return (force, _quotetoolpath(toolpath))
214 213 else:
215 214 # mimic HGMERGE if given tool not found
216 215 return (force, force)
217 216
218 217 # HGMERGE takes next precedence
219 218 hgmerge = encoding.environ.get(b"HGMERGE")
220 219 if hgmerge:
221 220 if changedelete and not supportscd(hgmerge):
222 221 return b":prompt", None
223 222 else:
224 223 return (hgmerge, hgmerge)
225 224
226 225 # then patterns
227 226
228 227 # whether binary capability should be checked strictly
229 228 binarycap = binary and strictcheck
230 229
231 230 for pat, tool in ui.configitems(b"merge-patterns"):
232 231 mf = match.match(repo.root, b'', [pat])
233 232 if mf(path) and check(tool, pat, symlink, binarycap, changedelete):
234 233 if binary and not hascapability(tool, b"binary", strict=True):
235 234 ui.warn(
236 235 _(
237 236 b"warning: check merge-patterns configurations,"
238 237 b" if %r for binary file %r is unintentional\n"
239 238 b"(see 'hg help merge-tools'"
240 239 b" for binary files capability)\n"
241 240 )
242 241 % (pycompat.bytestr(tool), pycompat.bytestr(path))
243 242 )
244 243 toolpath = _findtool(ui, tool)
245 244 return (tool, _quotetoolpath(toolpath))
246 245
247 246 # then merge tools
248 247 tools = {}
249 248 disabled = set()
250 249 for k, v in ui.configitems(b"merge-tools"):
251 250 t = k.split(b'.')[0]
252 251 if t not in tools:
253 252 tools[t] = int(_toolstr(ui, t, b"priority"))
254 253 if _toolbool(ui, t, b"disabled"):
255 254 disabled.add(t)
256 255 names = tools.keys()
257 256 tools = sorted(
258 257 [(-p, tool) for tool, p in tools.items() if tool not in disabled]
259 258 )
260 259 uimerge = ui.config(b"ui", b"merge")
261 260 if uimerge:
262 261 # external tools defined in uimerge won't be able to handle
263 262 # change/delete conflicts
264 263 if check(uimerge, path, symlink, binary, changedelete):
265 264 if uimerge not in names and not changedelete:
266 265 return (uimerge, uimerge)
267 266 tools.insert(0, (None, uimerge)) # highest priority
268 267 tools.append((None, b"hgmerge")) # the old default, if found
269 268 for p, t in tools:
270 269 if check(t, None, symlink, binary, changedelete):
271 270 toolpath = _findtool(ui, t)
272 271 return (t, _quotetoolpath(toolpath))
273 272
274 273 # internal merge or prompt as last resort
275 274 if symlink or binary or changedelete:
276 275 if not changedelete and len(tools):
277 276 # any tool is rejected by capability for symlink or binary
278 277 ui.warn(_(b"no tool found to merge %s\n") % path)
279 278 return b":prompt", None
280 279 return b":merge", None
281 280
282 281
283 282 def _eoltype(data):
284 283 """Guess the EOL type of a file"""
285 284 if b'\0' in data: # binary
286 285 return None
287 286 if b'\r\n' in data: # Windows
288 287 return b'\r\n'
289 288 if b'\r' in data: # Old Mac
290 289 return b'\r'
291 290 if b'\n' in data: # UNIX
292 291 return b'\n'
293 292 return None # unknown
294 293
295 294
296 295 def _matcheol(file, backup):
297 296 """Convert EOL markers in a file to match origfile"""
298 297 tostyle = _eoltype(backup.data()) # No repo.wread filters?
299 298 if tostyle:
300 299 data = util.readfile(file)
301 300 style = _eoltype(data)
302 301 if style:
303 302 newdata = data.replace(style, tostyle)
304 303 if newdata != data:
305 304 util.writefile(file, newdata)
306 305
307 306
308 307 @internaltool(b'prompt', nomerge)
309 308 def _iprompt(repo, mynode, local, other, base, toolconf):
310 309 """Asks the user which of the local `p1()` or the other `p2()` version to
311 310 keep as the merged version."""
312 311 ui = repo.ui
313 312 fd = local.fctx.path()
314 313 uipathfn = scmutil.getuipathfn(repo)
315 314
316 315 # Avoid prompting during an in-memory merge since it doesn't support merge
317 316 # conflicts.
318 317 if local.fctx.changectx().isinmemory():
319 318 raise error.InMemoryMergeConflictsError(
320 319 b'in-memory merge does not support file conflicts'
321 320 )
322 321
323 322 prompts = partextras([local.label, other.label])
324 323 prompts[b'fd'] = uipathfn(fd)
325 324 try:
326 325 if other.fctx.isabsent():
327 326 index = ui.promptchoice(_localchangedotherdeletedmsg % prompts, 2)
328 327 choice = [b'local', b'other', b'unresolved'][index]
329 328 elif local.fctx.isabsent():
330 329 index = ui.promptchoice(_otherchangedlocaldeletedmsg % prompts, 2)
331 330 choice = [b'other', b'local', b'unresolved'][index]
332 331 else:
333 332 # IMPORTANT: keep the last line of this prompt ("What do you want to
334 333 # do?") very short, see comment next to _localchangedotherdeletedmsg
335 334 # at the top of the file for details.
336 335 index = ui.promptchoice(
337 336 _(
338 337 b"file '%(fd)s' needs to be resolved.\n"
339 338 b"You can keep (l)ocal%(l)s, take (o)ther%(o)s, or leave "
340 339 b"(u)nresolved.\n"
341 340 b"What do you want to do?"
342 341 b"$$ &Local $$ &Other $$ &Unresolved"
343 342 )
344 343 % prompts,
345 344 2,
346 345 )
347 346 choice = [b'local', b'other', b'unresolved'][index]
348 347
349 348 if choice == b'other':
350 349 return _iother(repo, mynode, local, other, base, toolconf)
351 350 elif choice == b'local':
352 351 return _ilocal(repo, mynode, local, other, base, toolconf)
353 352 elif choice == b'unresolved':
354 353 return _ifail(repo, mynode, local, other, base, toolconf)
355 354 except error.ResponseExpected:
356 355 ui.write(b"\n")
357 356 return _ifail(repo, mynode, local, other, base, toolconf)
358 357
359 358
360 359 @internaltool(b'local', nomerge)
361 360 def _ilocal(repo, mynode, local, other, base, toolconf):
362 361 """Uses the local `p1()` version of files as the merged version."""
363 362 return 0, local.fctx.isabsent()
364 363
365 364
366 365 @internaltool(b'other', nomerge)
367 366 def _iother(repo, mynode, local, other, base, toolconf):
368 367 """Uses the other `p2()` version of files as the merged version."""
369 368 if other.fctx.isabsent():
370 369 # local changed, remote deleted -- 'deleted' picked
371 370 _underlyingfctxifabsent(local.fctx).remove()
372 371 deleted = True
373 372 else:
374 373 _underlyingfctxifabsent(local.fctx).write(
375 374 other.fctx.data(), other.fctx.flags()
376 375 )
377 376 deleted = False
378 377 return 0, deleted
379 378
380 379
381 380 @internaltool(b'fail', nomerge)
382 381 def _ifail(repo, mynode, local, other, base, toolconf):
383 382 """
384 383 Rather than attempting to merge files that were modified on both
385 384 branches, it marks them as unresolved. The resolve command must be
386 385 used to resolve these conflicts."""
387 386 # for change/delete conflicts write out the changed version, then fail
388 387 if local.fctx.isabsent():
389 388 _underlyingfctxifabsent(local.fctx).write(
390 389 other.fctx.data(), other.fctx.flags()
391 390 )
392 391 return 1, False
393 392
394 393
395 394 def _underlyingfctxifabsent(filectx):
396 395 """Sometimes when resolving, our fcd is actually an absentfilectx, but
397 396 we want to write to it (to do the resolve). This helper returns the
398 397 underyling workingfilectx in that case.
399 398 """
400 399 if filectx.isabsent():
401 400 return filectx.changectx()[filectx.path()]
402 401 else:
403 402 return filectx
404 403
405 404
406 405 def _premerge(repo, local, other, base, toolconf, backup):
407 406 tool, toolpath, binary, symlink, scriptfn = toolconf
408 407 if symlink or local.fctx.isabsent() or other.fctx.isabsent():
409 408 return 1
410 409
411 410 ui = repo.ui
412 411
413 412 validkeep = [b'keep', b'keep-merge3', b'keep-mergediff']
414 413
415 414 # do we attempt to simplemerge first?
416 415 try:
417 416 premerge = _toolbool(ui, tool, b"premerge", not binary)
418 417 except error.ConfigError:
419 418 premerge = _toolstr(ui, tool, b"premerge", b"").lower()
420 419 if premerge not in validkeep:
421 420 _valid = b', '.join([b"'" + v + b"'" for v in validkeep])
422 421 raise error.ConfigError(
423 422 _(b"%s.premerge not valid ('%s' is neither boolean nor %s)")
424 423 % (tool, premerge, _valid)
425 424 )
426 425
427 426 if premerge:
428 427 mode = b'merge'
429 428 if premerge == b'keep-mergediff':
430 429 mode = b'mergediff'
431 430 elif premerge == b'keep-merge3':
432 431 mode = b'merge3'
433 432 r = simplemerge.simplemerge(
434 433 ui, local, base, other, quiet=True, mode=mode
435 434 )
436 435 if not r:
437 436 ui.debug(b" premerge successful\n")
438 437 return 0
439 438 if premerge not in validkeep:
440 439 # restore from backup and try again
441 440 _restorebackup(local.fctx, backup)
442 441 return 1 # continue merging
443 442
444 443
445 444 def _mergecheck(repo, mynode, fcd, fco, fca, toolconf):
446 445 tool, toolpath, binary, symlink, scriptfn = toolconf
447 446 uipathfn = scmutil.getuipathfn(repo)
448 447 if symlink:
449 448 repo.ui.warn(
450 449 _(b'warning: internal %s cannot merge symlinks for %s\n')
451 450 % (tool, uipathfn(fcd.path()))
452 451 )
453 452 return False
454 453 if fcd.isabsent() or fco.isabsent():
455 454 repo.ui.warn(
456 455 _(
457 456 b'warning: internal %s cannot merge change/delete '
458 457 b'conflict for %s\n'
459 458 )
460 459 % (tool, uipathfn(fcd.path()))
461 460 )
462 461 return False
463 462 return True
464 463
465 464
466 465 def _merge(repo, local, other, base, mode):
467 466 """
468 467 Uses the internal non-interactive simple merge algorithm for merging
469 468 files. It will fail if there are any conflicts and leave markers in
470 469 the partially merged file. Markers will have two sections, one for each side
471 470 of merge, unless mode equals 'union' which suppresses the markers."""
472 471 ui = repo.ui
473 472
474 473 r = simplemerge.simplemerge(ui, local, base, other, mode=mode)
475 474 return True, r, False
476 475
477 476
478 477 @internaltool(
479 478 b'union',
480 479 fullmerge,
481 480 _(
482 481 b"warning: conflicts while merging %s! "
483 482 b"(edit, then use 'hg resolve --mark')\n"
484 483 ),
485 484 precheck=_mergecheck,
486 485 )
487 486 def _iunion(repo, mynode, local, other, base, toolconf, backup):
488 487 """
489 488 Uses the internal non-interactive simple merge algorithm for merging
490 489 files. It will use both left and right sides for conflict regions.
491 490 No markers are inserted."""
492 491 return _merge(repo, local, other, base, b'union')
493 492
494 493
495 494 @internaltool(
496 495 b'merge',
497 496 fullmerge,
498 497 _(
499 498 b"warning: conflicts while merging %s! "
500 499 b"(edit, then use 'hg resolve --mark')\n"
501 500 ),
502 501 precheck=_mergecheck,
503 502 )
504 503 def _imerge(repo, mynode, local, other, base, toolconf, backup):
505 504 """
506 505 Uses the internal non-interactive simple merge algorithm for merging
507 506 files. It will fail if there are any conflicts and leave markers in
508 507 the partially merged file. Markers will have two sections, one for each side
509 508 of merge."""
510 509 return _merge(repo, local, other, base, b'merge')
511 510
512 511
513 512 @internaltool(
514 513 b'merge3',
515 514 fullmerge,
516 515 _(
517 516 b"warning: conflicts while merging %s! "
518 517 b"(edit, then use 'hg resolve --mark')\n"
519 518 ),
520 519 precheck=_mergecheck,
521 520 )
522 521 def _imerge3(repo, mynode, local, other, base, toolconf, backup):
523 522 """
524 523 Uses the internal non-interactive simple merge algorithm for merging
525 524 files. It will fail if there are any conflicts and leave markers in
526 525 the partially merged file. Marker will have three sections, one from each
527 526 side of the merge and one for the base content."""
528 527 return _merge(repo, local, other, base, b'merge3')
529 528
530 529
531 530 @internaltool(
532 531 b'merge3-lie-about-conflicts',
533 532 fullmerge,
534 533 b'',
535 534 precheck=_mergecheck,
536 535 )
537 536 def _imerge3alwaysgood(*args, **kwargs):
538 537 # Like merge3, but record conflicts as resolved with markers in place.
539 538 #
540 539 # This is used for `diff.merge` to show the differences between
541 540 # the auto-merge state and the committed merge state. It may be
542 541 # useful for other things.
543 542 b1, junk, b2 = _imerge3(*args, **kwargs)
544 543 # TODO is this right? I'm not sure what these return values mean,
545 544 # but as far as I can tell this will indicate to callers tha the
546 545 # merge succeeded.
547 546 return b1, False, b2
548 547
549 548
550 549 @internaltool(
551 550 b'mergediff',
552 551 fullmerge,
553 552 _(
554 553 b"warning: conflicts while merging %s! "
555 554 b"(edit, then use 'hg resolve --mark')\n"
556 555 ),
557 556 precheck=_mergecheck,
558 557 )
559 558 def _imerge_diff(repo, mynode, local, other, base, toolconf, backup):
560 559 """
561 560 Uses the internal non-interactive simple merge algorithm for merging
562 561 files. It will fail if there are any conflicts and leave markers in
563 562 the partially merged file. The marker will have two sections, one with the
564 563 content from one side of the merge, and one with a diff from the base
565 564 content to the content on the other side. (experimental)"""
566 565 return _merge(repo, local, other, base, b'mergediff')
567 566
568 567
569 568 @internaltool(b'merge-local', mergeonly, precheck=_mergecheck)
570 569 def _imergelocal(repo, mynode, local, other, base, toolconf, backup):
571 570 """
572 571 Like :merge, but resolve all conflicts non-interactively in favor
573 572 of the local `p1()` changes."""
574 573 return _merge(repo, local, other, base, b'local')
575 574
576 575
577 576 @internaltool(b'merge-other', mergeonly, precheck=_mergecheck)
578 577 def _imergeother(repo, mynode, local, other, base, toolconf, backup):
579 578 """
580 579 Like :merge, but resolve all conflicts non-interactively in favor
581 580 of the other `p2()` changes."""
582 581 return _merge(repo, local, other, base, b'other')
583 582
584 583
585 584 @internaltool(
586 585 b'tagmerge',
587 586 mergeonly,
588 587 _(
589 588 b"automatic tag merging of %s failed! "
590 589 b"(use 'hg resolve --tool :merge' or another merge "
591 590 b"tool of your choice)\n"
592 591 ),
593 592 )
594 593 def _itagmerge(repo, mynode, local, other, base, toolconf, backup):
595 594 """
596 595 Uses the internal tag merge algorithm (experimental).
597 596 """
598 597 success, status = tagmerge.merge(repo, local.fctx, other.fctx, base.fctx)
599 598 return success, status, False
600 599
601 600
602 601 @internaltool(b'dump', fullmerge, binary=True, symlink=True)
603 602 def _idump(repo, mynode, local, other, base, toolconf, backup):
604 603 """
605 604 Creates three versions of the files to merge, containing the
606 605 contents of local, other and base. These files can then be used to
607 606 perform a merge manually. If the file to be merged is named
608 607 ``a.txt``, these files will accordingly be named ``a.txt.local``,
609 608 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
610 609 same directory as ``a.txt``.
611 610
612 611 This implies premerge. Therefore, files aren't dumped, if premerge
613 612 runs successfully. Use :forcedump to forcibly write files out.
614 613 """
615 614 a = _workingpath(repo, local.fctx)
616 615 fd = local.fctx.path()
617 616
618 617 from . import context
619 618
620 619 if isinstance(local.fctx, context.overlayworkingfilectx):
621 620 raise error.InMemoryMergeConflictsError(
622 621 b'in-memory merge does not support the :dump tool.'
623 622 )
624 623
625 624 util.writefile(a + b".local", local.fctx.decodeddata())
626 625 repo.wwrite(fd + b".other", other.fctx.data(), other.fctx.flags())
627 626 repo.wwrite(fd + b".base", base.fctx.data(), base.fctx.flags())
628 627 return False, 1, False
629 628
630 629
631 630 @internaltool(b'forcedump', mergeonly, binary=True, symlink=True)
632 631 def _forcedump(repo, mynode, local, other, base, toolconf, backup):
633 632 """
634 633 Creates three versions of the files as same as :dump, but omits premerge.
635 634 """
636 635 return _idump(repo, mynode, local, other, base, toolconf, backup)
637 636
638 637
639 638 def _xmergeimm(repo, mynode, local, other, base, toolconf, backup):
640 639 # In-memory merge simply raises an exception on all external merge tools,
641 640 # for now.
642 641 #
643 642 # It would be possible to run most tools with temporary files, but this
644 643 # raises the question of what to do if the user only partially resolves the
645 644 # file -- we can't leave a merge state. (Copy to somewhere in the .hg/
646 645 # directory and tell the user how to get it is my best idea, but it's
647 646 # clunky.)
648 647 raise error.InMemoryMergeConflictsError(
649 648 b'in-memory merge does not support external merge tools'
650 649 )
651 650
652 651
653 652 def _describemerge(ui, repo, mynode, fcl, fcb, fco, env, toolpath, args):
654 653 tmpl = ui.config(b'command-templates', b'pre-merge-tool-output')
655 654 if not tmpl:
656 655 return
657 656
658 657 mappingdict = templateutil.mappingdict
659 658 props = {
660 659 b'ctx': fcl.changectx(),
661 660 b'node': hex(mynode),
662 661 b'path': fcl.path(),
663 662 b'local': mappingdict(
664 663 {
665 664 b'ctx': fcl.changectx(),
666 665 b'fctx': fcl,
667 666 b'node': hex(mynode),
668 667 b'name': _(b'local'),
669 668 b'islink': b'l' in fcl.flags(),
670 669 b'label': env[b'HG_MY_LABEL'],
671 670 }
672 671 ),
673 672 b'base': mappingdict(
674 673 {
675 674 b'ctx': fcb.changectx(),
676 675 b'fctx': fcb,
677 676 b'name': _(b'base'),
678 677 b'islink': b'l' in fcb.flags(),
679 678 b'label': env[b'HG_BASE_LABEL'],
680 679 }
681 680 ),
682 681 b'other': mappingdict(
683 682 {
684 683 b'ctx': fco.changectx(),
685 684 b'fctx': fco,
686 685 b'name': _(b'other'),
687 686 b'islink': b'l' in fco.flags(),
688 687 b'label': env[b'HG_OTHER_LABEL'],
689 688 }
690 689 ),
691 690 b'toolpath': toolpath,
692 691 b'toolargs': args,
693 692 }
694 693
695 694 # TODO: make all of this something that can be specified on a per-tool basis
696 695 tmpl = templater.unquotestring(tmpl)
697 696
698 697 # Not using cmdutil.rendertemplate here since it causes errors importing
699 698 # things for us to import cmdutil.
700 699 tres = formatter.templateresources(ui, repo)
701 700 t = formatter.maketemplater(
702 701 ui, tmpl, defaults=templatekw.keywords, resources=tres
703 702 )
704 703 ui.status(t.renderdefault(props))
705 704
706 705
707 706 def _xmerge(repo, mynode, local, other, base, toolconf, backup):
708 707 fcd = local.fctx
709 708 fco = other.fctx
710 709 fca = base.fctx
711 710 tool, toolpath, binary, symlink, scriptfn = toolconf
712 711 uipathfn = scmutil.getuipathfn(repo)
713 712 if fcd.isabsent() or fco.isabsent():
714 713 repo.ui.warn(
715 714 _(b'warning: %s cannot merge change/delete conflict for %s\n')
716 715 % (tool, uipathfn(fcd.path()))
717 716 )
718 717 return False, 1, None
719 718 localpath = _workingpath(repo, fcd)
720 719 args = _toolstr(repo.ui, tool, b"args")
721 720
722 721 with _maketempfiles(
723 722 repo, fco, fca, repo.wvfs.join(backup.path()), b"$output" in args
724 723 ) as temppaths:
725 724 basepath, otherpath, localoutputpath = temppaths
726 725 outpath = b""
726
727 def format_label(input):
728 if input.label_detail:
729 return b'%s: %s' % (input.label, input.label_detail)
730 else:
731 return input.label
732
727 733 env = {
728 734 b'HG_FILE': fcd.path(),
729 735 b'HG_MY_NODE': short(mynode),
730 736 b'HG_OTHER_NODE': short(fco.changectx().node()),
731 737 b'HG_BASE_NODE': short(fca.changectx().node()),
732 738 b'HG_MY_ISLINK': b'l' in fcd.flags(),
733 739 b'HG_OTHER_ISLINK': b'l' in fco.flags(),
734 740 b'HG_BASE_ISLINK': b'l' in fca.flags(),
735 b'HG_MY_LABEL': local.label,
736 b'HG_OTHER_LABEL': other.label,
737 b'HG_BASE_LABEL': base.label,
741 b'HG_MY_LABEL': format_label(local),
742 b'HG_OTHER_LABEL': format_label(other),
743 b'HG_BASE_LABEL': format_label(base),
738 744 }
739 745 ui = repo.ui
740 746
741 747 if b"$output" in args:
742 748 # read input from backup, write to original
743 749 outpath = localpath
744 750 localpath = localoutputpath
745 751 replace = {
746 752 b'local': localpath,
747 753 b'base': basepath,
748 754 b'other': otherpath,
749 755 b'output': outpath,
750 b'labellocal': local.label,
751 b'labelother': other.label,
752 b'labelbase': base.label,
756 b'labellocal': format_label(local),
757 b'labelother': format_label(other),
758 b'labelbase': format_label(base),
753 759 }
754 760 args = util.interpolate(
755 761 br'\$',
756 762 replace,
757 763 args,
758 764 lambda s: procutil.shellquote(util.localpath(s)),
759 765 )
760 766 if _toolbool(ui, tool, b"gui"):
761 767 repo.ui.status(
762 768 _(b'running merge tool %s for file %s\n')
763 769 % (tool, uipathfn(fcd.path()))
764 770 )
765 771 if scriptfn is None:
766 772 cmd = toolpath + b' ' + args
767 773 repo.ui.debug(b'launching merge tool: %s\n' % cmd)
768 774 _describemerge(ui, repo, mynode, fcd, fca, fco, env, toolpath, args)
769 775 r = ui.system(
770 776 cmd, cwd=repo.root, environ=env, blockedtag=b'mergetool'
771 777 )
772 778 else:
773 779 repo.ui.debug(
774 780 b'launching python merge script: %s:%s\n' % (toolpath, scriptfn)
775 781 )
776 782 r = 0
777 783 try:
778 784 # avoid cycle cmdutil->merge->filemerge->extensions->cmdutil
779 785 from . import extensions
780 786
781 787 mod = extensions.loadpath(toolpath, b'hgmerge.%s' % tool)
782 788 except Exception:
783 789 raise error.Abort(
784 790 _(b"loading python merge script failed: %s") % toolpath
785 791 )
786 792 mergefn = getattr(mod, scriptfn, None)
787 793 if mergefn is None:
788 794 raise error.Abort(
789 795 _(b"%s does not have function: %s") % (toolpath, scriptfn)
790 796 )
791 797 argslist = procutil.shellsplit(args)
792 798 # avoid cycle cmdutil->merge->filemerge->hook->extensions->cmdutil
793 799 from . import hook
794 800
795 801 ret, raised = hook.pythonhook(
796 802 ui, repo, b"merge", toolpath, mergefn, {b'args': argslist}, True
797 803 )
798 804 if raised:
799 805 r = 1
800 806 repo.ui.debug(b'merge tool returned: %d\n' % r)
801 807 return True, r, False
802 808
803 809
804 def _populate_label_detail(input, template, pad):
805 """Applies the given template to the ctx, prefixed by the label.
806
807 Pad is the minimum width of the label prefix, so that multiple markers
808 can have aligned templated parts.
809 """
810 def _populate_label_detail(input, template):
811 """Applies the given template to the ctx and stores it in the input."""
810 812 ctx = input.fctx.changectx()
811 813 if ctx.node() is None:
812 814 ctx = ctx.p1()
813 815
814 816 props = {b'ctx': ctx}
815 817 templateresult = template.renderdefault(props)
816
817 label = (b'%s:' % input.label).ljust(pad + 1)
818 mark = b'%s %s' % (label, templateresult)
819 mark = mark.splitlines()[0] # split for safety
820
821 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
822 input.label = stringutil.ellipsis(mark, 80 - 8)
818 input.label_detail = templateresult.splitlines()[0] # split for safety
823 819
824 820
825 821 def _populate_label_details(repo, inputs, tool=None):
826 """Formats the given labels using the conflict marker template.
827
828 Returns a list of formatted labels.
829 """
822 """Populates the label details using the conflict marker template."""
830 823 ui = repo.ui
831 824 template = ui.config(b'command-templates', b'mergemarker')
832 825 if tool is not None:
833 826 template = _toolstr(ui, tool, b'mergemarkertemplate', template)
834 827 template = templater.unquotestring(template)
835 828 tres = formatter.templateresources(ui, repo)
836 829 tmpl = formatter.maketemplater(
837 830 ui, template, defaults=templatekw.keywords, resources=tres
838 831 )
839 832
840 pad = max(len(input.label) for input in inputs)
841
842 833 for input in inputs:
843 _populate_label_detail(input, tmpl, pad)
834 _populate_label_detail(input, tmpl)
844 835
845 836
846 837 def partextras(labels):
847 838 """Return a dictionary of extra labels for use in prompts to the user
848 839
849 840 Intended use is in strings of the form "(l)ocal%(l)s".
850 841 """
851 842 if labels is None:
852 843 return {
853 844 b"l": b"",
854 845 b"o": b"",
855 846 }
856 847
857 848 return {
858 849 b"l": b" [%s]" % labels[0],
859 850 b"o": b" [%s]" % labels[1],
860 851 }
861 852
862 853
863 854 def _restorebackup(fcd, backup):
864 855 # TODO: Add a workingfilectx.write(otherfilectx) path so we can use
865 856 # util.copy here instead.
866 857 fcd.write(backup.data(), fcd.flags())
867 858
868 859
869 860 def _makebackup(repo, ui, wctx, fcd):
870 861 """Makes and returns a filectx-like object for ``fcd``'s backup file.
871 862
872 863 In addition to preserving the user's pre-existing modifications to `fcd`
873 864 (if any), the backup is used to undo certain premerges, confirm whether a
874 865 merge changed anything, and determine what line endings the new file should
875 866 have.
876 867
877 868 Backups only need to be written once since their content doesn't change
878 869 afterwards.
879 870 """
880 871 if fcd.isabsent():
881 872 return None
882 873 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
883 874 # merge -> filemerge). (I suspect the fileset import is the weakest link)
884 875 from . import context
885 876
886 877 backup = scmutil.backuppath(ui, repo, fcd.path())
887 878 inworkingdir = backup.startswith(repo.wvfs.base) and not backup.startswith(
888 879 repo.vfs.base
889 880 )
890 881 if isinstance(fcd, context.overlayworkingfilectx) and inworkingdir:
891 882 # If the backup file is to be in the working directory, and we're
892 883 # merging in-memory, we must redirect the backup to the memory context
893 884 # so we don't disturb the working directory.
894 885 relpath = backup[len(repo.wvfs.base) + 1 :]
895 886 wctx[relpath].write(fcd.data(), fcd.flags())
896 887 return wctx[relpath]
897 888 else:
898 889 # Otherwise, write to wherever path the user specified the backups
899 890 # should go. We still need to switch based on whether the source is
900 891 # in-memory so we can use the fast path of ``util.copy`` if both are
901 892 # on disk.
902 893 if isinstance(fcd, context.overlayworkingfilectx):
903 894 util.writefile(backup, fcd.data())
904 895 else:
905 896 a = _workingpath(repo, fcd)
906 897 util.copyfile(a, backup)
907 898 # A arbitraryfilectx is returned, so we can run the same functions on
908 899 # the backup context regardless of where it lives.
909 900 return context.arbitraryfilectx(backup, repo=repo)
910 901
911 902
912 903 @contextlib.contextmanager
913 904 def _maketempfiles(repo, fco, fca, localpath, uselocalpath):
914 905 """Writes out `fco` and `fca` as temporary files, and (if uselocalpath)
915 906 copies `localpath` to another temporary file, so an external merge tool may
916 907 use them.
917 908 """
918 909 tmproot = None
919 910 tmprootprefix = repo.ui.config(b'experimental', b'mergetempdirprefix')
920 911 if tmprootprefix:
921 912 tmproot = pycompat.mkdtemp(prefix=tmprootprefix)
922 913
923 914 def maketempfrompath(prefix, path):
924 915 fullbase, ext = os.path.splitext(path)
925 916 pre = b"%s~%s" % (os.path.basename(fullbase), prefix)
926 917 if tmproot:
927 918 name = os.path.join(tmproot, pre)
928 919 if ext:
929 920 name += ext
930 921 f = open(name, "wb")
931 922 else:
932 923 fd, name = pycompat.mkstemp(prefix=pre + b'.', suffix=ext)
933 924 f = os.fdopen(fd, "wb")
934 925 return f, name
935 926
936 927 def tempfromcontext(prefix, ctx):
937 928 f, name = maketempfrompath(prefix, ctx.path())
938 929 data = ctx.decodeddata()
939 930 f.write(data)
940 931 f.close()
941 932 return name
942 933
943 934 b = tempfromcontext(b"base", fca)
944 935 c = tempfromcontext(b"other", fco)
945 936 d = localpath
946 937 if uselocalpath:
947 938 # We start off with this being the backup filename, so remove the .orig
948 939 # to make syntax-highlighting more likely.
949 940 if d.endswith(b'.orig'):
950 941 d, _ = os.path.splitext(d)
951 942 f, d = maketempfrompath(b"local", d)
952 943 with open(localpath, b'rb') as src:
953 944 f.write(src.read())
954 945 f.close()
955 946
956 947 try:
957 948 yield b, c, d
958 949 finally:
959 950 if tmproot:
960 951 shutil.rmtree(tmproot)
961 952 else:
962 953 util.unlink(b)
963 954 util.unlink(c)
964 955 # if not uselocalpath, d is the 'orig'/backup file which we
965 956 # shouldn't delete.
966 957 if d and uselocalpath:
967 958 util.unlink(d)
968 959
969 960
970 961 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
971 962 """perform a 3-way merge in the working directory
972 963
973 964 mynode = parent node before merge
974 965 orig = original local filename before merge
975 966 fco = other file context
976 967 fca = ancestor file context
977 968 fcd = local file context for current/destination file
978 969
979 970 Returns whether the merge is complete, the return value of the merge, and
980 971 a boolean indicating whether the file was deleted from disk."""
981 972
982 973 if not fco.cmp(fcd): # files identical?
983 974 return None, False
984 975
985 976 ui = repo.ui
986 977 fd = fcd.path()
987 978 uipathfn = scmutil.getuipathfn(repo)
988 979 fduipath = uipathfn(fd)
989 980 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
990 981 symlink = b'l' in fcd.flags() + fco.flags()
991 982 changedelete = fcd.isabsent() or fco.isabsent()
992 983 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
993 984 scriptfn = None
994 985 if tool in internals and tool.startswith(b'internal:'):
995 986 # normalize to new-style names (':merge' etc)
996 987 tool = tool[len(b'internal') :]
997 988 if toolpath and toolpath.startswith(b'python:'):
998 989 invalidsyntax = False
999 990 if toolpath.count(b':') >= 2:
1000 991 script, scriptfn = toolpath[7:].rsplit(b':', 1)
1001 992 if not scriptfn:
1002 993 invalidsyntax = True
1003 994 # missing :callable can lead to spliting on windows drive letter
1004 995 if b'\\' in scriptfn or b'/' in scriptfn:
1005 996 invalidsyntax = True
1006 997 else:
1007 998 invalidsyntax = True
1008 999 if invalidsyntax:
1009 1000 raise error.Abort(_(b"invalid 'python:' syntax: %s") % toolpath)
1010 1001 toolpath = script
1011 1002 ui.debug(
1012 1003 b"picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
1013 1004 % (
1014 1005 tool,
1015 1006 fduipath,
1016 1007 pycompat.bytestr(binary),
1017 1008 pycompat.bytestr(symlink),
1018 1009 pycompat.bytestr(changedelete),
1019 1010 )
1020 1011 )
1021 1012
1022 1013 if tool in internals:
1023 1014 func = internals[tool]
1024 1015 mergetype = func.mergetype
1025 1016 onfailure = func.onfailure
1026 1017 precheck = func.precheck
1027 1018 isexternal = False
1028 1019 else:
1029 1020 if wctx.isinmemory():
1030 1021 func = _xmergeimm
1031 1022 else:
1032 1023 func = _xmerge
1033 1024 mergetype = fullmerge
1034 1025 onfailure = _(b"merging %s failed!\n")
1035 1026 precheck = None
1036 1027 isexternal = True
1037 1028
1038 1029 toolconf = tool, toolpath, binary, symlink, scriptfn
1039 1030
1040 1031 if not labels:
1041 1032 labels = [b'local', b'other']
1042 1033 if len(labels) < 3:
1043 1034 labels.append(b'base')
1044 1035 local = simplemerge.MergeInput(fcd, labels[0])
1045 1036 other = simplemerge.MergeInput(fco, labels[1])
1046 1037 base = simplemerge.MergeInput(fca, labels[2])
1047 1038 if mergetype == nomerge:
1048 1039 return func(
1049 1040 repo,
1050 1041 mynode,
1051 1042 local,
1052 1043 other,
1053 1044 base,
1054 1045 toolconf,
1055 1046 )
1056 1047
1057 1048 if orig != fco.path():
1058 1049 ui.status(
1059 1050 _(b"merging %s and %s to %s\n")
1060 1051 % (uipathfn(orig), uipathfn(fco.path()), fduipath)
1061 1052 )
1062 1053 else:
1063 1054 ui.status(_(b"merging %s\n") % fduipath)
1064 1055
1065 1056 ui.debug(b"my %s other %s ancestor %s\n" % (fcd, fco, fca))
1066 1057
1067 1058 if precheck and not precheck(repo, mynode, fcd, fco, fca, toolconf):
1068 1059 if onfailure:
1069 1060 if wctx.isinmemory():
1070 1061 raise error.InMemoryMergeConflictsError(
1071 1062 b'in-memory merge does not support merge conflicts'
1072 1063 )
1073 1064 ui.warn(onfailure % fduipath)
1074 1065 return 1, False
1075 1066
1076 1067 backup = _makebackup(repo, ui, wctx, fcd)
1077 1068 r = 1
1078 1069 try:
1079 1070 internalmarkerstyle = ui.config(b'ui', b'mergemarkers')
1080 1071 if isexternal:
1081 1072 markerstyle = _toolstr(ui, tool, b'mergemarkers')
1082 1073 else:
1083 1074 markerstyle = internalmarkerstyle
1084 1075
1085 1076 if mergetype == fullmerge:
1086 1077 # conflict markers generated by premerge will use 'detailed'
1087 1078 # settings if either ui.mergemarkers or the tool's mergemarkers
1088 1079 # setting is 'detailed'. This way tools can have basic labels in
1089 1080 # space-constrained areas of the UI, but still get full information
1090 1081 # in conflict markers if premerge is 'keep' or 'keep-merge3'.
1091 1082 labeltool = None
1092 1083 if markerstyle != b'basic':
1093 1084 # respect 'tool's mergemarkertemplate (which defaults to
1094 1085 # command-templates.mergemarker)
1095 1086 labeltool = tool
1096 1087 if internalmarkerstyle != b'basic' or markerstyle != b'basic':
1097 1088 _populate_label_details(
1098 1089 repo, [local, other, base], tool=labeltool
1099 1090 )
1100 1091
1101 1092 r = _premerge(
1102 1093 repo,
1103 1094 local,
1104 1095 other,
1105 1096 base,
1106 1097 toolconf,
1107 1098 backup,
1108 1099 )
1109 1100 # we're done if premerge was successful (r is 0)
1110 1101 if not r:
1111 1102 return r, False
1112 1103
1113 1104 # Reset to basic labels
1114 local.label = labels[0]
1115 other.label = labels[1]
1116 base.label = labels[2]
1105 local.label_detail = None
1106 other.label_detail = None
1107 base.label_detail = None
1117 1108
1118 1109 if markerstyle != b'basic':
1119 1110 _populate_label_details(repo, [local, other, base], tool=tool)
1120 1111
1121 1112 needcheck, r, deleted = func(
1122 1113 repo,
1123 1114 mynode,
1124 1115 local,
1125 1116 other,
1126 1117 base,
1127 1118 toolconf,
1128 1119 backup,
1129 1120 )
1130 1121
1131 1122 if needcheck:
1132 1123 r = _check(repo, r, ui, tool, fcd, backup)
1133 1124
1134 1125 if r:
1135 1126 if onfailure:
1136 1127 if wctx.isinmemory():
1137 1128 raise error.InMemoryMergeConflictsError(
1138 1129 b'in-memory merge '
1139 1130 b'does not support '
1140 1131 b'merge conflicts'
1141 1132 )
1142 1133 ui.warn(onfailure % fduipath)
1143 1134 _onfilemergefailure(ui)
1144 1135
1145 1136 return r, deleted
1146 1137 finally:
1147 1138 if not r and backup is not None:
1148 1139 backup.remove()
1149 1140
1150 1141
1151 1142 def _haltmerge():
1152 1143 msg = _(b'merge halted after failed merge (see hg resolve)')
1153 1144 raise error.InterventionRequired(msg)
1154 1145
1155 1146
1156 1147 def _onfilemergefailure(ui):
1157 1148 action = ui.config(b'merge', b'on-failure')
1158 1149 if action == b'prompt':
1159 1150 msg = _(b'continue merge operation (yn)?$$ &Yes $$ &No')
1160 1151 if ui.promptchoice(msg, 0) == 1:
1161 1152 _haltmerge()
1162 1153 if action == b'halt':
1163 1154 _haltmerge()
1164 1155 # default action is 'continue', in which case we neither prompt nor halt
1165 1156
1166 1157
1167 1158 def hasconflictmarkers(data):
1168 1159 # Detect lines starting with a string of 7 identical characters from the
1169 1160 # subset Mercurial uses for conflict markers, followed by either the end of
1170 1161 # line or a space and some text. Note that using [<>=+|-]{7} would detect
1171 1162 # `<><><><><` as a conflict marker, which we don't want.
1172 1163 return bool(
1173 1164 re.search(
1174 1165 br"^([<>=+|-])\1{6}( .*)$",
1175 1166 data,
1176 1167 re.MULTILINE,
1177 1168 )
1178 1169 )
1179 1170
1180 1171
1181 1172 def _check(repo, r, ui, tool, fcd, backup):
1182 1173 fd = fcd.path()
1183 1174 uipathfn = scmutil.getuipathfn(repo)
1184 1175
1185 1176 if not r and (
1186 1177 _toolbool(ui, tool, b"checkconflicts")
1187 1178 or b'conflicts' in _toollist(ui, tool, b"check")
1188 1179 ):
1189 1180 if hasconflictmarkers(fcd.data()):
1190 1181 r = 1
1191 1182
1192 1183 checked = False
1193 1184 if b'prompt' in _toollist(ui, tool, b"check"):
1194 1185 checked = True
1195 1186 if ui.promptchoice(
1196 1187 _(b"was merge of '%s' successful (yn)?$$ &Yes $$ &No")
1197 1188 % uipathfn(fd),
1198 1189 1,
1199 1190 ):
1200 1191 r = 1
1201 1192
1202 1193 if (
1203 1194 not r
1204 1195 and not checked
1205 1196 and (
1206 1197 _toolbool(ui, tool, b"checkchanged")
1207 1198 or b'changed' in _toollist(ui, tool, b"check")
1208 1199 )
1209 1200 ):
1210 1201 if backup is not None and not fcd.cmp(backup):
1211 1202 if ui.promptchoice(
1212 1203 _(
1213 1204 b" output file %s appears unchanged\n"
1214 1205 b"was merge successful (yn)?"
1215 1206 b"$$ &Yes $$ &No"
1216 1207 )
1217 1208 % uipathfn(fd),
1218 1209 1,
1219 1210 ):
1220 1211 r = 1
1221 1212
1222 1213 if backup is not None and _toolbool(ui, tool, b"fixeol"):
1223 1214 _matcheol(_workingpath(repo, fcd), backup)
1224 1215
1225 1216 return r
1226 1217
1227 1218
1228 1219 def _workingpath(repo, ctx):
1229 1220 return repo.wjoin(ctx.path())
1230 1221
1231 1222
1232 1223 def loadinternalmerge(ui, extname, registrarobj):
1233 1224 """Load internal merge tool from specified registrarobj"""
1234 1225 for name, func in pycompat.iteritems(registrarobj._table):
1235 1226 fullname = b':' + name
1236 1227 internals[fullname] = func
1237 1228 internals[b'internal:' + name] = func
1238 1229 internalsdoc[fullname] = func
1239 1230
1240 1231 capabilities = sorted([k for k, v in func.capabilities.items() if v])
1241 1232 if capabilities:
1242 1233 capdesc = b" (actual capabilities: %s)" % b', '.join(
1243 1234 capabilities
1244 1235 )
1245 1236 func.__doc__ = func.__doc__ + pycompat.sysstr(b"\n\n%s" % capdesc)
1246 1237
1247 1238 # to put i18n comments into hg.pot for automatically generated texts
1248 1239
1249 1240 # i18n: "binary" and "symlink" are keywords
1250 1241 # i18n: this text is added automatically
1251 1242 _(b" (actual capabilities: binary, symlink)")
1252 1243 # i18n: "binary" is keyword
1253 1244 # i18n: this text is added automatically
1254 1245 _(b" (actual capabilities: binary)")
1255 1246 # i18n: "symlink" is keyword
1256 1247 # i18n: this text is added automatically
1257 1248 _(b" (actual capabilities: symlink)")
1258 1249
1259 1250
1260 1251 # load built-in merge tools explicitly to setup internalsdoc
1261 1252 loadinternalmerge(None, None, internaltool)
1262 1253
1263 1254 # tell hggettext to extract docstrings from these functions:
1264 1255 i18nfunctions = internals.values()
@@ -1,524 +1,538
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 276 def _verifytext(text, path, ui, opts):
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 281 if not opts.get('quiet'):
282 282 ui.warn(_(b'warning: %s\n') % msg)
283 283 if not opts.get('text'):
284 284 raise error.Abort(msg)
285 285 return text
286 286
287 287
288 288 def _format_labels(*inputs):
289 pad = max(len(input.label) if input.label else 0 for input in inputs)
289 290 labels = []
290 291 for input in inputs:
291 292 if input.label:
292 labels.append(input.label)
293 if input.label_detail:
294 label = (
295 (input.label + b':').ljust(pad + 1)
296 + b' '
297 + input.label_detail
298 )
299 else:
300 label = input.label
301 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
302 labels.append(stringutil.ellipsis(label, 80 - 8))
293 303 else:
294 304 labels.append(None)
295 305 return labels
296 306
297 307
298 308 def _detect_newline(m3):
299 309 if len(m3.a) > 0:
300 310 if m3.a[0].endswith(b'\r\n'):
301 311 return b'\r\n'
302 312 elif m3.a[0].endswith(b'\r'):
303 313 return b'\r'
304 314 return b'\n'
305 315
306 316
307 317 def _minimize(a_lines, b_lines):
308 318 """Trim conflict regions of lines where A and B sides match.
309 319
310 320 Lines where both A and B have made the same changes at the beginning
311 321 or the end of each merge region are eliminated from the conflict
312 322 region and are instead considered the same.
313 323 """
314 324 alen = len(a_lines)
315 325 blen = len(b_lines)
316 326
317 327 # find matches at the front
318 328 ii = 0
319 329 while ii < alen and ii < blen and a_lines[ii] == b_lines[ii]:
320 330 ii += 1
321 331 startmatches = ii
322 332
323 333 # find matches at the end
324 334 ii = 0
325 335 while ii < alen and ii < blen and a_lines[-ii - 1] == b_lines[-ii - 1]:
326 336 ii += 1
327 337 endmatches = ii
328 338
329 339 lines_before = a_lines[:startmatches]
330 340 new_a_lines = a_lines[startmatches : alen - endmatches]
331 341 new_b_lines = b_lines[startmatches : blen - endmatches]
332 342 lines_after = a_lines[alen - endmatches :]
333 343 return lines_before, new_a_lines, new_b_lines, lines_after
334 344
335 345
336 346 def render_minimized(
337 347 m3,
338 348 name_a=None,
339 349 name_b=None,
340 350 start_marker=b'<<<<<<<',
341 351 mid_marker=b'=======',
342 352 end_marker=b'>>>>>>>',
343 353 ):
344 354 """Return merge in cvs-like form."""
345 355 newline = _detect_newline(m3)
346 356 conflicts = False
347 357 if name_a:
348 358 start_marker = start_marker + b' ' + name_a
349 359 if name_b:
350 360 end_marker = end_marker + b' ' + name_b
351 361 merge_groups = m3.merge_groups()
352 362 lines = []
353 363 for what, group_lines in merge_groups:
354 364 if what == b'conflict':
355 365 conflicts = True
356 366 base_lines, a_lines, b_lines = group_lines
357 367 minimized = _minimize(a_lines, b_lines)
358 368 lines_before, a_lines, b_lines, lines_after = minimized
359 369 lines.extend(lines_before)
360 370 lines.append(start_marker + newline)
361 371 lines.extend(a_lines)
362 372 lines.append(mid_marker + newline)
363 373 lines.extend(b_lines)
364 374 lines.append(end_marker + newline)
365 375 lines.extend(lines_after)
366 376 else:
367 377 lines.extend(group_lines)
368 378 return lines, conflicts
369 379
370 380
371 381 def render_merge3(m3, name_a, name_b, name_base):
372 382 """Render conflicts as 3-way conflict markers."""
373 383 newline = _detect_newline(m3)
374 384 conflicts = False
375 385 lines = []
376 386 for what, group_lines in m3.merge_groups():
377 387 if what == b'conflict':
378 388 base_lines, a_lines, b_lines = group_lines
379 389 conflicts = True
380 390 lines.append(b'<<<<<<< ' + name_a + newline)
381 391 lines.extend(a_lines)
382 392 lines.append(b'||||||| ' + name_base + newline)
383 393 lines.extend(base_lines)
384 394 lines.append(b'=======' + newline)
385 395 lines.extend(b_lines)
386 396 lines.append(b'>>>>>>> ' + name_b + newline)
387 397 else:
388 398 lines.extend(group_lines)
389 399 return lines, conflicts
390 400
391 401
392 402 def render_mergediff(m3, name_a, name_b, name_base):
393 403 """Render conflicts as conflict markers with one snapshot and one diff."""
394 404 newline = _detect_newline(m3)
395 405 lines = []
396 406 conflicts = False
397 407 for what, group_lines in m3.merge_groups():
398 408 if what == b'conflict':
399 409 base_lines, a_lines, b_lines = group_lines
400 410 base_text = b''.join(base_lines)
401 411 b_blocks = list(
402 412 mdiff.allblocks(
403 413 base_text,
404 414 b''.join(b_lines),
405 415 lines1=base_lines,
406 416 lines2=b_lines,
407 417 )
408 418 )
409 419 a_blocks = list(
410 420 mdiff.allblocks(
411 421 base_text,
412 422 b''.join(a_lines),
413 423 lines1=base_lines,
414 424 lines2=b_lines,
415 425 )
416 426 )
417 427
418 428 def matching_lines(blocks):
419 429 return sum(
420 430 block[1] - block[0]
421 431 for block, kind in blocks
422 432 if kind == b'='
423 433 )
424 434
425 435 def diff_lines(blocks, lines1, lines2):
426 436 for block, kind in blocks:
427 437 if kind == b'=':
428 438 for line in lines1[block[0] : block[1]]:
429 439 yield b' ' + line
430 440 else:
431 441 for line in lines1[block[0] : block[1]]:
432 442 yield b'-' + line
433 443 for line in lines2[block[2] : block[3]]:
434 444 yield b'+' + line
435 445
436 446 lines.append(b"<<<<<<<" + newline)
437 447 if matching_lines(a_blocks) < matching_lines(b_blocks):
438 448 lines.append(b"======= " + name_a + newline)
439 449 lines.extend(a_lines)
440 450 lines.append(b"------- " + name_base + newline)
441 451 lines.append(b"+++++++ " + name_b + newline)
442 452 lines.extend(diff_lines(b_blocks, base_lines, b_lines))
443 453 else:
444 454 lines.append(b"------- " + name_base + newline)
445 455 lines.append(b"+++++++ " + name_a + newline)
446 456 lines.extend(diff_lines(a_blocks, base_lines, a_lines))
447 457 lines.append(b"======= " + name_b + newline)
448 458 lines.extend(b_lines)
449 459 lines.append(b">>>>>>>" + newline)
450 460 conflicts = True
451 461 else:
452 462 lines.extend(group_lines)
453 463 return lines, conflicts
454 464
455 465
456 466 def _resolve(m3, sides):
457 467 lines = []
458 468 for what, group_lines in m3.merge_groups():
459 469 if what == b'conflict':
460 470 for side in sides:
461 471 lines.extend(group_lines[side])
462 472 else:
463 473 lines.extend(group_lines)
464 474 return lines
465 475
466 476
467 477 @attr.s
468 478 class MergeInput(object):
469 479 fctx = attr.ib()
470 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)
471 485
472 486
473 487 def simplemerge(ui, local, base, other, **opts):
474 488 """Performs the simplemerge algorithm.
475 489
476 490 The merged result is written into `localctx`.
477 491 """
478 492
479 493 def readctx(ctx):
480 494 # Merges were always run in the working copy before, which means
481 495 # they used decoded data, if the user defined any repository
482 496 # filters.
483 497 #
484 498 # Maintain that behavior today for BC, though perhaps in the future
485 499 # it'd be worth considering whether merging encoded data (what the
486 500 # repository usually sees) might be more useful.
487 501 return _verifytext(ctx.decodeddata(), ctx.path(), ui, opts)
488 502
489 503 try:
490 504 localtext = readctx(local.fctx)
491 505 basetext = readctx(base.fctx)
492 506 othertext = readctx(other.fctx)
493 507 except error.Abort:
494 508 return True
495 509
496 510 m3 = Merge3Text(basetext, localtext, othertext)
497 511 conflicts = False
498 512 mode = opts.get('mode', b'merge')
499 513 if mode == b'union':
500 514 lines = _resolve(m3, (1, 2))
501 515 elif mode == b'local':
502 516 lines = _resolve(m3, (1,))
503 517 elif mode == b'other':
504 518 lines = _resolve(m3, (2,))
505 519 else:
506 520 if mode == b'mergediff':
507 521 labels = _format_labels(local, other, base)
508 522 lines, conflicts = render_mergediff(m3, *labels)
509 523 elif mode == b'merge3':
510 524 labels = _format_labels(local, other, base)
511 525 lines, conflicts = render_merge3(m3, *labels)
512 526 else:
513 527 labels = _format_labels(local, other)
514 528 lines, conflicts = render_minimized(m3, *labels)
515 529
516 530 mergedtext = b''.join(lines)
517 531 if opts.get('print'):
518 532 ui.fout.write(mergedtext)
519 533 else:
520 534 # local.fctx.flags() already has the merged flags (done in
521 535 # mergestate.resolve())
522 536 local.fctx.write(mergedtext, local.fctx.flags())
523 537
524 538 return conflicts
@@ -1,25 +1,31
1 1 == New Features ==
2 2
3 3
4 4 == Default Format Change ==
5 5
6 6 These changes affects newly created repositories (or new clone) done with
7 7 Mercurial XXX.
8 8
9 9
10 10 == New Experimental Features ==
11 11
12 12 == Bug Fixes ==
13 13
14 14 The `--no-check` and `--no-merge` now properly overwrite the behavior from `commands.update.check`.
15 15
16 16 == Backwards Compatibility Changes ==
17 17
18 18 The remotefilelog extension now requires an appropiate excludepattern
19 19 for subrepositories.
20 20
21 The labels passed to merge tools have changed slightly. Merge tools can get
22 labels passed to them if you include `$labellocal`, `$labelbase`, and/or
23 `$labelother` in the `merge-tool.<tool name>.args` configuration. These labels
24 used to have some space-padding, and truncation to fit within 72 columns. Both
25 the padding and the truncation has been removed.
26
21 27 == Internal API Changes ==
22 28
23 29 The following functions have been removed:
24 30
25 31 Miscellaneous:
General Comments 0
You need to be logged in to leave comments. Login now