通常の git rebase と同様に、git with は--preserve-merges
最初にコミット グラフの一部で行われたコミットのリストを識別し、次にそれらのコミットを別の部分の上で再生します。--preserve-merges
リプレイ用に選択されるコミットと、そのリプレイがマージ コミットに対してどのように機能するかに関する違い。
通常のリベースとマージ保存リベースの主な違いをより明確にするには:
- 通常のリベースはマージ コミットを完全に無視しますが、マージを維持するリベースは (一部の) マージ コミットをリプレイします。
- マージ コミットをリプレイする意思があるため、マージを保持するリベースでは、マージ コミットをリプレイすることの意味を定義し、いくつかの余分な問題に対処する必要があります。
- 概念的に最も興味深い部分は、おそらく、新しいコミットのマージの親がどうあるべきかを選択することです。
- マージ コミットを再生するには、特定のコミット (
git checkout <desired first parent>
) を明示的にチェックアウトする必要もありますが、通常のリベースではそれについて心配する必要はありません。
- マージ保存リベースでは、リプレイ用に浅いコミット セットが考慮されます。
- 特に、最新のマージ ベース (つまり、2 つのブランチが分岐した最新の時間) 以降に行われたコミットのリプレイのみが考慮されますが、通常のリベースでは、2 つのブランチが最初に分岐した時点までさかのぼってコミットがリプレイされる可能性があります。
- 暫定的で不明確ですが、これは最終的には、マージコミットに既に「組み込まれている」「古いコミット」の再生を除外する手段であると考えています。
最初に、rebase が行うことを「十分に正確に」説明--preserve-merges
してから、いくつかの例を示します。もちろん、それがより有用であると思われる場合は、例から始めることができます。
「ブリーフ」のアルゴリズム
本格的に雑草を掘り下げたい場合は、git ソースをダウンロードしてファイルを調べてgit-rebase--interactive.sh
ください。(リベースは 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 とのマージ コミットです。
通常の非マージ保持リベースを使用して、マスター (C) の上にトピック (H) をリベースするとします。(たとえば、checkout topic; rebase master。) その場合、git はリプレイ用に次のコミットを選択します。
次に、コミット グラフを次のように更新します。
B---C <-- master
/ \
A D'---E'---F'---G'---H' <-- topic
(D' は D などの再生に相当するものです。)
マージ コミット m はリプレイ用に選択されていないことに注意してください。
代わりに--preserve-merges
、C の上に H のリベースを行った場合 (たとえば、checkout トピック; リベース --preserve-merges master )。この新しいケースでは、git はリプレイ用に次のコミットを選択します。
- Dを選ぶ
- Eを選ぶ
- F を選択します (「サブトピック」ブランチの D に)
- G を選択します (「サブトピック」ブランチの F に)
- pick ブランチ「サブトピック」をトピックにマージ
- Hを選ぶ
今度は mがリプレイに選ばれました。また、マージの親 E と G は、マージ コミット m の前に含めるために選択されたことにも注意してください。
結果のコミット グラフは次のとおりです。
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--/
C (マスター) の上に H (トピック) をリベースする場合、リベースに選択されるコミットは次のとおりです。
- Dを選ぶ
- Eを選ぶ
- Fを選ぶ
- Gを選ぶ
- 私を選ぶ
- Hを選ぶ
結果は次のようになります。
B---C <-- master
/ | \
A | D'----E'---m'----H' <-- topic
\ |
F'----G'---/
例 3
上記の例では、元のマージ コミットが持つ元の親ではなく、マージ コミットとその 2 つの親の両方が再生されたコミットです。ただし、他のリベースでは、リプレイされたマージ コミットは、マージ前にコミット グラフに既に存在していた親で終了する可能性があります。
たとえば、次のことを考慮してください。
B--C---D <-- master
/ \
A---E--m------F <-- topic
トピックをマスターにリベースする (マージを保持する) 場合、再生するコミットは次のようになります。
書き直されたコミット グラフは次のようになります。
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 で統合された変更 (B, F) は、既に D に組み込まれている必要があることに注意してください (m2 は B と F の子を一緒にマージするため、これらの変更は既に m2 に組み込まれている必要があります)。 D はおそらく何もしないか、空のコミット (つまり、連続するリビジョン間の差分が空であるコミット) を作成する必要があります。
ただし、代わりに、git は D の上で m1 を再生しようとする試みを拒否する場合があります。次のようなエラーが発生する可能性があります。
error: Commit 90caf85 is a merge but no -m option was given.
fatal: cherry-pick failed
フラグを git に渡すのを忘れたように見えますが、根本的な問題は、git が空のコミットの作成を嫌うということです。