深夜2時、PagerDutyのアラートが鳴り響く。決済ゲートウェイからHTTP 500エラーが大量発生しているとの通知。原因は、5分前にmainブランチへマージされたプルリクエストによってCI/CDパイプラインがトリガーされ、本番クラスタへ自動デプロイされたことだった。
寝ぼけた頭で、多くのエンジニアが反射的にターミナルを開き、git revert <commit_hash> を叩く。しかし、Gitは無情にもエラーを返す:
error: commit 9a4f21b is a merge but no -m option was given.
fatal: revert failed
本番サービスは停止中。ダウンタイムが1分伸びるごとに多大な損害が発生する。ここでは、チームのGitツリーを壊すことなく、30秒でインシデントを鎮火させる手順を紹介する。
緊急対応の3ステップ
本番環境で障害が発生し、即座にコードを安全な状態へ戻す必要がある場合は、以下の手順に従ってください:
ステップ1:直前にデプロイされたマージコミットのハッシュを取得する
mainブランチの直近5件のコミットを確認します:
git log --oneline -n 5
ターミナルに以下のようなログが出力されます:
9a4f21b (HEAD -> main) Merge pull request #42 from feature/payment-v2
8c1e34a feat: update checkout form
5d2b10f fix: handle payment gateway timeout
1a2b3c4 chore: update dependencies
ステップ2:-m 1 オプションを付けてrevertを実行する
親コミット(メインライン)を基準として明示的に指定します:
git revert -m 1 9a4f21b
Gitが自動的にエディタを開き、コミットメッセージの確認を求めます。デフォルトの Revert "Merge pull request #42 from feature/payment-v2" のまま保存して終了します。
ステップ3:リモートへ直接pushしてロールバックビルドを走らせる
git push origin main
CI/CDパイプラインが即座に走り、マージ前の安全なコードベースで再ビルドされます。システムが安定したら、Gitの内部挙動について落ち着いて理解を深めましょう。
-m フラグの本質と親コミット番号(Parent Number)の仕組み
通常のコミットには親コミット(parent commit)が1つしか存在しません。そのため、git revert <hash> を実行すると、Gitはそのコミットと親コミットを比較して変更を打ち消すコミットを作成します。
しかしマージコミットは異なります。複数のブランチが合流する地点であるため、親コミットを2つ以上持っています:
- Parent 1(メインライン):マージ先のブランチ(通常は
mainやproduction)。 - Parent 2:マージ元のブランチ(例:
feature/payment-v2)。
親コミットの順序を確認するには、以下のコマンドを実行します:
git show 9a4f21b
出力結果の2行目に注目してください:
commit 9a4f21b0e419b48...
Merge: 1a2b3c4 8c1e34a
Author: Senior Dev <[email protected]>
Date: Fri Oct 2 02:00:00 2026
各ハッシュの意味は以下の通りです:
1a2b3c4は Parent 1(マージ直前のmainの HEAD)。8c1e34aは Parent 2(feature/payment-v2ブランチの最新コミット)。
-m 1(--mainline 1 の略)オプションは、「Parent 1 を基準とし、Parent 2 によって取り込まれた変更をすべて打ち消す」という指示を与えます。
マージコミットをrevertした際に見落としがちな落とし穴
「revertすればフィーチャーブランチがコミット履歴から消滅する」と誤解しているエンジニアも少なくありません。しかし、Gitの挙動はそうではありません。
過去のコミット履歴はGitツリー上にそのまま残ります。Gitは単に「変更を打ち消す新しいコミット」を作成しただけです。これが原因で、後からフィーチャーブランチを再マージしようとした際に大きなトラブルが発生します。
バグ修正後に正しく再マージ(Re-merge)する手順
よくあるシナリオ:翌日、チームはNull Pointerエラーの原因を特定し、修正コミットを feature/payment-v2 にプッシュしました。自信を持って main へPRを作成したものの、「Files Changed」タブを見て愕然とします。フィーチャーブランチの既存コードがすべて消え、PRには今回の修正コミット数行しか表示されていません。
なぜこうなるのでしょうか? main ブランチの履歴上では、過去のコミット群は「一度取り込まれた後、意図的に削除された」と記録されています。Gitはこれを意図的な変更と判断するため、以前の変更内容を再適用せずスキップしてしまいます。
安全に再マージするための3ステップ:
ステップ1:以前実行した revert コミット自体を revert する
まず、過去のロールバックコミットを打ち消すことで、元のコードを main 上に復活させます:
# 最新の main ブランチを同期
git checkout main
git pull origin main
# revert コミットのハッシュを取得(例:a1b2c3d)
git log --oneline -n 3
# revert コミットをさらに revert(注:単一コミットのため -m オプションは不要)
git revert a1b2c3d
これでフィーチャーブランチの元のコードが main に復活します。
ステップ2:バグ修正済みのフィーチャーブランチをマージする
git merge feature/payment-v2
これで、復活した既存コードと最新のバグ修正コミットの両方が統合されます。
ステップ3:コンフリクトを解消(必要な場合)してリモートへpushする
git add .
git commit -m "fix: resolve conflicts and re-merge feature/payment-v2"
git push origin main
実戦で役立つGit障害対応のベストプラクティス
日々数十回のデプロイが行われるマイクロサービス運用を長年経験してきた中で、守るべき4つの鉄則を紹介します:
- 共有ブランチで
git reset --hardを絶対に使わない:git reset --hard HEAD~1とgit push --forceの組み合わせは、他のチームメンバーのローカル環境を破壊します。さらに悪いことに、CI/CDパイプラインの監査ログも失われてしまいます。 - コミットメッセージに障害のコンテキストを明記する: revert時には、後からログを追跡しやすいように障害チケット番号を含めましょう:
git revert -m 1 9a4f21b --no-edit && git commit --amend -m "revert: rollback PR #42 due to checkout timeout (INC-1042)"。なお、revertコマンド実行時に-m "message"と-m 1を同時に渡すことはできません(Gitでは-mが--mainlineとして解釈されるため)。 - 再マージ時は中間ブランチを作成する:
mainへ直接マージするのではなく、re-merge/payment-v2ブランチを作成してPull Requestを出しましょう。マージ前にチームメンバーと差分(diff)を再レビューし、ロジックの欠落がないか確認します。 - チームの運用手順書(Runbook)を標準化する:
git revert -m 1の手順をプロジェクトのオンコール資料に記載しておきます。これにより、夜間障害時でもジュニアエンジニアや新入社員がTech Leadを起こすことなく、2分以内に自力で安全にロールバックできるようになります。

