git mergeの–no-ffと–ff-onlyの違いを図で見る
git merge に何も付けないと、早送り(fast-forward)で済むときはマージコミットを作らず、ブランチの先端を進めるだけです。--no-ff を付けると、早送りで済むときでもマージコミットを作ります。--ff-only を付けると、早送りで済むときだけマージし、済まないときは何もせずに止まります。作業ブランチを main に取り込むとき、履歴をどんな形で残すかを選ぶためのオプションです。検証環境で git を実際に打ち、マージの前と後の git log --graph の出力を並べました。
検証環境は Ubuntu 24.04、Git 2.43.0 です。作者の名前とメールアドレスは例示用の値(Neko <neko@example.com>)で、コミットの日時は環境変数で決めました。そのため、同じ手順で作ったコミットは、どのリポジトリでも同じハッシュになっています。マージの前の状態に戻すのではなく、比べる場合ごとにリポジトリを作り直して試しました。ページャーは使わない設定です。リモートのリポジトリとのやりとり(git pull)や、内容がぶつかる(コンフリクトする)マージ、Windows・macOS での動作は試していません。
比べ方:同じ履歴を作って、マージの前と後で git log –graph を見る
履歴の図は git log --oneline --graph --decorate --all で出します。--decorate を付けると、どのブランチがどのコミットを指しているかが、ハッシュの右のかっこに出ます。マージでブランチの先端がどう動くかを見るには、これがいちばん分かりやすい手がかりです。グラフの線の読み方は、このブログの「git log –graphでブランチの分かれ方と合流を見る」にまとめています。
比べる履歴は2つ作りました。1つめは、feature/search ブランチを作ったあと、main には何もコミットしていない一直線の履歴です。2つめは、main にも1件コミットを足して、2つのブランチが枝分かれした履歴です。どちらも、main にいる状態で git merge feature/search を実行します。
一直線の履歴では、3つの結果が分かれる
マージする前の図です。main は f20c99c を指していて、feature/search はその2つ先の 96a7a06 を指しています。main から見ると、feature/search は自分の先にそのままつながっている状態です。
$ git log --oneline --graph --decorate --all
* 96a7a06 (feature/search) 検索に絞り込みを追加
* 483bc38 検索画面を追加
* f20c99c (HEAD -> main) ログイン画面を追加
* e563966 最初のコミット
何も付けない場合:早送りになり、main が先へ進むだけ
何も付けずにマージすると、出力に Fast-forward と出ました。
$ git merge feature/search
Updating f20c99c..96a7a06
Fast-forward
src/search.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 src/search.txt
マージの後の図は、線の形がマージの前とまったく同じです。変わったのは、main の位置が f20c99c から 96a7a06 に移ったことだけで、新しいコミットはできていません。
$ git log --oneline --graph --decorate --all
* 96a7a06 (HEAD -> main, feature/search) 検索に絞り込みを追加
* 483bc38 検索画面を追加
* f20c99c ログイン画面を追加
* e563966 最初のコミット
git merge の公式リファレンスでは、この動きを「早送りマージ」と呼んでいます。今のブランチの先端が、取り込むコミットの祖先になっているときは、まとめた履歴を残すための新しいコミットは要らないので、HEAD を取り込むコミットに移すだけ、という説明です。
–ff-only の場合:早送りできるので、何も付けないときと同じ
同じ履歴を作り直して --ff-only を付けると、出力も図も、何も付けないときと同じでした。早送りで済む履歴では、--ff-only を付けても、何も付けないマージと区別がつきません。
$ git merge --ff-only feature/search
Updating f20c99c..96a7a06
Fast-forward
src/search.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 src/search.txt
$ git log --oneline --graph --decorate --all
* 96a7a06 (HEAD -> main, feature/search) 検索に絞り込みを追加
* 483bc38 検索画面を追加
* f20c99c ログイン画面を追加
* e563966 最初のコミット
–no-ff の場合:早送りできても、マージコミットを作る
同じ履歴を作り直して --no-ff を付けました。--no-edit は、マージコミットのメッセージを決める画面(エディター)を開かず、自動で作られるメッセージをそのまま使うためのオプションです。出力の1行目が Merge made by the 'ort' strategy. に変わりました。
$ git merge --no-ff --no-edit feature/search
Merge made by the 'ort' strategy.
src/search.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 src/search.txt
図には、新しいコミット 7685479 ができ、feature/search の2件が横に分かれた線の上に並びました。feature/search は 96a7a06 を指したままで、main だけが新しいマージコミットに進んでいます。
$ git log --oneline --graph --decorate --all
* 7685479 (HEAD -> main) Merge branch 'feature/search'
|\
| * 96a7a06 (feature/search) 検索に絞り込みを追加
| * 483bc38 検索画面を追加
|/
* f20c99c ログイン画面を追加
* e563966 最初のコミット
%p で親のハッシュを出すと、マージコミットは、マージする前の main の先端(f20c99c)と feature/search の先端(96a7a06)の2つを親に持っていました。早送りで進んだ場合に main が指す 96a7a06 は、親が1つの普通のコミットのままです。上が --no-ff でマージした main、下が早送りでマージした main の出力です。
$ git log -1 --format="%h 親: %p %s" main
7685479 親: f20c99c 96a7a06 Merge branch 'feature/search'
$ git log -1 --format="%h 親: %p %s" main
96a7a06 親: 483bc38 検索に絞り込みを追加
ここまでの3つの結果を、図にまとめると次のようになります。

