Patchwork [3,of,7] merge: add a internal:merge3 tools

login
register
mail settings
Submitter Pierre-Yves David
Date Aug. 5, 2014, 12:28 a.m.
Message ID <823ba13f92d50d668145.1407198489@marginatus.alto.octopoid.net>
Download mbox | patch
Permalink /patch/5259/
State Changes Requested
Headers show

Comments

Pierre-Yves David - Aug. 5, 2014, 12:28 a.m.
# HG changeset patch
# User Pierre-Yves David <pierre-yves.david@ens-lyon.org>
# Date 1348740176 -7200
#      Thu Sep 27 12:02:56 2012 +0200
# Node ID 823ba13f92d50d66814594a20ee80e6d8c6f481f
# Parent  8917721c8a9310b77b257b7ce920d682f6ea09b5
merge: add a internal:merge3 tools.

This variant gives access to a feature already present in ``internal:merge``:
displaying merge base content.

In the basic merge (calling ``hg merge``) case, including more context to the
merge markers is an interesting addition.

But this extra information is the only viable option in case conflict from
grafting (, rebase, etc…).

When grafting ``source`` on ``destination``, the parent of ``source`` is
used as the ``base``. When all three changesets add content in the same
location, the marker for ``source`` will contains both ``base`` and ``source``
content. Without the content of base exposed, there is no way for the user
to discriminate content coming from ``base`` and content commit from ``source``.

Practical example (all addition are in the same place):

* ``destination`` adds ``Dest-Content``
* ``base``        adds ``Base-Content``
* ``source``      adds ``Src-Content``

Grafting ``source`` on ``destination`` will produce the following conflict:

  <<<<<<< destination
  Dest-Content
  =======
  Base-Content
  Src-Content
  >>>>>>> source

This that case there is no way to distinct ``base`` from ``source``. As a result
content from ``base`` are likely to slip in the resolution result.

However, adding the base make the situation very clear:

  <<<<<<< destination
  Dest-Content
  ||||||| base
  Base-Content
  ======= base
  Base-Content
  Src-Content
  >>>>>>> source

Once the base is added, the addition from the grafted changeset is made clear.
User can compare the content from ``base`` and ``source`` to make an enlightened
decision during merge resolution.
Matt Mackall - Aug. 5, 2014, 9:44 p.m.
On Mon, 2014-08-04 at 17:28 -0700, pierre-yves.david@ens-lyon.org wrote:
> # HG changeset patch
> # User Pierre-Yves David <pierre-yves.david@ens-lyon.org>
> # Date 1348740176 -7200
> #      Thu Sep 27 12:02:56 2012 +0200
> # Node ID 823ba13f92d50d66814594a20ee80e6d8c6f481f
> # Parent  8917721c8a9310b77b257b7ce920d682f6ea09b5
> merge: add a internal:merge3 tools.

Trailing period.

This wants to be 3-5 patches.

> +  abort: display of base incompatible conflict minimization

The goal of simplemerge should have always been to be a drop-in
replacement for RCS merge. Please nuke this minimization thing entirely.

Patch

diff --git a/mercurial/filemerge.py b/mercurial/filemerge.py
--- a/mercurial/filemerge.py
+++ b/mercurial/filemerge.py
@@ -205,11 +205,12 @@  def _premerge(repo, toolconf, files, lab
                 "(edit conflicts, then use 'hg resolve --mark')\n"))
 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
     """
     Uses the internal non-interactive simple merge algorithm for merging
     files. It will fail if there are any conflicts and leave markers in
-    the partially merged file."""
+    the partially merged file. Markers will have two sections, one for each side
+    of merge."""
     tool, toolpath, binary, symlink = toolconf
     if symlink:
         repo.ui.warn(_('warning: internal:merge cannot merge symlinks '
                        'for %s\n') % fcd.path())
         return False, 1
@@ -221,10 +222,25 @@  def _imerge(repo, mynode, orig, fcd, fco
 
         r = simplemerge.simplemerge(ui, a, b, c, label=labels, no_minimal=True)
         return True, r
     return False, 0
 
+@internaltool('merge3', True,
+              _("merging %s incomplete! "
+                "(edit conflicts, then use 'hg resolve --mark')\n"))
+def _imerge3(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
+    """
+    Uses the internal non-interactive simple merge algorithm for merging
+    files. It will fail if there are any conflicts and leave markers in
+    the partially merged file. Marker will have three sections, one from each
+    side of the merge and one for the base content."""
+    if not labels:
+        labels = _defaultconflictlabels
+    if len(labels) < 3:
+        labels.append('base')
+    return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
+
 @internaltool('tagmerge', True,
               _("automatic tag merging of %s failed! "
                 "(use 'hg resolve --tool internal:merge' or another merge "
                 "tool of your choice)\n"))
 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
@@ -311,27 +327,31 @@  def _formatconflictmarker(repo, ctx, tem
     '{ifeq(branch, "default", "", "{branch} ")}' +
     '- {author|user}: {desc|firstline}')
 
 _defaultconflictlabels = ['local', 'other']
 
-def _formatlabels(repo, fcd, fco, labels):
+def _formatlabels(repo, fcd, fco, fca, labels):
     """Formats the given labels using the conflict marker template.
 
     Returns a list of formatted labels.
     """
     cd = fcd.changectx()
     co = fco.changectx()
+    ca = fca.changectx()
 
     ui = repo.ui
     template = ui.config('ui', 'mergemarkertemplate', _defaultconflictmarker)
     template = templater.parsestring(template, quoted=False)
-    tmpl = templater.templater(None, cache={ 'conflictmarker' : template })
+    tmpl = templater.templater(None, cache={'conflictmarker': template})
 
-    pad = max(len(labels[0]), len(labels[1]))
+    pad = max(len(l) for l in labels)
 
-    return [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
-            _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
+    newlabels = [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
+                 _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
+    if len(labels) > 2:
+        newlabels.append(_formatconflictmarker(repo, ca, tmpl, labels[2], pad))
+    return newlabels
 
 def filemerge(repo, mynode, orig, fcd, fco, fca, labels=None):
     """perform a 3-way merge in the working directory
 
     mynode = parent node before merge
