Saturday, July 6, 2013

Conflict Resolution with Vim Conflict Slides

The Conflict Slides Vim plugin allows you to lock to a conflict range and replace that whole range with individual sections inside the conflict markers.  This has several advantages.  For example, you are independent from how the other buffers are named and because modifications to the buffer are fully controlled, you gain some safety while exploring the conflict.

I recently added display helper functions that allow you to toggle the diff option in selected windows.  That in turn makes it possible to see 2-way diffs at different points of the conflict resolution process.  I am going to show you how such a procedure could look like.

There is more documentation on the Conflict Slides frontpage and the functions are documented in more detail in the plugin file.

The tutorial part focuses on Git somewhat, but as long as you have the same conflict markers, this should work with any VCS.


I would recommend to turn the folding off in the post-lock callback to mitigate jolting when changing content.  You can turn the folding back on with the argument 'on' instead of 'off', and the folding can be toggled with the argument 'toggle'.

fun! g:conflict_slides_post_lock_callback()
    call CS_DiffChangeFolding('off')

If your VCS offers to insert the base content into the markers, you should consider to turn that on. Otherwise the base slide is not available.  In Git you can add the base content with the following command:

git config merge.conflictstyle diff3

Generally, I am using three diff windows with ours, dest, theirs content.  That is the setup provided by vim-fugitive.  You should really check out that plugin, if you haven't done so already.  It is tremendously easy to find the commit for a line in a conflict (among many other things).

Example Mappings

Here are some mappings that extend the default mappings from Conflict Slides (CS) a bit.  You can use this as a template for your own mappings.  The ^[1-3] mappings do essentially turn the diff on in all 3 windows, except in the given window where the diff is toggled.  This allows you to see a two-way diff for any combination of the three windows, as well as return to a three-way diff.

nnoremap <silent> ^l :call CS_LockNextConflict('lock-current')<CR>zz
nnoremap <silent> ^n :call CS_LockNextConflict()<CR>zz
nnoremap <silent> ^N :call CS_LockNextConflict('backward')<CR>zz

nnoremap <silent> ^u :call CS_ReleaseLockedConflict()<Bar>
                     \ call CS_DiffChangeFolding('on')<CR>

nnoremap <silent> ^f :call CS_DiffChangeFolding('toggle')<CR>zz

nnoremap <silent> ^1 :call CS_DiffSwitch3Toggle(1)<CR>
nnoremap <silent> ^2 :call CS_DiffSwitch3Toggle(2)<CR>
nnoremap <silent> ^3 :call CS_DiffSwitch3Toggle(3)<CR>

You can simply define a g:conflictslides_locked_mappings dictionary in your Vim configuration to override the one from CS.  The top section in the following snippet is the same as in the plugin.  Below that are a few mappings that allow you to insert a slide while turning diff on/off in selected windows.  The first two of those mappings are useful immediately after you locked to a conflict to explore the changes in each parent.

