How do I use vimdiff to resolve a git merge conflict?

88,733

Solution 1

All four buffers provide a different view of the same file. The top left buffer (LOCAL) is how the file looked in your target branch (what you are merging into). The top right buffer (REMOTE) is how the file looked in your source branch (where you are merging from). The middle buffer (BASE) is the common ancestor of the two (so you can compare how the left and right versions have diverged from each other).

I may be mistaken on the following point. I think the source of the merge conflict is that both files have changed the same portion of the file since BASE; LOCAL has changed the quotes from double to single, and REMOTE has made the same change but also changed the background value from a color to a URL. (I think the merge is not smart enough to notice that all the changes to LOCAL are also present in REMOTE; it just knows that LOCAL has made changes since BASE in the same places that REMOTE has).

In any case, the bottom buffer contains the file you can actually edit—the one sitting in your working directory. You can make any changes you like; vim is showing you how it differs from each of the top views, which are the areas that the automatic merge couldn't not handle. Pull changes from LOCAL if you don't want the REMOTE changes. Pull changes from REMOTE if you prefer those to the LOCAL changes. Pull from BASE if you think both REMOTE and LOCAL are wrong. Do something completely different if you have a better idea! In the end, the changes you make here are the ones that will actually be committed.

Solution 2

@chepner's answer is great, I would like to add some details on "how should I proceed to fix the merge conflict" part of the question. If you look into how to actually use vimdiff in this case, it goes below.


First, to address the "abort everything" option - if you do not want to use "vimdiff" and want to abort the merge: press Esc, then type :qa! and hit Enter. (see also How do I exit the Vim editor?). Git will ask you if the merge was complete, reply with n.


If you want to use vimdiff, here are some useful shortcuts. This assumes you know basics of Vim (navigation and insert/normal mode):

  • navigate to the bottom buffer (merge result): Ctrl-W j
  • navigate to next diff with j/k; or, better, use ] c and [ c to navigate to the next and previous diff respectively
  • use z o while on a fold to open it, if you want to see more context
  • for each diff, as per @chepner's answer, you can either get the code from a local, remote or base version, or edit it and redo as you see fit
    • to get it from the local version, use :diffget LO
    • from remote: :diffget RE
    • from base: :diffget BA
    • or, if you want to edit code yourself, get a version from local/remote/base first, and then go to the insert mode and edit the rest
  • once done, save the merge result, and quit all windows :wqa
  • normally, git detects that the merge was made and creates the merge commit

It does not appear to be possible to add both local and remote conflict hunks without copy pasting or custom shortcuts: https://vi.stackexchange.com/questions/10534/is-there-a-way-to-take-both-when-using-vim-as-merge-tool which is a shame since add add is such a common conflict type.

To prevent vimdiff from asking you to press enter every time it starts, add to your .vimrc:

set shortmess=Ot

as mentioned at: https://vi.stackexchange.com/questions/771/how-can-i-suppress-the-press-enter-prompt-when-opening-files-in-diff-mode

You can search the Internet for other vimdiff shortcuts. I have found this one useful: https://gist.github.com/hyamamoto/7783966

Solution 3

The ultimate mergetool to replace vimdiff

This is kind of tongue-in-cheek, but it is what I ended up converging to as a vimmer after trying vimdiff.

To resolve a merge conflict, what I almost always need is to see:

  • REMOTE
  • LOCAL
  • two diffs:
    • diff BASE REMOTE
    • diff BASE LOCAL

to then try to put both of them together.

While vimdiff does show BASE, LOCAL and REMOTE in the screen:

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

I don't know how to make it clearly show those two diffs that I need besides by looking right left right left a bunch of times.

Furthermore, LOCAL and REMOTE are already visible in the git merge conflict markers, so I don't gain that much from a tool that shows them again.

Therefore, I instead created my own tiny "difftool" that actually shows the diffs that I was missing:

~/bin/cirosantilli-mergetool

#!/usr/bin/env bash
BASE="$1"
LOCAL="$2"
REMOTE="$3"
diff --color -u "$BASE" "$LOCAL"
diff --color -u "$BASE" "$REMOTE"
exit 1

GitHub upstream.

And install it with:

git config --global mergetool.cirosantilli-mergetool.cmd 'cirosantilli-mergetool $BASE $LOCAL $REMOTE'
git config --global mergetool.cirosantilli-mergetool.trustExitCode true
# If you want this to become your default mergetool.
#git config --global merge.tool 'cirosantilli-mergetool'

Now, when you do:

git mergetool -t cirosantilli-mergetool

it shows the two diffs that I want on the terminal, e.g. something along:

--- ./src/dev/arm/RealView_BASE_15560.py        2019-12-27 13:46:41.967021591 +0000
+++ ./src/dev/arm/RealView_LOCAL_15560.py       2019-12-27 13:46:41.979021479 +0000
@@ -994,7 +994,7 @@                                                              
                                       
     def setupBootLoader(self, cur_sys, loc):
         if not cur_sys.boot_loader:                           
-            cur_sys.boot_loader = [ loc('boot_emm.arm64'), loc('boot_emm.arm') ]
+            cur_sys.boot_loader = [ loc('boot.arm64'), loc('boot.arm') ]
         cur_sys.atags_addr = 0x8000000                  
         cur_sys.load_offset = 0x80000000                    

@@ -1054,7 +1054,7 @@                                           
             ]                                                     
                       
     def setupBootLoader(self, cur_sys, loc):
