← Vim研究所

git mergetoolでvimdiffを使う コンフリクト解消の4画面と:diffget

2026-10-10 公開

git mergeでコンフリクトが出たあと、git mergetool --tool=vimdiffを打つとVimが4分割で開きます。上に3つ、下に1つ。下の窓でとりあえずdoを押すと、E101: More than two buffers in diff mode, don't know which one to useと返されて何も起きません。ふだん2ファイルの差分で使っている操作が、ここでは効きません。

4つの窓が何を表示しているのか、下の窓で何を打てば片付くのかを、小さなリポジトリでコンフリクトを起こして確かめました。終わり方を間違えると、マーカーが残ったまま「解消済み」になる落とし穴もありました。

検証環境:git 2.42.0、Vim 9.1.1752、NVIM v0.12.4(macOS)。一時ディレクトリに作ったリポジトリで2つのブランチが同じ行を書き換え、git mergeでコンフリクトを2か所起こしてから、tmuxの中でgit mergetoolを起動して操作しました。設定ファイル(~/.gitconfigや~/.vimrc)は読ませていません。掲載している画面とメッセージはすべて実行結果です。

目次(10項目)
操作これだけ覚える
:diffget LO自分のブランチ側(LOCAL)を取り込む
:diffget REMOTEマージしてきた側(REMOTE)を取り込む
]c [c次・前のコンフリクトへ移る
:wqa保存して解消済みにする
:cqやめる。未解決のまま残る

git mergetoolでvimdiffを使う設定

一度だけ試すなら、ツール名を引数で渡します。

git mergetool --tool=vimdiff

毎回使うならmerge.toolに登録しておくと、git mergetoolだけで同じものが開きます。

git config --global merge.tool vimdiff

どちらも設定していないと、gitは手元のツールから候補を選び、確認を挟んでから起動します。

'git mergetool' will now attempt to use one of the following tools:
tortoisemerge emerge vimdiff nvimdiff
Merging:
app.conf

Normal merge conflict for 'app.conf':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (vimdiff):

--toolかmerge.toolで明示したときは、この確認を挟まずにすぐVimが開きました。使えるツール名の一覧はgit mergetool --tool-helpで出せます。Vim系だけ抜き出すと次の12個で、末尾の数字は画面の並びの違いです(後の節で扱います)。

nvimdiff         Use Neovim with a custom layout (see `git help mergetool`'s `BACKEND SPECIFIC HINTS` section)
nvimdiff1        Use Neovim with a 2 panes layout (LOCAL and REMOTE)
nvimdiff2        Use Neovim with a 3 panes layout (LOCAL, MERGED and REMOTE)
nvimdiff3        Use Neovim where only the MERGED file is shown
vimdiff          Use Vim with a custom layout (see `git help mergetool`'s `BACKEND SPECIFIC HINTS` section)
vimdiff1         Use Vim with a 2 panes layout (LOCAL and REMOTE)
vimdiff2         Use Vim with a 3 panes layout (LOCAL, MERGED and REMOTE)
vimdiff3         Use Vim where only the MERGED file is shown
gvimdiff         Use gVim (requires a graphical session) with a custom layout (...)
gvimdiff1        Use gVim (requires a graphical session) with a 2 panes layout (LOCAL and REMOTE)
gvimdiff2        Use gVim (requires a graphical session) with a 3 panes layout (LOCAL, MERGED and REMOTE)
gvimdiff3        Use gVim (requires a graphical session) where only the MERGED file is shown

開く4画面の意味 LOCAL・BASE・REMOTE・MERGED

検証に使ったのは、mainでcolorを"red"・sizeを12に、featureブランチでcolorを"green"・sizeを14に変えた設定ファイルです。mainにいる状態でgit merge featureすると2か所がぶつかります。この状態でgit mergetool --tool=vimdiffを打ったときの画面がこれです(幅を78桁にして撮っています)。

  title = "draft"        |  title = "draft"         |  title = "draft"
  color = "red"          |  color = "blue"          |  color = "green"
  -----------------------|  ------------------------|  -----------------------
  -----------------------|  ------------------------|  -----------------------
  font = "mono"          |  font = "mono"           |  font = "mono"
  lang = "ja"            |  lang = "ja"             |  lang = "ja"
  wrap = true            |  wrap = true             |  wrap = true
  theme = "dark"         |  theme = "dark"          |  theme = "dark"
  size = 12              |  size = 10               |  size = 14
  -----------------------|  ------------------------|  -----------------------
./app_LOCAL_29858.conf    ./app_BASE_29858.conf      ./app_REMOTE_29858.conf
  title = "draft"
  <<<<<<< HEAD
  color = "red"
  =======
  color = "green"
  >>>>>>> feature
  font = "mono"
  lang = "ja"
  wrap = true
  theme = "dark"
  <<<<<<< HEAD
  size = 12
  =======
  size = 14
  >>>>>>> feature
app.conf

上段の区切り線(----の行)は一部省いています。4つの窓の役割は次のとおりです。

窓中身この例では
LOCAL(左上)マージを受け入れる側。いまいるブランチmainの"red"
BASE(中上)2つのブランチが分かれる前の共通の祖先元の"blue"
REMOTE(右上)マージしてくる側featureの"green"
MERGED(下)作業ツリーの本物のファイル。ここを直して保存するマーカー入りのapp.conf

上段の3つはgitが一時的に書き出したファイルで、名前は元の名前_LOCAL_数字.拡張子の形になります。数字は起動のたびに変わりました。置き場所は作業ツリーの同じディレクトリで、mergetoolを抜けると消えます。カーソルは最初から下のMERGEDにあります。窓の移動はウインドウ間をカーソル移動するCtrl-wの操作がそのまま使えます。

BASEは判断の材料になります。この例ではBASEが"blue"なので、両方のブランチが別々に色を変えたと分かります。片方だけが変えた行ならgitが自動で取り込むので、コンフリクトになるのは両方が触った行だけです。

MERGEDでdoが効かない理由(E101)と:diffget LO / RE

MERGEDの最初のコンフリクトにカーソルを置いて、素の:diffgetを打った結果です。doでも同じでした。

E101: More than two buffers in diff mode, don't know which one to use

:diffgetは「差分の相手から取ってくる」コマンドで、相手が1つしかないときは引数を省けます。2ファイルの差分を:diffget / :diffputでマージするときに引数なしで済むのはこのためです。4画面ではdiffモードのバッファが4つあり、MERGEDから見た相手はLOCAL・BASE・REMOTEの3つです。どれから取るかをVimが決められないので、E101で止まります。

引数にはバッファの名前の一部を渡せます。上段の3つは名前にLOCAL・BASE・REMOTEを含むので、そこを切り出して指定します。

:diffget LO
:diffget REMOTE
:diffget BA

最初のコンフリクトで:diffget LOを打つと、マーカーを含む5行がcolor = "red"の1行に置き換わりました。マーカーの消し忘れを手で拾う必要はありません。:lsに出るバッファ番号でも指定でき、この例では:diffget 3がREMOTEを指しました。

ここで1つ引っかかりました。:diffget REと2文字で打ったところ、E93: More than one match for REで失敗したのです。原因は名前の照合のしかたにあります。:diffgetの引数はバッファ名に対するパターンとして扱われ、フルパスにも当てられます。検証用のリポジトリをrepoというディレクトリに作っていたので、どのバッファのパスにもreが含まれていました。macOSのVimは'fileignorecase'がオンなので大文字小文字も区別しません。ディレクトリ名をprojに変えて作り直すと、同じ:diffget REがREMOTEを取り込みました。

パスしだいで当たり外れが出るので、手元では:diffget REMOTEのように省略せずに打つことにしました。

コンフリクト間の移動と、両方残したいときの手直し

次のコンフリクトへは]c、前へは[cで移ります。この例では1行目から]cで2行目(1つ目のマーカー)、もう一度で11行目(2つ目のマーカー)に飛び、[cで2行目に戻りました。:diffgetで片付けた箇所は差分でなくなるので、]cと:diffgetを交互に打てば上から順に処理できます。

迷うのは「両方の変更を残したい」ときです。:diffgetはどちらか一方で丸ごと置き換えるので、両方を並べる用途には使えません。この場合はMERGEDのマーカー行を消すのがいちばん早く、<<<<<<<・=======・>>>>>>>の3行をddで落とせば両側の行が残ります。範囲を決めて一度に消すなら次の形です(2〜6行目が1つ目のコンフリクトの場合)。

:2,6g/^\(<<<<<<<\|=======\|>>>>>>>\)/d

これでcolor = "red"とcolor = "green"の2行が残りました。設定ファイルのように同じキーが2回あると壊れるものでは、残したあとにどちらかを手で直します。

画面を減らす vimdiff1〜3 と mergetool.vimdiff.layout

4画面が広すぎるなら、並びを変えられます。--tool=vimdiff1〜vimdiff3は決まった並びの省略形で、git help mergetoolの「BACKEND SPECIFIC HINTS」節によると、mergetool.vimdiff.layoutに次の値を入れたのと同じです。

ツール名layout の値実際に開いた画面
vimdiff1@LOCAL, REMOTELOCALとREMOTEの2つ。カーソルはREMOTE
vimdiff2LOCAL, MERGED, REMOTE3列。カーソルはREMOTE
vimdiff3MERGEDMERGEDだけ。ほかは隠れバッファ

画面を減らすとdoがそのまま効くか、という点は並びによって違いました。vimdiff2は窓が3つでdiffモードのバッファも3つなので、真ん中のMERGEDに移ってdoを押してもE101のままです。しかも開いた直後のカーソルは右のREMOTEにありました。

vimdiff1は事情が違います。@LOCALの@は「保存したものを結果として使う窓」を指す印で、MERGEDの代わりにLOCALを直して保存します。カーソルは右のREMOTEにあるので、Ctrl-w hでLOCALへ移り、]cとdoを2回ずつ打って:wqaで抜けたところ、app.confはREMOTE側の"green"と14になりました。相手が1つなので引数なしのdoで済みます。REMOTEの窓のまま押すと逆向きに取り込むので、移動を忘れないようにします。

並びは自分で書くこともできます。BASEを外してMERGEDとLOCALだけにする例です。

git config --global mergetool.vimdiff.layout "LOCAL,MERGED"

,が縦分割、/が横分割、+がタブの区切りです。この2画面ではMERGEDでdoがそのまま効きました。ただし、マーカー付きのファイルとLOCALの比較になるので、差分の単位が細かくなります。1つ目のコンフリクトは<<<<<<< HEADの行と、=======から>>>>>>> featureまでの3行という2つの差分に分かれ、do1回では片方しか消えませんでした。コンフリクト2つを片付けるのに]cとdoを4回ずつ、または:%diffgetの1回が必要でした。

