Patchwork filemerge: support passing python script to custom merge-tools

login
register
mail settings
Submitter Tom Hindle
Date April 17, 2018, 10:28 p.m.
Message ID <0835104f-2ea3-5a23-c899-aee1923cefc0@sil.org>
Download mbox | patch
Permalink /patch/31174/
State New
Headers show

Comments

Tom Hindle - April 17, 2018, 10:28 p.m.

Patch

# HG changeset patch
# User hindlemail <tom_hindle@sil.org>
# Date 1523988043 21600
#      Tue Apr 17 12:00:43 2018 -0600
# Node ID 701f7b1145cb8105428141c22260c89d6b942bef
# Parent  62ebfda864de35f306963989303b70235aa63952
filemerge: support passing python script to custom merge-tools

Eliminates the need to specify a python execuatable, 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"

diff -r 62ebfda864de -r 701f7b1145cb mercurial/filemerge.py
--- a/mercurial/filemerge.py	Fri Apr 13 11:54:13 2018 -0700
+++ b/mercurial/filemerge.py	Tue Apr 17 12:00:43 2018 -0600
@@ -6,16 +6,17 @@ 
 # GNU General Public License version 2 or any later version.
 
 from __future__ import absolute_import
 
 import contextlib
 import os
 import re
 import shutil
+import sys
 import tempfile
 
 from .i18n import _
 from .node import nullid, short
 
 from . import (
     encoding,
     error,
@@ -109,16 +110,19 @@  class absentfilectx(object):
         return False
 
     def isabsent(self):
         return True
 
 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):
     for kn in ("regkey", "regkeyalt"):
         k = _toolstr(ui, tool, kn)
         if not k:
             continue
         p = util.lookupreg(k, _toolstr(ui, tool, "regname"))
@@ -320,17 +324,17 @@  def _underlyingfctxifabsent(filectx):
     underyling workingfilectx in that case.
     """
     if filectx.isabsent():
         return filectx.changectx()[filectx.path()]
     else:
         return filectx
 
 def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None):
-    tool, toolpath, binary, symlink = toolconf
+    tool, toolpath, binary, symlink, script = toolconf
     if symlink or fcd.isabsent() or fco.isabsent():
         return 1
     unused, unused, unused, back = files
 
     ui = repo.ui
 
     validkeep = ['keep', 'keep-merge3']
 
@@ -356,17 +360,17 @@  def _premerge(repo, fcd, fco, fca, toolc
             ui.debug(" premerge successful\n")
             return 0
         if premerge not in validkeep:
             # restore from backup and try again
             _restorebackup(fcd, back)
     return 1 # continue merging
 
 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
-    tool, toolpath, binary, symlink = toolconf
+    tool, toolpath, binary, symlink, script = toolconf
     if symlink:
         repo.ui.warn(_('warning: internal %s cannot merge symlinks '
                        'for %s\n') % (tool, fcd.path()))
         return False
     if fcd.isabsent() or fco.isabsent():
         repo.ui.warn(_('warning: internal %s cannot merge change/delete '
                        'conflict for %s\n') % (tool, fcd.path()))
         return False
@@ -425,17 +429,17 @@  def _imerge3(repo, mynode, orig, fcd, fc
     return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
 
 def _imergeauto(repo, mynode, orig, fcd, fco, fca, toolconf, files,
                 labels=None, localorother=None):
     """
     Generic driver for _imergelocal and _imergeother
     """
     assert localorother is not None
-    tool, toolpath, binary, symlink = toolconf
+    tool, toolpath, binary, symlink, script = toolconf
     r = simplemerge.simplemerge(repo.ui, fcd, fca, fco, label=labels,
                                 localorother=localorother)
     return True, r
 
 @internaltool('merge-local', mergeonly, precheck=_mergecheck)
 def _imergelocal(*args, **kwargs):
     """
     Like :merge, but resolve all conflicts non-interactively in favor
@@ -505,17 +509,17 @@  def _xmergeimm(repo, mynode, orig, fcd, 
     # raises the question of what to do if the user only partially resolves the
     # file -- we can't leave a merge state. (Copy to somewhere in the .hg/
     # directory and tell the user how to get it is my best idea, but it's
     # clunky.)
     raise error.InMemoryMergeConflictsError('in-memory merge does not support '
                                             'external merge tools')
 
 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
-    tool, toolpath, binary, symlink = toolconf
+    tool, toolpath, binary, symlink, script = toolconf
     if fcd.isabsent() or fco.isabsent():
         repo.ui.warn(_('warning: %s cannot merge change/delete conflict '
                        'for %s\n') % (tool, fcd.path()))
         return False, 1, None
     unused, unused, unused, back = files
     localpath = _workingpath(repo, fcd)
     args = _toolstr(repo.ui, tool, "args")
 
@@ -551,17 +555,27 @@  def _xmerge(repo, mynode, orig, fcd, fco
         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 not script:
+            r = ui.system(cmd, cwd=repo.root, environ=env,
+                          blockedtag='mergetool')
+        else:
+            r = 0
+            try:
+                sys.argv = procutil.shellsplit(cmd)
+                execfile(procutil.shellsplit(toolpath)[0], {})
+            except Exception as err:
+                r = 1
+                repo.ui.debug('merge tool threw exception: %s\n' % err.args[0])
         repo.ui.debug('merge tool returned: %d\n' % r)
         return True, r, False
 
 def _formatconflictmarker(ctx, template, label, pad):
     """Applies the given template to the ctx, prefixed by the label.
 
     Pad is the minimum width of the label prefix, so that multiple markers
     can have aligned templated parts.
@@ -746,19 +760,23 @@  def _filemerge(premerge, repo, wctx, myn
         return True, None, False
 
     ui = repo.ui
     fd = fcd.path()
     binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
     symlink = 'l' in fcd.flags() + fco.flags()
     changedelete = fcd.isabsent() or fco.isabsent()
     tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
+    script = False
     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:'):
+        toolpath = toolpath.replace('python:', '')
+        script = True
     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)))
 
     if tool in internals:
         func = internals[tool]
         mergetype = func.mergetype
         onfailure = func.onfailure
@@ -769,17 +787,17 @@  def _filemerge(premerge, repo, wctx, myn
             func = _xmergeimm
         else:
             func = _xmerge
         mergetype = fullmerge
         onfailure = _("merging %s failed!\n")
         precheck = None
         isexternal = True
 
-    toolconf = tool, toolpath, binary, symlink
+    toolconf = tool, toolpath, binary, symlink, script
 
     if mergetype == nomerge:
         r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
         return True, r, deleted
 
     if premerge:
         if orig != fco.path():
             ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))