デスマッチ:Git リベース vs マージ
時代を超える問い…
デスマッチ: Git Rebase vs. (Squash) Merge!
Rebaseすべきか?それともSquash Mergeか?
- 個人の好みの問題か?
- 答え: 複数チームが関わる場合は違う! どちらを選んでも相手側の使い勝手に影響する
なぜこのテーマは宗教的熱狂を呼び起こすのか?
一部のエンジニアは git(とターミナル)の知識を自分の相対的なスキルレベルの指標として使う。自己認識やエゴに結びついたプラクティスは、客観的に分析することすら困難であり、変えることはなおさら難しい。
他にも、慣れや生存者バイアスといった要因が入り混じり、我々自身の評価や前提をさらに曇らせている可能性がある。
重要な質問: git コミットの目的は何ですか?
- 早く頻繁にコミットしますか?「チェックポイント」やバックアップ的な考え方ですか?
- 失敗した試みや実験もすべて記録する?(例:
git commit -am "Updated deps" && git pushを定期的に繰り返す) - コードの方がコミットメッセージより重要だと考えますか?
- 失敗した試みや実験もすべて記録する?(例:
- それとも、コミットは入念に選別された芸術作品のようにしたいですか?
- 各コミットが自己完結した原子的な作業単位ですか?(例:
git add package.json && git commit -m "Updated deps") - 「ごちゃごちゃした」コミットログが耐えられないですか?
- PR のレビューでコミット単位での確認が頻繁に行われますか?
- 各コミットが自己完結した原子的な作業単位ですか?(例:
| 💡 他にどんなメンタルモデルがコミットの捉え方を決めていますか? @justsml に教えてください!
自分やチーム、組織に 最大の価値 を提供する形で git を考えていますか?
さまざまなコミット戦略に対する考え方が大きく異なるため、git の「正しい」使い方について混乱が生じるのは当然です。
シナリオ: 修正されたリリースタグを作成する
main 上の最近のコミットを除外してタグリリースを作成するプロセスを比較してみましょう。
リベース方式
メンタルモデル: 「既存の履歴の別バージョンを作りたい。(例: 16 回マージする前にミスをしたので、細かく修正したい。結果として、終わりの見えないコンフリクトと --continue のサイクルに陥る可能性がある。)」
- 最新を取得:
git checkout main&&git pull - 新しいブランチを作成:
git checkout -b release/hot-newness-and-stuff - インタラクティブリベースを開始し、遡りたい地点の git ref を指定。
git rebase -i HEAD~6(注:HEAD~6は「6 コミット前」の省略形) - 取り除きたいコミットの行頭を
dropに変更して削除。エディタを保存して閉じる。 - コンフリクトを解消し、
git add .&&git rebase --continue(git commitは実行しない)。 - 完了するまで前項を繰り返す。
- 現行の手順でタグ付け・プッシュ。例:
git tag -a v1.2.3 -m 'Release v1.2.3'&&git push --tags
メリット
- 🔌 絶対的な権限。履歴を書き換えられる。
デメリット
- 😰 絶対的な権限。履歴を書き換えられる。(メリットでもデメリットでもある…)
- 🔂 コンフリクトと
--continueの無限ループに陥りやすい。(git rerereを使っても時に失敗することがある) - 🙀 共同作業機能が壊れる: PR コメントが失われたり孤立したりする。失礼。
- 🖇️ パーマリンクが永続的でなくなる可能性がある。
(Squash) マージ方式
メンタルモデル: 「特定の地点から始まり、任意のブランチを含むカスタムリリースが欲しい」。
- 最新状態を取得:
git checkout main&&git pull - 新しいブランチを作成:
git checkout -b release/hot-newness-and-stuff - 必要なブランチやコミットをマージ:
git merge --no-ff feature/hot-newness bug/fix-123(可能な限り--no-ffフラグを付ける。) - マージコンフリクトが出たら修正(出た場合。)
- 現行プロセスでタグ付け・プッシュ。例:
git tag -a v1.2.3 -m 'Release v1.2.3'&&git push --tags
メリット
- 💪 手順が少なく、全体的なコンフリクトも減り、既存の git コマンド知識で済む。
- 🚀 PR/ブランチレベルで考えられ、コミット単位の粒度は必要なときだけ意識すればよい。
- 🦺 破壊的でない。いつでも戻したり新しいブランチを作ったりできる。
- 🎥 既存のコミットとメッセージをそのまま残すので、
blameのノイズが減る。
デメリット
- 🔏 コミットメッセージの変更が難しい。
- 🤐 作業内容を隠すのが難しい。
結論
結局のところ、リスクが少なく、手順がシンプルな方が勝ります。
Rebaser たちが問題を解決(あるいは回避)する手段を持っているのは事実ですが、結論は変わりません:結局は git の黒帯が必要になる(例:ささやかな git push でも余計な複雑さに直面することがあります。git push --force か git push --force-with-lease か?そもそもそれを扱う必要があるのか?)
もう一つの理由として、履歴を書き換えるためのリベース は git merge ... に比べて常に不利です。git merge は git CLI に高度なアルゴリズムを適用させ、各ブランチの HEAD を解析してコンフリクトを回避します。
この方法は、各マージが対象ブランチの最新状態だけを見ればよいため、より賢くなります。一方、リベースは指定された順序でコミット履歴を再生(あるいは除外)しなければなりません。そのため、git が最適化できる余地が制限され、一度に比較できるのは 2 つのコミットだけになります。
結局のところ、リベースを行うと、不要になった古いコミットやコンフリクトを再び体験することが時折発生します。たとえそれらがすでに削除または解決されていると分かっていてもです。
Summary
-
💃 Answer: SQUASH MERGE your PRs onto
main。- 必要ならブランチの履歴は残りますし、
mainは比較的「クリーン」な状態を保てます。
- 必要ならブランチの履歴は残りますし、
-
🔤 Always Be Committing!
- 企業プロジェクトの 95 %以上では、「バックアップ志向」の方が「彫刻的アート志向」より好まれます。時間が経つにつれ、コミットメッセージの意味はコードのロジックやテストに比べてはるかに早く薄れます。
-
git checkoutの特別な--区切り子を使えば、現在のブランチに留まりながら指定したファイルやフォルダをコピーできます。 -
git checkout feature/half-a-feature **--** <フォルダまたはファイルのパス> -
まず、残しておきたい変更はすべてコミットしておいてください。これを実行するとローカルの変更が上書きされます。