├── .gitignore ├── LICENSE ├── README.md ├── _utils ├── README.md ├── conflict-markers.png ├── default-vimdiff.png ├── make-conflicts.sh ├── make-conflicts_hg.sh ├── poem-resolved.txt └── vim-diffconflicts.png ├── doc └── diffconflicts.txt └── plugin └── diffconflicts.vim /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Seth House 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vim-diffconflicts 2 | 3 | A better Vimdiff mergetool. 4 | 5 | tl;dr: 6 | 7 | * Call `:DiffConflicts` to convert a file containing conflict markers into 8 | a two-way diff. 9 | * Install as a Git or Mercurial mergetool to do that automatically. (See 10 | [Installation](#installation) below.) 11 | 12 | ## Why? 13 | 14 | Watch a demonstration version of this README on YouTube: 15 | 16 | [![diffconflicts Video Demonstration](https://img.youtube.com/vi/Pxgl3Wtf78Y/0.jpg)](https://www.youtube.com/watch?v=Pxgl3Wtf78Y) 17 | 18 | Contents: 19 | 20 | * [Three-Way Diffs are Hard](#three-way-diffs-are-hard) 21 | * [Editing-Conflict-Markers-is-Hard](#editing-conflict-markers-is-hard) 22 | * [Two-Way Diffs are Eas(ier)](#two-way-diffs-are-easier) 23 | * [Conflict-Markers **are** a Two-Way Diff](#conflict-markers-are-a-two-way-diff) 24 | 25 | ### Three-Way Diffs are Hard 26 | 27 | When Git cannot automatically resolve conflicts it writes a file with conflict 28 | markers surrounding the conflicting areas. These conflicts must be resolved 29 | manually. This is often done via a three-way comparison. 30 | 31 | Vim supports three-way diffs however syntax highlighting alone is not 32 | sufficient to showcase the differences between that many versions. In addition, 33 | the default keybindings are not well suited to moving individual changes 34 | between that many windows. 35 | 36 | The screenshot below is an example of Vimdiff as a Git mergetool using default 37 | settings. None of the conflicts have an obvious resolution: 38 | 39 | ![](./_utils/default-vimdiff.png) 40 | 41 | ### Editing Conflict Markers is Hard 42 | 43 | When human intervention is needed it is rarely as simple as choosing the "left" 44 | change or the "right" change. The correct resolution often involves a mix of 45 | both changes. It is difficult to manually edit a file containing Git conflict 46 | markers because the human eye isn't well suited to spotting subtle differences, 47 | particularly when the differences are not adjacent: 48 | 49 | ![](./_utils/conflict-markers.png) 50 | 51 | ### Two-Way Diffs are Eas(ier) 52 | 53 | A two-way diff more simply highlights just the relevant differences which makes 54 | the resolution more clear. The merge base and history of each version of the 55 | conflict is a useful reference to learn the intent of each conflicting change, 56 | however those are not as useful to see in the diff. 57 | 58 | Vimdiff is well suited to two-way diffs: 59 | 60 | ![](./_utils/vim-diffconflicts.png) 61 | 62 | ### Conflict-Markers **are** a Two-Way Diff 63 | 64 | Git does an admirable job of automatically resolving conflicts. We want to 65 | retain all the work and resolve only the things that Git could not. That work 66 | _**is**_ reflected in the files containing conflict markers, but it _**is 67 | not**_ reflected in a two-way diff between LOCAL and REMOTE. 68 | 69 | Rather than editing the conflict markers directly, it is better to perform 70 | a two-way diff on _**only**_ the "left" and "right" sides of the conflict 71 | markers by splitting them apart. 72 | 73 | ## Installation 74 | 75 | 1. Install this plugin using your favorite Vim plugin manager, or just clone 76 | the repo into your packages directory (see `:help packages`). 77 | 78 | 2. Configure Git to use this plugin as a mergetool: 79 | 80 | ``` 81 | git config --global merge.tool diffconflicts 82 | git config --global mergetool.diffconflicts.cmd 'vim -c DiffConflicts "$MERGED" "$BASE" "$LOCAL" "$REMOTE"' 83 | git config --global mergetool.diffconflicts.trustExitCode true 84 | git config --global mergetool.keepBackup false 85 | ``` 86 | 87 | Or, if you'd prefer to always open both the diff view and the history view 88 | call `DiffConflictsWithHistory` instead: 89 | 90 | ``` 91 | git config --global mergetool.diffconflicts.cmd 'vim -c DiffConflictsWithHistory "$MERGED" "$BASE" "$LOCAL" "$REMOTE"' 92 | ``` 93 | 94 | 3. During a merge you can call `:DiffConflictsShowHistory` to open a new tab 95 | containing the merge BASE and full copies of the LOCAL and REMOTE versions 96 | of the conflicted file. This can help to understand the history or intent 97 | behind the conflicting changes to help you decide how best to combine the 98 | changes. 99 | 100 | This tab is not opened by default so that Vim starts more quickly. 101 | 102 | 103 | ## Mercurial 104 | 105 | Configure Mercurial to use diffconflicts as a mergetool by adding: 106 | 107 | [merge-tools] 108 | diffconflicts.executable=vim 109 | diffconflicts.args=-c 'let g:diffconflicts_vcs="hg"' -c DiffConflicts "$output" $base $local $other 110 | diffconflicts.premerge=keep 111 | diffconflicts.check=conflicts 112 | diffconflicts.priority=99 113 | 114 | to your `.hgrc` file. 115 | Or, if you prefer to always open both the diff view and the history view use 116 | 117 | diffconflicts.args=-c 'let g:diffconflicts_vcs="hg"' -c DiffConflictsWithHistory "$output" $base $local $other 118 | 119 | as the args setting to call `DiffConflictsWithHistory`. 120 | -------------------------------------------------------------------------------- /_utils/README.md: -------------------------------------------------------------------------------- 1 | # A mergetool rant 2 | 3 | diffconflicts is not special or noteworthy. That is true for the original `sed` 4 | shell script and the Vim/vimscript plugin. The core idea from this plugin is 5 | extremely simple and can be expressed as a sed one-liner. What is special is 6 | that most people and most mergetools don't recognize the following points. 7 | 8 | The goal of this rant is to get all mergetools to recognize these ideas and to 9 | adopt this technique. Editor holy wars are not important; making merge conflict 10 | resolution easier is important. 11 | 12 | - Git provides a great deal of value by automatically resolving many conflicts. 13 | The fruit of this effort is only expressed in the file containing conflict 14 | markers. 15 | - It is difficult for a human to look at a file containing conflict markers. It 16 | is impossible to reliably spot subtle differences. 17 | - A human should never manually edit a file containing conflict markers. It is 18 | too easy to make mistakes. Eyeballing two hunks of changes, positioned 19 | vertically, is a fool's errand without tooling support. 20 | - Often a conflict resolution requires a mix of both left and right and not 21 | just all of left or all of right. Because it is difficult to carefully read 22 | and mentally compare each change many people simply choose the left or the 23 | right change which can result in the loss of a wanted change. 24 | - A diff between `LOCAL` and `REMOTE` is a bad approach. It bypasses all the 25 | work Git already did to automatically resolve conflicts and and forces the 26 | user to resolve those yet again but manually. It presents unecessary visual 27 | noise to the user by showing things that were already resolved to the user. 28 | 29 | ## Mergetool Benchmarks 30 | 31 | There's a few noteworthy things in the conflicts that the `make-conflicts.sh` 32 | script produces. These points can be used to benchmark the effectiveness of 33 | a given mergetool. More should be added. 34 | 35 | 1. The `bri1lig` -> `brillig` conflict was automatically resolved. It should 36 | not be shown to the user. 37 | 2. The `m0me` -> `mome` conflict was automatically resolved. It should not be 38 | shown to the user. 39 | 3. The `did` -> `Did` conflict was automatically resolved. It should not be 40 | shown to the user. 41 | 4. All conflicts in the second stanza were automatically resolved. They should 42 | not be shown to the user. 43 | 5. The conflict on the first line _is_ an "ours vs. theirs" situation. We only 44 | want theirs. 45 | 6. The conflict on the third line _is not_ an "ours vs. theirs" situation. We 46 | want changes from both: 47 | 48 | - Want the capitalization change from theirs. 49 | - Want the extra 'r' removal from ours. 50 | - Want the hanging punctuation change from ours. 51 | 52 | 7. The conflict on the fourth line should be easily noticeable. We want the 53 | 'r'. 54 | -------------------------------------------------------------------------------- /_utils/conflict-markers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiteinge/diffconflicts/4972d1401e008c5e9afeb703eddd1b2c2a1d1199/_utils/conflict-markers.png -------------------------------------------------------------------------------- /_utils/default-vimdiff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiteinge/diffconflicts/4972d1401e008c5e9afeb703eddd1b2c2a1d1199/_utils/default-vimdiff.png -------------------------------------------------------------------------------- /_utils/make-conflicts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | git init testrepo 4 | cd testrepo 5 | 6 | cat << EOF > poem.txt 7 | twas bri1lig, and the slithy toves 8 | did gyre and gimble in the wabe 9 | all mimsy were the borogroves 10 | and the m0me raths outgabe. 11 | 12 | "Beware the Jabberwock, my son! 13 | The jaws that bite, the claws that catch! 14 | Beware the Jub jub bird, and shun 15 | The frumious bandersnatch!" 16 | EOF 17 | 18 | git add poem.txt 19 | git commit -m 'Commit One' 20 | 21 | git branch branchA 22 | 23 | cat << EOF > poem.txt 24 | twas brillig, and the slithy toves 25 | Did gyre and gimble in the wabe: 26 | all mimsy were the borogoves, 27 | And the mome raths outgrabe. 28 | 29 | "Beware the Jabberwock, my son! 30 | The jaws that bite, the claws that catch! 31 | Beware the Jubjub bird, and shun 32 | The frumious Bandersnatch!" 33 | EOF 34 | 35 | git add poem.txt 36 | git commit -m 'Fix syntax mistakes' 37 | 38 | git checkout branchA 39 | 40 | cat << EOF > poem.txt 41 | 'Twas brillig, and the slithy toves 42 | Did gyre and gimble in the wabe: 43 | All mimsy were the borogroves 44 | And the mome raths outgabe. 45 | 46 | "Beware the Jabberwock, my son! 47 | The jaws that bite, the claws that catch! 48 | Beware the Jub jub bird, and shun 49 | The frumious bandersnatch!" 50 | EOF 51 | 52 | git add poem.txt 53 | git commit -m 'Buncha fixes' 54 | 55 | git checkout master 56 | git merge branchA 57 | -------------------------------------------------------------------------------- /_utils/make-conflicts_hg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | hg init testrepo_hg 4 | cd testrepo_hg 5 | 6 | cat << EOF > poem.txt 7 | twas bri1lig, and the slithy toves 8 | did gyre and gimble in the wabe 9 | all mimsy were the borogroves 10 | and the m0me raths outgabe. 11 | 12 | "Beware the Jabberwock, my son! 13 | The jaws that bite, the claws that catch! 14 | Beware the Jub jub bird, and shun 15 | The frumious bandersnatch!" 16 | EOF 17 | 18 | hg add poem.txt 19 | hg bookmark master 20 | hg commit -m 'Commit One' 21 | 22 | cat << EOF > poem.txt 23 | 'Twas brillig, and the slithy toves 24 | Did gyre and gimble in the wabe: 25 | All mimsy were the borogroves 26 | And the mome raths outgabe. 27 | 28 | "Beware the Jabberwock, my son! 29 | The jaws that bite, the claws that catch! 30 | Beware the Jub jub bird, and shun 31 | The frumious bandersnatch!" 32 | EOF 33 | 34 | hg bookmark branchA 35 | hg commit -m 'Buncha fixes' 36 | 37 | 38 | hg update -Cr master 39 | cat << EOF > poem.txt 40 | twas brillig, and the slithy toves 41 | Did gyre and gimble in the wabe: 42 | all mimsy were the borogoves, 43 | And the mome raths outgrabe. 44 | 45 | "Beware the Jabberwock, my son! 46 | The jaws that bite, the claws that catch! 47 | Beware the Jubjub bird, and shun 48 | The frumious Bandersnatch!" 49 | EOF 50 | hg commit -m 'Fix syntax mistakes' 51 | 52 | hg --config ui.merge=internal:merge merge branchA 53 | -------------------------------------------------------------------------------- /_utils/poem-resolved.txt: -------------------------------------------------------------------------------- 1 | 'Twas brillig, and the slithy toves 2 | Did gyre and gimble in the wabe: 3 | All mimsy were the borogoves, 4 | And the mome raths outgrabe. 5 | 6 | "Beware the Jabberwock, my son! 7 | The jaws that bite, the claws that catch! 8 | Beware the Jubjub bird, and shun 9 | The frumious Bandersnatch!" 10 | -------------------------------------------------------------------------------- /_utils/vim-diffconflicts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiteinge/diffconflicts/4972d1401e008c5e9afeb703eddd1b2c2a1d1199/_utils/vim-diffconflicts.png -------------------------------------------------------------------------------- /doc/diffconflicts.txt: -------------------------------------------------------------------------------- 1 | *diffconflicts.txt* A better Vimdiff Git/Mercurial mergetool 2 | 3 | This plugin converts a file containing Git conflict markers into a two-way diff 4 | in Vimdiff. Additional mappings can be given to see the full three-way diff. 5 | 6 | Configure this plugin as a mergetool in Git via the following shell commands: 7 | 8 | git config --global merge.tool diffconflicts 9 | git config --global mergetool.diffconflicts.cmd 'vim -c DiffConflicts "$MERGED" "$BASE" "$LOCAL" "$REMOTE"' 10 | git config --global mergetool.diffconflicts.trustExitCode true 11 | git config --global mergetool.keepBackup false 12 | 13 | For Mercurial use the following settings in your .hgrc: 14 | 15 | [merge-tools] 16 | diffconflicts.executable=vim 17 | diffconflicts.args=-c 'let g:diffconflicts_vcs="hg"' -c DiffConflicts "$output" $base $local $other 18 | diffconflicts.premerge=keep 19 | diffconflicts.check=conflicts 20 | 21 | Commands: 22 | :DiffConflicts 23 | Convert a file containing Git conflict markers into a two-way diff. 24 | 25 | :DiffConflictsShowHistory 26 | Open a new tab containing the merge base and the local and remote 27 | version of the conflicted file. 28 | 29 | :DiffConflictsWithHistory 30 | Call both DiffConflicts and DiffConflictsShowHistory. This is useful in 31 | the Git mergetool configuration to always open the history by default. 32 | 33 | *DiffConflicts-settings* 34 | Use g:diffconflicts_vcs to indicate which version control system produced 35 | the conflict file (Git or Mercurial). 36 | -------------------------------------------------------------------------------- /plugin/diffconflicts.vim: -------------------------------------------------------------------------------- 1 | " Two-way diff each side of a file with Git conflict markers 2 | " Maintainer: Seth House 3 | " License: MIT 4 | 5 | if exists("g:loaded_diffconflicts") 6 | finish 7 | endif 8 | let g:loaded_diffconflicts = 1 9 | 10 | let s:save_cpo = &cpo 11 | set cpo&vim 12 | 13 | " CONFIGURATION 14 | if !exists("g:diffconflicts_vcs") 15 | " Default to git 16 | let g:diffconflicts_vcs = "git" 17 | endif 18 | 19 | let g:loaded_diffconflicts = 1 20 | function! s:hasConflicts() 21 | try 22 | silent execute "%s/^<<<<<<< //gn" 23 | return 1 24 | catch /Pattern not found/ 25 | return 0 26 | endtry 27 | endfunction 28 | 29 | function! s:diffconfl() 30 | let l:origBuf = bufnr("%") 31 | let l:origFt = &filetype 32 | 33 | if g:diffconflicts_vcs == "git" 34 | " Obtain the git setting for the conflict style. 35 | let l:conflictStyle = system("git config --get merge.conflictStyle")[:-2] 36 | else 37 | " Assume 2way conflict style otherwise. 38 | let l:conflictStyle = "diff" 39 | endif 40 | 41 | " Set up the right-hand side. 42 | rightb vsplit 43 | enew 44 | silent execute "read #". l:origBuf 45 | 1delete 46 | silent execute "file RCONFL" 47 | silent execute "set filetype=". l:origFt 48 | diffthis " set foldmethod before editing 49 | silent execute "g/^<<<<<<< /,/^=======\\r\\?$/d" 50 | silent execute "g/^>>>>>>> /d" 51 | setlocal nomodifiable readonly buftype=nofile bufhidden=delete nobuflisted 52 | 53 | " Set up the left-hand side. 54 | wincmd p 55 | diffthis " set foldmethod before editing 56 | if l:conflictStyle ==? "diff3" || l:conflictStyle ==? "zdiff3" 57 | silent execute "g/^||||||| \\?/,/^>>>>>>> /d" 58 | else 59 | silent execute "g/^=======\\r\\?$/,/^>>>>>>> /d" 60 | endif 61 | silent execute "g/^<<<<<<< /d" 62 | 63 | diffupdate 64 | endfunction 65 | 66 | function! s:showHistory() 67 | " Create the tab and windows. 68 | tabnew 69 | vsplit 70 | vsplit 71 | wincmd h 72 | wincmd h 73 | 74 | " Populate each window. 75 | if g:diffconflicts_vcs == "hg" 76 | buffer ~local. 77 | file LOCAL 78 | else 79 | buffer LOCAL 80 | endif 81 | setlocal nomodifiable readonly 82 | diffthis 83 | 84 | wincmd l 85 | if g:diffconflicts_vcs == "hg" 86 | buffer ~base. 87 | file BASE 88 | else 89 | buffer BASE 90 | endif 91 | setlocal nomodifiable readonly 92 | diffthis 93 | 94 | wincmd l 95 | if g:diffconflicts_vcs == "hg" 96 | buffer ~other. 97 | file OTHER 98 | else 99 | buffer REMOTE 100 | endif 101 | setlocal nomodifiable readonly 102 | diffthis 103 | 104 | " Put cursor in back in BASE. 105 | wincmd h 106 | endfunction 107 | 108 | function! s:checkThenShowHistory() 109 | if g:diffconflicts_vcs == "hg" 110 | let l:filecheck = 'v:val =~# "\\~base\\." || v:val =~# "\\~local\\." || v:val =~# "\\~other\\."' 111 | else 112 | let l:filecheck = 'v:val =~# "BASE" || v:val =~# "LOCAL" || v:val =~# "REMOTE"' 113 | endif 114 | let l:xs = 115 | \ filter( 116 | \ map( 117 | \ filter( 118 | \ range(1, bufnr('$')), 119 | \ 'bufexists(v:val)' 120 | \ ), 121 | \ 'bufname(v:val)' 122 | \ ), 123 | \ l:filecheck 124 | \ ) 125 | 126 | if (len(l:xs) < 3) 127 | echohl WarningMsg 128 | \ | echo "Missing one or more of BASE, LOCAL, REMOTE." 129 | \ ." Was Vim invoked by a Git mergetool?" 130 | \ | echohl None 131 | return 1 132 | else 133 | call s:showHistory() 134 | return 0 135 | endif 136 | endfunction 137 | 138 | function! s:checkThenDiff() 139 | if (s:hasConflicts()) 140 | redraw 141 | echohl WarningMsg 142 | \ | echon "Resolve conflicts leftward then save. Use :cq to abort." 143 | \ | echohl None 144 | return s:diffconfl() 145 | else 146 | echohl WarningMsg | echo "No conflict markers found." | echohl None 147 | endif 148 | endfunction 149 | 150 | command! DiffConflicts call s:checkThenDiff() 151 | command! DiffConflictsShowHistory call s:checkThenShowHistory() 152 | command! DiffConflictsWithHistory call s:checkThenShowHistory() 153 | \ | 1tabn 154 | \ | call s:checkThenDiff() 155 | 156 | let &cpo = s:save_cpo 157 | unlet s:save_cpo 158 | --------------------------------------------------------------------------------