Git の mergetool で vimdiff を指定して Vim でマージコンフリクトを解決してみた


マージコンフリクトを解決する時に, git mergetool でいくつかあるうちの中かから一つのツールを指定することができますが, 今回は vimdiff を指定してみました. そしたらとても便利でした. Vimmer の方必見でござるよ.

マージコンフリクトを意図的に作る

まず git mergetool を使うためには, マージコンフリクトが発生している必要がありますので, 次のようなマージコンフリクトを意図的に作ってみました.

ヒストリは次のようになります:

            D <- hotfix
           /
          /
    A -- B --- C <- master <- HEAD

そして A から D までのコミットで example.txt を次のように編集してみました.

Aexample.txt:

Alfa

Bexample.txt:

Alfa
Bravo

Cexample.txt:

Alpha & Bravo
Chirlie

Dexample.txt:

Alfa
Bravo
Delta

そして次のコマンドで hotfix ブランチを master ブランチにマージさせると:

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

このように example.txt にマージコンフリクトが発生し, example.txt は次のようになります:

<<<<<<< HEAD
Alfa & Bravo
Charlie
||||||| merged common ancestors
Alfa
Bravo
=======
Alfa
Bravo
Delta
>>>>>>> hotfix

このように, 競合しているところに <<<<<<<, =======, >>>>>>> というマーカに加えて, 僕は merge.conflictStyle の値を diff3 に設定しているので ||||||| という四種類のマーカが表示されます.

||||||| というマーカによって, CD の共通祖先 Bexample.txt の内容を知ることができます. この場合は一行目が Alfa で二行目が Bravo だったと |||||||======= の間の二行からわかります.

より詳しく ||||||| についてお知りになりたい方は, 前回の記事 “Git の merge.conflictStyle を merge から diff3 に変更したら, マージコンフリクトがより解決しやすくなった” をよろしければ参考にしていただければと思います.

という感じで, example.txt のファイルを直接開くと, そのようなマーカが書き加えられているので, それを頼りにマージコンフリクトを解決することができます.

vimdiff でマージコンフリクトを解決する

今度は git mergetool というマージコンフリクトを解決するプログラムを起動するコマンドで vimdiff を指定して先ほどの example.txt のマージコンフリクトを解決してみたいと思います.

まだ先ほどのマージコンフリクトが残っている状態で:

$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)

        both modified:   example.txt

no changes added to commit (use "git add" and/or "git commit -a")

次のコマンドで, git mergetool を通して vimdiff を起動させます:

$ git mergetool -t vimdiff
Merging:
example.txt

Normal merge conflict for 'example.txt':
  {local}: modified file
  {remote}: modified file
4 files to edit

もし -t vimdiff を省略したい場合, 僕も設定しているのですが, 次の設定をすると git mergetool と入力するだけで vimdiff を起動できるようになります:

git config --global merge.tool vimdiff

そして次のような画面が表示されます:

Git mergetool vimdiff

画面上半分の左からローカルの C, 真ん中がベース (共通祖先) の B, 右がリモートの D となり, 画面下半分がマージコンフリクト解決中の example.txt となります.

つまりは, 上半分の 3 つを参考にして, 下半分の example.txt のマージコンフリクトを解決することができます.

このようにそれら 3 点のコミットの example.txt の内容を直接見ることができるので, どのようにマージコンフリクトを解決するかという上でとても参考になります.

ちなみにどうして 3 点なのかというのは, Git のマージが 1 つのブランチを HEAD にマージさせる場合, 3-way マージとなり, マージするブランチの先端 2 つ (C, D) と, その 2 つの先端の共通祖先 (B) の計 3 点のコミットが使われるためです.

ちなみに上半分の 3 つのファイルのそれぞれの 40 桁のチェックサム (SHA-1) は, マージコンフリクト発生中に次のコマンドで表示させることができます:

$ git ls-files -u
100644 90f9b648f654153a5f42619b8d2404f79b7122ca 1       example.txt
100644 ccbb6e7de8e70b118e4f663c836fdd800f7c0d35 2       example.txt
100644 807eec38ba1be39da929b95d0d6179fc82778915 3       example.txt

一行目がベースで, 二行目がローカルで, 三行目がリモートのファイルになります.

それぞれのコミット内容を直接みる場合は, 次のコマンドで見ることができます:

$ git show :1:example.txt
Alfa
Bravo