終わり方 :wqaで解消、:cqで中断

MERGEDを直したら:wqaで全部の窓を閉じます。gitはVimの終了コードを見て、0なら解消済みとしてgit addまで済ませます。

$ git status --short
M  app.conf
?? app.conf.orig

途中でやめたいときは:cqです。Vimが終了コード1で抜けるので、gitは失敗として扱い、ファイルは未解決のまま残ります。

merge of app.conf failed
$ git status --short
UU app.conf

注意が要るのはここからです。gitがVimの終了コードを信用するということは、何も直さずに:wqaや:qaで抜けても、終了コードは0なので解消済みになります。実際にマーカーを1つも消さずに:qaで抜けたところ、app.confはM(ステージ済み)になり、中身はマーカーが入ったままでした。直さずに抜けるときは必ず:cqを使います。

うっかり解消済みにしてしまった場合は、コミットする前なら取り返せます。git diff --cached --checkで残ったマーカーを見つけ、git checkout -mでコンフリクトの状態に戻します。

$ git diff --cached --check
app.conf:2: leftover conflict marker
app.conf:4: leftover conflict marker
app.conf:6: leftover conflict marker
app.conf:11: leftover conflict marker
app.conf:13: leftover conflict marker
app.conf:15: leftover conflict marker
$ git checkout -m -- app.conf
$ git status --short
UU app.conf

