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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?