-        cur_sys.boot_loader = [ loc('boot_emm_v2.arm64') ]
+        cur_sys.boot_loader = [ loc('boot_v2.arm64') ]
         super(VExpress_GEM5_V2_Base,self).setupBootLoader(
                 cur_sys, loc)                             
                                                           
--- ./src/dev/arm/RealView_BASE_15560.py        2019-12-27 13:46:41.967021591 +0000
+++ ./src/dev/arm/RealView_REMOTE_15560.py      2019-12-27 13:46:41.991021366 +0000
@@ -610,10 +610,10 @@           
     def attachIO(self, *args, **kwargs):              
         self._attach_io(self._off_chip_devices(), *args, **kwargs)
                                      
-    def setupBootLoader(self, cur_sys, loc):
-        cur_sys.boot_loader = loc('boot.arm') 
-        cur_sys.atags_addr = 0x100                           
-        cur_sys.load_offset = 0       
+    def setupBootLoader(self, cur_sys, boot_loader, atags_addr, load_offset):
+        cur_sys.boot_loader = boot_loader      
+        cur_sys.atags_addr = atags_addr     
+        cur_sys.load_offset = load_offset

So you can see here the two diffs dumped into the terminal:

  • RealView_BASE_15560.py vs RealView_LOCAL_15560.py
  • RealView_BASE_15560.py vs RealView_REMOTE_15560.py

If the diffs are big, I'll just search up with my tmux superpowers.

TODO: to achieve Nirvana the last thing left would be a way to show only diffs for the conflicting hunk. Because if the diffs are large but only a small hunk conflicts, it is annoying to find it.

Yes, you you do lose some shortcuts that vimdiff provides, but in general solving conflicts requires careful copy paste from both versions, which I can do fine inside a normal vim session with the git conflict markers.

Observing and diffing files while vimdiff is running

Before I sat down and automated my perfect setup with cirosantilli-mergetool, this is what I was doing to get the two diffs I needed.

While git mergetool is running vimdiff, if there is a conflict on a file named, say, main.py, git generates files for each of the versions, named as:

main_BASE_1367.py
main_LOCAL_1367.py
main_REMOTE_1367.py

in the same directory as main.py where 1367 is the PID of git mergetool, and therefore a "random" integer, as mentioned at: In a git merge conflict, what are the BACKUP, BASE, LOCAL, and REMOTE files that are generated?

So, to see the diffs that I want, I first find the generated files with git status, and then open new terminals and do a vimdiff between the pairs of files that I care about:

vim -d main_BASE_1367.py main_LOCAL_1367.py
vim -d main_BASE_1367.py main_REMOTE_1367.py

Together with git mergetool, this information helps A LOT to figure out what is going on quickly!

Also, even while mergetool is running, you can just open the file:

vim main.py

directly and edit it there if you feel that it is going to be easier with a larger editor window.

Jump directly to merge conflicts

While ]c jumps to the next diff point inside vimdiff, there isn't always a merge conflict there.

To help with this, I have in my ~/.vimrc:

# Git Merge conflict
nnoremap <leader>gm /\v^\<\<\<\<\<\<\< \|\=\=\=\=\=\=\=$\|\>\>\>\>\>\>\> /<cr>

which finds the conflicts directly.

git imerge

Maybe the best option is to just give up on using vimdiff and rely on regular vim + git imerge which was mentioned at: How can I find out which Git commits cause conflicts? since vimdiff's learning curve is annoying, and it doesn't do the functions we need the most.

Share:
88,733

Related videos on Youtube

Cool Guy Yo
Author by

Cool Guy Yo

reading, cooking, gardening, and making websites is what I like to do.

Updated on July 08, 2022

Comments

  • Cool Guy Yo
    Cool Guy Yo almost 2 years

    I just merged a branch into my master in git and I got Automatic merge failed; fix conflicts and then commit the result. Now I ran git mergetool and vimdiff opened with the image below. I don't know how to use vimdiff. What does each panel here mean and how should I proceed to fix the merge conflict?

    enter image description here

  • Cool Guy Yo
    Cool Guy Yo about 11 years
    Quick question how do I save in vim?
  • Jonathan Leffler
    Jonathan Leffler about 11 years
    :x or :w (:x exits too) plus 'return'.
  • chepner
    chepner about 11 years
    Anders: there are other merge tools you can use if you are not familiar with how to use vim.
  • Cool Guy Yo
    Cool Guy Yo about 11 years
    @chepner is there one you reccomend?
  • romainl
    romainl about 11 years
    @AndersKitson, since you are on Mac OS X, FileMerge is perfect, free and comes with XCode.
  • chepner
    chepner about 11 years
    Why the downvote? If there is something factually incorrect, please fix it, or at least point it out.
  • Danny
    Danny about 11 years
    Can FileMerge be started off the command line with git, @romainl? If so, how?
  • romainl
    romainl about 11 years
    Do $ git mergetool -t opendiff but I'm not in front of my Mac right now so I can't explain from memory. I think that I use a wrapper shell script.
  • Jayen
    Jayen about 10 years
    i normally use kdiff3 and the view is similar, so i understand it, but over a remote connection, when i try to use vimdiff, i have no idea what the shortcut keys are
  • user2441441
    user2441441 over 7 years
    Quick question, is there a way to 'pull from remote, local or base' without manually editing the bottom window and re-typing in it all over?
  • chepner
    chepner over 7 years
    I think there is an option for git merge that selects one branch or the other to resolve all conflicts.
  • Julien Lamarche
    Julien Lamarche over 6 years
    The comment would be more usefull with key bindings. What I have found so far elsewhere is do and dp, but that's only usefull with two window pane.
  • Andrey Portnoy
    Andrey Portnoy about 6 years
    This should be upvoted x1000 times and accepted as a better answer.
  • Apit John Ismail
    Apit John Ismail about 5 years
    to quick jump to next conflict, just do search for === . do " /===" and enter
  • VonC
    VonC almost 5 years
    Upvoted. I think I mentioned that 9 years ago in stackoverflow.com/a/3052118/6309. (see the last part of the answer)
  • Ciro Santilli OurBigBook.com
    Ciro Santilli OurBigBook.com almost 5 years
    @VonC yes, I think you won this one! XD
  • Jason
    Jason almost 4 years
    See this post (stackoverflow.com/questions/51520705/…) if more than one match found using :diffget.
  • Arthur Bowers
    Arthur Bowers over 2 years
    My vimdiff does not show the MERGED pane...only the 3 at the top. How can i get it to show the merged pane?