$ git show :2:example.txt
Alfa & Bravo
Charlie

$ git show :3:example.txt
Alfa
Bravo
Delta

ローカルとリモートは次のコマンドでも見ることができます:

$ git show HEAD
$ git show MERGE_HEAD

Vim の diff モードで使えるコマンド

で先ほどの vimdiff の画面に戻りますが, vimdiff は Vim の diff モードなので, マージコンフリクトを解決する時に便利なコマンドを使うことができます.

画面下半分の example.txt の競合部分にカーソルを合わせている状態で:

Git mergetool vimdiff

1do と打つとローカルの内容をその部分に持ってこれます:

Vimdiff 1do

2do と打つとベースの内容をその部分に持ってこれます:

Vimdiff 2do

3do と打つとリモートの内容をその部分に持ってこれます:

Vimdiff 3do

1do, 2do, 3do のそれぞれは :diffget LOCAL, :diffget BASE, :diffget REMOTE というコマンドでも同等のことができます.

というのも git mergetool -t vimdiff による Vim の起動中に次のような一時的なファイルが作られるためです:

$ git status -s
UU example.txt
?? example_BACKUP_71903.txt
?? example_BASE_71903.txt
?? example_LOCAL_71903.txt
?? example_REMOTE_71903.txt

そして先ほどのコマンドに戻りますと, もしくはローカルの競合部分にカーソルを移動させ:

Vimdiff local matching cursor

4dp でそのローカルの内容を下半分の example.txt に持ってこれます:

Vimdiff local 4dp

ベースの競合部分にカーソルを移動させ:

Vimdiff base matching cursor

ベースの内容も 4dp で持ってこれます:

Vimdiff base 4dp

リモートの競合部分にカーソルを移動させ:

Vimdiff remote matching cursor

リモートの内容も 4dp で持ってこれます:

Vimdiff remote 4dp

2 画面分割の diff モードだったら do, dp と数字を付けずにコマンドを打つことができるのですが, 4 画面分割なので 1, 2, 3, 4 という数字を使ってバッファを指定する必要があります.

あと [c]c を使って, 前後の競合箇所にカーソルを移動させることもできます.

今回の場合は, 競合が発生している箇所が一箇所のみなので, [c]c の使用をお見せすることができないのですが sweat, 1 つのファイル内で競合が二箇所以上発生している時にカーソル操作が楽になります.

Vim の diff モードに関するヘルプは, コマンドラインモードより :help diff.txt で見ることができます.

そして例えば次のようにマージコンフリクトを解決し終えたら:

Vimdiff resolve merge conflict

画面下半分の example.txt:w で保存し, 4 画面全てを :qa で終了させたら, 自動的に example.txt がステージされるので git add example.txt と入力する必要はありません:

$ git status
On branch master
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:

    modified:   example.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    example.txt.orig

あと Vim を終了させると, この場合は example.txt.orig というファイルですが, .orig という拡張子が付いたマージコンフリクト発生直後のファイルがバックアップ目的で作られるので, もう必要ない場合次のコマンドで削除します:

$ git clean -f
Removing example.txt.orig

ただこのコマンドは Git にトラックされていない全てのファイルを削除するので, 心配な方は次のように厳密に削除されることをオススメします:

rm example.txt.orig

もし git mergetool -t vimdiff で Vim を起動させて, Vim を終了させた後, その .orig ファイルを自動で作りたくない場合, 一例として次のコマンドでそのように設定できます:

git config --global mergetool.keepBackup false

あとは git commit でマージコミットを作るだけとなります:

Merge branch 'hotfix'

# Conflicts:
#   example.txt
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
#   .git/MERGE_HEAD
# and try again.


# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch master
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
#   modified:   example.txt
#

コミットメッセージの確認ができたら, ZZ でコミットして, マージが完了します.

まとめ

Vim をメインのテキストエディタとして使われている方は, ぜひ Git のマージコンフリクトを解決する際は git mergetool -t vimdiff より vimdiff を使われてみてはどうでしょうか.

Vim のポテンシャルを最大限発揮してマージコンフリクトを解決することができるので, Vim に慣れ親しまれている方はより快適にマージコンフリクトを解決することができます sparkle

僕は Vim が好きなので git mergetool -t vimdiff によって起動される Vim の diff モードでマージコンフリクトを解決できると知ってから, そのコマンドを使って解決しています slightly_smiling_face (-t vimdiff は省略していますが)

参考資料