Skip to content

Instantly share code, notes, and snippets.

@karenyyng
Last active September 29, 2024 03:15
Show Gist options
  • Save karenyyng/f19ff75c60f18b4b8149 to your computer and use it in GitHub Desktop.
Save karenyyng/f19ff75c60f18b4b8149 to your computer and use it in GitHub Desktop.
How to use `git mergetool` to resolve conflicts in Vim / NeoVim

Table Of Content

Skip to the relevant sections if needed.

Concepts for resolving Git conflicts

For using mergetool in git, we need to understand the following terminology to understand what is being merged:

  • LOCAL - the head for the file(s) from the current branch on the machine that you are using.
  • REMOTE - the head for files(s) from a remote location that you are trying to merge into your LOCAL branch.
  • BASE - the common ancestor(s) of LOCAL and REMOTE.
  • MERGED - the tag / HEAD object after the merge - this is saved as a new commit.

Common mergetool from editors will display both LOCAL and REMOTE so you can decide which changes to keep. Please read this tutorial explaining the HEAD objects if you do not know what it is. It will help your understanding of Git tremendously.

Setting up different editors / tool for using git mergetool

We have to change the git config to set a default mergetool. In this example, we will use vimdiff:

$ git config merge.tool vimdiff      

We can also set the editor to display the common ancestor BASE while we examine what changes are in LOCAL and REMOTE with the following setting:

$ git config merge.conflictstyle diff3  

back to top

Finding out what mergetool editors are supported

$ git mergetool --tool-help

And we list a few of them:

Command line mergetool editors

  • Emacs based diff tools: emerge, or Ediff
  • Vim based diff tool: vimdiff

GUI mergetool editors

  • gvimdiff - almost identical to vimdiff but uses the Linux GUI for Vim, please refer to vimdiff if you still use the keyboard commands for GVim.
  • kdiff3
  • meld
  • tortoisemerge

Or consult the community of your favorite editor to see how to do the equivalent operations for your editor.

Other useful mergetool settings

Do not prompt before launching the merge resolution tool

$ git config mergetool.prompt false

back to top

mergetool simple code example

Ref1 for the example
Ref2

creating the git repo

$ mkdir galaxyZoo
$ cd galaxyZoo
$ git init
$ vim astrophy_obj.txt

Add some galaxy types into astrophy_obj.txt then save the file.

# content of astrophy_obj.txt
spiral
elliptical
bar 
irregular

save then commit the file.

$ git add astrophy_obj.txt
$ git commit -m 'Initial commit'
$ git branch astrophy_objects   # create a new branch
$ git checkout astrophy_objects # change to new branch
$ vim astrophy_obj.txt          # make changes to file 

Change bar to barred in the file.

$ git commit -am 'changed bar to barred'
$ git checkout master   # change back to master branch
$ vim astrophy_obj.txt  
# add the word `galaxy` to the end of each line using Vim REGEX 
# type `:%s/$/ galaxy/g` in Vim then press enter and save `:wq`

$ git commit -am 'added galaxy to each line'
# merge from the astrophy_objects branch to current branch, i.e. master
$ git merge astrophy_objects  

Then you will see some error messages:

Auto-merging astrophy_obj.txt
CONFLICT (content): Merge conflict in astrophy_obj.txt
Automatic merge failed; fix conflicts and then commit the result.

We can bring up the mergetool:

$ git mergetool

Then it will bring up the different versions of the file in different Vim splits panels.

+--------------------------------+
| LOCAL  |     BASE     | REMOTE |
+--------------------------------+
|             MERGED             |
+--------------------------------+

The top left split panel is the LOCAL, top middle split is BASE and top right split is REMOTE. The bottom split refers to the MERGED version. You can find this info in the bottom bar of each split (I have put 3 yellow rectangles to highlight that info).

As you can see form the below image, my Vim has highlighted the differences in red for me. Vim mergetool image

Now if your terminal has any GUI capability and you have compiled Vim correctly with GUI support, you can use your mouse to click on the bottom split to edit it. Or if you are a Vim ninja, you can use the keyboard shortcut to move to different splits.

Ctrl w + h   # move to the split on the left 
Ctrl w + j   # move to the split below
Ctrl w + k   # move to the split on top
Ctrl w + l   # move to the split on the right

