Git入門:バージョン管理のきほん
マージの仕組み(fast-forward と マージコミット)
このレッスンで分かること
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 mainmain は分岐点から動いていません。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 側は何も変えていないのだから、ぶつかりようがないためです。マージ後は main と feature が同じコミットを指しているので、git branch -v で確認できます。
ターミナル
$ git branch -v
feature 8f3a91c ログインフォームの雛形を追加
* main 8f3a91c ログインフォームの雛形を追加同じハッシュが並んでいれば、取り込みは完了しています。この状態なら git branch -d feature が通ります。
マージコミットは「合流点を新しく作る」
今度は feature で作業している間に、main にも別のコミットが入った場合です。
プレーンテキスト
A---B---C feature
/
o---o---D---E mainmain は 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 mainM がマージコミットです。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..feature と git 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 cleanOn 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 コミットにまとめる、と形を選べる