Git入門:バージョン管理のきほん

マージの仕組み(fast-forward と マージコミット)

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部
マージの形は2種類

このレッスンで分かること

  • git merge が何をしているのか、履歴の形として理解できる
  • fast-forward になる条件と、マージコミットが作られる条件の見分け方
  • --no-ff--squash で履歴の見え方を選ぶという発想

マージは 2 つの履歴を 1 つに合流させること

ブランチで分かれた作業を、元のブランチに戻して 1 本にまとめる操作がマージです。コマンドは git merge ですが、同じコマンドでも履歴の形が 2 通りに分かれます。この分岐条件を知らないと、履歴が思っていた形にならず混乱します。

まず基本の打ち方です。マージは「取り込みたい側」に立って実行します。feature の成果を main に入れたいなら、main に移動してから feature を指定します。

ターミナル

$ git switch main Switched to branch 'main' $ git merge feature

方向を間違えて feature に立ったまま git merge main と打つと、main の内容が feature に入るだけです。エラーにはならないので気づきにくい失敗です。「マージは自分の足元に相手を引き込む操作」と覚えてください。

マージのときに Git がやっていることは、実は 3 つの状態を見比べる作業です。分岐点のコミット、自分側の最新、相手側の最新。この 3 つを並べて、分岐点からどちらが何を変えたかを行単位で調べます。片方だけが変えた行はそのまま採用し、両方が同じ行を違う内容に変えていた場合だけ手が止まります。この「3 つを見る」やり方があるので、単なる上書きにはなりません。

fast-forward は「名札を前に進めるだけ」

feature を切ったあと main に一度もコミットしていない場合を考えます。履歴はこの形です。

プレーンテキスト

A---B---C feature / o---o main

main は分岐点から動いていません。feature の A・B・C は、すべて main の続きとして一直線に並んでいます。この状態では、main の名札を C の位置まで滑らせるだけで合流が完了します。新しいコミットは要りません。これが fast-forward です。

ターミナル

$ git merge feature Updating 4c2b70e..8f3a91c Fast-forward login.html | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 login.html

マージ後の履歴はこうなります。

プレーンテキスト

o---o---A---B---C main, feature

出力の 1 行目に Fast-forward と出ているのが目印です。枝分かれの跡は残らず、最初から 1 本道だったように見えます。履歴は一直線で読みやすくなりますが、どこからどこまでが 1 つの作業だったかは分からなくなります

fast-forward ではコミットが 1 つも作られないので、コンフリクトも起きません。main 側は何も変えていないのだから、ぶつかりようがないためです。マージ後は mainfeature が同じコミットを指しているので、git branch -v で確認できます。

ターミナル

$ git branch -v feature 8f3a91c ログインフォームの雛形を追加 * main 8f3a91c ログインフォームの雛形を追加

同じハッシュが並んでいれば、取り込みは完了しています。この状態なら git branch -d feature が通ります。

マージコミットは「合流点を新しく作る」

今度は feature で作業している間に、main にも別のコミットが入った場合です。

プレーンテキスト

A---B---C feature / o---o---D---E main

main は D と E の分だけ先に進んでいます。もう名札を滑らせるだけでは済みません。C の内容と E の内容の両方を持った状態を、新しく作る必要があります。そこで Git は 親を 2 つ持つコミット を作ります。これがマージコミットです。

ターミナル

$ git merge feature Merge made by the 'recursive' strategy. login.html | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 login.html

履歴はこの形になります。

プレーンテキスト

A---B---C / \ o---o---D---E---M main

M がマージコミットです。M は C と E の両方を親に持ちます。枝分かれと合流の跡がそのまま履歴に残るので、git log --graph --oneline で作業の単位が見えます。

ターミナル

$ git log --graph --oneline * 3d9c05a Merge branch 'feature' |\ | * 8f3a91c ログインフォームの雛形を追加 | * 1a4f7b2 ログイン用のCSSを追加 * | 7e2d804 フッターの著作権表記を更新 * | 5b1c93f READMEに開発手順を追記 |/ * 4c2b70e 初期コミット

マージコミットを作るとき、Git はコミットメッセージを聞くためにエディタを開きます。既定のメッセージ (Merge branch 'feature') のままで問題ありません。エディタが Vim の場合は :wq と打って Enter で保存終了、VS Code を設定している場合はタブを閉じれば進みます。

Windows の Git Bash でも Mac のターミナルでも、開くエディタは core.editor の設定で決まります。Vim が開いて出られなくなったら、Esc を押してから :wq と入力して Enter です。事前に git config --global core.editor "code --wait" としておけば VS Code が開きます。

どちらになるかの見分け方

判断はひとつだけです。分岐したあとに、取り込む側のブランチが 1 つでも進んでいるかを見ます。

状況結果履歴の形
分岐後 main が進んでいないfast-forward一直線になる
分岐後 main にもコミットがあるマージコミット合流点が残る
--no-ff を付けた常にマージコミット進んでいなくても合流点を作る
--ff-only を付けたfast-forward できなければ失敗履歴を必ず一直線に保つ
--squash を付けた1 つの普通のコミット枝の跡は残らない