You can either incorporate the changes by manually editing the MERGED split, or use Vim shortcuts pull from one of the LOCAL, BASE ad REMOTE versions.

:diffg RE  # get from REMOTE
:diffg BA  # get from BASE
:diffg LO  # get from LOCAL

save the changes then quit with :wqa to close all the splits. Remember to commit the merge.

$ git commit -am 'merged from several branches'

Resolving conflict from a git pull

If you were trying to do a git pull when you ran into merge conflicts, follow all steps in the previous section for using the mergetool, then do:

$ git rebase –continue

This command will

Forward-port local commits to the updated upstream HEAD.

according to the documentation, meaning your local commits will be pushed to the upstream remote branch as a new forward commit that doesn't interfere with previous commits. Hooray now you can claim that you can collaborate with others with Git without messing up with your collaborators' commits.

back to top

Other vimdiff keyboard shortcuts

]c - Jump to the next change.
[c - Jump to the previous change.

ref

Other great references and tutorials

Update (2020/09/20)

Thanks to @ekalosak's comment, I have fixed a mistake confusing the difference branches.

back to top

@PegasusWang
Copy link

let mapleader=','
let g:mapleader=','

if &diff
    map <leader>1 :diffget LOCAL<CR>
    map <leader>2 :diffget BASE<CR>
    map <leader>3 :diffget REMOTE<CR>
endif

some useful maps

Copy link

ghost commented Oct 26, 2017

@darxtrix, I have the same question.

@simonmikkelsen
Copy link

Yes, @darxtrix is right: BASE is common of LOCAL and REMOTE.

@theholy7
Copy link

theholy7 commented Mar 1, 2018

Thanks for explaining this! 👍

@johnelliott
Copy link

This was handy for me today. 👍

@johnelliott
Copy link

let mapleader=','
let g:mapleader=','

if &diff
    map <leader>1 :diffget LOCAL<CR>
    map <leader>2 :diffget BASE<CR>
    map <leader>3 :diffget REMOTE<CR>
endif

some useful maps

In vim if you don't want to use maps, according to :help 'do' you can also type 3do to get from remote:

If you give a [count], it is used as the [bufspec] argument for ":diffget".

@antoine-morvan
Copy link

Hi. Nice tutorial. I was just wondering if, in the Concepts for resolving Git conflicts, BASE is the common ancestor(s) of LOCAL and REMOTE instead of LOCAL and BASE ?

@SantoshNGitHub
Copy link

It was very helpful. Thank you.

@xingyze
Copy link

xingyze commented Sep 7, 2019

really awesome!

@wimstefan
Copy link

Good explanation. Very helpful! 🙏🏼

@sriharshay
Copy link

Thanks for the mini tutorial, it clear now.

@ekalosak
Copy link

Has anyone here had any success modifying the colors of the highlighting for more potential differences than add, delete, & change? It would be useful to have different shades (for example) indicating which of the 3 files are different. I'm not proud of spending a lot of time sifting through code where LOCAL and REMOTE are identical but both differ from BASE, I've gotta say.

@karenyyng
Copy link
Author

Hi. Nice tutorial. I was just wondering if, in the Concepts for resolving Git conflicts, BASE is the common ancestor(s) of LOCAL and REMOTE instead of LOCAL and BASE ?

@ ekalosak I appreciate your comment. The sentence has now been fixed. Somehow I didn't get any notifications for the comments.

@haxpor
Copy link

haxpor commented Oct 17, 2020

I'm curious whey I didn't get BASE file shown? I only have 3 split window of LOCAL, RESULT, REMOTE. No BASE.

@FabienRCT
Copy link

FabienRCT commented Oct 23, 2020

Interesting, but I noted a mistake in the spelling of 'elliptical' written as 'ellipitcal'. I don't know if gists are editable though.
And in the following command "git checkout astrophy_objects # change to new branch"" the option -b is necessary to create the new branch.

@karenyyng
Copy link
Author

Interesting, but I noted a mistake in the spelling of 'elliptical' written as 'ellipitcal'. I don't know if gists are editable though.
And in the following command "git checkout astrophy_objects # change to new branch"" the option -b is necessary to create the new branch.

@FabienRCT I indeed spelled elliptical wrong. Haha. I blame the sticky keyboard on my laptop.
That 's been fixed. Gist can be edited after the fact.

As for your second comment, if you follow the previous step for creating the branch of astrophy_objects, i.e. git branch astrophy_objects before switching to the new branch, you do not need to use the -b flag. Of course, you can choose to create a new branch and switch to it in one step instead of using the two steps that I wrote in my note. :)

