git mergeの–no-ffと–ff-onlyの違いを図で見る

​​Git​コマンドライン

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 は自分の先にそのままつながっている状態です。

console
$ git log --oneline --graph --decorate --all
* 96a7a06 (feature/search) 検索に絞り込みを追加
* 483bc38 検索画面を追加
* f20c99c (HEAD -> main) ログイン画面を追加
* e563966 最初のコミット

何も付けない場合:早送りになり、main が先へ進むだけ

何も付けずにマージすると、出力に Fast-forward と出ました。

console
$ 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 に移ったことだけで、新しいコミットはできていません。

console
$ git log --oneline --graph --decorate --all
* 96a7a06 (HEAD -> main, feature/search) 検索に絞り込みを追加
* 483bc38 検索画面を追加
* f20c99c ログイン画面を追加
* e563966 最初のコミット

git merge の公式リファレンスでは、この動きを「早送りマージ」と呼んでいます。今のブランチの先端が、取り込むコミットの祖先になっているときは、まとめた履歴を残すための新しいコミットは要らないので、HEAD を取り込むコミットに移すだけ、という説明です。

–ff-only の場合:早送りできるので、何も付けないときと同じ

同じ履歴を作り直して --ff-only を付けると、出力も図も、何も付けないときと同じでした。早送りで済む履歴では、--ff-only を付けても、何も付けないマージと区別がつきません。

console
$ 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. に変わりました。

console
$ 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 だけが新しいマージコミットに進んでいます。

console
$ 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 の出力です。

console
$ git log -1 --format="%h 親: %p  %s" main
7685479 親: f20c99c 96a7a06  Merge branch 'feature/search'
console
$ git log -1 --format="%h 親: %p  %s" main
96a7a06 親: 483bc38  検索に絞り込みを追加

ここまでの3つの結果を、図にまとめると次のようになります。

一直線の履歴をマージした結果の図。マージする前は main が f20c99c、feature/search が 96a7a06 を指している。何も付けないか --ff-only の場合は、線はそのままで main が 96a7a06 まで進む。--no-ff の場合は、f20c99c と 96a7a06 を親に持つマージコミット 7685479 ができ、main がそれを指す
一直線の履歴に対する3つのマージの結果。ピンクは、マージで位置が変わった main と、新しくできたマージコミット

–no-ff で残したマージコミットは、–first-parent で1件にまとまる

--no-ff で残したマージコミットが役に立つのは、main の履歴を「どの作業ブランチを、いつ取り込んだか」の単位で見たいときです。git log --first-parent は、マージコミットでは1つめの親だけをたどって表示します。

--no-ff でマージした main では、feature/search の2件が表に出ず、マージコミットの1件にまとまりました。

console
$ git log --oneline --first-parent main
7685479 Merge branch 'feature/search'
f20c99c ログイン画面を追加
e563966 最初のコミット

早送りでマージした main では、feature/search で作ったコミットが、main に直接コミットしたものと見分けがつかない形で並びます。

console
$ git log --oneline --first-parent main
96a7a06 検索に絞り込みを追加
483bc38 検索画面を追加
f20c99c ログイン画面を追加
e563966 最初のコミット

マージコミットとその1つめの親(HEAD^1)の差分を見ると、feature/search で変えた分だけが出ました。--no-ff で残しておけば、ブランチ1本分の変更を、あとから1回の git diff でまとめて見られます。

console
$ git diff --stat HEAD^1 HEAD
 src/search.txt | 1 +
 1 file changed, 1 insertion(+)

枝分かれした履歴では、何も付けなくてもマージコミットができる

次は、main にも1件(c0131cd)足して、枝分かれさせた履歴です。この形では、main の先端が feature/search の祖先ではないので、早送りはできません。

console
$ git log --oneline --graph --decorate --all
* c0131cd (HEAD -> main) READMEに使い方の見出しを追加
| * 96a7a06 (feature/search) 検索に絞り込みを追加
| * 483bc38 検索画面を追加
|/  
* f20c99c ログイン画面を追加
* e563966 最初のコミット

何も付けない場合と –no-ff の場合:同じマージコミットができる

何も付けずにマージすると、2つの線を合わせるマージコミット f7ec9bf ができました。

console
$ 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 の違いはありません。

console
$ 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 でした。

console
$ 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 を指したままで、作業ツリーにも変更は残っていません。図もマージする前と同じです。

console
$ 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 ができました。

console
$ 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 を付けると、マージコミットを作れました。コマンドラインで付けたオプションが、設定より優先されています。

console
$ 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)に書かれます。

​Posted by 86