web-dev-qa-db-ja.com

git rebaseとgit merge --ff-onlyに違いはありますか

私が読んだことから、どちらも線形の歴史を得るのに役立ちます。

私が実験したことから、リベースは常に機能します。ただし、マージ--ff-onlyは、早送りできるシナリオでのみ機能します。

また、git mergeはマージコミットを作成しますが、-ff-onlyを使用すると、本質的にgit rebasingと同等の線形履歴が得られます。 --ff-onlyはgit mergeの目的を殺しますよね?

それでは、実際の違いは何ですか?

43
phoenix

_git rebase_には_git merge_(_--ff-only_の有無にかかわらず)とは異なるjobがあることに注意してください。 rebaseが行うことは、既存のコミットを取得してcopyすることです。たとえば、_branch1_を使用しており、ABの2つのコミットを行ったとします。

_...-o--o--A--B   <-- HEAD=branch1
        \
         o--C    <-- branch2
_

代わりに、これらの2つのコミットを_branch2_で実行することを決定します。あなたはできる:

  • Aで行った変更のリストを取得します(その親に対してAを差分します)
  • Bで行った変更のリストを取得します(Bとの差分A
  • _branch2_に切り替えます
  • Aで行ったのと同じ変更を加えてコミットし、Aからコミットメッセージをコピーします。このコミットを_A'_と呼びましょう
  • その後、Bで行ったのと同じ変更を加えてコミットし、Bからコミットメッセージをコピーします。これを_B'_と呼びましょう。

このdiff-and-then-copy-and-commitを実行するgitコマンドがあります:_git cherry-pick_。そう:

_git checkout branch2      # switch HEAD to branch2 (commit C)
git cherry-pick branch1^  # this copies A to A'
git cherry-pick branch1   # and this copies B
_

今、あなたはこれを持っています:

_...-o--o--A--B         <-- branch1
        \
         o--C--A'-B'   <-- HEAD=branch2
_

これで、_branch1_に切り替えて、元のABを削除できます。_git reset_を使用します(ここでは_--hard_を使用します。ワークツリーをクリーンアップするので便利です)も)):

_git checkout branch1
git reset --hard HEAD~2
_

これにより、元のAおよびBが削除されます。1 だから今あなたは持っています:

_...-o--o               <-- HEAD=branch1
        \
         o--C--A'-B'   <-- branch2
_

ここで作業を続けるには、_branch2_を再チェックアウトするだけです。

これは_git rebase_が行うことです:コミットを「移動」します(ただし、実際に移動することはできません。なぜなら、gitではコミットは変更できないため、親IDを変更するだけでもコピーが必要です新しいわずかに異なるコミット)。

つまり、_git cherry-pick_はoneコミットの自動化された差分とやり直しですが、_git rebase_は自動化されたやり直しプロセスmultipleコミット、さらに、最後に、ラベルを移動して「忘れる」か、オリジナルを隠します。

上記は、1つのローカルブランチ_branch1_から別のローカルブランチ_branch2_へのコミットの移動を示していますが、gitはexact same processを使用して、 _git fetch_(_git pull_の最初のステップであるfetchを含む)を実行したときにいくつかの新しいコミットを取得するリモート追跡ブランチ。 _Origin/feature_のアップストリームを持つブランチfeatureで作業を開始し、独自のコミットをいくつか行うことができます。

_...-o        <-- Origin/feature
     \
      A--B   <-- HEAD=feature
_

しかし、その後、上流で何が起こったのかを見る必要があると判断したので、_git fetch_を実行します。2 そして、ああ、上流の誰かがコミットCを書いた:

_...-o--C     <-- Origin/feature
     \
      A--B   <-- HEAD=feature
_

この時点で、featureABCにリベースするだけで、次のようになります:

_...-o--C     <-- Origin/feature
        \
         A'-B'  <-- HEAD=feature
_

これらは、元のAおよびBのコピーであり、コピーが完了すると元のファイルは破棄されます(ただし脚注1を参照)。


場合によっては、リベースするものがない、つまり、あなた自身が行った作業がないことがあります。つまり、fetchの前のグラフは次のようになります。

_...-o      <-- Origin/feature
           `-- HEAD=feature
_

_git fetch_をコミットしてCがコミットされた場合、yourfeatureブランチは古いコミットを指し、_Origin/feature_は移動しますフォワード:

_...-o--C   <-- Origin/feature
     `---- <-- HEAD=feature
_

これが_git merge --ff-only_の出番です:現在のブランチfeatureを_Origin/feature_とマージするように要求すると、gitは矢印をそのまま前にスライドすることが可能であることを確認し、featureが直接コミットを指すようにしますC。実際のマージは必要ありません。

ただし、独自のコミットAおよびBがあり、それらをCとマージするように要求した場合、gitは実際のマージを行い、新しいマージコミットMを作成します。

_...-o--C        <-- Origin/feature
     \   `-_
      A--B--M   <-- feature
_

ここでは、_--ff-only_が停止し、エラーが発生します。一方、リベースでは、AおよびBを_A'_および_B'_にコピーし、元のAおよびBを非表示にすることができます。

つまり、要するに(大丈夫、遅すぎる:-))、彼らは単に異なることをします。結果が同じ場合もあれば、そうでない場合もあります。 AおよびBをコピーしてもよい場合は、_git rebase_を使用できます。しかし、何らかの理由notをコピーする場合は、_git merge_を使用して、おそらく_--ff-only_を使用して、必要に応じてマージまたは失敗できます。


1Gitは、実際にはしばらく(通常はこの場合は1か月間)オリジナルを保持しますが、非表示にします。それらを見つける最も簡単な方法は、各変更がブランチやHEADを更新する前に、各ブランチがポイントした場所とHEADがポイントした場所の履歴を保持するgitの「reflogs」を使用することです。

最終的に、reflog履歴エントリは期限切れになり、その時点でこれらのコミットは garbage collection の対象となります。

2または、再び_git pull_を使用できます。これは、_git fetch_を実行することで開始される便利なスクリプトです。フェッチが完了すると、コンビニエンススクリプトは、構成および実行方法に応じて、_git merge_または_git rebase_を実行します。

86
torek

はい、違いがあります。 git merge --ff-onlyは、早送りできない場合は中止し、マージ(通常はブランチ)してマージします。早送りできない場合にのみマージコミットを作成します(つまり、--ff-only)。

git rebaseは、現在のブランチの履歴を書き換えます。または、既存のブランチを既存のブランチにリベースするために使用できます。その場合、マージではなくリベースであるため、マージコミットは作成されません。

5
abligh

はい、 --ff-onlyは、常にgit mergeは失敗し、プレーンgit mergeは成功します。それがポイントです-線形の履歴を保持しようとしていて、マージがそのようにできない場合、want失敗します。

失敗のケースをコマンドに追加するオプションは無駄ではありません。これは前提条件を検証する方法であるため、システムの現在の状態が予期したものでない場合、問題を悪化させることはありません。

3
user2404501