実行前に確かめたいときは git log --oneline main..featuregit log --oneline feature..main の両方を見ます。A..B は「B にはあるが A には無いコミット」という意味です。後者に何も出なければ main は進んでいないので fast-forward になります。

ターミナル

$ git log --oneline main..feature 8f3a91c ログインフォームの雛形を追加 1a4f7b2 ログイン用のCSSを追加 $ git log --oneline feature..main $

feature..main が空なので、この状態でマージすれば fast-forward です。逆にここに 1 行でも出れば、マージコミットが作られます。

マージのやり直しを避けたいときは --ff-only を付けて、意図と違う結果を先に弾く手もあります。

ターミナル

$ git merge --ff-only feature fatal: Not possible to fast-forward, aborting.

失敗した場合、履歴は 1 ミリも動いていません。この結果を見てから、マージコミットを作るか、先に rebase で一直線に整えるかを選べます。

履歴の形を自分で選ぶ

Git 任せにせず、意図した形を指定できます。

--no-ff は、fast-forward できる場合でもマージコミットを作ります。「この 3 コミットは 1 つの機能追加だった」という区切りを履歴に残せるので、チーム開発では好まれます。

ターミナル

$ git merge --no-ff feature Merge made by the 'ort' strategy.

--squash は毛色が違います。feature の変更内容をすべてまとめてステージに載せるだけで、コミットは作りません。自分でコミットして完了です。

ターミナル

$ git merge --squash feature Squash commit -- not updating HEAD Automatic merge went well; stopped before committing as requested $ git commit -m "ログイン機能を追加" [main 6c8f012] ログイン機能を追加 2 files changed, 41 insertions(+)

結果は普通のコミットが 1 つ増えるだけです。試行錯誤の細かいコミットを main に持ち込みたくないときに使います。ただし feature とのつながりは記録されないので、squash した feature ブランチはそのまま捨てるのが前提です。

--squash したあとに git branch -d feature を打つと、マージされていない扱いで拒否されます。内容は取り込み済みなので、確認したうえで -D で消してください。

やりがちな失敗と戻し方

マージ直後に「方向を間違えた」「まだ早かった」と気づくことがあります。まだ誰とも共有していないローカルのブランチであれば、直前の状態に戻せます。

ターミナル

$ git merge feature Merge made by the 'ort' strategy. $ git reset --hard HEAD~1 HEAD is now at 5b1c93f READMEに開発手順を追記

HEAD~1 は「1 つ前のコミット」を指します。--hard は作業ツリーごと戻すので、コミットしていない編集があると道連れに消えます。実行前に git status で作業ツリーがきれいなことを確かめてください。

戻したかどうかは git log --oneline -3 で確かめます。マージコミットが消えて、マージ前の先頭が戻っていれば成功です。

ターミナル

$ git log --oneline -3 5b1c93f READMEに開発手順を追記 7e2d804 フッターの著作権表記を更新 4c2b70e 初期コミット

すでに push して他の人が取り込んでいる場合は reset を使ってはいけません。その場合は打ち消しコミットを作る git revert -m 1 <マージコミット> を使います。考え方は 公開済みの履歴を安全に戻す(revert) と同じです。

なお、両方のブランチで同じ行を書き換えていた場合、マージは途中で止まってコンフリクトになります。その読み方と直し方は次の コンフリクトを解決する で扱います。

マージの前に必ず見る 3 つ

事故の大半は、状態を確かめずに git merge を打ったことから起きます。習慣にする確認は 3 つだけです。

まず、いま自分がどこに立っているか。

ターミナル

$ git status On branch main nothing to commit, working tree clean

On branch main で取り込み先に立っていること、working tree clean でコミットしていない編集が無いことを同時に確認できます。編集が残ったままマージすると、コンフリクトが起きたときに「Git が入れた変更」と「自分が書きかけの変更」の区別がつかなくなります。

次に、取り込む側が最新かどうか。リモートと共有しているリポジトリなら、マージの前に main を最新にしておきます。

ターミナル

$ git pull Already up to date.

最後に、何が入ってくるか。

ターミナル

$ git diff main..feature --stat login.html | 24 ++++++++++++++++++++++++ style.css | 8 ++++++++ 2 files changed, 32 insertions(+)

--stat を付けると、変更されるファイルと行数だけが並びます。想定外のファイルが混ざっていたら、その時点で止まって中身を確認してください。マージした後に気づくより、はるかに安く済みます。

この章のポイント
  • マージは取り込みたい側のブランチに立って git merge 相手 と打つ
  • 分岐後に取り込む側が進んでいなければ fast-forward、進んでいればマージコミットになる
  • fast-forward は履歴が一直線、マージコミットは枝と合流の跡が残る
  • --no-ff で作業の区切りを残す、--squash で 1 コミットにまとめる、と形を選べる