CH 23 / 75第4部 ブランチとマージ

マージの仕組み(Fast-forward と3-way)

このページで分かること

  • git merge でほかのブランチの変更を取り込める
  • Fast-forward と 3-way マージの違いを、履歴の形で説明できる
  • マージベースとマージコミット(親が2つのコミット)が分かる
  • git branch --merged で取り込み済みのブランチを確かめて片付けられる
このページの目次9 items

22で、クッキーのレシピを recipe-cookie ブランチに記録しました。このページでは、それを main に取り込みます。取り込む操作がマージ(merge、合流させる)です。マージには Fast-forward と 3-way の2つの形があり、どちらになるかは履歴の形で決まります。

マージの基本:取り込みたい側に立って git merge#

git merge ブランチ名 は、今いるブランチに、指定したブランチの変更を取り込むコマンドです。向きを間違えやすいので、「取り込まれる側(main)に立ってから、取り込むブランチを指定する」と覚えてください。

text3 lines
cd ~/git-practice
git switch main
git merge recipe-cookie
text5 lines
Updating 4e16e23..4b541f5
Fast-forward
 recipes/cookie.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 recipes/cookie.txt

2行目に Fast-forward(早送り)と出ました。

text1 lines
git log --oneline --decorate --graph -n 3
text3 lines
* 4b541f5 (HEAD -> main, recipe-cookie) クッキーのレシピを追加
* 4e16e23 やることに掃除を追加
* 370768c (tag: v1.0) ケーキのレシピを追加

main が recipe-cookie と同じコミットまで進みました。新しいコミットは作られていません。

左は Fast-forward で、main が進んでいないため main の付箋が recipe-cookie のコミットまで前に動くだけの図。右は 3-way マージで、マージベースから main と shopping-apple の2本に分かれた先端を、親が2つのマージコミットで1つに合わせる図
マージには Fast-forward と 3-way の2つの形がある

Fast-forward:付箋を前に動かすだけ#

recipe-cookie を作ってから、main には新しいコミットがありませんでした。つまり recipe-cookie の履歴は、main の履歴の「続き」になっています。

この場合、取り込むために合わせる作業は何もありません。main の付箋を recipe-cookie と同じコミットへ貼り替えれば、それで取り込み完了です。これが Fast-forward です。Updating 4e16e23..4b541f5 は「main を 4e16e23 から 4b541f5 へ動かした」という意味です。

3-way マージ:両方が進んでいるとき#

今度は、両方のブランチがそれぞれ進んだ状態を作ります。買い物メモの変更をブランチで、やることリストの変更を main で記録します。

text7 lines
git switch -c shopping-apple
echo "りんごを買う" >> shopping.txt
git commit -am "買い物メモにりんごを追加"
git switch main
echo "花に水をやる" >> todo.txt
git commit -am "やることに水やりを追加"
git log --oneline --decorate --graph --all -n 4
text5 lines
* dd3f00e (HEAD -> main) やることに水やりを追加
| * 587a247 (shopping-apple) 買い物メモにりんごを追加
|/  
* 4b541f5 (recipe-cookie) クッキーのレシピを追加
* 4e16e23 やることに掃除を追加

|/ のところで、履歴が2本に分かれています。main の付箋を前に動かすだけでは、shopping-apple のりんごが入りません。ここでマージします。

text1 lines
git merge shopping-apple
text3 lines
Merge made by the 'ort' strategy.
 shopping.txt | 1 +
 1 file changed, 1 insertion(+)
text1 lines
git log --oneline --decorate --graph -n 5
text7 lines
*   ca9807e (HEAD -> main) Merge branch 'shopping-apple'
|\  
| * 587a247 (shopping-apple) 買い物メモにりんごを追加
* | dd3f00e やることに水やりを追加
|/  
* 4b541f5 (recipe-cookie) クッキーのレシピを追加
* 4e16e23 やることに掃除を追加

2本に分かれた履歴が、ca9807e で1つに合流しました。このような、2本の流れを合わせるコミットをマージコミットと呼びます。