diff3 / zdiff3 と .origを残さない設定

MERGEDのマーカーには、既定ではLOCALとREMOTEの2段しか入りません。merge.conflictstyleをdiff3にすると、|||||||の段にBASEの内容が加わります。

git config --global merge.conflictstyle diff3
<<<<<<< HEAD
color = "red"
||||||| 653d62b
color = "blue"
=======
color = "green"
>>>>>>> feature

上段のBASEの窓と同じ情報がMERGEDの中にも並ぶので、2画面やvimdiff3のようにBASEを表示しない並びと組み合わせると効きます。git 2.35以降のzdiff3は、両側に共通する行をマーカーの外へ出す点が違います。両方のブランチがmode = 1という同じ行を足した例で比べると、diff3ではmode = 1がLOCALとREMOTEの段の両方に入ったのに対し、zdiff3ではマーカーの手前に1回だけ出ました。この記事の主な例のように共通の行が無い場合は、どちらも同じ出力です。

もう1つ、:wqaで抜けるたびにapp.conf.origが残ります。マーカー入りの状態を控えたバックアップで、コミットのときに混ざりやすいので、要らなければ止めます。

git config --global mergetool.keepBackup false

この設定を入れて(試したのは次の節のnvimdiffです)同じ手順を繰り返すと、解消後の作業ツリーにはapp.confだけが残りました。なお:cqで中断した回は、設定に関係なく.origは作られていませんでした。

