[PATCH] filemerge: support specfiying a python function to custom merge-tools

tom_hindle at sil.org tom_hindle at sil.org
Wed May 9 18:33:49 UTC 2018


# HG changeset patch
# User hindlemail <tom_hindle at sil.org>
# Date 1525890682 21600
#      Wed May 09 12:31:22 2018 -0600
# Node ID ac1af9a3417eddaee93635682d3ebcdb8a929a8a
# Parent  8b86acc7aa64130f5b6fa69f5fc20ef4d0b09c42
filemerge: support specfiying 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 portablity 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)

extensions and hooks were imported locally because of cycles:
cmdutil -> merge -> filemerge -> extensions -> cmdutil
cmdutil -> merge -> filemerge -> hook -> extensions -> cmdutil

diff -r 8b86acc7aa64 -r ac1af9a3417e mercurial/filemerge.py
--- a/mercurial/filemerge.py	Sat Apr 28 23:16:41 2018 -0700
+++ b/mercurial/filemerge.py	Wed May 09 12:31:22 2018 -0600
@@ -114,6 +114,9 @@
 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 findexternaltool(ui, tool):
@@ -325,7 +328,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 +364,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 +433,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 +513,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 +554,35 @@
         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:
+                from . import extensions
+                mod = extensions.loadpath(toolpath, 'hgmerge.%s' % scriptfn)
+            except Exception:
+                ui.write(_("loading %s python merge script failed\n") %
+                         toolpath)
+                raise
+            mergefn = getattr(mod, scriptfn, None)
+            if mergefn is None:
+                raise error.Abort(_("%s does not have function: %s\n") %
+                                  (toolpath, scriptfn))
+            argslist = procutil.shellsplit(args)
+            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 +777,25 @@
     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 procutil.shellsplit(toolpath)[0].startswith('python:'):
+        invalidsyntax = False
+        if toolpath.count(':') >= 2:
+            script, scriptfn = procutil.shellsplit(toolpath)[0][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 \n") % 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 +816,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)
diff -r 8b86acc7aa64 -r ac1af9a3417e mercurial/hook.py
--- a/mercurial/hook.py	Sat Apr 28 23:16:41 2018 -0700
+++ b/mercurial/hook.py	Wed May 09 12:31:22 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)


More information about the Mercurial-devel mailing list