第8章 マージでブランチを統合する

マージの 2 つの形

Fast-forward (早送り)

main を基点に feature を切って、その間に main が 1 歩も進んでいない状況を考える。 このとき mainfeature を取り込むには「main のポインタを feature の先端コミットまで進めるだけ」で済む。 新しいマージコミットは作られない。これがFast-forward (FF) である。

履歴は直線のまま保たれる。一見スッキリだが、「ここでブランチを合流させた」という分岐の痕跡は残らない。

3-way マージ

一方、feature を切っている間に main 側にも別のコミットが積まれていた場合はそうはいかない。 双方から分岐してそれぞれ前進しているので、両方の変更を合わせた新しいマージコミットが必要になる。 これが 3-way マージ。「双方の共通祖先 + 両ブランチの先端」の 3 点を参照するからこの名前がついている。

3-way では履歴がひし形 (ダイヤモンド型) のグラフになる。「誰が・いつブランチを合流させたか」がマージコミットとして残るのは、あとで履歴をたどるときの助けになる。

マージの形を選ぶオプション

Git は状況に合わせて FF / 3-way を自動選択するが、プロジェクトのポリシーによっては「常にマージコミットを残したい」「直列履歴を保ちたい」といった希望がある。 よく使われる 3 つのオプションを押さえておく。

--no-ff で必ずマージコミットを残す

Fast-forward できる状況でも、--no-ff を付けるとあえてマージコミットを作る。 「この feature ブランチはここで合流させた」という足跡を履歴に残したいチームで好まれる。

Bash
git merge --no-ff feature/add-user-login

--ff-only で Fast-forward 以外を拒否する

逆に「3-way マージコミットを混ぜたくない」ときは --ff-only。 FF できないと fatal: Not possible to fast-forward で止まる。止まったら、手元のブランチが遅れていたということなので、git rebase や別ブランチへの取り込みなど別手段を検討する。

Bash
git merge --ff-only feature/add-user-login

--squash で複数コミットを 1 つに集約する

feature ブランチで試行錯誤を重ねて細かいコミットが積み上がった場合、--squash を使うと 「すべての変更を 1 つにまとめた単一コミットの材料」としてステージングに展開される (マージコミット自体は作られない)。 そのあと手で git commit してメッセージを付ければ、クリーンな 1 コミットとして main に足せる。

Bash
git merge --squash feature/add-user-login

# 変更はステージングに積まれた状態。内容を確認してからコミット
git status
git commit -m "feat: ログイン機能を追加"
MEMO

どのオプションを採用するかはチームごとに違う。迷ったら既存リポジトリの git log --graph を眺めて、 マージコミットが多いか / 直列が多いかを見れば流儀がだいたい読める。最初は流儀に合わせるのが無難である。

実際にマージしてみる

前章で作った feature/add-user-login を統合ブランチに取り込む想定で、定番の手順をなぞってみる。 マージ先 (取り込む側) のブランチに自分が乗っている状態で、マージ元 (取り込まれる側) の名前を引数に渡すのがポイントである。

① feature → develop に取り込む

