--- 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