@@ -390,11 +410,11 @@  def filemerge(repo, mynode, orig, fcd, f
 
     markerstyle = ui.config('ui', 'mergemarkers', 'basic')
     if not labels:
         labels = _defaultconflictlabels
     if markerstyle != 'basic':
-        labels = _formatlabels(repo, fcd, fco, labels)
+        labels = _formatlabels(repo, fcd, fco, fca, labels)
 
     needcheck, r = func(repo, mynode, orig, fcd, fco, fca, toolconf,
                         (a, b, c, back), labels=labels)
     if not needcheck:
         if r:
diff --git a/mercurial/simplemerge.py b/mercurial/simplemerge.py
--- a/mercurial/simplemerge.py
+++ b/mercurial/simplemerge.py
@@ -413,17 +413,20 @@  def simplemerge(ui, local, base, other, 
                 raise util.Abort(msg)
         return text
 
     name_a = local
     name_b = other
+    name_base = None
     labels = opts.get('label', [])
     if len(labels) > 0:
         name_a = labels[0]
     if len(labels) > 1:
         name_b = labels[1]
     if len(labels) > 2:
-        raise util.Abort(_("can only specify two labels."))
+        name_base = labels[2]
+    if len(labels) > 3:
+        raise util.Abort(_("can only specify three labels."))
 
     try:
         localtext = readfile(local)
         basetext = readfile(base)
         othertext = readfile(other)
@@ -436,14 +439,22 @@  def simplemerge(ui, local, base, other, 
         out = opener(os.path.basename(local), "w", atomictemp=True)
     else:
         out = sys.stdout
 
     reprocess = not opts.get('no_minimal')
+    if reprocess and name_base:
+        msg = _("display of base incompatible conflict minimization")
+        raise util.Abort(msg, hint=_("add --no-minimal"))
+
 
     m3 = Merge3Text(basetext, localtext, othertext)
+    extrakwargs = {}
+    if name_base is not None:
+        extrakwargs['base_marker'] = '|||||||'
+        extrakwargs['name_base'] = name_base
     for line in m3.merge_lines(name_a=name_a, name_b=name_b,
-                               reprocess=reprocess):
+                               reprocess=reprocess, **extrakwargs):
         out.write(line)
 
     if not opts.get('print'):
         out.close()
 
diff --git a/tests/test-conflict.t b/tests/test-conflict.t
--- a/tests/test-conflict.t
+++ b/tests/test-conflict.t
@@ -196,5 +196,39 @@  Verify basic conflict markers
   3
   4
   5
   >>>>>>> other
   Hop we are done.
+
+internal:merge3
+
+  $ hg up -q --clean .
+
+  $ hg merge 1 --tool internal:merge3
+  merging a
+  warning: conflicts during merge.
+  merging a incomplete! (edit conflicts, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+  [1]
+  $ cat a
+  Small Mathematical Series.
+  <<<<<<< local
+  1
+  2
+  3
+  6
+  8
+  ||||||| base
+  One
+  Two
+  Three
+  Four
+  Five
+  =======
+  1
+  2
+  3
+  4
+  5
+  >>>>>>> other
+  Hop we are done.
diff --git a/tests/test-contrib.t b/tests/test-contrib.t
--- a/tests/test-contrib.t
+++ b/tests/test-contrib.t
@@ -195,11 +195,11 @@  2 labels
   [1]
 
 too many labels
 
   $ python simplemerge -p -L foo -L bar -L baz conflict-local base conflict-other
-  abort: can only specify two labels.
+  abort: display of base incompatible conflict minimization
   [255]
 
 binary file
 
   $ python -c "f = file('binary-local', 'w'); f.write('\x00'); f.close()"