Mercurial > hg
changeset 38041:242eb5132203
filemerge: support specifying a python function to custom merge-tools
Eliminates the need to specify a python executable, which may not exist on
system. Additionally launching script inprocess aids portability on systems
that can't execute python via the shell.
Example usage "merge-tools.myTool.executable=python:c:\myTool.py:mergefn"
where myTool.py contains a function:
"def mergefn(ui, repo, args, **kwargs):"
where args is list of args passed to merge tool.
(by default, expanded: $local $base $other)
Invoking the specified python function was done by exposing and invoking
(hook._pythonhook -> hook.pythonhook)
author | hindlemail <tom_hindle@sil.org> |
---|---|
date | Wed, 16 May 2018 14:11:41 -0600 |
parents | 37ef6ee87488 |
children | 5a7cf42ba6ef |
files | mercurial/filemerge.py mercurial/hook.py tests/test-merge-tools.t |
diffstat | 3 files changed, 238 insertions(+), 14 deletions(-) [+] |
line wrap: on
line diff
--- a/mercurial/filemerge.py Sun May 13 11:09:53 2018 +0900 +++ b/mercurial/filemerge.py Wed May 16 14:11:41 2018 -0600 @@ -114,8 +114,16 @@ def _findtool(ui, tool): if tool in internals: return tool + cmd = _toolstr(ui, tool, "executable", tool) + if cmd.startswith('python:'): + return cmd return findexternaltool(ui, tool) +def _quotetoolpath(cmd): + if cmd.startswith('python:'): + return cmd + return procutil.shellquote(cmd) + def findexternaltool(ui, tool): for kn in ("regkey", "regkeyalt"): k = _toolstr(ui, tool, kn) @@ -165,7 +173,7 @@ return ":prompt", None else: if toolpath: - return (force, procutil.shellquote(toolpath)) + return (force, _quotetoolpath(toolpath)) else: # mimic HGMERGE if given tool not found return (force, force) @@ -183,7 +191,7 @@ mf = match.match(repo.root, '', [pat]) if mf(path) and check(tool, pat, symlink, False, changedelete): toolpath = _findtool(ui, tool) - return (tool, procutil.shellquote(toolpath)) + return (tool, _quotetoolpath(toolpath)) # then merge tools tools = {} @@ -208,7 +216,7 @@ for p, t in tools: if check(t, None, symlink, binary, changedelete): toolpath = _findtool(ui, t) - return (t, procutil.shellquote(toolpath)) + return (t, _quotetoolpath(toolpath)) # internal merge or prompt as last resort if symlink or binary or changedelete: @@ -325,7 +333,7 @@ return filectx def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None): - tool, toolpath, binary, symlink = toolconf + tool, toolpath, binary, symlink, scriptfn = toolconf if symlink or fcd.isabsent() or fco.isabsent(): return 1 unused, unused, unused, back = files @@ -361,7 +369,7 @@ return 1 # continue merging def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf): - tool, toolpath, binary, symlink = toolconf + tool, toolpath, binary, symlink, scriptfn = toolconf if symlink: repo.ui.warn(_('warning: internal %s cannot merge symlinks ' 'for %s\n') % (tool, fcd.path())) @@ -430,7 +438,7 @@ Generic driver for _imergelocal and _imergeother """ assert localorother is not None - tool, toolpath, binary, symlink = toolconf + tool, toolpath, binary, symlink, scriptfn = toolconf r = simplemerge.simplemerge(repo.ui, fcd, fca, fco, label=labels, localorother=localorother) return True, r @@ -510,7 +518,7 @@ 'external merge tools') def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None): - tool, toolpath, binary, symlink = toolconf + tool, toolpath, binary, symlink, scriptfn = toolconf if fcd.isabsent() or fco.isabsent(): repo.ui.warn(_('warning: %s cannot merge change/delete conflict ' 'for %s\n') % (tool, fcd.path())) @@ -551,12 +559,36 @@ args = util.interpolate( br'\$', replace, args, lambda s: procutil.shellquote(util.localpath(s))) - cmd = toolpath + ' ' + args if _toolbool(ui, tool, "gui"): repo.ui.status(_('running merge tool %s for file %s\n') % (tool, fcd.path())) - repo.ui.debug('launching merge tool: %s\n' % cmd) - r = ui.system(cmd, cwd=repo.root, environ=env, blockedtag='mergetool') + if scriptfn is None: + cmd = toolpath + ' ' + args + repo.ui.debug('launching merge tool: %s\n' % cmd) + r = ui.system(cmd, cwd=repo.root, environ=env, + blockedtag='mergetool') + else: + repo.ui.debug('launching python merge script: %s:%s\n' % + (toolpath, scriptfn)) + r = 0 + try: + # avoid cycle cmdutil->merge->filemerge->extensions->cmdutil + from . import extensions + mod = extensions.loadpath(toolpath, 'hgmerge.%s' % scriptfn) + except Exception: + raise error.Abort(_("loading python merge script failed: %s") % + toolpath) + mergefn = getattr(mod, scriptfn, None) + if mergefn is None: + raise error.Abort(_("%s does not have function: %s") % + (toolpath, scriptfn)) + argslist = procutil.shellsplit(args) + # avoid cycle cmdutil->merge->filemerge->hook->extensions->cmdutil + from . import hook + ret, raised = hook.pythonhook(ui, repo, "merge", toolpath, + mergefn, {'args': argslist}, True) + if raised: + r = 1 repo.ui.debug('merge tool returned: %d\n' % r) return True, r, False @@ -751,9 +783,24 @@ symlink = 'l' in fcd.flags() + fco.flags() changedelete = fcd.isabsent() or fco.isabsent() tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete) + scriptfn = None if tool in internals and tool.startswith('internal:'): # normalize to new-style names (':merge' etc) tool = tool[len('internal'):] + if toolpath and toolpath.startswith('python:'): + invalidsyntax = False + if toolpath.count(':') >= 2: + script, scriptfn = toolpath[7:].rsplit(':', 1) + if not scriptfn: + invalidsyntax = True + # missing :callable can lead to spliting on windows drive letter + if '\\' in scriptfn or '/' in scriptfn: + invalidsyntax = True + else: + invalidsyntax = True + if invalidsyntax: + raise error.Abort(_("invalid 'python:' syntax: %s") % toolpath) + toolpath = script ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n" % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink), pycompat.bytestr(changedelete))) @@ -774,7 +821,7 @@ precheck = None isexternal = True - toolconf = tool, toolpath, binary, symlink + toolconf = tool, toolpath, binary, symlink, scriptfn if mergetype == nomerge: r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
--- a/mercurial/hook.py Sun May 13 11:09:53 2018 +0900 +++ b/mercurial/hook.py Wed May 16 14:11:41 2018 -0600 @@ -24,7 +24,7 @@ stringutil, ) -def _pythonhook(ui, repo, htype, hname, funcname, args, throw): +def pythonhook(ui, repo, htype, hname, funcname, args, throw): '''call python hook. hook is callable object, looked up as name in python module. if callable returns "true", hook fails, else passes. if hook raises exception, treated as @@ -242,7 +242,7 @@ r = 1 raised = False elif callable(cmd): - r, raised = _pythonhook(ui, repo, htype, hname, cmd, args, + r, raised = pythonhook(ui, repo, htype, hname, cmd, args, throw) elif cmd.startswith('python:'): if cmd.count(':') >= 2: @@ -258,7 +258,7 @@ hookfn = getattr(mod, cmd) else: hookfn = cmd[7:].strip() - r, raised = _pythonhook(ui, repo, htype, hname, hookfn, args, + r, raised = pythonhook(ui, repo, htype, hname, hookfn, args, throw) else: r = _exthook(ui, repo, htype, hname, cmd, args, throw)
--- a/tests/test-merge-tools.t Sun May 13 11:09:53 2018 +0900 +++ b/tests/test-merge-tools.t Wed May 16 14:11:41 2018 -0600 @@ -329,6 +329,183 @@ # hg resolve --list R f +executable set to python script that succeeds: + + $ cat > "$TESTTMP/myworkingmerge.py" <<EOF + > def myworkingmergefn(ui, repo, args, **kwargs): + > return False + > EOF + $ beforemerge + [merge-tools] + false.whatever= + true.priority=1 + true.executable=cat + # hg update -C 1 + $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/myworkingmerge.py:myworkingmergefn" + merging f + 0 files updated, 1 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + $ aftermerge + # cat f + revision 1 + space + # hg stat + M f + # hg resolve --list + R f + +executable set to python script that fails: + + $ cat > "$TESTTMP/mybrokenmerge.py" <<EOF + > def mybrokenmergefn(ui, repo, args, **kwargs): + > ui.write(b"some fail message\n") + > return True + > EOF + $ beforemerge + [merge-tools] + false.whatever= + true.priority=1 + true.executable=cat + # hg update -C 1 + $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/mybrokenmerge.py:mybrokenmergefn" + merging f + some fail message + abort: $TESTTMP/mybrokenmerge.py hook failed + [255] + $ aftermerge + # cat f + revision 1 + space + # hg stat + ? f.orig + # hg resolve --list + U f + +executable set to python script that is missing function: + + $ beforemerge + [merge-tools] + false.whatever= + true.priority=1 + true.executable=cat + # hg update -C 1 + $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/myworkingmerge.py:missingFunction" + merging f + abort: $TESTTMP/myworkingmerge.py does not have function: missingFunction + [255] + $ aftermerge + # cat f + revision 1 + space + # hg stat + ? f.orig + # hg resolve --list + U f + +executable set to missing python script: + + $ beforemerge + [merge-tools] + false.whatever= + true.priority=1 + true.executable=cat + # hg update -C 1 + $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/missingpythonscript.py:mergefn" + merging f + abort: loading python merge script failed: $TESTTMP/missingpythonscript.py + [255] + $ aftermerge + # cat f + revision 1 + space + # hg stat + ? f.orig + # hg resolve --list + U f + +executable set to python script but callable function is missing: + + $ beforemerge + [merge-tools] + false.whatever= + true.priority=1 + true.executable=cat + # hg update -C 1 + $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/myworkingmerge.py" + abort: invalid 'python:' syntax: python:$TESTTMP/myworkingmerge.py + [255] + $ aftermerge + # cat f + revision 1 + space + # hg stat + # hg resolve --list + U f + +executable set to python script but callable function is empty string: + + $ beforemerge + [merge-tools] + false.whatever= + true.priority=1 + true.executable=cat + # hg update -C 1 + $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/myworkingmerge.py:" + abort: invalid 'python:' syntax: python:$TESTTMP/myworkingmerge.py: + [255] + $ aftermerge + # cat f + revision 1 + space + # hg stat + # hg resolve --list + U f + +executable set to python script but callable function is missing and path contains colon: + + $ beforemerge + [merge-tools] + false.whatever= + true.priority=1 + true.executable=cat + # hg update -C 1 + $ hg merge -r 2 --config merge-tools.true.executable="python:$TESTTMP/some:dir/myworkingmerge.py" + abort: invalid 'python:' syntax: python:$TESTTMP/some:dir/myworkingmerge.py + [255] + $ aftermerge + # cat f + revision 1 + space + # hg stat + # hg resolve --list + U f + +executable set to python script filename that contains spaces: + + $ mkdir -p "$TESTTMP/my path" + $ cat > "$TESTTMP/my path/my working merge with spaces in filename.py" <<EOF + > def myworkingmergefn(ui, repo, args, **kwargs): + > return False + > EOF + $ beforemerge + [merge-tools] + false.whatever= + true.priority=1 + true.executable=cat + # hg update -C 1 + $ hg merge -r 2 --config "merge-tools.true.executable=python:$TESTTMP/my path/my working merge with spaces in filename.py:myworkingmergefn" + merging f + 0 files updated, 1 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + $ aftermerge + # cat f + revision 1 + space + # hg stat + M f + # hg resolve --list + R f + #if unix-permissions environment variables in true.executable are handled: