Alan <alan@clueserver.org> said:
masterブランチがあります。一部の開発者が取り組んでいるブランチがあります。
彼らはそれが準備ができていると主張します。我々はそれをmasterブランチにマージします。
そしたら何かが壊れたので、我々はマージを元に戻します。
彼らはコードに変更を加えます。
彼らが大丈夫だと言うところまでそれを取得し、そして我々は再びマージします。
調べてみると、
戻す前に行われたコード変更はmasterブランチにはありませんが、
その後のコード変更はmasteerブランチにあります。
そして、この状況から回復するための助けを求めています。
「マージの戻し」直後の履歴は以下のようになります:
---o---o---o---M---x---x---W
/
---A---B
ここで、AとBはあまり良くなかった傍流開発であり、Mはこれらの時期尚早な変更を本線にもたらすマージであり、xは傍流ブランチが行ったものとは無関係の変更であり、戻し作業以前にすでに本線で行われた変更です。Wは「マージMを元に戻した変更」(Mを逆さまにするとWに見えるので、ここではWを使いました)。 ええっとつまり、 diff W^..W
は diff -R M^..M
に似ています。
マージのこのような「戻し」(revert)は、以下の方法で行うことができます:
$ git revert -m 1 M
傍流ブランチの開発者が間違いを修正した後、履歴は以下のようになります:
---o---o---o---M---x---x---W---x
/
---A---B-------------------C---D
ここで、CとDは、AとBで壊れていたものを修正するためのものであり、そして、本線ではWの後に他の変更がすでに行われている可能性があります。
更新された傍流ブランチ(先端はD)を本線にマージすると、(Mのマージは)Wにて元に戻されたため、AまたはBで行われた変更は結果に反映されません。これがAlanが見たものです。
Linusが状況を説明します:
通常のコミットを元に戻す(revert)と、
そのコミットが行ったことを効果的に元に戻すことができ、かなり簡単です。
ただし、マージコミットを元に戻すと、コミットが変更した「データ」も元に戻されますが、
マージが行った「履歴への影響」にはまったく何の効果も及ぼしません。
したがって、そのマージは引き続き存在し、
2つのブランチを結合していると見なされ、
その後のマージでは、そのマージが最後の共有状態と見なされます。
そして、マージを元に戻した戻しは、それにまったく影響しません。
したがって、「revert」はデータの変更を元に戻しますが、
リポジトリ履歴に対するコミットの影響を元に戻さないという意味では、
「undo」ではありません。
したがって、あなたが「revert」を「undo」と考えてしまうと、常に
revertのこの部分を見逃します。はい、データを元に戻しますが、
しかし、履歴は元に戻しません。
このような状況では、最初に以前の戻しを戻すことをお勧めします。これにより、履歴は以下のようになります:
---o---o---o---M---x---x---W---x---Y
/
---A---B-------------------C---D
ここで、YはWの戻しです。このような「戻しの戻し」(revert of the revert)は、以下の方法で実行できます:
$ git revert W
この履歴は(WとW..Yが変更したものの間で発生する可能性のある競合を無視すると)、履歴にWまたはYがまったくないことと同等です:
---o---o---o---M---x---x-------x----
/
---A---B-------------------C---D
そして傍流ブランチを再度マージしても、以前の戻しと戻しの戻しから生じる競合は発生しません。
---o---o---o---M---x---x-------x-------*
/ /
---A---B-------------------C---D
もちろん、CとDで行われた変更は、xのいずれかによって行われた変更と競合する可能性がありますが、これは通常のマージの競合です。
一方、傍流ブランチの開発者が障害のあるAとBを破棄し、元に戻した後に更新された本線に加えて変更をやり直した場合、履歴は以下のようになります:
---o---o---o---M---x---x---W---x---x
/ \
---A---B A'--B'--C'
前の例のような場合に戻しを戻した場合:
---o---o---o---M---x---x---W---x---x---Y---*
/ \ /
---A---B A'--B'--C'
ここで、YはWの戻しであり、A' と B' は、 A と B に巻き直しされ、傍流ブランチにはさらに修正 C' がある場合もあります。 diff Y^..Y
は diff -R W^..W
に似ており(つまり、 diff M^..M
に似ており)、 diff A'^..C'
は、以前の変更の巻き直したリーズであるため、定義上は似ていますが、それとは異なります。競合する結果を生む重複する変更がたくさんあることでしょう。つまり、その、何がいいたいのかというと、何も考えずに盲目的に「戻しの戻し」をしてはいけないのです…
---o---o---o---M---x---x---W---x---x
/ \
---A---B A'--B'--C'
リベースされた傍流ブランチの履歴では、 W(およびM)は、更新されたブランチのマージベースと本線の先端の後ろにあり、過去の誤ったマージとその「戻し」が邪魔しないようマージする必要があります。
要約すると、これらは2つの非常に異なるシナリオであり、2つの非常に異なる解決戦略が必要です:
-
障害のある傍流ブランチが先頭に修正を追加することによって修正された場合、以前の戻しの戻しを行うことは正しいことです。
-
以前のマージの戻しによって影響が破棄された、障害のある傍流ブランチが、最初から再構築された場合(つまり、あなたが解釈したように、リベースと修正した場合)は、他に何もせずに結果を再マージするのが正しいことです。(元の分岐点を変更せずに分岐を最初から再構築する方法については、以下の ADDENDUM を参照してください。)
けれども、マージの戻し(そして、そのような戻し)の場合は、注意が必要なことがあります。
たとえば、マージの戻し(そしてそれから、戻しの戻し)が二分性(bisectability)でどうなるかを考えてみましょう。戻しの戻しがそれを元に戻すという事実を無視してください。つまり「多くのことをする単一のコミット」と考えてください。なぜならそれがやることはそれだからです。
あなたが追いかけている問題があり、「マージの戻し」がヒットした場合、ヒットしているのは基本的に、マージされたすべてのコミットのすべての変更(しかし明らかに逆)を含む単一のコミットです。つまり、デバッグ地獄です。これは、変更のどの部分を特定できるかを示す小さな変更があまりないためです。
でも、全部うまくいくのでしょうか? もちろんうまくいきます。純粋に技術的な観点から言えば、Gitは非常に自然にそれを行い、何の問題もありません。「マージ前の状態」から「マージ後の状態」への変更とみなすだけで、ただそれだけです。複雑でもなく、奇妙でもなく、本当に危険でもありません。Gitは何も考えずにそれを実行します。
したがって、技術的な観点からは、マージを元に戻すことには何の問題もありませんが、作業フローの観点からは、一般的には回避する必要があります。
可能であれば、たとえば、メインツリーにマージされた問題を見つけた場合は、マージを元に戻すのではなく、問題のbisectを頑張ってブランチに括りだしてください。そしてそれを修正するか、それを引き起こした個々のコミットを元に戻してみてください。
ええもちろん、それはより複雑で、上記が常にうまくいくとは限りません。(「おっとスマソ。私はまだ準備ができていなかった。私は本当はそれをマージするべきではなかった、だkら私は本当にすべてのマージを元に戻す必要がある」とかあるある) したがって、実際にマージを元に戻す必要がありますが、マージをやり直したい場合は、元に戻すことによってそれを行う必要があります。
ADDENDUM
あなたはトピックブランチのコミットの1つを書き直さなければならず、かつ、トピックの分岐点を変更できない場合があります。以下のような状況です:
P---o---o---M---x---x---W---x
\ /
A---B---C
ここで、コミットBが間違っていて、書き直す必要があることが判明したため、コミットWでコミットMを元に戻しましたが、コミットPから分岐するには、書き直されたトピックが必要です(おそらく、Pはさらに別のブランチの分岐点であり、そして、あなたはトピックを両方のブランチにマージできるようにしたい)。
この場合の自然なことは、A-B-C ブランチをチェックアウトし、 rebase -i P
を使用してコミットBを変更することです。ただし、 rebase -i
はデフォルトで pick コマンドで選択された最初のコミットを早送り(fast-forwards)するため、コミットAを書き換えません。結局、以下のようになります:
P---o---o---M---x---x---W---x
\ /
A---B---C <-- old branch
\
B'---C' <-- 素朴に書き直したブランチ
A-B'-C' を本線ブランチにマージするには、Aの変更を取得するために、最初にコミットWを元に戻す必要がありますが、 B' の変更は、Wの戻しによって再導入された元のBの変更と競合する可能性があります。
ただし、コミットAを含むブランチ全体を再作成すると、これらの問題を回避できます:
A'---B'---C' <-- 完全に書き直したブランチ
/
P---o---o---M---x---x---W---x
\ /
A---B---C
最初にWを元に戻すことを心配せずに、 A'-B'-C' を本線ブランチにマージできます。本線の履歴は以下のようになります:
A'---B'---C'------------------
/ \
P---o---o---M---x---x---W---x---M2
\ /
A---B---C
ただし、あなたがコミットAを実際に変更する必要がない場合は、同じ変更を加えた新しいコミットとして再作成する方法が必要です。 rebaseコマンドの --no-ff
オプションは、これを行う方法を提供します:
$ git rebase [-i] --no-ff P
‘--no-ff` オプションは、対話的に実際にコミットBを変更するだけの場合でも、まったく新しいコミットで新しいブランチ A’-B'-C' を作成します(SHA IDは全て異なります)。その後、マージできます。あなたはそれから、この新しいブランチを本線ブランチに直接接続し、ブランチのすべての変更を確実に取得できるようにします。
あんたがトピックにコミットを追加して修正する場合、 --no-ff
を使用することもできます。この文書の冒頭で説明した状況をもう一度見てみましょう:
P---o---o---M---x---x---W---x
\ /
A---B---C----------------D---E <-- 修正されたトピックブランチ
この時点で、あなたは `--no-ff` を使用してトピックブランチを再作成できます:
$ git checkout E
$ git rebase --no-ff P
これは以下のようになります
A'---B'---C'------------D'---E' <-- 再作成したトピックブランチ
/
P---o---o---M---x---x---W---x
\ /
A---B---C----------------D---E
あなたはコミットWを元に戻さずに、再作成されたブランチを本線にマージできます。本線の履歴は以下のようになります:
A'---B'---C'------------D'---E'
/ \
P---o---o---M---x---x---W---x---M2
\ /
A---B---C