Gitワークフローとリベースとマージに関する質問 質問する

Gitワークフローとリベースとマージに関する質問 質問する

私は他の開発者とプロジェクトで数か月間Gitを使用しています。私は数年の経験があります。SVNなので、私はこの関係に多くの重荷を持ち込んでいるのだと思います。

Git はブランチとマージに最適だと聞いていますが、今のところ、その効果は感じられません。確かに、ブランチは非常に簡単ですが、マージしようとすると、すべてが台無しになります。今では、SVN でそのことに慣れていますが、私には、1 つの劣ったバージョン管理システムを別のシステムに交換しただけのように思えます。

私のパートナーは、私の問題は、無計画にマージしたいという私の欲求から生じており、多くの状況ではマージではなくリベースを使用する必要があると言っています。たとえば、彼が定めたワークフローは次のとおりです。

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature
git checkout master
git merge my_new_feature

基本的には、機能ブランチを作成し、常にマスターからブランチにリベースし、ブランチからマスターにマージします。ブランチは常にローカルのままであることに注意してください。

これが私が始めたワークフローです

clone remote repository
create my_new_feature branch on remote repository
git checkout -b --track my_new_feature origin/my_new_feature
..work, commit, push to origin/my_new_feature
git merge master (to get some changes that my partner added)
..work, commit, push to origin/my_new_feature
git merge master
..finish my_new_feature, push to origin/my_new_feature
git checkout master
git merge my_new_feature
delete remote branch
delete local branch

重要な違いは 2 つあります (私の考えでは)。リベースではなく常にマージを使用し、機能ブランチ (および機能ブランチのコミット) をリモート リポジトリにプッシュします。

リモート ブランチを使用する理由は、作業中に作業内容をバックアップしたいからです。リポジトリは自動的にバックアップされるため、何か問題が発生した場合に復元できます。私のラップトップはバックアップされていないか、それほど徹底的ではありません。そのため、ラップトップに他の場所にミラーリングされていないコードがあるのは嫌なのです。

リベースではなくマージを使用する理由は、マージが標準で、リベースが高度な機能であるように思われるからです。直感的に、私がしようとしているのは高度な設定ではないので、リベースは不要であるはずです。Git に関する新しい Pragmatic Programming の本も熟読しましたが、マージについては広範囲に説明されており、リベースについてはほとんど触れられていません。

とにかく、最近のブランチでワークフローに従っていたのですが、それをマスターにマージしようとしたら、すべてが台無しになってしまいました。重要ではないはずのものと大量の競合が発生していました。競合は私にとってまったく意味がありませんでした。すべてを整理するのに 1 日かかり、最終的にはリモート マスターへの強制プッシュに至りました。ローカル マスターではすべての競合が解決されていたものの、リモート マスターはまだ満足していなかったからです。

このような場合の「正しい」ワークフローとは何でしょうか? Git はブランチとマージを非常に簡単にするはずですが、私にはそれが見えません。

2011-04-15 更新

これは非常によくある質問のようですので、最初に質問してから 2 年間の経験を振り返って更新しようと思いました。

少なくとも私たちの場合、元のワークフローは正しいことがわかりました。言い換えれば、これが私たちが行っていることであり、うまく機能しています。

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge my_new_feature

実際、私たちのワークフローは少し異なっており、生のマージではなくスカッシュ マージを行う傾向があります。(注: これは議論の余地があります。以下を参照してください。 ) これにより、機能ブランチ全体をマスター上の単一のコミットに変換できます。次に、機能ブランチを削除します。これにより、ブランチ上で少し乱雑であっても、マスター上のコミットを論理的に構造化できます。そのため、次の操作を行います。

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge --squash my_new_feature
git commit -m "added my_new_feature"
git branch -D my_new_feature

スカッシュ マージ論争- 何人かのコメント投稿者が指摘しているように、スカッシュ マージは機能ブランチのすべての履歴を破棄します。名前が示すように、すべてのコミットを 1 つにまとめます。小さな機能の場合、これは 1 つのパッケージに凝縮されるため理にかなっています。大きな機能の場合、特に個々のコミットがすでにアトミックである場合は、これはおそらく良いアイデアではありません。これは本当に個人の好みになります。

Github および Bitbucket (その他?) のプル リクエスト- マージ/リベースがプル リクエストとどのように関係するのか疑問に思われる場合は、マスターにマージする準備ができるまで、上記のすべての手順に従うことをお勧めします。git で手動でマージする代わりに、PR を受け入れるだけです。これはスカッシュ マージを実行しないことに注意してください (少なくともデフォルトでは実行されません)。ただし、プル リクエスト コミュニティでは、スカッシュなし、ファースト フォワードなしのマージ コンベンションが受け入れられています (私の知る限り)。具体的には、次のように動作します。

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git push # May need to force push
...submit PR, wait for a review, make any changes requested for the PR
git rebase master
git push # Will probably need to force push (-f), due to previous rebases from master
...accept the PR, most likely also deleting the feature branch in the process
git checkout master
git branch -d my_new_feature
git remote prune origin

私は Git が大好きになり、SVN に戻ることは決して望んでいません。苦労しているなら、とにかくそれを続ければ、やがてトンネルの先の光が見えてくるでしょう。

ベストアンサー1

要約

git rebaseワークフローは、競合解決が苦手な人やSVNワークフローに慣れている人からあなたを守ることはできません。Git の災害を回避する: 悲惨な話競合解決が面倒になり、不適切な競合解決からの回復が難しくなるだけです。代わりに、そもそもそれほど難しくならないように diff3 を使用してください。


リベースワークフローは競合解決には適していません。

私は、履歴をクリーンアップするためのリベースを大いに支持しています。ただし、競合が発生した場合は、すぐにリベースを中止し、代わりにマージを実行します。競合解決のために、マージ ワークフローよりもリベース ワークフローのほうがよい代替手段として推奨されている人がいることに、本当に腹が立ちます (これがまさにこの質問の目的です)。

マージ中に「すべてが地獄」になると、リベース中にも「すべてが地獄」になり、さらに地獄がひどくなる可能性があります。その理由は次のとおりです。

理由1: コミットごとに競合を解決するのではなく、競合を一度解決する

マージではなくリベースを行う場合、同じ競合に対して、リベースのコミットの数だけ競合解決を実行する必要があります。

実際のシナリオ

私はマスターから分岐して、ブランチ内の複雑なメソッドをリファクタリングします。リファクタリング作業は、リファクタリングとコードレビューの取得に取り組んでいるため、合計 15 のコミットで構成されています。リファクタリングの一部には、以前マスターに存在していたタブとスペースの混在を修正することが含まれます。これは必要なことですが、残念ながら、その後マスターでこのメソッドに加えられる変更と競合します。予想通り、私がこのメソッドに取り組んでいる間に、誰かがマスター ブランチの同じメソッドに単純で正当な変更を加え、それが私の変更とマージされるはずです。

ブランチをマスターにマージするときには、2 つのオプションがあります。

git merge:競合が発生します。master に加えられた変更を確認し、それを自分のブランチ (の最終製品) にマージします。完了です。

git rebase:最初のコミットで競合が発生します。競合を解決してリベースを続行します。2 番目のコミットで競合が発生します。競合を解決してリベースを続行します。3 番目のコミットで競合が発生します。競合を解決してリベースを続行します。4 番目のコミットで競合が発生します。競合を解決してリベースを続行します。5 番目のコミットで競合が発生します。競合を解決してリベースを続行します。6 番目のコミットで競合が発生します。競合を解決してリベースを続行します。 7 番目のコミットで競合が発生します。競合を解決してリベースを続行します。 8 番目のコミットで競合が発生します。競合を解決してリベースを続行します。9 番目のコミットで競合が発生します。競合を解決してリベースを続行します。10 番目のコミットで競合が発生します。競合を解決してリベースを続行します。11 番目のコミットで競合が発生します。競合を解決してリベースを続行します。 12 番目のコミットで競合が発生します。競合を解決してリベースを続行します。13番目のコミットで競合が発生します。競合を解決してリベースを続行します。14番目のコミットで競合が発生します。競合を解決してリベースを続行します。15 番目のコミットで競合が発生します。競合解決してリベースを続行します。

これがあなたの好みのワークフローだとしたら、冗談でしょう。マスターで行われた 1 つの変更と競合する空白の修正だけで、すべてのコミットが競合し、解決する必要があります。これは、空白の競合のみがある単純なシナリオです。ファイル間での大規模なコード変更を伴う実際の競合が発生し、それを複数回解決しなければならないなんてことは絶対に避けてください。

余分な競合解決を行う必要があるため、ミスをする可能性が高くなります。しかし、git では元に戻すことができるので、ミスは問題ありませんよね? もちろん、例外はありますが...

理由 2: リベースでは元に戻すことはできません。

競合の解決は難しいことがあり、また、一部の人はそれが非常に苦手であることに、私たち全員が同意できると思います。間違いが起きやすいので、git で簡単に元に戻せるのは素晴らしいことです。

ブランチをマージするとgit revert、Git はマージ コミットを作成します。このコミットは、競合の解決がうまくいかなかった場合に破棄または修正できます。不適切なマージ コミットをパブリック/権限のあるリポジトリにすでにプッシュしている場合でも、 を使用してマージによって導入された変更を元に戻し、新しいマージ コミットでマージを正しくやり直すことができます。

ブランチをリベースする場合、競合解決が間違って行われた場合、困ったことになります。すべてのコミットに不良マージが含まれるようになり、リベースをやり直すことはできません*。せいぜい、影響を受けたコミットをそれぞれ戻って修正する必要があります。楽しいことではありません。

リベース後、何が元々コミットの一部であったか、そして何が不適切な競合解決の結果として導入されたかを判断することは不可能です。

*git の内部ログから古い参照を掘り出すことができれば、またはリベース前の最後のコミットを指す 3 番目のブランチを作成すれば、リベースを元に戻すことが可能です。

競合解決をなくす:diff3を使う

この紛争を例に挙げてみましょう:

<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

競合を見ると、各ブランチで何が変更されたのか、その意図は何だったのかがわかりません。これが、競合解決が混乱を招き、困難になる最大の理由だと私は考えています。

diff3 が救世主です!

git config --global merge.conflictstyle diff3

diff3 を使用すると、新しい競合ごとに 3 番目のセクション (マージされた共通の祖先) が作成されます。

<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
||||||| merged common ancestor
EmailMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

まず、マージされた共通の祖先を調べます。次に、それぞれの側を比較して、各ブランチの意図を判断します。HEAD が EmailMessage を TextMessage に変更したことがわかります。その意図は、同じパラメータを渡すことで、使用されるクラスを TextMessage に変更することです。また、feature-branch の意図は、:include_timestamp オプションに true ではなく false を渡すことであることもわかります。これらの変更をマージするには、両方の意図を組み合わせます。

TextMessage.send(:include_timestamp => false)

一般的に:

  1. 共通の祖先を各ブランチと比較し、どのブランチが最も単純な変化をしたかを判断する
  2. その単純な変更を他のブランチのコードのバージョンに適用し、より単純な変更とより複雑な変更の両方が含まれるようにします。
  3. 変更をマージしたセクション以外の競合コードのすべてのセクションを削除します。

代替案: ブランチの変更を手動で適用して解決する

最後に、diff3 を使用しても理解するのが非常に困難な競合もあります。これは、特に、意味的には共通ではない共通行が diff で見つかった場合に発生します (例: 両方のブランチにたまたま同じ場所に空白行がある場合)。たとえば、1 つのブランチでクラスの本体のインデントが変更されたり、類似のメソッドが並べ替えられたりします。このような場合、より適切な解決方法は、マージのどちらかの側から変更を調べ、手動で他のファイルに diff を適用することです。

origin/feature1競合が発生するマージのシナリオで、競合を解決する方法を見てみましょうlib/message.rb

  1. 現在チェックアウトしているブランチ ( HEAD、または--ours) と、マージするブランチ ( origin/feature1、または--theirs) のどちらが、より簡単に適用できる変更であるかを判断します。トリプルドット ( ) 付きの diff を使用すると、からの最後の分岐以降にgit diff a...bで発生した変更が表示されます。つまり、 a と b の共通の祖先を b と比較します。ba

    git diff HEAD...origin/feature1 -- lib/message.rb # show the change in feature1
    git diff origin/feature1...HEAD -- lib/message.rb # show the change in our branch
    
  2. ファイルのより複雑なバージョンを確認してください。これにより、すべての競合マーカーが削除され、選択した側が使用されます。

    git checkout --ours -- lib/message.rb   # if our branch's change is more complicated
    git checkout --theirs -- lib/message.rb # if origin/feature1's change is more complicated
    
  3. 複雑な変更をチェックアウトした状態で、より単純な変更の diff を取得します (手順 1 を参照)。この diff からの各変更を競合するファイルに適用します。

おすすめ記事