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
を使うとき実際に何が起こりますか?デフォルトの動作(そのフラグなし)とどう違うのですか?マージなどを「再作成する」とはどういう意味ですか。
通常のgit rebaseと同様に、--preserve-merges
を伴うgitは最初にコミットグラフの一部で行われたコミットのリストを識別し、そして次にそれらのコミットを別の部分の上で再生します。 --preserve-merges
との違いは、どのコミットが再生のために選択されるか、そしてその再生がマージコミットのためにどのように機能するかに関係しています。
通常のリベースとマージ保存リベースの主な違いについてより明確にするために、
git checkout <desired first parent>
)を明示的にチェックアウトする必要もありますが、通常のリベースではそれを心配する必要はありません。最初に、私は--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は再生用に次のコミットを選択します。
そしてコミットグラフを次のように更新します。
B---C <-- master
/ \
A D'---E'---F'---G'---H' <-- topic
(D 'はDなどの再生されたものと同じです。)
マージコミットmは再生用に選択されていないことに注意してください。
代わりにCの上にHの--preserve-merges
リベースを行った場合(たとえば、チェックアウトトピック、rebase --preserve-merges master)、この新しい場合、gitは次のように選択します。再生をコミットします。
現在、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(マスター)の上にリベースすると、リベースに選択されたコミットは次のようになります。
結果は次のようになります。
B---C <-- master
/ | \
A | D'----E'---m'----H' <-- topic
\ |
F'----G'---/
例
上記の例では、元のマージコミットが持つ元の親ではなく、マージコミットとその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はm1をDの上で再生しようとする試みを拒絶するかもしれません。
error: Commit 90caf85 is a merge but no -m option was given.
fatal: cherry-pick failed
Gitにフラグを渡すのを忘れたように見えますが、根本的な問題はgitが空のコミットを作成するのを嫌うということです。