整然とした線形Git history
多くのGitベースのプロジェクトで見落とされていることの一つは、線形コミット履歴の価値です。 実際、多くのツールやガイドラインでは、線形履歴を目指すGitワークフローを推奨していません。 整頓された歴史は非常に貴重であり、線形の歴史を保証するまっすぐ進むワークフローがあるので、私はこれが悲しいと思います。
線形履歴と非線形履歴
線形履歴は、すべてのコミットが互いに続くGit履歴です。 I.e. 独立したコミット履歴を持つブランチのマージは見つかりません。
なぜ線形履歴が必要なのですか?
整然とした論理的であることに加えて、線形履歴は次の場合に便利です:
- 歴史を見る。 非線形の歴史は、時には歴史が理解できないという点まで、従うのが非常に難しい場合があります。
- バックトラッキングの変更。 例えば:”機能Aはbugfix Bの前または後に導入されましたか?”.
- バグの追跡。 GitにはGit bisectと呼ばれる非常にきれいな関数があり、どのコミットがバグや回帰を導入したかをすばやく見つけるために使用できます。 しかし、非線形の履歴では、Git bisectは使用が困難になるか、使用することさえ不可能になります。
- 変更を元に戻します。 回帰の原因となったコミットを見つけた、または特定のリリースで出て行くことになっていなかった機能を削除したいとします。 いくつかの運があれば(コードがあまり変更されていない場合)、Git revertを使用して不要なコミットを単純に元に戻すことができます。 ただし、非線形の履歴があり、おそらく多くの分岐マージがある場合、これは大幅に困難になります。
Gitの使い方によっては、線形履歴が非常に貴重な状況がもう一握りあるかもしれません。
ポイントは:あなたの歴史が線形でないほど、それはあまり価値がありません。
非線形履歴の原因
要するに、すべてのマージコミットは非線形履歴の潜在的なソースです。 ただし、マージコミットにはさまざまな種類があります。
トピックブランチからmasterへのマージ
トピックブランチの操作が完了し、それをmasterに統合したい場合、一般的な方法はトピックブランチをmasterにマー おそらく線に沿って何か:
git checkout mastergit pullgit merge --no-ff my-topic-branch
このメソッドの良いプロパティは、どのコミットがトピックブランチの一部であったかについての情報を保持することです(別の方法は、Gitがマージ
masterへのマージに関する問題は、トピックブランチがmasterの最新のヒントではなく古いmasterに基づいている場合に発生します。 この場合、必然的に非線形履歴が得られます。
これが一般的な問題になるかどうかは、Gitリポジトリのアクティブさ、同時に作業している開発者の数などによって大きく異なります。
masterからトピックブランチにマージ
最新のmasterに一致するようにブランチを更新したい場合があります(例: masterにはトピックブランチに入りたい新しい機能がいくつかありますが、競合があるためトピックブランチをmasterにマージできないことがわかります)。
一部の人にも推奨されている一般的な方法は、masterの先端をトピックブランチにマージすることです。 これは非線形の歴史の主要な源です!
解決策:Rebase!
Git rebaseは、線形履歴が必要な場合に使用する必要がある非常に便利な関数です。 いくつかは厄介なrebasingの概念を見つけるが、それは本当に非常に簡単です: 新しいコミットの上にブランチの変更(コミット)を再生します。
たとえば、Git rebaseを使用して、トピックブランチのルートを古いマスターから最新のマスターの先端に変更することができます。 あなたのトピックブランチがチェックアウトされていると仮定すると、次のことができます:
git fetch origingit rebase origin/master
対応するマージ操作は、masterの先端をトピックブランチにマージすることであることに注意してください(前の図に示すように)。 トピックブランチ内のファイルの結果の内容は、リベースまたはマージを行うかどうかにかかわらず、同じになります。 しかし、歴史は異なります(線形対非線形!).
これはすべてうまく聞こえる。 しかし、rebaseには注意すべきいくつかの注意点があります。
注意点1:Rebaseは新しいコミットを作成します
ブランチをRebasingすると、実際には新しいコミットが作成されます。 新しいコミットには、古いコミットとは異なるSHA:sがあります。 これは通常問題ではありませんが、ローカルリポジトリの外部に存在するブランチをリベースすると(たとえば、ブランチが既にoriginに存在する場合)、トラブ
リベースされたブランチを、すでに同じブランチが含まれている(ただし古い履歴がある)リモートリポジトリにプッシュする場合は、次のようにします:
- Gitは新しい履歴を既存のブランチにプッシュすることを許可しないので、ブランチを強制的にプッシュする必要があります(例:
git push --force-with-lease
)。 これにより、指定されたリモートブランチの履歴が新しい履歴に置き換えられます。 - 指定されたブランチのローカルバージョンがリモートのブランチと一致しなくなったため、他の誰かが非常に不幸になる可能性があります。
一般に、リモート上のブランチの履歴を上書きしないようにします(1つの例外は、コードレビューシステムの動作に応じて、コードレビュー中のブランチの履歴を書き換えることですが、それは別の議論です)。
リモート上の他のユーザーと共有されているブランチをリベースする必要がある場合、単純なワークフローは、元のブランチをリベースするのではなく、リベース あなたがmy-topic-branch
チェックアウトしていると仮定すると、次のことができます:
git checkout -b my-topic-branch-2git fetch origingit rebase origin/mastergit push -u origin my-topic-branch-2
…そして、my-topic-branch
に取り組んでいる人々に、代わりにmy-topic-branch-2
の作業を続けるように伝えます。 古いブランチは事実上廃止されており、masterにマージして戻すべきではありません。
注意点2:リベース内の競合の解決は、マージ
マージ操作で競合が発生した場合、そのマージコミットの一部としてすべての競合を解決します。
ただし、リベース操作では、リベースしたブランチ内のすべてのコミットに対して競合が発生する可能性があります。
実際には、コミットで競合が発生した場合、ブランチ内の後続のコミットで関連する(非常に似た)競合が発生することが何度もあります。
競合を最小限に抑える最善の方法は、masterで何が起こっているのかを追跡し、リベースせずにトピックブランチを長時間実行させないようにするこ すべての今してフロントアップ小さな紛争に対処することは、最後に一つの大きな幸せな紛争の混乱でそれらすべてを処理するよりも簡単です。
GitHubユーザーに対するいくつかの警告
GitHubは多くのことで素晴らしいです。 それはGitホスティングのための素晴らしいことだし、それはコードのブラウジング、素敵なMarkdown機能、要点などと素晴らしいwebインターフェイスを持一方、
プルリクエストには、線形Git履歴を積極的に妨害するいくつかの機能があります。 GitHubが実際にこれらの問題を修正した場合は非常に歓迎されますが、それまでは欠点を認識する必要があります:
“Merge pull request”は、マスターへの非線形マージを可能にします
トピックブランチをマスターにマージするために、緑色のフレンドリーな”Merge pull request”ボタンを押して魅力的 特に、”このブランチはベースブランチと最新です。 マージは自動的に行うことができます”。
GitHubがここで本当に言っていることは、ブランチを競合することなくマスターにマージできるということです。 プルリクエストブランチが最新のマスターに基づいているかどうかはチェックされません。
つまり、線形履歴が必要な場合は、プルリクエストブランチが最新のマスターの上にリベースされていることを確認する必要があります。 私が知る限り、そのような情報はGitHub webインターフェイスを介して利用できません(”保護されたブランチ”を使用していない限り–以下を参照)ので、ローカルGitク
プルリクエストが適切にリベースされていても、GitHub webインターフェイスでのマージ操作がアトミックになるという保証はありません(つまり、 誰かがあなたのマージ操作が通過する前にmasterに変更をプッシュすることができます–そしてGitHubは文句を言いません)。
実際には、ブランチが最新のマスターの上に適切にリベースされていることを確認する唯一の方法は、マージ操作をローカルで行い、結果のマスターを手動で 線に沿って何か:
git checkout mastergit pullgit checkout my-pullrequest-branchgit rebase mastergit checkout mastergit merge --no-ff my-pullrequest-branchgit push origin master
あなたが不運で、誰かがあなたのプル操作とプッシュ操作の間でマスターに変更をプッシュすることができた場合、プッシュ操作は拒否されます。 しかし、これはあなたの操作がアトミックであることを保証するので、良いことです。 ちょうどgit reset --hard origin/master
それが通過するまで上記の手順を繰り返します。
注:プロジェクトのガイドラインを尊重し、コードのレビューとテストを行います。 たとえば、プルリクエストの一部として自動テスト(ビルド、静的分析、単体テストなど)を実行している場合は、masterブランチを手動で更新するのではなく、
Protected branches機能はmasterからのマージを奨励します
GitHubプロジェクトで保護されたブランチとステータスチェックを使用している場合、最新のmasterに基づいていな
しかし、プルリクエストブランチが最新のマスターに基づいていない場合は、”Update branch”というフレンドリボタンが表示され、”This branch is out-of-date with the base branch. Masterからの最新の変更をこのブランチにマージします”。
この時点で、最良の選択肢は、ブランチをローカルにリベースし、プル要求に強制的にプッシュすることです。 my-pullrequest-branch
がチェックアウトされていると仮定すると、次のようにします:
git fetch origingit rebase origin/mastergit push -f
残念ながら、GitHubのプルリクエストは強制プッシュではうまく機能しないため、コードレビュー情報の一部がプロセスで失われる可能性があります。 それが受け入れられない場合は、新しいプルリクエストの作成を検討してください(つまり、リベースされたブランチを新しいリモートブランチにプッ
結論
線形履歴を気にするなら:
- トピックブランチをmasterにマージする前に、最新のmasterの上にリベースします。
- masterをトピックブランチにマージしないでください。 代わりにリベースします。
- トピックブランチを他の人と共有するときは、リベースする必要があるときはいつでも新しいブランチを作成します(
my-topic-branch-2
,my-topic-branch-3
, …). - GitHub pull requestを使用している場合は、注意してください。
- “Merge”ボタンは、PRブランチが最新のマスターに基づいていることを保証するものではありません。 必要に応じて手動でリベースします。
- ステータスチェックで保護されたブランチを使用している場合は、”ブランチの更新”ボタンを押さないでください。 代わりに手動でリベースします。
あなたが線形Gitの歴史についてあまり気にしないなら–幸せなマージ!
GitHubのウィッシュリスト
GitHubで働いているあなたに:プルリクエストでリベースワークフローのサポートを追加してください(これらはオプトインリポジトリオ
- ブランチが最新のマスターに基づいていない場合、プル要求のマージボタン/操作を無効にする可能性を追加します(これはステータスチェックの使用を必
- “最新マスターにリベース”ボタンを追加します。 多くの場合、これはwebインターフェイスを介して簡単に行うことができる競合のない操作でなければなりません。
- リベース/強制プッシュの後、プルリクエスト履歴(コミット、コメント、…)を保持します。
Leave a Reply