- Git
git merge –no-ff・–squashの違いを図解|ffとは
fast-forward(ff)とは、マージ先のブランチが指す位置を、マージ元の先頭コミットまで「進めるだけ」で済ませるマージのことです。 新しいコミットは作られません。これに対して --no-ff は必ずマージコミットを作り、--squash はブランチの変更を1つの普通のコミットにまとめ、--ff-only は fast-forward できるときだけマージします。 違いは「履歴にどんな形が残るか」の一点に集約されます。
この記事では、まったく同じ内容のリポジトリを4つ用意し、--ff / --no-ff / --ff-only / --squash でそれぞれマージして、git log --graph --oneline の出力を並べて比べます。さらに、merge.ff・pull.ff の設定値、GitHub の3つのマージボタンと GitLab のマージ方式との対応、squash 後に git branch -d が止まる理由、revert のやり方の違いまで、すべて手元で実行した結果つきで説明します(実行環境は Git 2.43.0。オプションの定義は git-scm.com の git-merge ドキュメント 2.54.0 版で確認しています)。
git mergeオプションの早見表
| オプション | マージコミット | 元のコミットC・Dは残るか | 履歴の形 | ffできないとき | 向いている場面 |
|---|---|---|---|---|---|
| --ff(既定) | 作らない(ffできるとき) | そのまま残る | 一直線 | マージコミットを作る | 手元の小さな取り込み、pull |
| --no-ff | 必ず作る | そのまま残る | 枝分かれして合流 | マージコミットを作る | 機能単位のまとまりを履歴に残したい |
| --ff-only | 作らない | そのまま残る | 一直線 | マージせずに止まる | 一直線の履歴を保ちたい、pull の安全策 |
| --squash | 作らない(普通のコミット1つ) | main には残らない | 一直線(1コミット) | 1つにまとめた変更を用意する | 作業途中の細かいコミットを1つにしたい |
| 知りたいこと | 答え |
|---|---|
| fast-forward とは | ブランチの指す位置を先へ進めるだけのマージ。コミットは増えない |
| マージコミットを作りたい | git merge --no-ff feature |
| マージコミットを作りたくない | git merge --ff-only feature(できないときは止まる) |
| 1コミットにまとめたい | git merge --squash feature のあと git commit |
| 毎回 --no-ff にしたい | git config merge.ff false |
| squash 後にブランチを消す | git branch -D feature(-d は止まる) |
| マージコミットを打ち消す | git revert -m 1 マージコミット |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「git merge --no-ff・--squashの違いを図解|ffとは」
fast-forwardとは何かを図でつかむ
ffできる状態とできない状態
fast-forward できるかどうかは、マージ先(main)の先頭が、マージ元(feature)の履歴の途中に含まれているかで決まります。main がブランチを切ったときから動いていなければ ff できます。
gitGraph
commit id: "A"
commit id: "B"
branch feature
commit id: "C"
commit id: "D"
この図では main は B のまま、feature は C・D と進んでいます。main から見ると D は「自分の延長線上」にあるので、main の指す位置を B から D に動かすだけでマージが終わります。これが fast-forward(早送り)です。
一方、ブランチを切ったあと main にも別のコミット X が入ると、main と feature は別々の方向に進んだ状態になります。
gitGraph
commit id: "A"
commit id: "B"
branch feature
commit id: "C"
commit id: "D"
checkout main
commit id: "X"
この状態では位置を動かすだけでは済まないので、2つの履歴をつなぐマージコミットが必要になります。
| 状態 | 見分け方 | ffできるか |
|---|---|---|
| main がブランチ作成時から動いていない | git log --oneline feature に main の先頭が出てくる | できる |
| main にも新しいコミットがある(分岐) | git log --graph --oneline --all で枝が2本に見える | できない |
| feature が main にすでに含まれている | git merge で Already up to date. と表示 | 何も起きない |
検証に使うリポジトリの作り方
以降の実行結果は、次のスクリプトで作ったリポジトリを4つコピーして得たものです。そのまま貼り付ければ同じ形を再現できます(コミットIDは日時や名前で変わります)。
mkdir base && cd base
git init -q -b main
echo "# demo" > README.md; git add .; git commit -qm "A: first commit"
echo "v1" > app.txt; git add .; git commit -qm "B: add app"
git switch -q -c feature
echo "login" > login.txt; git add .; git commit -qm "C: add login"
echo "logout" > logout.txt; git add .; git commit -qm "D: add logout"
git switch -q main
git log --graph --oneline --all
実行結果:
* 1328e95 D: add logout
* bdbf83d C: add login
* 82944fe B: add app
* 6607fe7 A: first commit
main は B(82944fe)、feature は D(1328e95)を指しています。ここから4通りのマージを行います。
4つのオプションでマージした結果を並べて比べる
--ff(既定):ブランチの位置を進めるだけ
git merge feature
git log --graph --oneline
Updating 82944fe..1328e95
Fast-forward
login.txt | 1 +
logout.txt | 1 +
2 files changed, 2 insertions(+)
create mode 100644 login.txt
create mode 100644 logout.txt
* 1328e95 D: add logout
* bdbf83d C: add login
* 82944fe B: add app
* 6607fe7 A: first commit
「Fast-forward」と表示され、コミットは1つも増えていません。 main が D を指すようになっただけです。履歴は一直線で読みやすい反面、「C と D が feature というブランチで作られた」という情報は履歴には残りません。
--no-ff:必ずマージコミットを作る
git merge --no-ff feature -m "Merge branch 'feature'"
git log --graph --oneline
Merge made by the 'ort' strategy.
login.txt | 1 +
logout.txt | 1 +
2 files changed, 2 insertions(+)
create mode 100644 login.txt
create mode 100644 logout.txt
* 5b5eff4 Merge branch 'feature'
|\
| * 1328e95 D: add logout
| * bdbf83d C: add login
|/
* 82944fe B: add app
* 6607fe7 A: first commit
ff できる状態でも、新しいマージコミット 5b5eff4 が作られ、枝分かれして合流する形が残ります。C と D が1つのまとまりだったことがグラフから読み取れます。
--no-ff の履歴は、--first-parent を付けると「main に何が取り込まれたか」だけを一覧できます。
git log --first-parent --oneline
5b5eff4 Merge branch 'feature'
82944fe B: add app
6607fe7 A: first commit
機能単位で1行になるので、リリース内容の確認や、あとで機能ごと打ち消すときに便利です。
--ff-only:ffできるときだけマージする
git merge --ff-only feature
git log --graph --oneline
Updating 82944fe..1328e95
Fast-forward
login.txt | 1 +
logout.txt | 1 +
2 files changed, 2 insertions(+)
create mode 100644 login.txt
create mode 100644 logout.txt
* 1328e95 D: add logout
* bdbf83d C: add login
* 82944fe B: add app
* 6607fe7 A: first commit
ff できる状態では --ff とまったく同じ結果になります。違いが出るのは ff できないときで、--ff はマージコミットを作りますが、--ff-only は何もせずに止まります(後半の「--ff-onlyが止まったときのメッセージと対処」で再現します)。
--squash:変更を1つのコミットにまとめる
--squash は2段階です。まず git merge --squash で変更をステージに載せ、そのあと自分で git commit します。
git merge --squash feature
git status
Updating 82944fe..1328e95
Fast-forward
Squash commit -- not updating HEAD
login.txt | 1 +
logout.txt | 1 +
2 files changed, 2 insertions(+)
create mode 100644 login.txt
create mode 100644 logout.txt
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: login.txt
new file: logout.txt
「Squash commit -- not updating HEAD」と表示され、この時点ではコミットされていません。 変更がステージに載っただけの状態です。続けてコミットします。
git commit -m "E: add login/logout (squash)"
git log --graph --oneline --all
git log -1 --format='%h parents=[%p]'
* 1328e95 D: add logout
* bdbf83d C: add login
| * 9f1f806 E: add login/logout (squash)
|/
* 82944fe B: add app
* 6607fe7 A: first commit
9f1f806 parents=[82944fe]
main には E という普通のコミットが1つできました。親は B の1つだけで、feature の C・D とはつながっていません。--all を付けたので feature 側の C・D も表示されていますが、main の履歴には入っていない点が --ff や --no-ff との大きな違いです。
なお、-m を付けずに git commit すると、Git が用意したメッセージの下書き(.git/SQUASH_MSG)がエディタに表示されます。中身は次のとおりで、元のコミットの一覧が入っています。
Squashed commit of the following:
commit 1328e95c7b0aeac98d5c6e0c4e2e6cff544b2d36
Author: taro <taro@example.com>
Date: Sat Oct 3 10:00:00 2026 +0900
D: add logout
commit bdbf83db328af6a97af533adfd49d7d0401d4fea
Author: taro <taro@example.com>
Date: Sat Oct 3 10:00:00 2026 +0900
C: add login
4つの結果を1枚にまとめる
| 項目 | --ff | --no-ff | --ff-only | --squash |
|---|---|---|---|---|
| main の先頭 | D(1328e95) | 新しいマージコミット(5b5eff4) | D(1328e95) | 新しいコミット E(9f1f806) |
| 先頭コミットの親の数 | 1 | 2 | 1 | 1 |
| main の履歴に C・D があるか | ある | ある | ある | ない(E に内容だけ入る) |
| 増えたコミット数 | 0 | 1 | 0 | 1 |
| git branch -d feature | 通る | 通る | 通る | 止まる(-D が必要) |
| 打ち消し方 | C・D を個別に revert | git revert -m 1 で1回 | C・D を個別に revert | git revert で1回 |
| 表示されるメッセージ | Fast-forward | Merge made by the 'ort' strategy. | Fast-forward | Squash commit -- not updating HEAD |
mainにも変更があるとき(分岐しているとき)の動き
ここまでは ff できる状態でした。次に、main にもコミット X を足して分岐させます。
echo v2 > app.txt
git commit -qam "X: update app on main"
git log --graph --oneline --all
* 1328e95 D: add logout
* bdbf83d C: add login
| * 1a4285b X: update app on main
|/
* 82944fe B: add app
* 6607fe7 A: first commit
既定の--ffはマージコミットを作る
git merge feature -m "Merge branch 'feature'"
git log --graph --oneline
Merge made by the 'ort' strategy.
login.txt | 1 +
logout.txt | 1 +
2 files changed, 2 insertions(+)
create mode 100644 login.txt
create mode 100644 logout.txt
* d3b9c4d Merge branch 'feature'
|\
| * 1328e95 D: add logout
| * bdbf83d C: add login
* | 1a4285b X: update app on main
|/
* 82944fe B: add app
* 6607fe7 A: first commit
--ff は「できるときは ff、できないときはマージコミット」という動きです。つまり同じ git merge feature でも、main が動いていたかどうかで履歴の形が変わります。形をそろえたいときに --no-ff や --ff-only を明示する、というのが使い分けの出発点です。
| 状態 | --ff | --no-ff | --ff-only | --squash |
|---|---|---|---|---|
| ffできる | 位置を進める | マージコミット | 位置を進める | 1コミットにまとめる準備 |
| 分岐している | マージコミット | マージコミット | 止まる | 1コミットにまとめる準備 |
| すでに取り込み済み | Already up to date. | Already up to date. | Already up to date. | 変更なし |
取り込み済みのブランチに --no-ff を付けても、空のマージコミットは作られず Already up to date. と表示されることも確認しました。
--ff-onlyが止まったときのメッセージと対処
分岐した状態で --ff-only を使うと、次のメッセージが出てマージは行われません(終了コードは 128)。
git merge --ff-only feature
hint: Diverging branches can't be fast-forwarded, you need to either:
hint:
hint: git merge --no-ff
hint:
hint: or:
hint:
hint: git rebase
hint:
hint: Disable this message with "git config advice.diverging false"
fatal: Not possible to fast-forward, aborting.
「fatal: Not possible to fast-forward, aborting.」は、作業ツリーにもブランチにも手を付けずに止まったという合図です。何も壊れていないので、落ち着いて次のどちらかを選びます。
| 対処 | コマンド | 結果の履歴 |
|---|---|---|
| マージコミットで合流する | git merge --no-ff feature | 枝分かれして合流 |
| feature を main の先頭に付け替えてから ff する | git switch feature → git rebase main → git switch main → git merge --ff-only feature | 一直線 |
2つめの手順を実行した結果です。
git switch -q feature
git rebase main
git switch -q main
git merge --ff-only feature
git log --graph --oneline
Successfully rebased and updated refs/heads/feature.
Updating 1a4285b..16ea9b4
Fast-forward
login.txt | 1 +
logout.txt | 1 +
2 files changed, 2 insertions(+)
create mode 100644 login.txt
create mode 100644 logout.txt
* 16ea9b4 D: add logout
* c9d4070 C: add login
* 1a4285b X: update app on main
* 82944fe B: add app
* 6607fe7 A: first commit
rebase によって C・D のコミットIDが bdbf83d・1328e95 から c9d4070・16ea9b4 に変わっています。rebase は「同じ内容の新しいコミットを作り直す」操作なので、すでに共有しているブランチでは、チームのルールを確認してから使うと安心です。
git pullで出る「divergent branches」も同じ仕組み
git pull は「fetch してから merge(または rebase)」を行うコマンドです。手元とリモートの両方にコミットがあり、方針が未設定だと、次のメッセージで止まります。
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint: git config pull.rebase false # merge
hint: git config pull.rebase true # rebase
hint: git config pull.ff only # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.
その場で進めるなら、git pull --no-rebase(マージで合流)か git pull --rebase(付け替え)を選びます。--no-rebase で進めた結果は次のとおりで、分岐時の --ff と同じ形のマージコミットができます。
Merge made by the 'ort' strategy.
b.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 b.txt
* a32a6f7 Merge branch 'main' of ../remote
|\
| * 22bcdff P: other
* | 2c545e2 Q: local
|/
* 82944fe B: add app
* 6607fe7 A: first commit
merge.ff・pull.ffの設定値一覧
毎回オプションを打つ代わりに、設定で既定の動きを変えられます。
| 設定 | 値 | 動き | 同じ意味のオプション |
|---|---|---|---|
| merge.ff | true(既定) | ffできるときは ff、できないときはマージコミット | --ff |
| merge.ff | false | 常にマージコミットを作る | --no-ff |
| merge.ff | only | ffできるときだけマージ。できないときは止まる | --ff-only |
| pull.ff | 未設定 | merge.ff の値に従う | ― |
| pull.ff | true | pull のとき、ffできれば ff | git pull --ff |
| pull.ff | false | pull のとき、常にマージコミット | git pull --no-ff |
| pull.ff | only | pull のとき、ffできなければ止まる | git pull --ff-only |
| pull.rebase | false / true | 分岐していたときに merge するか rebase するか | --no-rebase / --rebase |
# このリポジトリだけ
git config merge.ff false
# すべてのリポジトリ
git config --global pull.ff only
# 確認
git config --get-regexp 'ff$'
merge.ff falseにするとpullにも効く
merge.ff を false にしたリポジトリで、リモートに1コミットだけ進んだ状態(ff できる状態)から git pull した結果です。
Merge made by the 'ort' strategy.
* d74b8a4 Merge branch 'main' of ../r
|\
| * 22bcdff P: other
|/
ff で済むはずの pull でもマージコミットができました。 merge.ff は pull 時のマージにも使われるためです。pull のたびにマージコミットが増えるのを避けたいときは、pull.ff を別に設定します。同じ状況で5通りを実行した結果をまとめます。
| 設定 | ffできる状態での git pull の結果 |
|---|---|
| merge.ff=false のみ | Merge made(マージコミットができる) |
| merge.ff=false + pull.ff=true | Fast-forward |
| merge.ff=only + pull.ff=false | Merge made(マージコミットができる) |
| pull.ff=false のみ | Merge made(マージコミットができる) |
| pull.ff=only のみ | Fast-forward |
pull.ff を設定すると、pull のときは merge.ff より pull.ff が使われることが読み取れます。「機能ブランチのマージは --no-ff、pull は ff だけ」という方針なら、次の組み合わせになります。
git config --global merge.ff false
git config --global pull.ff only
merge.ff onlyで-mを付けたとき
merge.ff を only にして git merge feature -m "..." を実行すると、次の表示になりました。
Updating 82944fe..1328e95
Fast-forward (no commit created; -m option ignored)
ff ではコミットが作られないので、-m のメッセージは使われません。メッセージを残したいときは --no-ff を選びます。
--squashと--no-ffは同時に使えない
git merge --squash --no-ff feature
fatal: options '--squash' and '--no-ff.' cannot be used together
--squash はマージコミットを作らない操作、--no-ff はマージコミットを作る操作なので、組み合わせられません。merge.ff を false にしていても、--squash 単体は通常どおり使えます。
GitHub・GitLabのマージ方式との対応表
プルリクエスト(マージリクエスト)の画面で選ぶ方式は、手元の git コマンドと次のように対応します。GitHub と GitLab の公式ドキュメントを今回開いて確認した内容をもとにしています。
GitHubの3つのボタン
| GitHub のボタン | 近い git コマンド | マージコミット | 元のコミット | コミットID |
|---|---|---|---|---|
| Create a merge commit | git merge --no-ff | 作る | すべて残る | 元のまま |
| Squash and merge | git merge --squash + git commit | 作らない | 1つにまとまる | 新しくなる |
| Rebase and merge | git rebase + git merge --ff-only | 作らない | すべて残る | 新しくなる |
- Create a merge commit は --no-ff と同じで、ff できる状態でもマージコミットができます。
- Squash and merge は、プルリクエストのコミットを1つにまとめてベースブランチに追加します。
- Rebase and merge は、コミットを1つずつベースブランチの先頭に付け直します。GitHub のドキュメントには、この方式は常にコミッター情報を更新し、新しいコミットIDを作ると書かれています。手元の git rebase は「付け替える必要がなければIDを変えない」ので、ここが手元との違いです。
GitLabのマージ方式
| GitLab のマージ方式 | 近い git コマンド | 特徴 |
|---|---|---|
| Merge commit | git merge --no-ff | 常にマージコミットを作る |
| Merge commit with semi-linear history | rebase で最新にそろえたうえで git merge --no-ff | ffできる状態のときだけマージを許可し、マージコミットも作る |
| Fast-forward merge | git merge --ff-only | マージコミットなしの一直線。ffできないときは rebase が必要 |
| Fast-forward merge + squash | git merge --squash + git commit | 1コミットにまとめて一直線 |
| Merge commit + squash | squash したコミット + マージコミット | まとめたコミットとマージコミットの両方ができる |
手元のコマンドと画面の対応を逆引きする
| 残したい履歴 | 手元 | GitHub | GitLab |
|---|---|---|---|
| 機能ごとの合流点を残す | --no-ff | Create a merge commit | Merge commit |
| 一直線で、コミットは細かく残す | rebase + --ff-only | Rebase and merge | Fast-forward merge |
| 一直線で、1機能1コミット | --squash | Squash and merge | Fast-forward merge + squash |
squashした後の後始末
git branch -dが止まる理由
--squash でまとめたあと、feature ブランチを -d で消そうとすると止まります。
git branch -d feature
error: the branch 'feature' is not fully merged.
If you are sure you want to delete it, run 'git branch -D feature'
理由は、-d が「そのブランチの先頭コミットが、現在のブランチの履歴に含まれているか」で判定しているからです。squash で作った E は C・D と同じ内容を持っていますが、履歴としてはつながっていません。git branch --merged にも feature は出てきません。
git branch --merged
* main
内容が取り込まれていることは、差分が空であることで確かめられます。
git diff main feature --stat
何も表示されなければ、main と feature の中身は同じです。確認できたら -D で削除します。
git branch -D feature
Deleted branch feature (was 1328e95).
| マージ方法 | git branch -d feature | 理由 |
|---|---|---|
| --ff / --ff-only | 通る | main の履歴に D が含まれる |
| --no-ff | 通る | マージコミットの親として D が含まれる |
| --squash | 止まる | main の履歴に D が含まれない |
--no-ff のあとに -d を実行すると、Deleted branch feature (was 1328e95). と表示されてそのまま削除できました。
squashしたブランチで作業を続けるとき
squash 後も同じ feature ブランチでコミットを続け、もう一度 --squash すると、Git は「共通の祖先」を B と見なして差分を計算し直します。追加分がファイルの追加だけの場合は、次のように新しいファイルだけがステージに載りました。
Automatic merge went well; stopped before committing as requested
Squash commit -- not updating HEAD
A more.txt
一方、squash 後に main 側と feature 側の両方で同じファイル(login.txt)を書き換えた状態で再度 --squash すると、コンフリクトになりました。
Auto-merging login.txt
CONFLICT (add/add): Merge conflict in login.txt
Squash commit -- not updating HEAD
Automatic merge failed; fix conflicts and then commit the result.
AA login.txt
squash したブランチは役目を終えたものとして削除し、続きの作業は main から新しいブランチを切ると、こうした再計算に悩まずに済みます。
revertのやり方はマージ方法で変わる
取り込んだ機能を打ち消すときの手順は、マージ方法によって異なります。
--no-ffのマージコミットは-m 1が必要
マージコミットをそのまま revert しようとすると、次のメッセージで止まります。
git revert --no-edit HEAD
error: commit 5b5eff4ffe42847caad111becf05ab1124f73cf7 is a merge but no -m option was given.
fatal: revert failed
マージコミットには親が2つあるので、「どちらの親から見た差分を打ち消すのか」を -m で指定します。-m 1 は1番目の親(マージを実行した側=main)を基準にする、という意味です。
git revert --no-edit -m 1 HEAD
git log --graph --oneline
ls
[main 2044d28] Revert "Merge branch 'feature'"
2 files changed, 2 deletions(-)
delete mode 100644 login.txt
delete mode 100644 logout.txt
* 2044d28 Revert "Merge branch 'feature'"
* 5b5eff4 Merge branch 'feature'
|\
| * 1328e95 D: add logout
| * bdbf83d C: add login
|/
* 82944fe B: add app
* 6607fe7 A: first commit
README.md
app.txt
1回の revert で login.txt と logout.txt の両方が消え、機能まるごと打ち消せました。
マージをrevertした後に同じブランチを再マージするとき
マージを revert したあと、同じ feature をもう一度マージしようとすると、次のように表示されます。
git merge feature
Already up to date.
履歴の上では C・D はすでに main に含まれているため、Git は「取り込み済み」と判断します。機能を戻したいときは、revert のコミットをさらに revert します。
git revert --no-edit HEAD
git log --oneline
[main 6f30da5] Reapply "Merge branch 'feature'"
6f30da5 Reapply "Merge branch 'feature'"
2044d28 Revert "Merge branch 'feature'"
5b5eff4 Merge branch 'feature'
Git 2.43 では、revert の revert は「Reapply ...」という題名になります。これで login.txt と logout.txt が戻りました。
--squashのコミットは普通のrevertで打ち消せる
squash で作った E は親が1つの普通のコミットなので、-m は不要です。
git revert --no-edit HEAD
git log --oneline
[main 542e2b6] Revert "E: add login/logout (squash)"
2 files changed, 2 deletions(-)
delete mode 100644 login.txt
delete mode 100644 logout.txt
542e2b6 Revert "E: add login/logout (squash)"
9f1f806 E: add login/logout (squash)
82944fe B: add app
6607fe7 A: first commit
--ffで取り込んだときはコミットごとにrevertする
ff で取り込むと「機能の合流点」がないので、C と D を個別に打ち消します。git revert HEAD だけでは最後の D しか戻りません。
[main f29891a] Revert "D: add logout"
1 file changed, 1 deletion(-)
delete mode 100644 logout.txt
複数まとめて打ち消すときは、範囲を指定します(B の次から D まで、という意味で B..D と書きます)。
git revert --no-edit 82944fe..1328e95
git log --oneline
[main f29891a] Revert "D: add logout"
[main 590a687] Revert "C: add login"
590a687 Revert "C: add login"
f29891a Revert "D: add logout"
1328e95 D: add logout
新しいコミットから順に、コミットの数だけ revert コミットが作られます。
| マージ方法 | 機能を打ち消すコマンド | revert コミットの数 |
|---|---|---|
| --no-ff | git revert -m 1 マージコミット | 1 |
| --squash | git revert まとめたコミット | 1 |
| --ff / --ff-only | git revert 範囲(B..D) | 元のコミットの数だけ |
あとで機能単位で打ち消す可能性があるなら、--no-ff か --squash が扱いやすいことがわかります。
どれを選ぶかの判断表
flowchart TD
A[ブランチを取り込む] --> B{作業途中の細かいコミットを残したいか}
B -- 残さなくてよい --> S[--squash で1コミットに]
B -- 残したい --> C{機能の合流点を履歴に残したいか}
C -- 残したい --> N[--no-ff でマージコミット]
C -- 一直線にしたい --> F[rebase してから --ff-only]
| 重視すること | 選ぶ方法 | 理由 |
|---|---|---|
| 機能単位で履歴を追いたい・まとめて打ち消したい | --no-ff | --first-parent で機能一覧になり、revert -m 1 が1回で済む |
| main の履歴を一直線に保ちたい | --ff-only(必要なら先に rebase) | マージコミットが入らず、git log がそのまま時系列になる |
| 「WIP」「typo修正」などを main に残したくない | --squash | 1機能1コミットになる。元の細かい履歴はプルリクエスト側で参照する |
| git bisect で原因コミットを細かく絞りたい | --no-ff か --ff-only | コミットが細かいまま残るので、絞り込みの単位が小さい |
| 誰がどの行を書いたかを細かく残したい | --no-ff か --ff-only | squash では作者情報が1コミット分にまとまる |
| pull のたびのマージコミットを避けたい | pull.ff only か pull.rebase true | 分岐時は止まる、または付け替える |
チームで1つに決めるときは、「main に入るコミットの単位」を先に決めると選びやすくなります。1機能=1コミットなら squash、1機能=1つの合流点なら --no-ff、コミット単位をそのまま並べるなら ff-only という対応です。
つまずきやすい点と直し方
| 起きること | 原因 | 直し方 |
|---|---|---|
| --no-ff を付け忘れて一直線になった | ffできる状態で既定の --ff が働いた | push 前なら git reset --hard ORIG_HEAD で戻して --no-ff でやり直す。以後は merge.ff false を設定 |
| pull のたびにマージコミットが増える | merge.ff false が pull にも効いている | pull.ff only または pull.ff true を追加 |
| fatal: Not possible to fast-forward, aborting. | 分岐している状態で --ff-only | rebase して ff にするか、--no-ff で合流 |
| squash 後に branch -d が止まる | 履歴がつながっていない | git diff main feature で差分が空と確かめて -D |
| is a merge but no -m option was given | マージコミットを -m なしで revert | git revert -m 1 を使う |
| revert したブランチを再マージしても Already up to date. | 履歴上は取り込み済み | revert コミットを revert する |
| --squash のあと履歴に何も増えていない | git commit をしていない | git status で確認して git commit |
| --squash をやめたい | まだコミット前 | git reset --hard HEAD(ステージの変更を破棄する操作なので、未コミットの自分の変更がないことを確かめてから) |
マージ直後であれば、ORIG_HEAD にマージ前の位置が記録されています。git reset --hard ORIG_HEAD は手元の履歴を書き換える操作なので、push する前のやり直しに限って使うと安心です。
よくある質問
Q. fast-forward と --no-ff はどちらを使うのが一般的ですか? A. どちらも広く使われています。機能ブランチを main に取り込む場面では、合流点が残る --no-ff(GitHub の Create a merge commit と同じ形)がよく選ばれ、手元でリモートの更新を取り込む pull では ff がよく選ばれます。「取り込む相手が機能ブランチか、同じブランチの更新か」で分けると整理しやすくなります。
Q. --squash と git rebase -i の squash は何が違いますか? A. git merge --squash は「別のブランチの変更を、今のブランチに1コミットとして取り込む」操作で、元のブランチには手を加えません。git rebase -i の squash は「今のブランチの中でコミット同士をまとめ直す」操作で、ブランチ自身の履歴を書き換えます。結果としてできる1コミットの中身は同じにできますが、操作する対象が異なります。
Q. --squash でまとめると、誰が書いたかの情報はどうなりますか? A. main に入るコミットは1つなので、作者はそのコミットを作った人になります。元のコミットごとの作者は、下書きメッセージ(Squashed commit of the following: の一覧)に文字として残せます。GitHub のドキュメントにも、squash では個々の変更がいつ・誰によって行われたかの情報がまとまる点が挙げられています。細かい作者情報を履歴として残したいときは --no-ff が向いています。
Q. --no-ff を付けたのにマージコミットができません。 A. マージ元がすでに取り込み済みの場合は、--no-ff を付けても Already up to date. と表示され、コミットは作られません。また --squash と --no-ff は同時に指定できません。git log --graph --oneline --all で、取り込みたいコミットが現在のブランチに含まれていないかを確認してください。
まとめ
- fast-forward は、ブランチの位置を先へ進めるだけのマージ。コミットは増えず、履歴は一直線になる
- --no-ff は ff できるときでも必ずマージコミットを作る。機能の合流点が残り、revert -m 1 で1回で打ち消せる
- --ff-only は ff できるときだけマージし、分岐していたら何もせず止まる。rebase してから再実行すれば一直線に取り込める
- --squash は変更を1つの普通のコミットにまとめる。git commit が別に必要で、取り込んだ後のブランチ削除は -D を使う
- merge.ff(true / false / only)は pull にも効く。pull だけ変えたいときは pull.ff を設定する
- GitHub の Create a merge commit は --no-ff、Squash and merge は --squash、Rebase and merge は rebase + ff に対応する
- 迷ったら「main に入るコミットの単位」を先に決めると、方式が自然に決まる
参考・出典
- Git 公式ドキュメント git-merge(2.54.0 版):--ff / --no-ff / --ff-only / --squash と merge.ff の定義
- Git 公式ドキュメント git-pull:--ff-only と pull.rebase の説明
- GitHub Docs「About merge methods on GitHub」:3つのマージ方式の説明
- GitLab Docs「Merge methods」:Merge commit / semi-linear / Fast-forward merge の説明
- 実行結果はすべて Git 2.43.0(Linux)で取得。コミットIDは環境によって変わります
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「git merge --no-ff・--squashの違いを図解|ffとは」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。