@FabienRCT
Copy link

As for your second comment, if you follow the previous step for creating the branch of astrophy_objects, i.e. git branch astrophy_objects before switching to the new branch, you do not need to use the -b flag. Of course, you can choose to create a new branch and switch to it in one step instead of using the two steps that I wrote in my note

Well, look like I read it too fast and miss the 'git branch' line, however you corrected 'elliptical' wrong again as 'elliptcal'. ;)

@karenyyng
Copy link
Author

As for your second comment, if you follow the previous step for creating the branch of astrophy_objects, i.e. git branch astrophy_objects before switching to the new branch, you do not need to use the -b flag. Of course, you can choose to create a new branch and switch to it in one step instead of using the two steps that I wrote in my note

Well, look like I read it too fast and miss the 'git branch' line, however you corrected 'elliptical' wrong again as 'elliptcal'. ;)

Thanks. Spell check is definitely sorely needed for Gists.

@Sysa
Copy link

Sysa commented Jun 11, 2021

use the :cquit to exit vimdiff properly and start the merge again in case of issues.

@ianchanning
Copy link

ianchanning commented Jan 30, 2022

This is an excellent tutorial. Fugitive is also a great tool for vimdiff and as per vim-fugitive #1306 you can use these commands for mergetool:

git config --global mergetool.fugitive.cmd 'nvim -f -c "Gvdiffsplit!" "$MERGED"'
git config --global merge.tool fugitive

See also this vimdiff mergetool stackoverflow answer about fugitive

@hacker65536
Copy link

for neovim
~/.gitconfig

[merge]
  tool = vimdiff
  conflictstyle = diff3

[mergetool]
  keepBackup = false
  prompt = false

[mergetool "vimdiff"]
  cmd = "nvim  -d $MERGED $LOCAL $BASE $REMOTE -c 'wincmd J | wincmd ='"
  

@mrjohannchang
Copy link

mrjohannchang commented Apr 11, 2022

+--------------------------------+
| LOCAL  |     BASE     | REMOTE |
+--------------------------------+
|             MERGED             |
+--------------------------------+


Update 2023-06-07: https://gist.github.com/karenyyng/f19ff75c60f18b4b8149?permalink_comment_id=4592842#gistcomment-4592842


To make Neovim (nvim) display in the same layout as what's shown in the first post of this thread, the command should be:

nvim -d \"$LOCAL\" \"$MERGED\" \"$BASE\" \"$REMOTE\" -c \"wincmd w\" -c \"wincmd J\"

The minimum config that precisely targets the requirements should be:

For merge

Edit ~/.gitconfig directly or use the git CLI.

~/.gitconfig

[merge]
	tool = nvimdiff
[mergetool "nvimdiff"]
	cmd = nvim -d \"$LOCAL\" \"$MERGED\" \"$BASE\" \"$REMOTE\" -c \"wincmd w\" -c \"wincmd J\"

CLI

merge.tool = nvimdiff
mergetool.nvimdiff.cmd nvim -d "\$LOCAL" "\$REMOTE" "\$MERGED" -c "wincmd w" -c "wincmd J"

For diff

Edit ~/.gitconfig directly or use the git CLI.

~/.gitconfig

[diff]
	tool = nvimdiff
[difftool "nvimdiff"]
	cmd = nvim -d \"$LOCAL\" \"$REMOTE\" -c \"wincmd w\" -c \"wincmd L\"

CLI

git config --global diff.tool nvimdiff
git config --global difftool.nvimdiff.cmd 'nvim -d "$LOCAL" "$REMOTE" -c "wincmd w" -c "wincmd L"'

@owocado
Copy link

owocado commented Jun 19, 2022

