filemerge: support passing labels to external merge tools
authorKyle Lippincott <spectral@google.com>
Wed, 17 Jan 2018 17:35:05 -0800
changeset 35907 9037c29e9f53
parent 35906 cd2342302928
child 35908 a9802c9ecfb5
filemerge: support passing labels to external merge tools This adds $labellocal, $labelother, and $labelbase to the replacement set for merge-tools.<tool>.args config variables, and to the environment as HG_MY_LABEL, HG_OTHER_LABEL, and HG_BASE_LABEL, respectively. We also add merge-tools.<tool>.mergemarkers and merge-tools.<tool>.mergemarkertemplate config variables as counterparts of the variables available in [ui]. We are intentionally *not* respecting ui.mergemarkers when calling out to external merge programs; too often the default template will be too wide to display comfortably in most GUIs. Differential Revision: https://phab.mercurial-scm.org/D2011
mercurial/configitems.py
mercurial/filemerge.py
mercurial/help/config.txt
tests/test-merge-tools.t
--- a/mercurial/configitems.py	Fri Feb 02 23:20:55 2018 -0500
+++ b/mercurial/configitems.py	Wed Jan 17 17:35:05 2018 -0800
@@ -743,6 +743,16 @@
     generic=True,
     priority=-1,
 )
+coreconfigitem('merge-tools', br'.*\.mergemarkers$',
+    default='basic',
+    generic=True,
+    priority=-1,
+)
+coreconfigitem('merge-tools', br'.*\.mergemarkertemplate$',
+    default=dynamicdefault,  # take from ui.mergemarkertemplate
+    generic=True,
+    priority=-1,
+)
 coreconfigitem('merge-tools', br'.*\.priority$',
     default=0,
     generic=True,
--- a/mercurial/filemerge.py	Fri Feb 02 23:20:55 2018 -0500
+++ b/mercurial/filemerge.py	Wed Jan 17 17:35:05 2018 -0800
@@ -513,6 +513,11 @@
     b, c = _maketempfiles(repo, fco, fca)
     try:
         out = ""
+        mylabel, otherlabel = labels[:2]
+        if len(labels) >= 3:
+            baselabel = labels[2]
+        else:
+            baselabel = 'base'
         env = {'HG_FILE': fcd.path(),
                'HG_MY_NODE': short(mynode),
                'HG_OTHER_NODE': str(fco.changectx()),
@@ -520,6 +525,9 @@
                'HG_MY_ISLINK': 'l' in fcd.flags(),
                'HG_OTHER_ISLINK': 'l' in fco.flags(),
                'HG_BASE_ISLINK': 'l' in fca.flags(),
+               'HG_MY_LABEL': mylabel,
+               'HG_OTHER_LABEL': otherlabel,
+               'HG_BASE_LABEL': baselabel,
                }
         ui = repo.ui
 
@@ -528,7 +536,9 @@
             # read input from backup, write to original
             out = a
             a = repo.wvfs.join(back.path())
-        replace = {'local': a, 'base': b, 'other': c, 'output': out}
+        replace = {'local': a, 'base': b, 'other': c, 'output': out,
+                   'labellocal': mylabel, 'labelother': otherlabel,
+                   'labelbase': baselabel}
         args = util.interpolate(br'\$', replace, args,
                                 lambda s: util.shellquote(util.localpath(s)))
         cmd = toolpath + ' ' + args
@@ -566,7 +576,7 @@
 
 _defaultconflictlabels = ['local', 'other']
 
-def _formatlabels(repo, fcd, fco, fca, labels):
+def _formatlabels(repo, fcd, fco, fca, labels, tool=None):
     """Formats the given labels using the conflict marker template.
 
     Returns a list of formatted labels.
@@ -577,6 +587,8 @@
 
     ui = repo.ui
     template = ui.config('ui', 'mergemarkertemplate')
+    if tool is not None:
+        template = _toolstr(ui, tool, 'mergemarkertemplate', template)
     template = templater.unquotestring(template)
     tres = formatter.templateresources(ui, repo)
     tmpl = formatter.maketemplater(ui, template, defaults=templatekw.keywords,
@@ -706,6 +718,7 @@
         mergetype = func.mergetype
         onfailure = func.onfailure
         precheck = func.precheck
+        isexternal = False
     else:
         if wctx.isinmemory():
             func = _xmergeimm
@@ -714,6 +727,7 @@
         mergetype = fullmerge
         onfailure = _("merging %s failed!\n")
         precheck = None
+        isexternal = True
 
     toolconf = tool, toolpath, binary, symlink
 
@@ -743,19 +757,42 @@
     files = (None, None, None, back)
     r = 1
     try:
-        markerstyle = ui.config('ui', 'mergemarkers')
+        internalmarkerstyle = ui.config('ui', 'mergemarkers')
+        if isexternal:
+            markerstyle = _toolstr(ui, tool, 'mergemarkers')
+        else:
+            markerstyle = internalmarkerstyle
+
         if not labels:
             labels = _defaultconflictlabels
+        formattedlabels = labels
         if markerstyle != 'basic':
-            labels = _formatlabels(repo, fcd, fco, fca, labels)
+            formattedlabels = _formatlabels(repo, fcd, fco, fca, labels,
+                                            tool=tool)
 
         if premerge and mergetype == fullmerge:
-            r = _premerge(repo, fcd, fco, fca, toolconf, files, labels=labels)
+            # 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
+            # space-constrained areas of the UI, but still get full information
+            # in conflict markers if premerge is 'keep' or 'keep-merge3'.
+            premergelabels = labels
+            labeltool = None
+            if markerstyle != 'basic':
+                # respect 'tool's mergemarkertemplate (which defaults to
+                # ui.mergemarkertemplate)
+                labeltool = tool
+            if internalmarkerstyle != 'basic' or markerstyle != 'basic':
+                premergelabels = _formatlabels(repo, fcd, fco, fca,
+                                               premergelabels, tool=labeltool)
+
+            r = _premerge(repo, fcd, fco, fca, toolconf, files,
+                          labels=premergelabels)
             # complete if premerge successful (r is 0)
             return not r, r, False
 
         needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
-                                     toolconf, files, labels=labels)
+                                     toolconf, files, labels=formattedlabels)
 
         if needcheck:
             r = _check(repo, r, ui, tool, fcd, files)
--- a/mercurial/help/config.txt	Fri Feb 02 23:20:55 2018 -0500
+++ b/mercurial/help/config.txt	Wed Jan 17 17:35:05 2018 -0800
@@ -1363,13 +1363,18 @@
 ``args``
   The arguments to pass to the tool executable. You can refer to the
   files being merged as well as the output file through these
-  variables: ``$base``, ``$local``, ``$other``, ``$output``. The meaning
-  of ``$local`` and ``$other`` can vary depending on which action is being
-  performed. During and update or merge, ``$local`` represents the original
-  state of the file, while ``$other`` represents the commit you are updating
-  to or the commit you are merging with. During a rebase ``$local``
-  represents the destination of the rebase, and ``$other`` represents the
-  commit being rebased.
+  variables: ``$base``, ``$local``, ``$other``, ``$output``.
+
+  The meaning of ``$local`` and ``$other`` can vary depending on which action is
+  being performed. During an update or merge, ``$local`` represents the original
+  state of the file, while ``$other`` represents the commit you are updating to or
+  the commit you are merging with. During a rebase, ``$local`` represents the
+  destination of the rebase, and ``$other`` represents the commit being rebased.
+
+  Some operations define custom labels to assist with identifying the revisions,
+  accessible via ``$labellocal``, ``$labelother``, and ``$labelbase``. If custom
+  labels are not available, these will be ``local``, ``other``, and ``base``,
+  respectively.
   (default: ``$local $base $other``)
 
 ``premerge``
@@ -1405,6 +1410,21 @@
 ``gui``
   This tool requires a graphical interface to run. (default: False)
 
+``mergemarkers``
+  Controls whether the labels passed via ``$labellocal``, ``$labelother``, and
+  ``$labelbase`` are ``detailed`` (respecting ``mergemarkertemplate``) or
+  ``basic``. If ``premerge`` is ``keep`` or ``keep-merge3``, the conflict
+  markers generated during premerge will be ``detailed`` if either this option or
+  the corresponding option in the ``[ui]`` section is ``detailed``.
+  (default: ``basic``)
+
+``mergemarkertemplate``
+  This setting can be used to override ``mergemarkertemplate`` from the ``[ui]``
+  section on a per-tool basis; this applies to the ``$label``-prefixed variables
+  and to the conflict markers that are generated if ``premerge`` is ``keep` or
+  ``keep-merge3``. See the corresponding variable in ``[ui]`` for more
+  information.
+
 .. container:: windows
 
   ``regkey``
@@ -2120,6 +2140,8 @@
     markers is different from the encoding of the merged files,
     serious problems may occur.
 
+    Can be overridden per-merge-tool, see the ``[merge-tools]`` section.
+
 ``origbackuppath``
     The path to a directory used to store generated .orig files. If the path is
     not a directory, one will be created.  If set, files stored in this
--- a/tests/test-merge-tools.t	Fri Feb 02 23:20:55 2018 -0500
+++ b/tests/test-merge-tools.t	Wed Jan 17 17:35:05 2018 -0800
@@ -1059,6 +1059,150 @@
   # hg resolve --list
   R f
 
+premerge=keep respects ui.mergemarkers=basic:
+
+  $ beforemerge
+  [merge-tools]
+  false.whatever=
+  true.priority=1
+  true.executable=cat
+  # hg update -C 1
+  $ hg merge -r 4 --config merge-tools.true.premerge=keep --config ui.mergemarkers=basic
+  merging f
+  <<<<<<< working copy
+  revision 1
+  space
+  =======
+  revision 4
+  >>>>>>> merge rev
+  revision 0
+  space
+  revision 4
+  0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ aftermerge
+  # cat f
+  <<<<<<< working copy
+  revision 1
+  space
+  =======
+  revision 4
+  >>>>>>> merge rev
+  # hg stat
+  M f
+  # hg resolve --list
+  R f
+
+premerge=keep ignores ui.mergemarkers=basic if true.mergemarkers=detailed:
+
+  $ beforemerge
+  [merge-tools]
+  false.whatever=
+  true.priority=1
+  true.executable=cat
+  # hg update -C 1
+  $ hg merge -r 4 --config merge-tools.true.premerge=keep \
+  >     --config ui.mergemarkers=basic \
+  >     --config merge-tools.true.mergemarkers=detailed
+  merging f
+  <<<<<<< working copy: ef83787e2614 - test: revision 1
+  revision 1
+  space
+  =======
+  revision 4
+  >>>>>>> merge rev:    81448d39c9a0 - test: revision 4
+  revision 0
+  space
+  revision 4
+  0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ aftermerge
+  # cat f
+  <<<<<<< working copy: ef83787e2614 - test: revision 1
+  revision 1
+  space
+  =======
+  revision 4
+  >>>>>>> merge rev:    81448d39c9a0 - test: revision 4
+  # hg stat
+  M f
+  # hg resolve --list
+  R f
+
+premerge=keep respects ui.mergemarkertemplate instead of
+true.mergemarkertemplate if true.mergemarkers=basic:
+
+  $ beforemerge
+  [merge-tools]
+  false.whatever=
+  true.priority=1
+  true.executable=cat
+  # hg update -C 1
+  $ hg merge -r 4 --config merge-tools.true.premerge=keep \
+  >    --config ui.mergemarkertemplate='uitmpl {rev}' \
+  >    --config merge-tools.true.mergemarkertemplate='tooltmpl {short(node)}'
+  merging f
+  <<<<<<< working copy: uitmpl 1
+  revision 1
+  space
+  =======
+  revision 4
+  >>>>>>> merge rev:    uitmpl 4
+  revision 0
+  space
+  revision 4
+  0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ aftermerge
+  # cat f
+  <<<<<<< working copy: uitmpl 1
+  revision 1
+  space
+  =======
+  revision 4
+  >>>>>>> merge rev:    uitmpl 4
+  # hg stat
+  M f
+  # hg resolve --list
+  R f
+
+premerge=keep respects true.mergemarkertemplate instead of
+true.mergemarkertemplate if true.mergemarkers=detailed:
+
+  $ beforemerge
+  [merge-tools]
+  false.whatever=
+  true.priority=1
+  true.executable=cat
+  # hg update -C 1
+  $ hg merge -r 4 --config merge-tools.true.premerge=keep \
+  >    --config ui.mergemarkertemplate='uitmpl {rev}' \
+  >    --config merge-tools.true.mergemarkertemplate='tooltmpl {short(node)}' \
+  >    --config merge-tools.true.mergemarkers=detailed
+  merging f
+  <<<<<<< working copy: tooltmpl ef83787e2614
+  revision 1
+  space
+  =======
+  revision 4
+  >>>>>>> merge rev:    tooltmpl 81448d39c9a0
+  revision 0
+  space
+  revision 4
+  0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ aftermerge
+  # cat f
+  <<<<<<< working copy: tooltmpl ef83787e2614
+  revision 1
+  space
+  =======
+  revision 4
+  >>>>>>> merge rev:    tooltmpl 81448d39c9a0
+  # hg stat
+  M f
+  # hg resolve --list
+  R f
 
 Tool execution
 
@@ -1190,6 +1334,142 @@
   # hg resolve --list
   R f
 
+Merge using a tool that supports labellocal, labelother, and labelbase, checking
+that they're quoted properly as well. This is using the default 'basic'
+mergemarkers even though ui.mergemarkers is 'detailed', so it's ignoring both
+mergemarkertemplate settings:
+
+  $ beforemerge
+  [merge-tools]
+  false.whatever=
+  true.priority=1
+  true.executable=cat
+  # hg update -C 1
+  $ cat <<EOF > printargs_merge_tool
+  > while test \$# -gt 0; do echo arg: \"\$1\"; shift; done
+  > EOF
+  $ hg --config merge-tools.true.executable='sh' \
+  >    --config merge-tools.true.args='./printargs_merge_tool ll:$labellocal lo: $labelother lb:$labelbase": "$base' \
+  >    --config merge-tools.true.mergemarkertemplate='tooltmpl {short(node)}' \
+  >    --config ui.mergemarkertemplate='uitmpl {rev}' \
+  >    --config ui.mergemarkers=detailed \
+  >    merge -r 2
+  merging f
+  arg: "ll:working copy"
+  arg: "lo:"
+  arg: "merge rev"
+  arg: "lb:base: /tmp/f~base.*" (glob)
+  0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ rm -f 'printargs_merge_tool'
+
+Merge using a tool that supports labellocal, labelother, and labelbase, checking
+that they're quoted properly as well. This is using 'detailed' mergemarkers,
+even though ui.mergemarkers is 'basic', and using the tool's
+mergemarkertemplate:
+
+  $ beforemerge
+  [merge-tools]
+  false.whatever=
+  true.priority=1
+  true.executable=cat
+  # hg update -C 1
+  $ cat <<EOF > printargs_merge_tool
+  > while test \$# -gt 0; do echo arg: \"\$1\"; shift; done
+  > EOF
+  $ hg --config merge-tools.true.executable='sh' \
+  >    --config merge-tools.true.args='./printargs_merge_tool ll:$labellocal lo: $labelother lb:$labelbase": "$base' \
+  >    --config merge-tools.true.mergemarkers=detailed \
+  >    --config merge-tools.true.mergemarkertemplate='tooltmpl {short(node)}' \
+  >    --config ui.mergemarkertemplate='uitmpl {rev}' \
+  >    --config ui.mergemarkers=basic \
+  >    merge -r 2
+  merging f
+  arg: "ll:working copy: tooltmpl ef83787e2614"
+  arg: "lo:"
+  arg: "merge rev: tooltmpl 0185f4e0cf02"
+  arg: "lb:base: /tmp/f~base.*" (glob)
+  0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ rm -f 'printargs_merge_tool'
+
+The merge tool still gets labellocal and labelother as 'basic' even when
+premerge=keep is used and has 'detailed' markers:
+
+  $ beforemerge
+  [merge-tools]
+  false.whatever=
+  true.priority=1
+  true.executable=cat
+  # hg update -C 1
+  $ cat <<EOF > mytool
+  > echo labellocal: \"\$1\"
+  > echo labelother: \"\$2\"
+  > echo "output (arg)": \"\$3\"
+  > echo "output (contents)":
+  > cat "\$3"
+  > EOF
+  $ hg --config merge-tools.true.executable='sh' \
+  >    --config merge-tools.true.args='mytool $labellocal $labelother $output' \
+  >    --config merge-tools.true.premerge=keep \
+  >    --config merge-tools.true.mergemarkertemplate='tooltmpl {short(node)}' \
+  >    --config ui.mergemarkertemplate='uitmpl {rev}' \
+  >    --config ui.mergemarkers=detailed \
+  >    merge -r 2
+  merging f
+  labellocal: "working copy"
+  labelother: "merge rev"
+  output (arg): "$TESTTMP/f"
+  output (contents):
+  <<<<<<< working copy: uitmpl 1
+  revision 1
+  =======
+  revision 2
+  >>>>>>> merge rev:    uitmpl 2
+  space
+  0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ rm -f 'mytool'
+
+premerge=keep uses the *tool's* mergemarkertemplate if tool's
+mergemarkers=detailed; labellocal and labelother also use the tool's template
+
+  $ beforemerge
+  [merge-tools]
+  false.whatever=
+  true.priority=1
+  true.executable=cat
+  # hg update -C 1
+  $ cat <<EOF > mytool
+  > echo labellocal: \"\$1\"
+  > echo labelother: \"\$2\"
+  > echo "output (arg)": \"\$3\"
+  > echo "output (contents)":
+  > cat "\$3"
+  > EOF
+  $ hg --config merge-tools.true.executable='sh' \
+  >    --config merge-tools.true.args='mytool $labellocal $labelother $output' \
+  >    --config merge-tools.true.premerge=keep \
+  >    --config merge-tools.true.mergemarkers=detailed \
+  >    --config merge-tools.true.mergemarkertemplate='tooltmpl {short(node)}' \
+  >    --config ui.mergemarkertemplate='uitmpl {rev}' \
+  >    --config ui.mergemarkers=detailed \
+  >    merge -r 2
+  merging f
+  labellocal: "working copy: tooltmpl ef83787e2614"
+  labelother: "merge rev: tooltmpl 0185f4e0cf02"
+  output (arg): "$TESTTMP/f"
+  output (contents):
+  <<<<<<< working copy: tooltmpl ef83787e2614
+  revision 1
+  =======
+  revision 2
+  >>>>>>> merge rev:    tooltmpl 0185f4e0cf02
+  space
+  0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ rm -f 'mytool'
+
 Issue3581: Merging a filename that needs to be quoted
 (This test doesn't work on Windows filesystems even on Linux, so check
 for Unix-like permission)