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, 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) + 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/relnotes/next b/relnotes/next --- a/relnotes/next +++ b/relnotes/next @@ -25,6 +25,13 @@ == New Experimental Features == + * There is a new internal merge tool called `internal:mergediff` (can + be set as the value for the `merge` config in the `[ui]` + section). It resolves merges the same was as `internal:merge` and + `internal:merge3`, but it shows conflicts differently. Instead of + showing 2 or 3 snapshots of the conflicting pieces of code, it + shows one snapshot and a diff. This may be useful when at least one + side of the conflict is similar to the base. == Bug Fixes == 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/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.