–no-ff で残したマージコミットは、–first-parent で1件にまとまる
--no-ff で残したマージコミットが役に立つのは、main の履歴を「どの作業ブランチを、いつ取り込んだか」の単位で見たいときです。git log --first-parent は、マージコミットでは1つめの親だけをたどって表示します。
--no-ff でマージした main では、feature/search の2件が表に出ず、マージコミットの1件にまとまりました。
$ git log --oneline --first-parent main
7685479 Merge branch 'feature/search'
f20c99c ログイン画面を追加
e563966 最初のコミット
早送りでマージした main では、feature/search で作ったコミットが、main に直接コミットしたものと見分けがつかない形で並びます。
$ git log --oneline --first-parent main
96a7a06 検索に絞り込みを追加
483bc38 検索画面を追加
f20c99c ログイン画面を追加
e563966 最初のコミット
マージコミットとその1つめの親(HEAD^1)の差分を見ると、feature/search で変えた分だけが出ました。--no-ff で残しておけば、ブランチ1本分の変更を、あとから1回の git diff でまとめて見られます。
$ git diff --stat HEAD^1 HEAD
src/search.txt | 1 +
1 file changed, 1 insertion(+)
枝分かれした履歴では、何も付けなくてもマージコミットができる
次は、main にも1件(c0131cd)足して、枝分かれさせた履歴です。この形では、main の先端が feature/search の祖先ではないので、早送りはできません。
$ git log --oneline --graph --decorate --all
* c0131cd (HEAD -> main) READMEに使い方の見出しを追加
| * 96a7a06 (feature/search) 検索に絞り込みを追加
| * 483bc38 検索画面を追加
|/
* f20c99c ログイン画面を追加
* e563966 最初のコミット
何も付けない場合と –no-ff の場合:同じマージコミットができる
何も付けずにマージすると、2つの線を合わせるマージコミット f7ec9bf ができました。
$ git merge --no-edit feature/search
Merge made by the 'ort' strategy.
src/search.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 src/search.txt
$ git log --oneline --graph --decorate --all
* f7ec9bf (HEAD -> main) Merge branch 'feature/search'
|\
| * 96a7a06 (feature/search) 検索に絞り込みを追加
| * 483bc38 検索画面を追加
* | c0131cd READMEに使い方の見出しを追加
|/
* f20c99c ログイン画面を追加
* e563966 最初のコミット
同じ履歴を作り直して --no-ff を付けても、出力と図は同じで、マージコミットのハッシュまで f7ec9bf で一致しました。早送りできない履歴では、何も付けない場合と --no-ff の違いはありません。
$ git merge --no-ff --no-edit feature/search
Merge made by the 'ort' strategy.
src/search.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 src/search.txt
$ git log --oneline --graph --decorate --all
* f7ec9bf (HEAD -> main) Merge branch 'feature/search'
|\
| * 96a7a06 (feature/search) 検索に絞り込みを追加
| * 483bc38 検索画面を追加
* | c0131cd READMEに使い方の見出しを追加
|/
* f20c99c ログイン画面を追加
* e563966 最初のコミット
–ff-only の場合:マージせずに止まる
--ff-only を付けると、ヒントのあとに fatal: Not possible to fast-forward, aborting. と出て、終了コードは 128 でした。
$ git merge --ff-only feature/search; echo "終了コード: $?"
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.
終了コード: 128
main は c0131cd を指したままで、作業ツリーにも変更は残っていません。図もマージする前と同じです。
$ git rev-parse --short main
c0131cd
$ git status --short --branch
## main
$ git log --oneline --graph --decorate --all
* c0131cd (HEAD -> main) READMEに使い方の見出しを追加
| * 96a7a06 (feature/search) 検索に絞り込みを追加
| * 483bc38 検索画面を追加
|/
* f20c99c ログイン画面を追加
* e563966 最初のコミット
ヒントには、先へ進む方法として git merge --no-ff(マージコミットを作る)と git rebase が挙がっています。--ff-only は、早送りできない場面でいったん止めて、どうするかを自分で選びたいときに使えます。
2つの履歴での結果を表にする
| オプション | 一直線の履歴(早送りできる) | 枝分かれした履歴(早送りできない) |
|---|---|---|
| 何も付けない | 早送り。マージコミットはできない | マージコミットを作る |
--no-ff |
マージコミットを作る | マージコミットを作る |
--ff-only |
早送り。マージコミットはできない | 止まる(終了コード 128) |
表の内容は、公式リファレンスの --ff、--no-ff、--ff-only の説明と合っています(終了コードの 128 は検証で出た値で、リファレンスには「0 でない終了コード」とだけ書かれています)。何も付けないときは --ff と同じで、早送りできるなら早送りし、できないならマージコミットを作ります。ただし、refs/tags/ の下の本来の場所にない注釈付きタグをマージするときは、既定が --no-ff になると書かれています。
毎回付けるのが面倒なら、設定の merge.ff で決める
設定の merge.ff を false にすると、何も付けないときの動きが --no-ff と同じになり、only にすると --ff-only と同じになります。
一直線の履歴で merge.ff を false にしてから何も付けずにマージすると、--no-ff を付けたときと同じマージコミット 7685479 ができました。
$ git config merge.ff false
$ git merge --no-edit feature/search
Merge made by the 'ort' strategy.
src/search.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 src/search.txt
$ git log --oneline --graph --decorate --all
* 7685479 (HEAD -> main) Merge branch 'feature/search'
|\
| * 96a7a06 (feature/search) 検索に絞り込みを追加
| * 483bc38 検索画面を追加
|/
* f20c99c ログイン画面を追加
* e563966 最初のコミット
枝分かれした履歴で merge.ff を only にすると、何も付けない git merge が --ff-only と同じエラーで止まりました。ヒントの行は上と同じなので、次の出力では省いています。設定を変えずに、その場だけ --no-ff を付けると、マージコミットを作れました。コマンドラインで付けたオプションが、設定より優先されています。
$ git config merge.ff only
$ git merge --no-edit feature/search; echo "終了コード: $?"
fatal: Not possible to fast-forward, aborting.
終了コード: 128
$ git merge --no-ff --no-edit feature/search; echo "終了コード: $?"
Merge made by the 'ort' strategy.
src/search.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 src/search.txt
終了コード: 0
検証では --global を付けず、そのリポジトリだけの設定にしました。git config --global merge.ff false のように --global を付けると、公式リファレンスのとおり、設定は自分のユーザーの設定ファイル(~/.gitconfig)に書かれます。




