Claude Media
Claude Codeのgit revertでマージコミットを戻す — -m 1と再マージの罠

Claude Codeのgit revertでマージコミットを戻す — -m 1と再マージの罠

マージコミットのgit revertに-m 1が要る理由と、取り消したブランチを再マージしても変更が入らない仕組み、revertのrevertでの復旧を、手元で再現した出力とClaude Codeへの任せ方でまとめます。

mainに入れたばかりのブランチが本番で問題を起こし、いったん取り消したい。Claude Codeに「このマージをrevertして」と頼めば手は動きますが、マージコミットのrevertには通常のコミットにない決まりが2つあります。親を指定する -m 1 が必須であることと、取り消したあとに同じブランチを再マージしても、取り消した分の変更が戻ってこないことです。

後者は、気づかないまま「修正を足して再マージしたのに機能が一部しか入っていない」状態を作ります。この記事では、検証用リポジトリで両方を再現した出力を示し、復旧の手順と、Claude Codeに任せる範囲の決め方を順に説明します。

マージコミットは-mなしではrevertできない

通常のコミットなら git revert <commit> だけで打ち消しのコミットができます。マージコミットは親が2つあり、どちらの側を「元の状態」として戻すのかが決まらないため、-m を付けないと失敗します。git 2.50.1で、feature ブランチを --no-ff でmainに取り込んだ直後に実行した結果です。

git revert --no-edit HEAD
error: commit <マージコミットのハッシュ> is a merge but no -m option was given.
fatal: revert failed

git revert の公式の説明も同じ趣旨です。通常はどちらの側をメインラインとみなすか分からないのでmergeはrevertできず、-m で親の番号(1始まり)を指定すると、その親を基準に変更が打ち消されます。

-m 1と-m 2の違い

-m 1 は「第1親を残し、第2親から持ち込まれた変更を消す」指定です。mainにfeatureをマージした場合、第1親はmain側の直前のコミット、第2親はfeatureの先頭です。ほとんどの運用では -m 1 が正解になります。

迷ったときは、親の並びを実際に見ます。

git show -s --format='%P' <マージコミット>

出力された2つのハッシュの左が第1親、右が第2親です。第1親が「マージ先のブランチにいた側」であることを確かめてから -m に番号を渡します。

git revert -m 1 --no-edit <マージコミット>

検証用リポジトリでは、この1行でfeatureが持ち込んだ a.txt と b.txt が消え、main側で先に入っていた other.txt は残りました。打ち消しのコミットは Revert "M: merge feature" という題で、マージの題がそのまま引用されます。

実行前に作業ツリーを空にして、まず-nで試す

git revert は作業ツリーに未コミットの変更が無い状態でないと動きません。Claude Codeが別の編集を途中まで進めているセッションでは、先にコミットか退避を済ませます。コミットを作る前に結果だけ見たいときは -n(--no-commit)が使えます。

git revert -n -m 1 <マージコミット>
git status --short
git revert --abort

-n は打ち消しの変更をインデックスと作業ツリーに載せるだけでコミットしません。git status --short で消えるファイルを一覧し、問題なければコミット、やめるなら --abort で元に戻します。

取り消したブランチを再マージしても変更が戻らない

ここからが本題の罠です。revertのあとでfeatureに修正 D を足し、もう一度mainにマージします。期待するのは「featureの全部が入る」ことですが、実際は違いました。

git merge --no-ff feature -m "M2: remerge"
ls
app.txt
c.txt
other.txt

featureで最初に追加した a.txt と b.txt が無く、再マージ後に足した c.txt だけが入っています。revertは「データの変更」を打ち消しますが、「マージしたという履歴」は消しません。gitから見るとfeatureの元のコミットはmainに取り込み済みで、再マージで運ばれるのは、それ以降に増えたコミットの変更だけです。git-revertのオプション説明にも、後続のマージは以前revertしたマージの祖先でないコミットの変更だけを持ち込む、と書かれています。

落とし穴

衝突も出ずに、変更が黙って欠ける

新規ファイルだけの追加なら、上のようにエラーなしで「一部だけ入った」状態になります。revert後にfeature側で、取り消されたファイルを編集していた場合は、CONFLICT (modify/delete) のような衝突として表に出ることがあります。いずれも原因は同じで、取り消しの履歴が残っていることです。再マージ後は、差分の有無ではなく、入るはずのファイルが揃っているかを見ます。

再マージのあとに欠けがないかは、ブランチ同士の差分で機械的に調べられます。

git diff --stat main feature

マージが完全に済んでいれば、featureにだけ存在するファイルは出ません。上の例なら a.txt と b.txt が差分として現れ、取り込めていないことが分かります。Claude Codeにはこの出力を読ませて、欠けているファイルの一覧を作らせると確認が速くなります。

revertのrevertで元に戻してから再マージする