なぜ「3-way」なのか#

Git は、次の3点を比べてマージの結果を決めます。

比べるもの今回のコミット
2本が分かれる前の共通の祖先(マージベース)4b541f5 クッキーのレシピを追加
今いるブランチの先端dd3f00e(main)
取り込むブランチの先端587a247(shopping-apple)

マージベースは git merge-base A B で確かめられます。マージする前なら git merge-base main shopping-apple です。マージしたあとの今は、main が shopping-apple を含んでしまっているので、マージコミットの1つ目の親(HEAD^1)と2つ目の親(HEAD^2)を指定します。

text1 lines
git merge-base HEAD^1 HEAD^2
text1 lines
4b541f59822864b80799607d10f7a1950890b8f9

マージベースから見て、main では todo.txt が、shopping-apple では shopping.txt が変わっています。片方だけで変わった部分はその変更を採用し、両方の変更をすべて含んだ状態を作ります。2つの先端だけを比べると「main にはりんごの行がない」のが「削除した」のか「まだ追加していない」のか区別できません。祖先を合わせて3点で比べるので、それが判断できるのです。

マージコミットは親が2つ#

第3部の17の方法で、マージコミットの中身を開けてみます。

text1 lines
git cat-file -p HEAD
text7 lines
tree 996bb0821c0f4eedd73e735831e19049043e74cf
parent dd3f00e5f823cc55b226131612308da74f43fdf7
parent 587a2478031e9009c864c112f0ca164097bee80f
author Taro Yamada <[email protected]> 1793111602 +0900
committer Taro Yamada <[email protected]> 1793111602 +0900

Merge branch 'shopping-apple'

ふつうのコミットと違うのは、parent が 2行あることだけです。1つ目が main 側(マージしたときに立っていたブランチ)、2つ目が取り込んだ shopping-apple 側です。中身の tree は、両方の変更を合わせた状態のスナップショットです。

text1 lines
cat shopping.txt todo.txt
text8 lines
豆乳を買う
卵を買う
パンを買う
りんごを買う
歯医者を予約する
銀行に行く
部屋を掃除する
花に水をやる

りんごと水やりの両方が入っています。

取り込んだブランチを片付ける#

マージが済んだブランチは、役目を終えています。git branch --merged で、今いるブランチに取り込み済みのブランチを一覧できます。

text1 lines
git branch --merged
text3 lines
* main
  recipe-cookie
  shopping-apple

ここに出ているブランチは、-d で安全に消せます(22)。

text1 lines
git branch -d recipe-cookie shopping-apple
text2 lines
Deleted branch recipe-cookie (was 4b541f5).
Deleted branch shopping-apple (was 587a247).

ブランチ名は消えても、コミットはマージによって main の履歴に入っているので、何も失われません。

よくある疑問#

Q. マージを途中でやめたり、やり直したりできますか?
A. 途中で止まった場合(次の24のコンフリクト)は git merge --abort でマージ前に戻れます。完了したマージを取り消したいときは、まだ共有していなければ第2部の14の git reset --hard HEAD~1 で、マージ前のコミットへ戻せます。

Q. 3-way マージで、両方が同じ行を変えていたらどうなりますか?
A. Git にはどちらを採るべきか決められないので、マージが止まります。これがコンフリクトです。次のページで、実際に起こして解決します。

まとめ#

  • git merge ブランチ名 は、今いるブランチに指定したブランチを取り込む。取り込まれる側に立ってから実行する
  • 取り込まれる側が進んでいなければ Fast-forward になり、付箋を前に動かすだけで新しいコミットはできない
  • 両方が進んでいれば 3-way マージになり、マージベースと2つの先端を比べて、親が2つのマージコミットを作る
  • git merge-base でマージベース、git branch --merged で取り込み済みのブランチを確かめられる
  • 取り込み済みのブランチは git branch -d で片付けてよい

次のページ#

24. コンフリクトを解決する ── 2本のブランチで同じ行を書き換えてマージし、止まったマージを自分の手で仕上げます。