This was pretty helpful! Thank you beyond words. ❤️

@573
Copy link

573 commented Sep 9, 2022

Thank you so much for this article. I rebase a fork of nixos configuration to upstream rn using your guidance !

@0x61nas
Copy link

0x61nas commented Oct 27, 2022

Thanks ❤️

@gaesa
Copy link

gaesa commented Jan 31, 2023

Very helpful, thanks!

@gmmoreira
Copy link

Just a update, recent versions of Git support "vimdiff" and "nvimdiff" by default, there's no need to create a new entry in the config and specify the cmd. It's also possible to customize the layout. More details can be found here in the doc: https://github.com/git/git/blob/master/Documentation/mergetools/vimdiff.txt

@mrjohannchang
Copy link

mrjohannchang commented Jun 7, 2023

Just a update, recent versions of Git support "vimdiff" and "nvimdiff" by default, there's no need to create a new entry in the config and specify the cmd. It's also possible to customize the layout. More details can be found here in the doc: https://github.com/git/git/blob/master/Documentation/mergetools/vimdiff.txt

Thank you @gmmoreira. TL;DR, the simplest config for using Neovim as the diff and merge tool is:

git config --global diff.tool nvimdiff
git config --global merge.tool nvimdiff

If for some reason the window layout is not the classic one, it can be reconfigured by:

git config --global difftool.nvimdiff.layout "LOCAL,REMOTE"
git config --global mergetool.nvimdiff.layout "LOCAL,BASE,REMOTE / MERGED"

@GitMurf
Copy link

GitMurf commented Feb 25, 2024

git config --global difftool.nvimdiff.layout "LOCAL,REMOTE"
git config --global mergetool.nvimdiff.layout "LOCAL,BASE,REMOTE / MERGED"

fyi for anyone else who stumbles across this gist, the key for me was that for neovim I could NOT use mergetool.nvimdiff.layout and had to use mergetool.vimdiff.layout instead even though it is using neovim. Then my custom layout would work. Below is my config for diff/merge items in gitconfig that I was able to get working.

[diff]
  tool = nvimdiff
  guitool = nvimdiff
[difftool]
  prompt = false
  guiDefault = false
[merge]
  tool = nvimdiff
  guitool = nvimdiff
[mergetool]
  prompt = false
  guiDefault = false
  keepBackup = false

# NOTE: must set "vimdiff" here and NOT "nvimdiff" for custom layout to work
# But just for this [mergetool "vimdiff"] section.
# The other [merge] and [mergetool] sections above MUST be set to "nvimdiff"
[mergetool "vimdiff"]
  layout = (LOCAL,BASE,REMOTE) / MERGED + (LOCAL,MERGED,REMOTE) + LOCAL,REMOTE + (LOCAL,MERGED) / (REMOTE,MERGED) + (BASE,LOCAL) / (BASE,REMOTE)

@harrydt
Copy link

harrydt commented May 30, 2024

To setup local keymappings for nvim

-- Check if diff mode is enabled
if vim.opt.diff:get() then
	-- Define key mappings for diff mode
	vim.api.nvim_set_keymap("n", "<localleader>1", ":diffget LOCAL<CR>", { noremap = true, silent = true })
	vim.api.nvim_set_keymap("n", "<localleader>2", ":diffget BASE<CR>", { noremap = true, silent = true })
	vim.api.nvim_set_keymap("n", "<localleader>3", ":diffget REMOTE<CR>", { noremap = true, silent = true })
end

If you have which-key.nvim, you can also do

vim.api.nvim_create_autocmd("BufEnter", {
	pattern = "*",
	callback = function()
		if vim.opt.diff:get() then
			local wk = require("which-key")
			local opts = {
				mode = "n", -- NORMAL mode
				buffer = vim.api.nvim_get_current_buf(), -- Specify a buffer number for buffer local mappings
			}
			local mappings = {
				["<localleader>"] = {
					["1"] = { ":diffget LOCAL<CR>", "Get LOCAL" },
					["2"] = { ":diffget BASE<CR>", "Get BASE" },
					["3"] = { ":diffget REMOTE<CR>", "Get REMOTE" },
				},
			}

			wk.register(mappings, opts)
		end
	end,
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment