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