web-dev-qa-db-ja.com

git "rebase --preserve-merges"は正確には何をしますか(そしてそれはなぜですか?)

Gitの 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を使うとき実際に何が起こりますか?デフォルトの動作(そのフラグなし)とどう違うのですか?マージなどを「再作成する」とはどういう意味ですか。

313
Chris

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

通常のリベースとマージ保存リベースの主な違いについてより明確にするために、

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

最初に、私は--preserve-mergesのリベースが何をするのかを「十分に正確に」説明しようと試みます、そして次にいくつかの例があるでしょう。それがより有用であると思われるならば、もちろん例から始めることができます。

「概要」のアルゴリズム

あなたが本当に雑草に入りたいのなら、gitのソースをダウンロードしてgit-rebase--interactive.shというファイルを調べてください。 (RebaseはGitのCコアの一部ではありませんが、むしろbashで書かれています。そして、舞台裏では、それはコードを "interactive rebase"と共有しています。)

しかし、ここで私は私がそれの本質だと思うものをスケッチします。考えることの数を減らすために、私はいくつかの自由を取りました。 (例えば、私は計算が行われる正確な順序を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.

Rebase --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引数を使用したrebaseは非常によく似ているはずです。 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)の上にリベースしたとします。 (たとえば、チェックアウトトピック、マスターリベースなど)その場合、gitは再生用に次のコミットを選択します。

  • dを選ぶ
  • eを選ぶ
  • fを選ぶ
  • gを選ぶ
  • hを選ぶ

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

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

(D 'はDなどの再生されたものと同じです。)

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

代わりにCの上にHの--preserve-mergesリベースを行った場合(たとえば、チェックアウトトピック、rebase --preserve-merges master)、この新しい場合、gitは次のように選択します。再生をコミットします。

  • dを選ぶ
  • eを選ぶ
  • fを選ぶ( 'subtopic'ブランチのD 'へ)
  • gを選ぶ( 'subtopic'ブランチのF 'へ)
  • ブランチ「サブトピック」をトピックにマージ
  • hを選ぶ

現在、mが再生用に選択されています。また、マージコミットmの前にマージ親EとGが包含対象として選択されたことにも注意してください。

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

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

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

例2

通常のリベースとは異なり、マージ保存リベースでは上流のヘッドの子を複数作成できます。

たとえば、

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

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

  • dを選ぶ
  • eを選ぶ
  • fを選ぶ
  • gを選ぶ
  • メートルを選ぶ
  • hを選ぶ

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

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

上記の例では、元のマージコミットが持つ元の親ではなく、マージコミットとその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はおそらく何もしないか、空のコミット(つまり、連続するリビジョン間の差分が空のコミット)を作成するかのどちらかです。

しかしその代わりに、gitはm1をDの上で再生しようとする試みを拒絶するかもしれません。

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

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

426
Chris