Patchwork D9551: mergetools: add new conflict marker format with diffs in

login
register
mail settings
Submitter phabricator
Date Dec. 9, 2020, 9:29 a.m.
Message ID <differential-rev-PHID-DREV-muakv423rlkpx52bgjqx-req@mercurial-scm.org>
Download mbox | patch
Permalink /patch/47847/
State Superseded
Headers show

Comments

phabricator - Dec. 9, 2020, 9:29 a.m.
martinvonz created this revision.
Herald added a reviewer: hg-reviewers.
Herald added a subscriber: mercurial-patches.

REVISION SUMMARY
  I use 3-way conflict markers. Often when I resolve them, I manually
  compare one the base with one side and apply the differences to the
  other side. That can be hard when the conflict marker is large. This
  patch introduces a new type of conflict marker, which I'm hoping will
  make it easier to resolve conflicts.
  
  The new format uses `<<<<<<<` and `>>>>>>>` to open and close the
  markers, just like our existing 2-way and 3-way conflict
  markers. Instead of having 2 or 3 snapshots (left+right or
  left+base+right), it has a sequence of diffs. A diff looks like this:
  
    ------- base
    +++++++ left
     a
    -b
    +c
     d
  
  A diff that adds one side ("diff from nothing") does not have the
  `-------` line and does not have have `+` prefixed on its lines. A
  regular 3-way merge can be viewed as adding one side plus a diff
  between the base and the other side. It thus has two ways of being
  represented, depending on which side is used being diffed:
  
    <<<<<<<
    +++++++ left
    contents
    on
    left
    ------- base
    +++++++ right
     contents
     on
    -left
    +right
    >>>>>>>
  
  or
  
    <<<<<<<
    ------- base
    +++++++ left
     contents
     on
    -right
    +left
    +++++++ right
    contents
    on
    right
    >>>>>>>
  
  I've made it so the new merge tool tries to pick a version that has
  the most common lines (no difference in the example above).
  
  I've called the new tool "mergediff" to stick to the convention of
  starting with "merge" if the tool tries a regular 3-way merge.
  
  The idea came from my pet VCS (placeholder name `jj`), which has
  support for octopus merges and other ways of ending up with merges of
  more than 3 versions. I wanted to be able to represent such conflicts
  in the working copy and therefore thought of this format (although I
  have not yet implemented it in my VCS). I then attended a meeting with
  Larry McVoy, who said BitKeeper has an option (`bk smerge -g`) for
  showing a similar format, which reminded me to actually attempt this
  in Mercurial.

REPOSITORY
  rHG Mercurial

BRANCH
  default

REVISION DETAIL
  https://phab.mercurial-scm.org/D9551

AFFECTED FILES
  mercurial/filemerge.py
  mercurial/simplemerge.py
  tests/test-conflict.t
  tests/test-help.t

CHANGE DETAILS




To: martinvonz, #hg-reviewers
Cc: mercurial-patches, mercurial-devel

Patch

diff --git a/tests/test-help.t b/tests/test-help.t
--- a/tests/test-help.t
+++ b/tests/test-help.t
@@ -2052,6 +2052,13 @@ 
         partially merged file. Marker will have three sections, one from each
         side of the merge and one for the base content.
   
+      ":mergediff"
+        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 marker will have two sections, one with the
+        content from one side of the merge, and one with a diff from the base
+        content to the content on the other side. (experimental)
+  
       ":other"
         Uses the other 'p2()' version of files as the merged version.
   
diff --git a/tests/test-conflict.t b/tests/test-conflict.t
--- a/tests/test-conflict.t
+++ b/tests/test-conflict.t
@@ -280,6 +280,80 @@ 
   >>>>>>> merge rev
   Hop we are done.
 
