Mercurial > public > mercurial-scm > hg-stable
diff mercurial/filemerge.py @ 38074: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 | a8a902d7176e |
children | dea3903175ee |
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)