##// END OF EJS Templates
filemerge: support specifying a python function to custom merge-tools...
hindlemail -
r38052:242eb513 default
parent child Browse files
Show More
@@ -114,8 +114,16 b' class absentfilectx(object):'
114 def _findtool(ui, tool):
114 def _findtool(ui, tool):
115 if tool in internals:
115 if tool in internals:
116 return tool
116 return tool
117 cmd = _toolstr(ui, tool, "executable", tool)
118 if cmd.startswith('python:'):
119 return cmd
117 return findexternaltool(ui, tool)
120 return findexternaltool(ui, tool)
118
121
122 def _quotetoolpath(cmd):
123 if cmd.startswith('python:'):
124 return cmd
125 return procutil.shellquote(cmd)
126
119 def findexternaltool(ui, tool):
127 def findexternaltool(ui, tool):
120 for kn in ("regkey", "regkeyalt"):
128 for kn in ("regkey", "regkeyalt"):
121 k = _toolstr(ui, tool, kn)
129 k = _toolstr(ui, tool, kn)
@@ -165,7 +173,7 b' def _picktool(repo, ui, path, binary, sy'
165 return ":prompt", None
173 return ":prompt", None
166 else:
174 else:
167 if toolpath:
175 if toolpath:
168 return (force, procutil.shellquote(toolpath))
176 return (force, _quotetoolpath(toolpath))
169 else:
177 else:
170 # mimic HGMERGE if given tool not found
178 # mimic HGMERGE if given tool not found
171 return (force, force)
179 return (force, force)
@@ -183,7 +191,7 b' def _picktool(repo, ui, path, binary, sy'
183 mf = match.match(repo.root, '', [pat])
191 mf = match.match(repo.root, '', [pat])
184 if mf(path) and check(tool, pat, symlink, False, changedelete):
192 if mf(path) and check(tool, pat, symlink, False, changedelete):
185 toolpath = _findtool(ui, tool)
193 toolpath = _findtool(ui, tool)
186 return (tool, procutil.shellquote(toolpath))
194 return (tool, _quotetoolpath(toolpath))
187
195
188 # then merge tools
196 # then merge tools
189 tools = {}
197 tools = {}
@@ -208,7 +216,7 b' def _picktool(repo, ui, path, binary, sy'
208 for p, t in tools:
216 for p, t in tools:
209 if check(t, None, symlink, binary, changedelete):
217 if check(t, None, symlink, binary, changedelete):
210 toolpath = _findtool(ui, t)
218 toolpath = _findtool(ui, t)
211 return (t, procutil.shellquote(toolpath))
219 return (t, _quotetoolpath(toolpath))
212
220
213 # internal merge or prompt as last resort
221 # internal merge or prompt as last resort
214 if symlink or binary or changedelete:
222 if symlink or binary or changedelete:
@@ -325,7 +333,7 b' def _underlyingfctxifabsent(filectx):'
325 return filectx
333 return filectx
326
334
327 def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None):
335 def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None):
328 tool, toolpath, binary, symlink = toolconf
336 tool, toolpath, binary, symlink, scriptfn = toolconf
329 if symlink or fcd.isabsent() or fco.isabsent():
337 if symlink or fcd.isabsent() or fco.isabsent():
330 return 1
338 return 1
331 unused, unused, unused, back = files
339 unused, unused, unused, back = files
@@ -361,7 +369,7 b' def _premerge(repo, fcd, fco, fca, toolc'
361 return 1 # continue merging
369 return 1 # continue merging
362
370
363 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
371 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
364 tool, toolpath, binary, symlink = toolconf
372 tool, toolpath, binary, symlink, scriptfn = toolconf
365 if symlink:
373 if symlink:
366 repo.ui.warn(_('warning: internal %s cannot merge symlinks '
374 repo.ui.warn(_('warning: internal %s cannot merge symlinks '
367 'for %s\n') % (tool, fcd.path()))
375 'for %s\n') % (tool, fcd.path()))
@@ -430,7 +438,7 b' def _imergeauto(repo, mynode, orig, fcd,'
430 Generic driver for _imergelocal and _imergeother
438 Generic driver for _imergelocal and _imergeother
431 """
439 """
432 assert localorother is not None
440 assert localorother is not None
433 tool, toolpath, binary, symlink = toolconf
441 tool, toolpath, binary, symlink, scriptfn = toolconf
434 r = simplemerge.simplemerge(repo.ui, fcd, fca, fco, label=labels,
442 r = simplemerge.simplemerge(repo.ui, fcd, fca, fco, label=labels,
435 localorother=localorother)
443 localorother=localorother)
436 return True, r
444 return True, r
@@ -510,7 +518,7 b' def _xmergeimm(repo, mynode, orig, fcd, '
510 'external merge tools')
518 'external merge tools')
511
519
512 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
520 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
513 tool, toolpath, binary, symlink = toolconf
521 tool, toolpath, binary, symlink, scriptfn = toolconf
514 if fcd.isabsent() or fco.isabsent():
522 if fcd.isabsent() or fco.isabsent():
515 repo.ui.warn(_('warning: %s cannot merge change/delete conflict '
523 repo.ui.warn(_('warning: %s cannot merge change/delete conflict '
516 'for %s\n') % (tool, fcd.path()))
524 'for %s\n') % (tool, fcd.path()))
@@ -551,12 +559,36 b' def _xmerge(repo, mynode, orig, fcd, fco'
551 args = util.interpolate(
559 args = util.interpolate(
552 br'\$', replace, args,
560 br'\$', replace, args,
553 lambda s: procutil.shellquote(util.localpath(s)))
561 lambda s: procutil.shellquote(util.localpath(s)))
554 cmd = toolpath + ' ' + args
555 if _toolbool(ui, tool, "gui"):
562 if _toolbool(ui, tool, "gui"):
556 repo.ui.status(_('running merge tool %s for file %s\n') %
563 repo.ui.status(_('running merge tool %s for file %s\n') %
557 (tool, fcd.path()))
564 (tool, fcd.path()))
558 repo.ui.debug('launching merge tool: %s\n' % cmd)
565 if scriptfn is None:
559 r = ui.system(cmd, cwd=repo.root, environ=env, blockedtag='mergetool')
566 cmd = toolpath + ' ' + args
567 repo.ui.debug('launching merge tool: %s\n' % cmd)
568 r = ui.system(cmd, cwd=repo.root, environ=env,
569 blockedtag='mergetool')
570 else:
571 repo.ui.debug('launching python merge script: %s:%s\n' %
572 (toolpath, scriptfn))
573 r = 0
574 try:
575 # avoid cycle cmdutil->merge->filemerge->extensions->cmdutil
576 from . import extensions
577 mod = extensions.loadpath(toolpath, 'hgmerge.%s' % scriptfn)
578 except Exception:
579 raise error.Abort(_("loading python merge script failed: %s") %
580 toolpath)
581 mergefn = getattr(mod, scriptfn, None)
582 if mergefn is None:
583 raise error.Abort(_("%s does not have function: %s") %
584 (toolpath, scriptfn))
585 argslist = procutil.shellsplit(args)
586 # avoid cycle cmdutil->merge->filemerge->hook->extensions->cmdutil
587 from . import hook
588 ret, raised = hook.pythonhook(ui, repo, "merge", toolpath,
589 mergefn, {'args': argslist}, True)
590 if raised:
591 r = 1
560 repo.ui.debug('merge tool returned: %d\n' % r)
592 repo.ui.debug('merge tool returned: %d\n' % r)
561 return True, r, False
593 return True, r, False
562
594
@@ -751,9 +783,24 b' def _filemerge(premerge, repo, wctx, myn'
751 symlink = 'l' in fcd.flags() + fco.flags()
783 symlink = 'l' in fcd.flags() + fco.flags()
752 changedelete = fcd.isabsent() or fco.isabsent()
784 changedelete = fcd.isabsent() or fco.isabsent()
753 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
785 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
786 scriptfn = None
754 if tool in internals and tool.startswith('internal:'):
787 if tool in internals and tool.startswith('internal:'):
755 # normalize to new-style names (':merge' etc)
788 # normalize to new-style names (':merge' etc)
756 tool = tool[len('internal'):]
789 tool = tool[len('internal'):]
790 if toolpath and toolpath.startswith('python:'):
791 invalidsyntax = False
792 if toolpath.count(':') >= 2:
793 script, scriptfn = toolpath[7:].rsplit(':', 1)
794 if not scriptfn:
795 invalidsyntax = True
796 # missing :callable can lead to spliting on windows drive letter
797 if '\\' in scriptfn or '/' in scriptfn:
798 invalidsyntax = True
799 else:
800 invalidsyntax = True
801 if invalidsyntax:
802 raise error.Abort(_("invalid 'python:' syntax: %s") % toolpath)
803 toolpath = script
757 ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
804 ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
758 % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink),
805 % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink),
759 pycompat.bytestr(changedelete)))
806 pycompat.bytestr(changedelete)))
@@ -774,7 +821,7 b' def _filemerge(premerge, repo, wctx, myn'
774 precheck = None
821 precheck = None
775 isexternal = True
822 isexternal = True
776
823
777 toolconf = tool, toolpath, binary, symlink
824 toolconf = tool, toolpath, binary, symlink, scriptfn
778
825
779 if mergetype == nomerge:
826 if mergetype == nomerge:
780 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
827 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
@@ -24,7 +24,7 b' from .utils import ('
24 stringutil,
24 stringutil,
25 )
25 )
26
26
27 def _pythonhook(ui, repo, htype, hname, funcname, args, throw):
27 def pythonhook(ui, repo, htype, hname, funcname, args, throw):
28 '''call python hook. hook is callable object, looked up as
28 '''call python hook. hook is callable object, looked up as
29 name in python module. if callable returns "true", hook
29 name in python module. if callable returns "true", hook
30 fails, else passes. if hook raises exception, treated as
30 fails, else passes. if hook raises exception, treated as
@@ -242,7 +242,7 b' def runhooks(ui, repo, htype, hooks, thr'
242 r = 1
242 r = 1
243 raised = False
243 raised = False
244 elif callable(cmd):
244 elif callable(cmd):
245 r, raised = _pythonhook(ui, repo, htype, hname, cmd, args,
245 r, raised = pythonhook(ui, repo, htype, hname, cmd, args,
246 throw)
246 throw)
247 elif cmd.startswith('python:'):
247 elif cmd.startswith('python:'):
248 if cmd.count(':') >= 2:
248 if cmd.count(':') >= 2:
@@ -258,7 +258,7 b' def runhooks(ui, repo, htype, hooks, thr'
258 hookfn = getattr(mod, cmd)
258 hookfn = getattr(mod, cmd)
259 else:
259 else:
260 hookfn = cmd[7:].strip()
260 hookfn = cmd[7:].strip()
261 r, raised = _pythonhook(ui, repo, htype, hname, hookfn, args,
261 r, raised = pythonhook(ui, repo, htype, hname, hookfn, args,
262 throw)
262 throw)
263 else:
263 else:
264 r = _exthook(ui, repo, htype, hname, cmd, args, throw)
264 r = _exthook(ui, repo, htype, hname, cmd, args, throw)
@@ -329,6 +329,183 b' and true.executable set to cat with path'
329 # hg resolve --list
329 # hg resolve --list
330 R f
330 R f
331
331
332 executable set to python script that succeeds:
333
334 $ cat > "$TESTTMP/myworkingmerge.py" <<EOF
335 > def myworkingmergefn(ui, repo, args, **kwargs):
336 > return False
337 > EOF
338 $ beforemerge
339 [merge-tools]
340 false.whatever=
341 true.priority=1
342 true.executable=cat
343 # hg update -C 1
344 $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/myworkingmerge.py:myworkingmergefn"
345 merging f
346 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
347 (branch merge, don't forget to commit)
348 $ aftermerge
349 # cat f
350 revision 1
351 space
352 # hg stat
353 M f
354 # hg resolve --list
355 R f
356
357 executable set to python script that fails:
358
359 $ cat > "$TESTTMP/mybrokenmerge.py" <<EOF
360 > def mybrokenmergefn(ui, repo, args, **kwargs):
361 > ui.write(b"some fail message\n")
362 > return True
363 > EOF
364 $ beforemerge
365 [merge-tools]
366 false.whatever=
367 true.priority=1
368 true.executable=cat
369 # hg update -C 1
370 $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/mybrokenmerge.py:mybrokenmergefn"
371 merging f
372 some fail message
373 abort: $TESTTMP/mybrokenmerge.py hook failed
374 [255]
375 $ aftermerge
376 # cat f
377 revision 1
378 space
379 # hg stat
380 ? f.orig
381 # hg resolve --list
382 U f
383
384 executable set to python script that is missing function:
385
386 $ beforemerge
387 [merge-tools]
388 false.whatever=
389 true.priority=1
390 true.executable=cat
391 # hg update -C 1
392 $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/myworkingmerge.py:missingFunction"
393 merging f
394 abort: $TESTTMP/myworkingmerge.py does not have function: missingFunction
395 [255]
396 $ aftermerge
397 # cat f
398 revision 1
399 space
400 # hg stat
401 ? f.orig
402 # hg resolve --list
403 U f
404
405 executable set to missing python script:
406
407 $ beforemerge
408 [merge-tools]
409 false.whatever=
410 true.priority=1
411 true.executable=cat
412 # hg update -C 1
413 $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/missingpythonscript.py:mergefn"
414 merging f
415 abort: loading python merge script failed: $TESTTMP/missingpythonscript.py
416 [255]
417 $ aftermerge
418 # cat f
419 revision 1
420 space
421 # hg stat
422 ? f.orig
423 # hg resolve --list
424 U f
425
426 executable set to python script but callable function is missing:
427
428 $ beforemerge
429 [merge-tools]
430 false.whatever=
431 true.priority=1
432 true.executable=cat
433 # hg update -C 1
434 $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/myworkingmerge.py"
435 abort: invalid 'python:' syntax: python:$TESTTMP/myworkingmerge.py
436 [255]
437 $ aftermerge
438 # cat f
439 revision 1
440 space
441 # hg stat
442 # hg resolve --list
443 U f
444
445 executable set to python script but callable function is empty string:
446
447 $ beforemerge
448 [merge-tools]
449 false.whatever=
450 true.priority=1
451 true.executable=cat
452 # hg update -C 1
453 $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/myworkingmerge.py:"
454 abort: invalid 'python:' syntax: python:$TESTTMP/myworkingmerge.py:
455 [255]
456 $ aftermerge
457 # cat f
458 revision 1
459 space
460 # hg stat
461 # hg resolve --list
462 U f
463
464 executable set to python script but callable function is missing and path contains colon:
465
466 $ beforemerge
467 [merge-tools]
468 false.whatever=
469 true.priority=1
470 true.executable=cat
471 # hg update -C 1
472 $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/some:dir/myworkingmerge.py"
473 abort: invalid 'python:' syntax: python:$TESTTMP/some:dir/myworkingmerge.py
474 [255]
475 $ aftermerge
476 # cat f
477 revision 1
478 space
479 # hg stat
480 # hg resolve --list
481 U f
482
483 executable set to python script filename that contains spaces:
484
485 $ mkdir -p "$TESTTMP/my path"
486 $ cat > "$TESTTMP/my path/my working merge with spaces in filename.py" <<EOF
487 > def myworkingmergefn(ui, repo, args, **kwargs):
488 > return False
489 > EOF
490 $ beforemerge
491 [merge-tools]
492 false.whatever=
493 true.priority=1
494 true.executable=cat
495 # hg update -C 1
496 $ hg merge -r 2 --config "merge-tools.true.executable=python:$TESTTMP/my path/my working merge with spaces in filename.py:myworkingmergefn"
497 merging f
498 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
499 (branch merge, don't forget to commit)
500 $ aftermerge
501 # cat f
502 revision 1
503 space
504 # hg stat
505 M f
506 # hg resolve --list
507 R f
508
332 #if unix-permissions
509 #if unix-permissions
333
510
334 environment variables in true.executable are handled:
511 environment variables in true.executable are handled:
General Comments 0
You need to be logged in to leave comments. Login now