let g:conflictslides_locked_mappings = {
        \ 'b' : ":call CS_ModifyConflictContent('base')<CR>",
        \ 'B' : ":call CS_ModifyConflictContent('base', 'append')<CR>",
        \ 'o' : ":call CS_ModifyConflictContent('ours')<CR>",
        \ 'O' : ":call CS_ModifyConflictContent('ours', 'append')<CR>",
        \ 't' : ":call CS_ModifyConflictContent('theirs')<CR>",
        \ 'T' : ":call CS_ModifyConflictContent('theirs', 'append')<CR>",
        \ 'a' : ":call CS_ModifyConflictContent('ours theirs')<CR>",
        \ 'A' : ":call CS_ModifyConflictContent('theirs ours')<CR>",
        \ 'f' : ":call CS_ModifyConflictContent('forward')<CR>",
        \ 'r' : ":call CS_ModifyConflictContent('reverse')<CR>",
        \ 'F' : ":call CS_ModifyConflictContent('forward-nobase')<CR>",
        \ 'R' : ":call CS_ModifyConflictContent('reverse-nobase')<CR>",
        \ 'e' : ":call CS_ReleaseLockedConflict()<CR>",
        \ 'q' : ":call CS_ModifyConflictContent('forward')<Bar>"
        \               . "call CS_ReleaseLockedConflict()<CR>",
        \ '<CR>' : ":call CS_LockNextConflict()<CR>",
        \ '<BS>' : ":call CS_LockNextConflict('restore-conflict')<CR>",
        \ 'V' : ":call CS_SelectCurrentConflictRange(0)<CR>",
        \ 'v' : ":call CS_SelectCurrentConflictRange(500)<CR>",
        \ '<space>b'        : ":call CS_ModifyConflictContent('base')<Bar>"
        \                           . "call CS_DiffSwitch3Off(3)<CR>",
        \ '<space><space>b' : ":call CS_ModifyConflictContent('base')<Bar>"
        \                           . "call CS_DiffSwitch3Off(1)<CR>",
        \ '<space>o'        : ":call CS_ModifyConflictContent('ours')<Bar>"
        \                           . "call CS_DiffSwitch3Off(1)<CR>",
        \ '<space>t'        : ":call CS_ModifyConflictContent('theirs')<Bar>"
        \                           . "call CS_DiffSwitch3Off(3)<CR>",
        \ '<space>a'        : ":call CS_DiffSwitch3AllOn()<CR>",
        \ '<space>1'        : ":call CS_DiffSwitch3Toggle(1)<CR>",
        \ '<space>2'        : ":call CS_DiffSwitch3Toggle(2)<CR>",
        \ '<space>3'        : ":call CS_DiffSwitch3Toggle(3)<CR>",
        \ '<space>f'        : ":call CS_DiffChangeFolding('on')<CR>zz",
        \ '<space>F'        : ":call CS_DiffChangeFolding('off')<CR>zz",
        \ }

There is nothing in there that cannot be done with the global mappings from above.  So, it's just a template in case you want to create more convenient mappings with the mechanism provided by CS.  You may need to adapt the window number in the calls if you use a different setup.

Prepare for Conflict

There is this awesome merge resolution boot camp with solutions and I've picked one merge to demonstrate how to resolve a conflict.  You can follow if you like.  This is the commit:

736a2dd Merge tag 'virtio-next-for-linus' of git://...

First, prepare two tags and a branch.

git tag ExpDest 736a2dd2571ac56b11ed95a7814d838d5311be04
git tag ExpTheirs ExpDest^2
git checkout -b ExpMaster ExpDest^1

Inspect the result with this command:

git log --graph --oneline --decorate --simplify-by-decoration -3 ExpDest

It should look similar to this:

Now, let's try to merge.

git merge --log ExpTheirs

This should result in a conflict with several unmerged files.


If you use Git without Fugitive, you can start the setup described above by adding the following section to your ~/.gitconfig file,

[mergetool "gv3"]
cmd = gvim -f -d  \"$LOCAL\" \"$MERGED\" \"$REMOTE\" \"+2wincmd w\"

and start that mergetool for drivers/char/virtio_console.c like so:

git mergetool -t gv3 drivers/char/virtio_console.c

By pressing ^l you can lock to the one and only conflict in this file.  This will produce a beautiful rainbow of colors.

The initial 3-way diff of the conflict

Now you can use the <space>b and <space><space>b mappings to see two-way diffs between the base slide and ours/theirs respectively.  (The same can be accomplished with b and then ^1 or ^3)

Difference ours/base

Difference base/theirs

Aha!  The conditional branch has been wrapped in a spinlock on our side and a very important call has been replaced by an even more important call on their side.  If you are using Fugitive you can very conveniently open a blame view for their commit and try to find out more about that change.

Fugitive's blame view started from their modification

To resolve it you can take our side with <space>o, and append their side with T.

Spotting the surplus line in the difference dest/theirs

Because this is a two-way diff with theirs, the wrong call can be easily spotted.  Let's unlock with e (for edit) and delete the wrong line.  You can visualize the changes on each side by using the mappings ^1 and ^3.

Resolved difference ours/dest
Resolved difference dest/theirs

Resolve more

If you would like to resolve some more conflicts yourself, search the repository with the following command:

git  log  --merges  --grep Conflicts  master