1020

私は、他の開発者とのプロジェクトで数か月間 Git を使用しています。私はSVNで数年の経験があるので、関係に多くの荷物を持ってきたと思います。

Git は分岐とマージに優れていると聞いたことがありますが、これまでのところ、そのようには見えません。確かに、分岐は非常に単純ですが、マージしようとすると、すべてがうまくいきません。今、私はSVNからそれに慣れていますが、ある標準以下のバージョン管理システムを別のシステムと交換しただけのようです。

私のパートナーは、私の問題は意地悪にマージしたいという私の欲求から生じていること、そして多くの状況でマージの代わりにリベースを使用する必要があることを教えてくれました。たとえば、彼が作成したワークフローは次のとおりです。

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 に関する新しいプラグマティック プログラミングの本も熟読しましたが、それらはマージを広範囲にカバーし、リベースについてはほとんど言及していません。

とにかく、私は最近のブランチで自分のワークフローに従っていましたが、それを master にマージしようとすると、すべてがうまくいきませんでした。重要ではないはずのものとの衝突がたくさんありました。対立は私には意味がありませんでした。すべてを整理するのに 1 日を要し、最終的にリモート マスターへの強制的なプッシュが行われました。

このようなものの「正しい」ワークフローは何ですか? Git は分岐とマージを非常に簡単にするはずですが、私はそれを見ていません。

2011 年 4 月 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

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

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 には戻りたくありません。苦労している場合は、そのまま続けてください。最終的には、トンネルの終わりに光が見えます.

4

11 に答える 11

414

TL;DR

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


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

私は履歴をクリーンアップするためのリベースを非常に支持しています。ただし、競合が発生した場合は、すぐにリベースを中止し、代わりにマージを行います! 競合解決のためのマージ ワークフローのより良い代替手段として、人々がリベース ワークフローを推奨していることは本当に残念です (これはまさにこの質問の内容でした)。

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

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

マージの代わりにリベースする場合、同じ競合に対して、リベースへのコミットと同じ回数まで競合解決を実行する必要があります!

実際のシナリオ

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

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

git merge: コンフリクトが発生します。彼らがマスタリングのために行った変更を確認し、それを私のブランチ (の最終製品) にマージします。終わり。

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

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

コンフリクトを解決する必要があるため、間違いを犯す可能性が高くなります。しかし、元に戻すことができるので、間違いは問題ありません。もちろん除いて...

理由 #2: rebase を使用すると、元に戻すことができません!

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

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

ブランチをリベースするとき、競合の解決が間違っている可能性が高い場合、失敗します。すべてのコミットに不適切なマージが含まれるようになり、リベースをやり直すことはできません*。せいぜい、影響を受ける各コミットに戻って修正する必要があります。楽しくない。

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

