mercurial/filemerge.py
branchstable
changeset 49366 288de6f5d724
parent 49167 7af798e497f5
child 49644 5744ceeb9067
child 49837 59466b13a3ae
--- a/mercurial/filemerge.py	Thu Jun 16 15:15:03 2022 +0200
+++ b/mercurial/filemerge.py	Thu Jun 16 15:28:54 2022 +0200
@@ -5,7 +5,6 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
-from __future__ import absolute_import
 
 import contextlib
 import os
@@ -85,7 +84,7 @@
 )
 
 
-class absentfilectx(object):
+class absentfilectx:
     """Represents a file that's ostensibly in a context but is actually not
     present in it.
 
@@ -849,7 +848,7 @@
 
     props = {b'ctx': ctx}
     templateresult = template.renderdefault(props)
-    input.label_detail = templateresult.splitlines()[0]  # split for safety
+    input.label_detail = stringutil.firstline(templateresult)  # avoid '\n'
 
 
 def _populate_label_details(repo, inputs, tool=None):
@@ -1052,6 +1051,7 @@
             markerstyle = internalmarkerstyle
 
         if mergetype == fullmerge:
+            _run_partial_resolution_tools(repo, local, other, base)
             # conflict markers generated by premerge will use 'detailed'
             # settings if either ui.mergemarkers or the tool's mergemarkers
             # setting is 'detailed'. This way tools can have basic labels in
@@ -1116,6 +1116,77 @@
             backup.remove()
 
 
+def _run_partial_resolution_tools(repo, local, other, base):
+    """Runs partial-resolution tools on the three inputs and updates them."""
+    ui = repo.ui
+    if ui.configbool(b'merge', b'disable-partial-tools'):
+        return
+    # Tuples of (order, name, executable path, args)
+    tools = []
+    seen = set()
+    section = b"partial-merge-tools"
+    for k, v in ui.configitems(section):
+        name = k.split(b'.')[0]
+        if name in seen:
+            continue
+        patterns = ui.configlist(section, b'%s.patterns' % name, [])
+        is_match = True
+        if patterns:
+            m = match.match(repo.root, b'', patterns)
+            is_match = m(local.fctx.path())
+        if is_match:
+            if ui.configbool(section, b'%s.disable' % name):
+                continue
+            order = ui.configint(section, b'%s.order' % name, 0)
+            executable = ui.config(section, b'%s.executable' % name, name)
+            args = ui.config(section, b'%s.args' % name)
+            tools.append((order, name, executable, args))
+
+    if not tools:
+        return
+    # Sort in configured order (first in tuple)
+    tools.sort()
+
+    files = [
+        (b"local", local.fctx.path(), local.text()),
+        (b"base", base.fctx.path(), base.text()),
+        (b"other", other.fctx.path(), other.text()),
+    ]
+
+    with _maketempfiles(files) as temppaths:
+        localpath, basepath, otherpath = temppaths
+
+        for order, name, executable, args in tools:
+            cmd = procutil.shellquote(executable)
+            replace = {
+                b'local': localpath,
+                b'base': basepath,
+                b'other': otherpath,
+            }
+            args = util.interpolate(
+                br'\$',
+                replace,
+                args,
+                lambda s: procutil.shellquote(util.localpath(s)),
+            )
+
+            cmd = b'%s %s' % (cmd, args)
+            r = ui.system(cmd, cwd=repo.root, blockedtag=b'partial-mergetool')
+            if r:
+                raise error.StateError(
+                    b'partial merge tool %s exited with code %d' % (name, r)
+                )
+            local_text = util.readfile(localpath)
+            other_text = util.readfile(otherpath)
+            if local_text == other_text:
+                # No need to run other tools if all conflicts have been resolved
+                break
+
+        local.set_text(local_text)
+        base.set_text(util.readfile(basepath))
+        other.set_text(other_text)
+
+
 def _haltmerge():
     msg = _(b'merge halted after failed merge (see hg resolve)')
     raise error.InterventionRequired(msg)
@@ -1199,7 +1270,7 @@
 
 def loadinternalmerge(ui, extname, registrarobj):
     """Load internal merge tool from specified registrarobj"""
-    for name, func in pycompat.iteritems(registrarobj._table):
+    for name, func in registrarobj._table.items():
         fullname = b':' + name
         internals[fullname] = func
         internals[b'internal:' + name] = func