履歴を安全に書き換える
commit --amend、reset、rebase、filter-repo などのすべての書き換え操作は、新しいコミットを生成し、ブランチポインタを古いコミットから移動させます。これは他の誰も古いコミットを持っていない限り無害ですが、誰かが持っていると破壊的になります。その人のクローンには古い履歴が残っており、次に push または pull したときに重複や競合として戻ってきます。したがって、最初の質問は決して「どうやって」ではなく、「他に誰がこれらのコミットを持っているか?」です。
1. 判断:書き換えるか、元に戻すか
git fetch --prune
git status --short --branch # ahead/behind、およびダーティ状態
git branch -r --contains <commit> # どのリモートブランチがすでに保持しているか
git log --oneline @{u}..HEAD # ローカルのみに存在するコミット
| コミットが存在する場所 | 実行すること |
|---|---|
| ローカルのみ、プッシュされていない | 自由に書き換える。 |
| このユーザーのみが作業するブランチ(PR ブランチ)にプッシュされた | 書き換え、その後 lease 付きでプッシュ(ステップ5)。 |
共有ブランチまたは保護ブランチ:main、master、develop、release/*、または他の人のコミットが上に積まれているもの | 書き換えない。 git revert を使用する。 |
| すでにリリースまたはデプロイ済み | git revert。書き換えではデプロイを取り消せない。 |
ユーザーが「main からあのコミットを削除して」と言った場合、彼らが望んでいるのは通常、その影響をなくすことであり、SHA を消すことではありません。git revert <sha> はそれを実現し、履歴を正直に保ち、通常の push だけで済みます。マージコミットの場合は、git revert -m 1 <merge-sha> を使用します。なぜ revert を選んだかを説明し、ユーザーが共有ブランチから消去することに固執する場合は、それが誰に影響するかを伝え、明示的な了承を得てください。
後続のコミットが同じ行を編集した場合、revert は競合します。これはまさに、悪いコミットの上にコミットが積み上げられたケースです。通常の競合解決(git-conflict-resolution を参照)と同じように解決します。悪い変更を削除し、後続の作業を保持してから git revert --continue を実行します。
revert の落とし穴が1つあります。マージを revert した後、同じブランチを再度マージしても何も戻ってきません。Git はそれらのコミットはすでに取り込まれたと認識するからです。作業を再適用するには、まず revert を revert します。
2. 何かを触る前にリカバリポイントを作成する
git rev-parse HEAD # 記録
git branch backup/<branch>-pre-rewrite # 実在の参照。reflog だけではない
reflog もブランチがどこにあったかを記録しますが、このクローンにローカルであり、期限があります(到達可能なエントリは90日、デフォルトでは到達不能なものは30日)。reflog expire 後の git gc で終了します。名前付きのバックアップブランチはコストがかかりません。ユーザーが結果を確認するまでそのままにしておいてください。
3. 最も破壊的でないコマンドを選ぶ
| 目標 | コマンド | 注意 |
|---|---|---|
| 最後のコミットのメッセージを修正する、または追加し忘れたファイルを追加する | git commit --amend | -m "..." または --no-edit でエディタの起動を回避。 |
| コミットを取り消すが変更をステージングしたままにする | git reset --soft HEAD~1 | |
| コミットを取り消し、ステージングも解除するがファイルは保持する | git reset --mixed HEAD~1(デフォルト) | |
| ブランチを戻し、ローカルの編集を失わないようにする | git reset --keep <commit> | コミットされていない変更を上書きする場合は中断。--hard よりもこちらを推奨。 |
| コミットされていない作業も捨てる | git reset --hard | コミットされていない作業は reflog に含まれず、復元できません。 不要なものがないことを確認した後にのみ使用。不安な場合は最初に git stash -u。 |
| 1つのファイルの変更を破棄する | git restore <file> | |
| 修正を前のコミットに squash または fold する | git commit --fixup=<sha> の後に autosquash | 以下を参照。 |
| コミットを削除または並べ替える | 提供された todo で rebase -i | 以下を参照。 |
pre-commit フックが失敗した場合、コミットは作成されていません。 git commit --amend を実行して「再試行」すると、前のコミットが変更され、新しい変更が間違ったコミットに折り込まれます。フックが失敗した後は、問題を修正し、再ステージングして、通常の新しいコミットを作成します。
インタラクティブエディタを使わないインタラクティブリベース
git rebase -i はエディタを開きますが、非インタラクティブシェルではハングします。これに依存しないでください。機能する2つの方法:
# fix-up コミットをターゲットに折り込む - git が生成した todo を受け入れる:
git commit --fixup=<target-sha>
GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash <base>
# 削除または並べ替え:自分で todo を書く。
GIT_SEQUENCE_EDITOR="sed -i '/^pick <short-sha> /d'" git rebase -i <base>
<base> は、変更したい最初のコミットの前のコミットです。リベースが競合で停止した場合は、解決し、git add してから、GIT_EDITOR=true git rebase --continue を実行します。問題が発生した場合は、git rebase --abort で開始前に戻ります。
4. 書き換えが意図した通りに変更されたかを確認する
メッセージの編集、並べ替え、squash では、最終的なツリーが同一である必要があります:
git diff backup/<branch>-pre-rewrite HEAD --stat # 空 = 同じ内容
git range-diff backup/<branch>-pre-rewrite...HEAD # コミットごとの比較
コンテンツを削除する意図があった場合、diff はそのコンテンツのみを表示し、それ以外は何も表示しないはずです。予期しないものがあれば、バックアップから復元してやり直します。
5. 公開 — ユーザーの許可を得た場合のみ
書き換え結果をプッシュすることは、外部に見え、元に戻すのが難しいステップです。ユーザーがすでにプッシュを要求していない限り、最初に確認します。
git push --force-with-lease=<branch>:<expected-remote-sha> --force-if-includes origin <branch>
- 決してベアの
--forceを使用しないでください。 リモートにあるものをすべて上書きします。これには、あなたが見たことのないチームメイトのプッシュも含まれます。 - 期待される SHA を指定します。 単なる
--force-with-leaseはリモート追跡参照と比較しますが、IDE のバックグラウンドでのgit fetchがその参照を静かに更新するため、lease は何も保護しなくなります。ステップ1で確認した SHA を使用します(git rev-parse origin/<branch>)。 --force-if-includes(git 2.30+)は、リモートがローカルブランチが統合したことのないものに移動した場合、さらに拒否します。main/master/develop/release/*に force push しないでください。 ホスティングのルールがそれをブロックしている場合、そのルールは正しいです。回避しないでください。
プッシュ後、古いブランチをチェックアウトしている人は pull ではなく新しいブランチにリセットする必要があること、およびオープンな PR のレビューコメントがアンカーを失う可能性があることをユーザーに伝えます。
ガードレール
- 署名付きコミットは書き換えられると署名を失います。
-Sで再署名するか、ユーザーに伝えます。 - 他の人が作成したコミットは、たとえフィーチャーブランチ上でも、尋ねずに書き換えないでください。
- 書き換えられたコミットの上にあるスタックブランチは古くなります。
git-stacked-pr-rebaseを使用して手動でリベースするのではなく、再スタックします。 - コミットされた秘密情報は、書き換えでは修正されません。 最初に認証情報をローテーションします。漏洩したと想定します。
- 明確に報告します: 古い先端、新しい先端、バックアップ参照の場所、プッシュされていないものがあるかどうか。revert が正しい選択だった場合は、それを実行したことを伝えます。