git の「rebase --preserve-merges」は具体的に何をするのでしょうか (また、その理由は?) 質問する

git の「rebase --preserve-merges」は具体的に何をするのでしょうか (また、その理由は?) 質問する

ギッツrebaseコマンドのドキュメント非常に簡潔です:

--preserve-merges
    Instead of ignoring merges, try to recreate them.

This uses the --interactive machinery internally, but combining it
with the --interactive option explicitly is generally not a good idea
unless you know what you are doing (see BUGS below).

では、 を使用すると実際に何が起こるのでしょうか--preserve-merges? デフォルトの動作 (そのフラグなし) とどう違うのでしょうか? マージなどを「再作成する」とはどういう意味でしょうか?

ベストアンサー1

通常の git rebase と同様に、 git は--preserve-mergesまずコミット グラフの一部で行われたコミットのリストを識別し、次にそれらのコミットを別の部分の上に再生します。 との違いは、--preserve-mergesどのコミットが再生対象として選択されるか、およびその再生がマージ コミットに対してどのように機能するかに関するものです。

通常のリベースとマージ保持リベースの主な違いをより明確に説明します。

  • マージ保持リベースでは、(一部の)マージ コミットを再実行しますが、通常のリベースではマージ コミットが完全に無視されます。
  • マージコミットをリプレイする可能性があるため、マージ保持リベースでは、マージコミットをリプレイする意味を定義し、いくつかの追加の問題に対処する 必要があります。
    • 概念的に最も興味深い部分は、おそらく、新しいコミットのマージの親が何になるかを選択することです。
    • マージコミットを再生するには、特定のコミット ( ) を明示的にチェックアウトする必要もありますgit checkout <desired first parent>が、通常のリベースではそのことを心配する必要はありません。
  • マージ保持リベースでは、再生のためにより浅いコミットのセットを考慮します。
    • 特に、通常のリベースでは、2 つのブランチが最初に分岐した時点までさかのぼってコミットが再生される可能性があるのに対し、最新のマージ ベース (つまり、 2 つのブランチが分岐した最新の時点)以降に行われたコミットの再生のみを考慮します。
    • 暫定的で不明瞭ですが、これは最終的には、マージ コミットにすでに「組み込まれている」古いコミットの再生を除外する手段であると考えています。

まず、rebase が何をするのかを「十分に正確に」説明し--preserve-merges、その後でいくつか例を挙げます。もちろん、例の方が役に立つと思われる場合は、例から始めてもかまいません。

「Brief」のアルゴリズム

本当に詳細を知りたい場合は、git ソースをダウンロードしてファイルを調べてくださいgit-rebase--interactive.sh。(Rebase は Git の C コアの一部ではなく、bash で書かれています。また、舞台裏では「インタラクティブ リベース」とコードを共有しています。)

しかし、ここでは、その本質だと思われるものを概説します。考えるべきことの数を減らすために、私はいくつかの自由を取っています。(たとえば、計算が行われる正確な順序を 100% の精度で捉えようとはせず、ブランチ間で既にチェリーピックされたコミットをどうするかなど、あまり重要ではないと思われるトピックを無視します)。

まず、マージを保持しないリベースはかなり単純であることに注意してください。大体次のようになります。

Find all commits on B but not on A ("git log A..B")
Reset B to A ("git reset --hard A") 
Replay all those commits onto B one at a time in order.

リベース--preserve-mergesは比較的複雑です。ここでは、かなり重要と思われるものを失うことなく、できるだけシンプルにする方法を紹介します。

Find the commits to replay:
  First find the merge-base(s) of A and B (i.e. the most recent common ancestor(s))
    This (these) merge base(s) will serve as a root/boundary for the rebase.
    In particular, we'll take its (their) descendants and replay them on top of new parents
  Now we can define C, the set of commits to replay. In particular, it's those commits:
    1) reachable from B but not A (as in a normal rebase), and ALSO
    2) descendants of the merge base(s)
  If we ignore cherry-picks and other cleverness preserve-merges does, it's more or less:
    git log A..B --not $(git merge-base --all A B)
Replay the commits:
  Create a branch B_new, on which to replay our commits.
  Switch to B_new (i.e. "git checkout B_new")
  Proceeding parents-before-children (--topo-order), replay each commit c in C on top of B_new:
    If it's a non-merge commit, cherry-pick as usual (i.e. "git cherry-pick c")
    Otherwise it's a merge commit, and we'll construct an "equivalent" merge commit c':
      To create a merge commit, its parents must exist and we must know what they are.
      So first, figure out which parents to use for c', by reference to the parents of c:
        For each parent p_i in parents_of(c):
          If p_i is one of the merge bases mentioned above:
            # p_i is one of the "boundary commits" that we no longer want to use as parents
            For the new commit's ith parent (p_i'), use the HEAD of B_new.
          Else if p_i is one of the commits being rewritten (i.e. if p_i is in R):
            # Note: Because we're moving parents-before-children, a rewritten version
            # of p_i must already exist. So reuse it:
            For the new commit's ith parent (p_i'), use the rewritten version of p_i.
          Otherwise:
            # p_i is one of the commits that's *not* slated for rewrite. So don't rewrite it
            For the new commit's ith parent (p_i'), use p_i, i.e. the old commit's ith parent.
      Second, actually create the new commit c':
        Go to p_1'. (i.e. "git checkout p_1'", p_1' being the "first parent" we want for our new commit)
        Merge in the other parent(s):
          For a typical two-parent merge, it's just "git merge p_2'".
          For an octopus merge, it's "git merge p_2' p_3' p_4' ...".
        Switch (i.e. "git reset") B_new to the current commit (i.e. HEAD), if it's not already there
  Change the label B to apply to this new branch, rather than the old one. (i.e. "git reset --hard B")

