図で分かるgit-mergeの--ff, --no-ff, --squashの違い

git-merge の--ff, --no-ff, --squashの違いをまとめてみた。

git helpから引用

まずは、git helpを読みましょう

  • git merge --helpから引用(抜粋)
NAME
       git-merge - Join two or more development histories together

SYNOPSIS
       git merge [-n] [--stat] [--no-commit] [--squash]
               [-s <strategy>] [-X <strategy-option>]
               [--[no-]rerere-autoupdate] [-m <msg>] <commit>...
       git merge <msg> HEAD <commit>...
       git merge --abort
OPTIONS
       --ff, --no-ff
           Do not generate a merge commit if the merge resolved as a fast-forward, only update the branch pointer. This is the default behavior of git-merge.

           With --no-ff Generate a merge commit even if the merge resolved as a fast-forward.
       --squash, --no-squash
           Produce the working tree and index state as if a real merge happened (except for the merge information), but do not actually make a commit or move the HEAD, nor record
           $GIT_DIR/MERGE_HEAD to cause the next git commit command to create a merge commit. This allows you to create a single commit on top of the current branch whose effect is the same as
           merging another branch (or more in case of an octopus).

           With --no-squash perform the merge and commit the result. This option can be used to override --squash.
       --ff-only
           Refuse to merge and exit with a non-zero status unless the current HEAD is already up-to-date or the merge can be resolved as a fast-forward.

fast-forward(ファーストフォワード)って何?

マージの説明の前に、まずはGitのfast-forwardについて。

  • 入門Git p.230 から引用
  2つのコミットAとBとがあるときに、コミットAにいたる歴史がコミットBにいたる歴
史にすべて含まれている場合、2つのコミットはファーストフォワードの関係にある、と
か、コミットAはコミットBにファーストフォワードする、といいます。

少し分かりにくいので、絵にしてみた。

fast-forwardな関係

  • コミットAの歴史: X -> A
  • コミットBの歴史: X -> A -> Y -> B

コミットBの歴史に、コミットAの歴史が全て含まれている。

fast-forwardでない関係

  • コミットAの歴史: X -> Y -> A
  • コミットBの歴史: X -> Y -> Z -> B

コミットBの歴史に、コミットAの歴史が含まれていない(Aのコミットが含まれていない)

各マージの解説

本題の各マージについて。

0. マージ前の状態

マージ前は下記のようなコミットグラフだったとします。

この時、各オプションを指定した場合のマージ結果が1〜3です。

1. --ff

デフォルトの設定です。
もしマージ対象のブランチとfast-forwardの関係にある場合、マージコミットは作られず、ブランチの参照先の更新だけを行います。
(fast-forwardの関係でない場合、マージコミットが作られます)

$ git merge topic


2. --no-ff

fast-forwardの関係であっても、必ずマージコミットを作ります。

$ git merge --no-ff topic


3. --squash

ワークツリーとインデックスの状態はマージ後の状態になりますが、マージコミットは作りません。

$ git merge --squash topic

マージコミットが作られないため、--squashの場合は自分でコミットする必要があります。

$ git commit

各オプションの使い分けの前に

まずはプロジェクトのリポジトリの運用について決める必要があります。

  • コミットの粒度
  • rebase は必須であるか?
  • トピックブランチのマージの時にマージコミットを作るか?

などを決め、プロジェクトのポリシーに沿ったコミットログを作るのが良いと思います。

オプションの使い分け

私はこうやって使っている・・・という例を紹介します。
他の便利な例や突っ込みがあれば、コメントで。

--ff

ログが一本になり、トピックブランチがあったという情報がなくなる。
トピックブランチの存在をログに残す必要がなく、そのままマージしてもコミット粒度が問題無い場合に使う。

--no-ff

--ffと異なり、トピックブランチがあったという情報が残る。
基本的には「A successful Git branching model」を参考に--no-ffを使うのが良い。

--squash

トピックブランチがあった情報も、コミットログも全て残らない。
トピックブランチで行った作業が少なく、1コミットにまとめてしまっても問題ない場合に使う。

git mergeで常に--no-ffにする

ついでに、常に--no-ffにする設定を紹介します。
この方法は2つあります。

方法1. エイリアスを使う

エイリアスを設定し、--no-ffのマージを行う時はgit nffmerge を使うようにする。

$ git config --global alias.nffmerge "merge --no-ff"

方法2. mergeoptions

マージオプションを設定すると、指定したブランチのマージでは設定値がデフォルトとして使用されます。はブランチ名。

git config branch.<name>.mergeoptions "--no-ff"

ただし、この設定を行うとgit pull の実行時にもマージコミットが出来てしまい、マージコミットが大量に出来てしまうため、リモートの変更を取得したい時はgit pull --rebase を使う必要があります。

特定のブランチで必ずpull --rebase を有効にする

次のコマンドで設定できる。

$ git config branch.<name>.rebase true
全てのブランチでpull --rebase を有効にする

もし全てのブランチで設定したい場合、次のコマンドで設定する。

$ git config branch.autosetuprebase always