Neovimで使う nvimdiff

Neovimなら--tool=nvimdiffです。並びの設定名はmergetool.nvimdiff.layoutに変わりますが、4画面の構成、素の:diffgetでE101が出ること、:wqaと:cqの扱いはVimと同じでした。

1つだけ挙動が違いました。NVIM v0.12.4で1つ目のコンフリクトに:diffget LOを打つと、5行が1行に置き換わらず、<<<<<<< HEADの行だけが消えて残りの4行はそのまま残りました。原因は'diffopt'の既定値の違いです。

Vim 9.1.1752: diffopt=filler,closeoff,inline:simple
NVIM v0.12.4: diffopt=internal,filler,closeoff,indent-heuristic,inline:char,linematch:40

Neovimの既定にはlinematch:40が入っていて、差分のかたまりを行どうしで対応づけて細かく分けます。上段の窓でも、Neovimでは"red"と"green"が別々の行に並んで見えました。:diffgetが一度に取る範囲もそのぶん細かくなります。手元で効いたのは次の2つです。

" マージのあいだだけ linematch を外す
:set diffopt-=linematch:40
" 範囲を明示して取り込む(2〜6行目が1つ目のコンフリクト)
:2,6diffget LO

前者を打ってから]cと:diffget LOを使うと、Vimと同じく1回でコンフリクト1つが片付きました。Neovim標準の:DiffToolなどふだんの差分表示ではlinematchが見やすいので、外すのはマージのときだけにしています。

使ってみて

以前はgit mergetoolでVimが4分割になった時点で:qaして、エディタで直接マーカーを消していました。終了コードの扱いを調べて、その:qaで「解消済み」にしていた可能性に気づき、冷や汗が出ました。以来git diff --cached --checkをコミット前に打っています。もう1つ予想外だったのが:diffget REのE93で、最初は自分の打ち間違いを疑い、:lsとbufname()を何度も見比べてやっとrepoというディレクトリ名のせいだと分かりました。

まとめ

git mergetool --tool=vimdiffの4画面では、下のMERGEDが本物のファイルで、上の3つは参照用です。相手が3つあるので素のdoはE101で止まり、:diffget LOCALや:diffget REMOTEで取り込み元を名前で指定します。

引数なしのdoで済ませたいなら、vimdiff1かmergetool.vimdiff.layoutで窓を2つにします。終わるときは、直したら:wqa、やめるなら:cq。直さずに:qaで抜けても解消済みになってしまう点だけは覚えておくと、マーカー入りのコミットを防げます。2ファイルの差分の見方そのものはvimdiffで差分を表示する記事で扱っています。

関連記事

← Vim研究所 トップへ戻る