*git の内部ログから古い ref を掘り出すことができる場合、またはリベースする前に最後のコミットを指す 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 が意味的に共通ではない共通の行を見つけた場合に発生します (たとえば、両方の分岐でたまたま同じ場所に空白行があった!)。たとえば、ある分岐でクラスの本体のインデントが変更されたり、同様のメソッドが並べ替えられたりします。このような場合、より良い解決戦略は、マージのどちらかの側から変更を調べて、差分を手動で他のファイルに適用することです。

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

  1. 現在チェックアウトされているブランチ(HEADまたは トリプル ドット ( ) を使用して diff を使用すると、 からの最後の分岐以降に発生した変更が表示されます。つまり、a と b の共通の祖先を b と比較します。--oursorigin/feature1--theirsgit diff a...bba

    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. 複雑な変更をチェックアウトしたら、単純な変更の差分を取得します (手順 1 を参照)。この diff からの各変更を競合するファイルに適用します。

于 2012-06-27T04:25:29.937 に答える
386

「競合」とは、「同じコンテンツの並行進化」を意味します。したがって、マージ中に「地獄に落ちた」場合は、同じファイルセットに大規模な進化があることを意味します。

リベースがマージよりも優れている理由は次のとおりです。

  • マスターのコミット履歴でローカルコミット履歴を書き換えます(その後、作業を​​再適用して、競合を解決します)
  • マスターのすべてのコミット履歴と、再適用する変更のみが含まれるため、最終的なマージは確かに「早送り」になります。

その場合の正しいワークフロー (共通のファイル セットでの進化) は、まず rebase であり、次に mergeであることを確認します。

ただし、これは、(バックアップの理由で) ローカル ブランチをプッシュする場合、そのブランチを他のユーザーがプル (または少なくとも使用) してはならないことを意味します (コミット履歴は後続のリベースによって書き換えられるため)。


そのトピック (リベースしてからマージするワークフロー) について、barrapontoはコメントで 2 つの興味深い投稿に言及しています。どちらもrandyfay.comからのものです。

この手法を使用すると、現在の .xml で最新のパッチのように、作業は常に public ブランチの上に置かれますHEAD

(同様の手法が bazaar にも存在します)

于 2009-01-19T15:32:24.947 に答える
35

私のワークフローでは、可能な限りリベースを行っています (頻繁にリベースするようにしています。不一致が蓄積されないようにすることで、ブランチ間の衝突の量と重大度が大幅に減少します)。

ただし、主にリベース ベースのワークフローであっても、マージの場所があります。

マージは実際には 2 つの親を持つノードを作成することを思い出してください。次の状況を考えてみましょう: 私は 2 つの独立したフィーチャー ブランチ A と B を持っており、A と B がレビューされている間に、A と B の両方に依存するフィーチャー ブランチ C で何かを開発したいと考えています。

私が次にすることは、次のとおりです。

  1. A の上にブランチ C を作成 (およびチェックアウト) します。
  2. Bと合流

現在、ブランチ C には A と B の両方からの変更が含まれており、開発を続けることができます。A に何らかの変更を加えた場合、次の方法で分岐のグラフを再構築します。

  1. A の新しいトップにブランチ T を作成します
  2. T を B とマージする
  3. C を T にリベースする
  4. ブランチ T を削除

このようにして、ブランチの任意のグラフを実際に維持できますが、親が変更されたときにリベースを行う自動ツールがないことを考えると、上記の状況よりも複雑なことを行うことはすでに複雑すぎます。

于 2009-05-12T20:54:12.417 に答える
26

ほとんどの場合、git push origin --mirror を使用しないでください。

ローカルボックスにないリモートブランチをすべて消去するため、これを実行するかどうかを確認することはありません。

http://twitter.com/dysinger/status/1273652486

于 2009-04-10T01:06:06.243 に答える
15

あなたの状況では、あなたのパートナーは正しいと思います。リベースの良いところは、部外者には、変更がすべてクリーンなシーケンスですべて単独で行われたように見えることです。これの意味は

  • 変更は非常に簡単に確認できます
  • 小さなコミットを続けて作成することもできますが、それらのコミットのセットを(マスターにマージすることで)一度に公開することもできます。
  • パブリックマスターブランチを見ると、さまざまな開発者によるさまざまな機能のさまざまな一連のコミットが表示されますが、すべてが混在しているわけではありません

バックアップのためにプライベート開発ブランチをリモートリポジトリにプッシュし続けることはできますが、リベースするため、他の人はそれを「パブリック」ブランチとして扱わないでください。ところで、これを行うための簡単なコマンドはですgit push --mirror origin

Gitを使用したパッケージングソフトウェアの記事は、マージとリベースのトレードオフを説明するかなり素晴らしい仕事をしています。コンテキストは少し異なりますが、プリンシパルは同じです。基本的には、ブランチがパブリックかプライベートか、およびそれらをメインラインに統合する方法に依存します。

于 2009-01-19T16:15:02.367 に答える
15

あなたの説明を読んだ後に 1 つ質問があります。

git checkout master
git pull origin
git checkout my_new_feature

機能ブランチで「git rebase/merge master」を実行する前に?

マスター ブランチは友人のリポジトリから自動的に更新されないためです。でそれを行う必要がありgit pull originます。つまり、変更されないローカル マスター ブランチから常にリベースするのでしょうか? そして、プッシュの時間になると、見たことのない (ローカル) コミットを持つリポジトリにプッシュしているため、プッシュは失敗します。

于 2009-05-25T20:52:22.290 に答える
13

とにかく、私は最近のブランチで自分のワークフローに従っていましたが、それを master にマージしようとすると、すべてがうまくいきませんでした。重要ではないはずのものとの衝突がたくさんありました。対立は私には意味がありませんでした。すべてを整理するのに 1 日を要し、最終的にリモート マスターへの強制的なプッシュが行われました。

パートナーのワークフローでも提案されたワークフローでも、意味のない競合に遭遇することはありません。あったとしても、提案されたワークフローに従っている場合は、解決後に「強制的な」プッシュは必要ありません。プッシュ先のブランチを実際にはマージしていませんが、リモート ヒントの子孫ではないブランチをプッシュする必要があったことを示唆しています。

何が起こったのかを注意深く見る必要があると思います。あなたがローカル ブランチを作成してから、それをローカル ブランチにマージしようとした時点までの間に、他の誰かが (意図的かどうかにかかわらず) リモート マスター ブランチを巻き戻した可能性はありますか?

他の多くのバージョン管理システムと比較して、Git を使用するとツールとの戦いが少なくなり、ソース ストリームの基本的な問題に取り組むことができることがわかりました。Git は魔法を実行しないため、競合する変更は競合を引き起こしますが、コミットの親子関係を追跡することで、書き込みを簡単に行うことができます。

于 2009-01-19T19:30:25.967 に答える
11

「少数のブランチしか持たない単一の開発者であっても、適切にリベースとマージを使用する習慣を身につける価値があります。基本的な作業パターンは次のようになります。

  • 既存のブランチ A から新しいブランチ B を作成する

  • ブランチ B に変更を追加/コミットする

  • ブランチ A からのリベース更新

  • ブランチ B からの変更をブランチ A にマージします」

https://www.atlassian.com/git/tutorials/merging-vs-rebasing/

于 2012-03-22T16:16:53.250 に答える
8

私が観察したところによると、git マージはマージ後もブランチを分離したままにする傾向がありますが、リベースしてからマージすると、それが 1 つのブランチに結合されます。後者ははるかにクリーンになりますが、前者では、マージ後でもどのコミットがどのブランチに属しているかを簡単に見つけることができます。

于 2011-08-25T05:40:35.623 に答える
5

Gitには、「正しい」ワークフローはありません。ボートに浮かぶものは何でも使用してください。ただし、ブランチをマージするときに常に競合が発生する場合は、他の開発者とより適切に作業を調整する必要がありますか?二人で同じファイルを編集し続けているようですね。また、空白とSubversionのキーワード(「$ Id $」など)にも注意してください。

于 2009-01-19T15:25:30.750 に答える