git merge
の紹介
マージの基本的な構文は:git merge <branch>
です。
マージ操作は、複数のブランチの開発履歴を一つに統合し、全てのブランチのコミットを保持します。そして、マージ操作の完了を示す新しいマージコミット(merge commit)が追加されます。
ここで、以下のような fix
と master
ブランチ、およびコミット履歴があるとします:
A --- B --- C fix
/
D --- E --- F --- G master
この時点で、fix
ブランチの最新のコードを master
ブランチにマージしたいと考えています。
master
ブランチで git merge fix
コマンドを実行することができます。
これにより、fix
ブランチのコミット内容(現在のブランチから分岐して以来の全てのコミット)が master
ブランチにマージされます。
その結果、コミット履歴は次のようになります:
A --- B --- C fix
/ \
D --- E --- F --- G --- H master
master
ブランチのコミット履歴には、新しいマージコミット H が追加されます。
H のコミットには、fix
ブランチの全ての A、B、C のコミットが含まれます。
git merge
を避けるべき場面
git merge
を使うと、ブランチのすべてのコミット履歴が保存され、さらに新しいマージコミットが追加されます。これは公共のブランチでは良いマージ戦略です。
しかし、プライベートブランチや共有されていないブランチでこれを行うと、問題が生じやすいです。
次のような feature
と master
ブランチ、およびコミット履歴があるとします:
D --- E --- F --- G master
\
A --- B feature
feature
ブランチを開発している間に、master
ブランチが二度更新されました。
feature
ブランチのコードを develop
ブランチにマージしてテストする前に、コードの整合性を保つために master
ブランチの最新のコードを feature
ブランチにマージする必要があります。
この時点で git merge
を続けると、コミット履歴は次のようになります:
D --- E --- F --- G master
\ \
A --- B --- H feature
つまり、feature
ブランチのコミット履歴は次のようになります:
A --- B --- H(F と G が含まれている) feature
H のコミットには master
ブランチの最新のコミット、すなわち F と G が含まれます。
コードのマージが成功した後、feature
ブランチのコードを git merge
を使って公共のブランチ develop
にマージします。
コミット履歴は次のようになります:
D --- E --- F --- G master
\ \
A --- B --- H feature
\
X --- Y --- Z ----------- M develop
develop
ブランチには M コミットが追加され、M コミットには feature
ブランチのすべてのコミット(A、B、H)が含まれ、H コミットには master
ブランチの F と G が含まれます。
この例では、コミット数がわずか数回でも、2 回のマージ後に feature
ブランチと develop
ブランチの Git コミット履歴がこれほど複雑になります。
コミット数が増え、マージ回数が増えると、コードレビューやデバッグやコードの履歴追跡が非常に困難になります。
私が経験した中で、コミット数が 200 を超える場合の MR がありましたが、これはメインブランチの最新コードをマージした結果です。
したがって、余分なコミット履歴を削除し、コミット履歴をクリーンに保つことが重要です。
これにより、コードのレビューが楽になり、コードのデバッグやコードの履歴追跡も理解しやすくなります。
git rebase
の紹介
リベースの基本的な構文は: git rebase <branch>
です。
コードの統合を行う際、不要なコミット履歴を取り除き、履歴をシンプルで直線的に保つには、git rebase
を使用する必要があります。
git merge
とは異なり、git rebase
はマージコミットを作成せず、現在のブランチのコミットを基準ブランチの最新コミットに再適用します。これにより、現在のブランチの履歴が基準ブランチの最新コミットから直接派生したかのように見え、履歴が直線的でシンプルになります。
上記の git merge
が適さない場面の問題解決
上記と同じ、以下のような feature
と master
ブランチ、およびコミット履歴があるとします:
D --- E --- F --- G master
\
A --- B feature
feature
ブランチを開発している間に、master
ブランチが二度更新されました。
feature
ブランチのコードを develop
ブランチにマージしてテストする前に、コードの一貫性を保つために master
ブランチの最新コードを feature
ブランチにマージする必要があります。
この時点で git merge
ではなく git rebase
を使用すると、コミット履歴は次のようになります:
D --- E --- F --- G master
\
A --- B feature
つまり、feature
ブランチのコミット履歴は次のようになります:
A --- B feature
不要な H、F、G のコミットが含まれていません。
リベースが成功した後、feature
ブランチのコードを git merge
を使用して公共のブランチ develop
にマージします。
コミット履歴は次のようになります:
D --- E --- F --- G master
\
A --- B feature
\
X --- Y --- Z --------------- M develop
develop
ブランチには M コミットが追加され、M コミットには feature
ブランチの全てのコミット(A と B)が含まれます。
以前の git merge
を使用した場合と比較して、develop
ブランチに追加された M コミットには feature
ブランチの全てのコミット(A、B、H)が含まれ、H には master
ブランチの F と G のコミットが含まれています。
現在、M コミットには feature
ブランチのコミット(A と B)のみが含まれています。
上記の例から、プライベートブランチや共有されていないブランチで git rebase
を使用して基準ブランチの最新コードを取り込むことで、次のような利点があります:
-
コミット履歴を直線的で分かりやすく保つ
これにより、プロジェクトのコミット履歴が読みやすく理解しやすくなり、コードレビューが容易になります。
また、デバッグやコードの遡りにおいても、コードの進化過程が理解しやすくなります。
-
不要なマージコミットを避ける
-
早期にコンフリクトを解決し、協力効率を向上させる
git rebase
によって早期にコンフリクトを解決することで、最終的なマージ時の複雑さを軽減できます。さらに、基準ブランチの最新の変更を定期的にプライベートブランチに適用することで、常に最新のコードベースに基づいて作業を行い、チームメンバー間のコードコンフリクトを減らすことができます。
コンフリクトがない場合
git rebase <branch>
を実行した後、コードにコンフリクトがなければ、そのままリベースが完了しています。
その後は git push -f
を使ってリモートブランチにコードをプッシュするだけです。
なぜ
git push -f
を使う必要があるのでしょうか?
git rebase
を実行すると、Git は各コミットに新しいコミットオブジェクトを作成します。これらの新しいコミットオブジェクトは、内容が同じであっても元のコミットとは異なります。そのため、ローカルブランチのコミット履歴がリモートブランチの履歴と一致しなくなります。
これらの変更をリモートリポジトリにプッシュしようとすると、Git はローカルのコミット履歴がリモートの履歴とコンフリクトしていると判断します。なぜなら、それらは異なるコミットハッシュを持っているからです。
これらの変更を強制的にプッシュするには、
git push -f
を使用する必要があります。これにより、リモートブランチの履歴が上書きされ、ローカルブランチと一致するようになります。
git push -f
を使用する必要があるため、プライベートブランチや共有されていないブランチのみでリベース操作を行うことが重要です。
コードのコンフリクトの処理
git rebase <branch>
を実行した際にコードにコンフリクトが発生した場合、Git は次のようなメッセージを表示します:
(base) nansenho@mb-nansyou-01 dragonfly_frontend % git rebase develop
Auto-merging tests/e2e/specs/error/404.spec.ts
CONFLICT (add/add): Merge conflict in tests/e2e/specs/error/404.spec.ts
error: could not apply 03652542... fix:404エラーページのe2eテスト
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 03652542... fix:404エラーページのe2eテスト
このメッセージは、コンフリクトを解決するための三つの方法を教えてくれます:
-
git rebase --continue
git rebase
の途中でコンフリクトが発生し、手動で全てのコンフリクトを解決した後、git add <file>
を使って解決済みのファイルをマークします。その後、
git rebase --continue
を実行してリベース操作を続行します。リベース操作が完了したら、
git push -f
を使用してローカルの変更をリモートブランチにプッシュします。 -
git rebase --abort
リベース中に問題が発生し、現在のリベース操作を中止することを決定した場合、
git rebase --abort
を使用できます。このコマンドは現在のリベース操作をキャンセルし、リベースを開始する前の状態にブランチを戻します。
-
git rebase --skip
現在のコードコンフリクトを無視することを決定した場合、
git rebase --skip
を使用できます。このコマンドは現在のコンフリクトを引き起こしているコミットをスキップします。
実際にコンフリクトが発生した場合、最も推奨される方法は、手動でコンフリクトを解決した後に git rebase --continue
を使用してリベースを続行することです。
解決できないコンフリクトが発生した場合や、現在のリベース操作が実行不可能であると判明した場合は、git rebase --abort
を使用してリベース操作をキャンセルし、元の状態に戻してから他の解決策を考えることをお勧めします。
git rebase --skip
を使用するのは推奨されません。
git rebase
と git merge
の使用シーン
一般的に、すべてのブランチの完全なコミット履歴を保持したい場合はマージを使用し、コミット履歴をシンプルかつ直線的に保ちたい場合はリベースを使用します。
具体的には、以下の二つの広く認識されている使用シーンがあります:
-
プライベートブランチや共有されていないブランチ(例:
feature-xxx
、fix-xxx
、refactor-xxx
など)で開発を行う際、他のブランチの最新コードを取り込む場合、コミット履歴をシンプルかつ直線的に保つためにgit rebase
を使用するのが適しています。 -
プライベートブランチや共有されていないブランチ(例:
feature-xxx
、fix-xxx
、refactor-xxx
など)を共有ブランチ(例:master
、develop
、main
など)にマージする際には、すべてのコミット履歴を記録するためにgit merge
を使用し、コード変更の追跡を容易にするのが適しています。