引数付きのリベースも--onto C非常に似ています。コミットの再生を B の HEAD から開始する代わりに、C の HEAD からコミットの再生を開始します。(B_new の代わりに C_new を使用します。)

例1

例えば、コミットグラフを見てみましょう

  B---C <-- master
 /                     
A-------D------E----m----H <-- topic
         \         /
          F-------G

m は親 E と G とのマージコミットです。

通常のマージを保持しないリベースを使用して、トピック (H) をマスター (C) の上にリベースしたとします。(たとえば、checkout topic; rebase master ) この場合、git は次のコミットを再生用に選択します。

  • Dを選択
  • Eを選択
  • Fを選択
  • Gを選択
  • Hを選択

そして、コミット グラフを次のように更新します。

  B---C <-- master
 /     \                
A       D'---E'---F'---G'---H' <-- topic

(D' は D の再生版です。など)

マージコミット m は再生対象として選択されていないことに注意してください。

代わりに、C の上に H をリベースした場合--preserve-merges(たとえば、checkout topic; rebase --preserve-merges master )、この新しいケースでは、git は次のコミットを再生用に選択します。

  • Dを選択
  • Eを選択
  • F を選択 (「サブトピック」ブランチの D' に移動)
  • G を選択 (「サブトピック」ブランチの F に)
  • ブランチ「サブトピック」をトピックにマージする
  • Hを選択

ここで、m が再生対象として選択されました。また、マージ コミット m の前に、マージ親 E と G が含めるように選択されていることに注意してください。

結果のコミット グラフは次のとおりです。

 B---C <-- master
/     \                
A      D'-----E'----m'----H' <-- topic
        \          / 
         F'-------G'

繰り返しますが、D' は D の厳選された (つまり再作成された) バージョンです。E' などについても同様です。マスター上にないすべてのコミットが再生されています。E と G (m のマージの親) は両方とも E' と G' として再作成され、m' の親として機能します (リベース後、ツリーの履歴は同じままです)。

例2

通常のリベースとは異なり、マージ保持リベースでは、アップストリーム ヘッドの複数の子を作成できます。

たとえば、次のことを考慮してください。

  B---C <-- master
 /                     
A-------D------E---m----H <-- topic
 \                 |
  ------- F-----G--/ 

H (トピック) を C (マスター) の上にリベースする場合、リベースに選択されるコミットは次のようになります。

  • Dを選択
  • Eを選択
  • Fを選択
  • Gを選択
  • mを選ぶ
  • Hを選択

結果は次のようになります:

  B---C  <-- master
 /    | \                
A     |  D'----E'---m'----H' <-- topic
       \            |
         F'----G'---/

例3

上記の例では、マージ コミットとその 2 つの親は両方とも、元のマージ コミットが持つ元の親ではなく、再生されたコミットです。ただし、他のリベースでは、再生されたマージ コミットは、マージ前にコミット グラフに既に存在していた親を持つ可能性があります。

たとえば、次のことを考慮してください。

  B--C---D <-- master
 /    \                
A---E--m------F <-- topic

トピックをマスターにリベースすると(マージを保持)、再生するコミットは次のようになります。

  • 選択マージコミットm
  • Fを選択

書き換えられたコミット グラフは次のようになります。

                     B--C--D <-- master
                    /       \             
                   A-----E---m'--F'; <-- topic

ここで、再生されたマージコミット m' は、コミットグラフに以前から存在していた親、つまり D (マスターの HEAD) と E (元のマージコミット m の親の 1 つ) を取得します。

例4

マージ保持リベースは、特定の「空のコミット」の場合に混乱する可能性があります。少なくとも、これは Git の古いバージョン (例: 1.7.8) にのみ当てはまります。

次のコミット グラフを見てみましょう。

                   A--------B-----C-----m2---D <-- master
                    \        \         /
                      E--- F--\--G----/
                            \  \
                             ---m1--H <--topic

コミット m1 と m2 の両方に、B と F からのすべての変更が組み込まれていることに注意してください。

H (トピック) を D (マスター) に実行しようとするとgit rebase --preserve-merges、次のコミットが再生用に選択されます。

  • m1を選択
  • Hを選択

m1 で統合された変更 (B、F) は、すでに D に組み込まれている必要があることに注意してください。(m2 は B と F の子をマージするため、これらの変更はすでに m2 に組み込まれている必要があります。) したがって、概念的には、D 上で m1 を再生すると、何も実行されないか、空のコミット (つまり、連続するリビジョン間の差分が空であるコミット) が作成されます。

ただし、代わりに、git は D 上で m1 を再生する試みを拒否する場合があります。次のようなエラーが発生する可能性があります。

error: Commit 90caf85 is a merge but no -m option was given.
fatal: cherry-pick failed

git にフラグを渡すのを忘れたように見えますが、根本的な問題は、git が空のコミットを作成することを好まないことです。

おすすめ記事