第 7 章で触れた main + develop + feature/* の 3 層運用を採用しているチームなら、 完成した feature ブランチはまず develop に取り込む。

Bash
# 取り込み先である develop に切り替える
git switch develop

# 念のためリモートと同期する (他の人の更新が入っていないか確認)
git pull

# feature/add-user-login を develop に取り込む
git merge feature/add-user-login

Fast-forward でマージできた場合は Fast-forward と出て、ポインタが進むだけで終わる。 3-way になる場合はマージコミット用のメッセージを書くエディタが開くので、そのまま保存して終了すれば OK である。

② リリース時に develop → main に取り込む

develop に積み上がった変更を「本番に出す」ものとして確定させるタイミングで、今度は main に取り込む。 手順はさっきと同じで、取り込み先の名前だけ developmain に読み替える。

Bash
git switch main
git pull
git merge develop

このあと main を push すれば、リリース用のブランチが更新されて本番デプロイのトリガに使える状態になる。

MEMO

GitHub Flow のように develop を使わず main + feature/* だけで運用するチームでは、①②の 2 段階を踏まずに git switch main → git pull → git merge feature/add-user-login の一発で済ませる。 取り込み先のブランチ名が違うだけで、git merge の使い方自体は同じである。

コンフリクトが出たら

双方が同じファイルの同じ行を編集していると、Git は自動では解決できずコンフリクトを起こしてマージを一時停止する。 解消の手順は発展編で詳しく扱う予定だが、とりあえず今はこう覚えておけばよい。

マージを取り消したい

マージしたあとで「やっぱりやり直したい」と気づくこともある。 まだ push していなければORIG_HEAD というマージ直前の位置を覚えておいてくれる参照を使って巻き戻せる。

Bash
# マージ直前の状態に戻す (作業ツリーも含めて)
git reset --hard ORIG_HEAD
注意

git reset --hard は作業ツリーの変更も問答無用で捨てる破壊的操作である。 マージ以降に別のコミットを積んでしまっているとそれらも消えるので、事前に git log で位置を確認すること。 push 済みのマージを巻き戻すと共有ブランチの履歴が書き換わるので、これは絶対にやってはいけない (発展編で詳しく)。

履歴をグラフで眺める

マージ後にどんな形の履歴になったかを視覚的に把握するには、git log にいくつかオプションを足した形が定番である。

Bash
git log --graph --oneline --all --decorate

--graph で左端にグラフ、--oneline で 1 行要約、--all で全ブランチ、--decorate でブランチ名表示。 打ちやすさのためにエイリアス化しておく人も多い。

Bash
git config --global alias.lg "log --graph --oneline --all --decorate"
# 以降は git lg で呼び出せる
git lg

マージ済みブランチを掃除する

feature/add-user-login の役目は終わった。第 7 章でも触れたが、マージ済みブランチは消してよい。

Bash
# ローカルを削除
git branch -d feature/add-user-login

# GitHub 側 (リモート) も削除
git push origin --delete feature/add-user-login

複数ブランチが溜まってきた場合、「どれが現在のブランチに取り込み済みでどれがまだか」を一覧できると掃除しやすい。 develop 運用なら git switch develop してから実行すれば「develop に取り込み済みの feature ブランチ一覧」が取れる。

Bash
# 現在のブランチ (main / develop など) に取り込み済みのブランチ一覧
git branch --merged

# まだ取り込まれていないブランチ一覧
git branch --no-merged

--merged に出てきたブランチはだいたい安全に git branch -d できる。 一方 --no-merged に残っているものは「まだ作業中」「マージし忘れ」「別の場所に取り込まれた」のいずれか。名前を見て心当たりがなければ一応確認する。

POINT

GitHub の Pull Request を使ったワークフローでは、PR をマージすると GitHub 側でリモートブランチを自動削除する設定がある (Settings → General → Automatically delete head branches)。 有効にしておくと、手動で git push origin --delete する手間が省ける。

実務では PR マージが主流

ここまで学んだ git merge は、自分のローカルで統合ブランチに feature を取り込む操作だった。 実際のチーム開発では、これを自分の手で直接打つことは意外と少ない。代わりに GitHub のPull Request (PR) 上でレビューを経てからマージする流れが主流である。

  1. feature ブランチを git push -u origin feature/xxx で公開
  2. GitHub 上で PR を作成 (base ブランチには develop または main を指定)
  3. レビュワーのコメントを受けて必要なら追加コミットを push
  4. approve されたら GitHub の「Merge pull request」ボタンでマージ

ボタンを押した瞬間、内部では実質 git merge (設定によっては --squash や rebase マージ) が走っている。 つまりこの章で押さえた知識は、PR のボタンの裏側でも同じように動いていることになる。

MEMO

develop 運用のチームでは、feature/* の PR は develop をベースに作成し、 リリース時に別途 develop → main の PR を切って本番反映させるのが一般的である。 PR 作成画面の「base」ドロップダウンで取り込み先を選ぶ箇所をよく確認する癖をつけると、取り込みミスが減る。

発展編で扱う予定

PR を実際に作る手順、レビューの依頼 / 応答の流れ、コンフリクトが出たときの具体的な解消方法は、次回追加する発展編でまとめて解説する。 まずは手元の git merge の挙動と、PR マージがその延長線上にあるイメージを掴んでおけば十分である。

Git の一歩目、ここまで

お疲れさまでした。本編はここまで。

第 1 章の三層モデルから始まって、add / commit でローカルに記録し、GitHub とつなぎ、push / pull で往復して、 ブランチを切ってマージして戻す — これで日常のコード作業で Git が担う部分の骨格は一通り押さえられた。 あとは手を動かして、自分のプロジェクトや普段の業務で少しずつ慣らしていくのが上達への近道である。

プルリクエストを使ったレビュー前提のワークフローや、コンフリクトが起きたときの具体的な解消手順は、次回追加する「発展編」で扱う予定。 ここまでの内容で一旦区切り、気になったら辞書的に章を読み返しにきてほしい。

まとめ