featureに修正コミットを足して同じブランチを使い続けるなら、先に打ち消しコミット(W)自体をrevertします。W を打ち消すと、マージで入った変更がmainに復活します。

手順

修正を足して再マージするときの手順

  1. 1

    打ち消しコミットを探す

    git log --grep='^Revert' で、マージをrevertしたコミットのハッシュを控えます。

  2. 2

    打ち消しを打ち消す

    git revert --no-edit <W> を実行します。-m は不要です。通常のコミットだからです。

  3. 3

    feature を再マージする

    git merge --no-ff feature で、修正コミットだけが新たに加わります。

  4. 4

    ファイルを確かめる

    ls や git diff --stat で、a.txt と b.txt を含む全体が揃ったか見ます。

同じ検証用リポジトリでこの順に進めると、最終的なファイルは a.txt b.txt c.txt app.txt other.txt の5つになりました。git 2.50.1ではrevertのrevertの題が Reapply "M: merge feature" になります。履歴を模式的に描くと次のとおりです(実際の git log --graph --oneline の出力ではなく、親子関係を示す図です)。

*   M2: remerge
|\
| * D: c.txt
* | Reapply "M: merge feature"
* | Revert "M: merge feature"
* | M: merge feature

featureを作り直したときは逆に何もしない

revertのrevertが正解なのは、元のコミットを残したまま修正を重ねた場合です。開発者がfeatureを捨てて、mainの更新後に同じ変更を作り直した場合(コミットが A' B' に置き換わる場合)は、revertのrevertをすると、作り直した変更と元の変更が重なって衝突が増えます。この場合は何もせず、そのまま再マージします。

元の分岐点を変えずに作り直したいときの方法も、gitのHow-Toが紹介しています。

git rebase --no-ff <分岐点>

--no-ff を付けると、変更していないコミットも含めてすべて新しいコミットとして作り直され、revertのrevertを挟まずに再マージできます。

Claude Codeに任せる範囲と権限

一連の作業はClaude Codeに頼めますが、親番号の選択と再マージの判断は、依頼文で先に固定しておくと取り違えが減ります。たとえば次のように書きます。

マージコミット <ハッシュ> を revert したい。
まず git show -s --format='%P' で親の順を見せて、-m 1 が main 側か確認して。
実行前に、消えるファイルの一覧を git diff --stat で出して。

実行前に差分の概要を出させると、-m 2 を選んで逆側を消す事故に気づけます。変更を反映する前に提案だけ見たいときは、プランモード(claude --permission-mode plan、またはShift+Tab)が使えます。プランモードではClaudeが計画を示すだけで、承認するまで編集しません。詳しい使い方はプランモードの解説にあります。

git revertは確認つきにしておく

打ち消しコミットは履歴に残る操作です。確認なしで走らせたくなければ、ask ルールでコマンド単位に確認を挟みます。.claude/settings.json に次のように書きます。

{
  "permissions": {
    "ask": [
      "Bash(git revert *)"
    ]
  }
}

ルールはdeny、ask、allowの順に評価され、askに一致すれば別のallowがあっても確認が出ます。ただしBashルールが一致するのはClaudeが書いたコマンドの文字列で、git -C . revert のように書き方が違うと対象外になります。確認を強制する境界としては使えないので、制限したい場合は権限設定の解説も合わせて見てください。

revertを選ぶ前に考える代替案

gitのドキュメントに収録された議論では、マージのrevertは技術的には問題なく動く一方、運用としては避けたほうがよい操作とされています。revertしたマージは、のちに原因を追うとき「大量の変更をまとめて逆にした1コミット」になり、どの変更が悪かったのかを分けて追えなくなります。

マージ済みの問題は、まずブランチ内のどのコミットが原因かを突き止め、そのコミットだけをrevertするか修正するほうが扱いやすくなります。原因の絞り込みはgit bisectで回帰を特定する手順、特定のコミットだけを別ブランチへ移す場面はcherry-pickとバックポートが参考になります。「ブランチ全体を入れるべきではなかった」と判断できる場合に限って、-m 1 のrevertを選ぶ、という順です。

どのコミットが原因か分からないときは、履歴から経緯を引く方法がgit履歴から経緯を調べる手順にあります。

まとめ

マージコミットのrevertは git revert -m 1 <コミット> で1行ですが、本当の作業は取り消した後に始まります。同じブランチに修正を足して使い続けるなら再マージの前にrevertのrevertを挟み、作り直したなら挟まない。この分かれ目を先に決めてからClaude Codeに頼むと、「再マージしたのに一部しか入っていない」状態を避けられます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/common-workflows
  2. git-scm.com/docs/git-revert
  3. git-scm.com/docs/howto/revert-a-faulty-merge
  4. code.claude.com/docs/en/permissions
この記事を共有:XはてブLinkedIn