+internal:mergediff
+
+  $ hg co -C 1
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cat << EOF > a
+  > Small Mathematical Series.
+  > 1
+  > 2
+  > 3
+  > 4
+  > 4.5
+  > 5
+  > Hop we are done.
+  > EOF
+  $ hg co -m 2 -t internal:mergediff
+  merging a
+  warning: conflicts while merging a! (edit, 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
+  [1]
+  $ cat a
+  Small Mathematical Series.
+  1
+  2
+  3
+  <<<<<<<
+  ------- base
+  +++++++ working copy
+   4
+  +4.5
+   5
+  +++++++ destination
+  6
+  8
+  >>>>>>>
+  Hop we are done.
+Test the same thing as above but modify a bit more so we instead get the working
+copy in full and the diff from base to destination.
+  $ hg co -C 1
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cat << EOF > a
+  > Small Mathematical Series.
+  > 1
+  > 2
+  > 3.5
+  > 4.5
+  > 5.5
+  > Hop we are done.
+  > EOF
+  $ hg co -m 2 -t internal:mergediff
+  merging a
+  warning: conflicts while merging a! (edit, 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
+  [1]
+  $ cat a
+  Small Mathematical Series.
+  1
+  2
+  <<<<<<<
+  +++++++ working copy
+  3.5
+  4.5
+  5.5
+  ------- base
+  +++++++ destination
+   3
+  -4
+  -5
+  +6
+  +8
+  >>>>>>>
+  Hop we are done.
+
 Add some unconflicting changes on each head, to make sure we really
 are merging, unlike :local and :other
 
diff --git a/mercurial/simplemerge.py b/mercurial/simplemerge.py
--- a/mercurial/simplemerge.py
+++ b/mercurial/simplemerge.py
@@ -455,6 +455,68 @@ 
     return ctx.node() != nodemod.nullid
 
 
+def _mergediff(m3, name_a, name_b, name_base):
+    lines = []
+    conflicts = False
+    for group in m3.merge_groups():
+        if group[0] == b'conflict':
+            base_lines, a_lines, b_lines = group[1:]
+            base_text = b''.join(base_lines)
+            b_blocks = list(
+                mdiff.allblocks(
+                    base_text,
+                    b''.join(b_lines),
+                    lines1=base_lines,
+                    lines2=b_lines,
+                )
+            )
+            a_blocks = list(
+                mdiff.allblocks(
+                    base_text,
+                    b''.join(a_lines),
+                    lines1=base_lines,
+                    lines2=b_lines,
+                )
+            )
+
+            def matching_lines(blocks):
+                return sum(
+                    block[1] - block[0]
+                    for block, kind in blocks
+                    if kind == b'='
+                )
+
+            def diff_lines(blocks, lines1, lines2):
+                for block, kind in blocks:
+                    if kind == b'=':
+                        for line in lines1[block[0] : block[1]]:
+                            yield b' ' + line
+                    else:
+                        for line in lines1[block[0] : block[1]]:
+                            yield b'-' + line
+                        for line in lines2[block[2] : block[3]]:
+                            yield b'+' + line
+
+            lines.append(b"<<<<<<<\n")
+            if matching_lines(a_blocks) < matching_lines(b_blocks):
+                lines.append(b"+++++++ %s\n" % name_a)
+                lines.extend(a_lines)
+                lines.append(b"------- %s\n" % name_base)
+                lines.append(b"+++++++ %s\n" % name_b)
+                lines.extend(diff_lines(b_blocks, base_lines, b_lines))
+            else:
+                lines.append(b"------- %s\n" % name_base)
+                lines.append(b"+++++++ %s\n" % name_a)
+                lines.extend(diff_lines(a_blocks, base_lines, a_lines))
+                lines.append(b"+++++++ %s\n" % name_b)
+                lines.extend(b_lines)
+            lines.append(b">>>>>>>\n")
+            conflicts = True
+        else:
+            lines.extend(group[1])
+    return lines, conflicts
+
+
 def simplemerge(ui, localctx, basectx, otherctx, **opts):
     """Performs the simplemerge algorithm.
 
@@ -499,9 +561,15 @@ 
         extrakwargs[b'name_base'] = name_base
         extrakwargs[b'minimize'] = False
 
-    lines = m3.merge_lines(
-        name_a=name_a, name_b=name_b, **pycompat.strkwargs(extrakwargs)
-    )
+    if mode == b'mergediff':
+        lines, conflicts = _mergediff(m3, name_a, name_b, name_base or b'',)
+    else:
+        lines = list(
+            m3.merge_lines(
+                name_a=name_a, name_b=name_b, **pycompat.strkwargs(extrakwargs)
+            )
+        )
+        conflicts = m3.conflicts
 
     # merge flags if necessary
     flags = localctx.flags()
@@ -519,5 +587,5 @@ 
     else:
         localctx.write(mergedtext, flags)
 
-    if m3.conflicts and not mode == b'union':
+    if conflicts and not mode == b'union':
         return 1
diff --git a/mercurial/filemerge.py b/mercurial/filemerge.py
--- a/mercurial/filemerge.py
+++ b/mercurial/filemerge.py
@@ -532,6 +532,33 @@ 
     return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
 
 
+@internaltool(
+    b'mergediff',
+    fullmerge,
+    _(
+        b"warning: conflicts while merging %s! "
+        b"(edit, then use 'hg resolve --mark')\n"
+    ),
+    precheck=_mergecheck,
+)
+def _imerge_diff(
+    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 marker will have two sections, one with the
+    content from one side of the merge, and one with a diff from the base
+    content to the content on the other side. (experimental)"""
+    if not labels:
+        labels = _defaultconflictlabels
+    if len(labels) < 3:
+        labels.append(b'base')
+    return _merge(
+        repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, b'mergediff'
+    )
+
+
 def _imergeauto(
     repo,
     mynode,