「詰んだ」と思ったら見るGit復旧チートシート ── 症状から探す、困ったときの一手
「あ、やってしまった」
Gitで何かやらかした瞬間、頭が真っ白になって「なんてコマンド打てばいいんだっけ」がまず出てこない、という経験は誰にでもあると思います。コマンド名から検索しようにも、そもそも今の状況に効くコマンド名が分からない。
このメモは、そんなときのために 「今、目の前で何が起きているか」から逆引き できるようにした復旧集です。「reflogとは何か」のような解説記事は他にたくさんあるので、ここでは割り切って「この症状ならこれを打つ」だけを並べます。
使い方
下の早見表から、今の状況に一番近いものを探してクリックしてください。該当の項目にジャンプします。
各項目には「その状況でgit statusを打つとどう見えるか」も添えています。「今の状況」の説明文だけでは自分のケースと合っているか自信が持てない、という場合は、実際に手元でgit statusを打って見比べてみてください。
※branchの名前やファイル名は例なので、実際の表示とは異なります。
| 今の状況 | 効くコマンド |
|---|---|
| 作業中のファイルの変更を、コミット前の状態に全部戻したい | git checkout -- . |
| git addしたファイルを、コミットする前にステージから外したい | git restore --staged <file> |
| 直前のコミットメッセージのtypoに気づいた(まだpushしていない) | git commit --amend |
| 直前のコミットにファイルを追加し忘れた(まだpushしていない) |
git add + git commit --amend --no-edit
|
| 直前のコミットをやり直したい、変更内容は手元に残したい | git reset --soft HEAD~1 |
| 直前のコミットごと、変更内容も全部なかったことにしたい | git reset --hard HEAD~1 |
| rebaseかresetをしたら、あったはずのコミットが消えた | git reflog |
| ブランチを間違えて削除してしまった |
git reflog → git branch
|
| rebase中、コンフリクトが辛すぎて元に戻りたい | git rebase --abort |
| mergeが訳の分からないことになった、元に戻りたい | git merge --abort |
| 間違ったブランチにコミットしてしまった(まだpushしていない) | git cherry-pick |
| pushしたコミットの内容を取り消したい、もうみんなが見ている | git revert |
| rebaseで整理した後pushしたら拒否された | git push --force-with-lease |
| 気づいたら見慣れない「detached HEAD」という表示が出ている | git switch -c <新ブランチ名> |
| git stashした内容、どこにあるか忘れた | git stash list |
なお前提として、まだpushしていない操作は基本的に全部やり直しがききます。焦らず、まず自分が「pushする前か後か」を確認してから該当項目を見てください。
作業中のファイルの変更を、コミット前の状態に全部戻したい
症状
コミットはまだしていない。もう全部やめて、直前のコミット時点の状態に戻したい。
git statusを打つとこうなる
On branch feature/A
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: src/App.js
modified: src/utils.js
no changes added to commit (use "git add" and/or "git commit -a")
「Changes not staged for commit」に見覚えのある変更ファイルが並んでいれば、このケースです。
打つコマンド
git checkout -- .
Gitのバージョンが新しければ以下でも同じことができます。
git restore .
解説
作業ディレクトリ内の未コミットの変更が全部消え、直前のコミット時点の状態に戻ります。コミット自体には触れていないので、reflogに記録が残るような操作でもありません。単純に「今作業中の内容を綺麗さっぱりリセットする」だけなので、気軽に打って大丈夫です。
git addしたファイルを、コミットする前にステージから外したい
症状
git addしたあとで、「あ、これはまだコミットする段階じゃなかった」と気づいた。ファイルの中身自体は消したくない。ステージから外すだけにしたい。
git statusを打つとこうなる
On branch feature/A
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: src/App.js
「Changes to be committed」に載っている、つまりすでにadd済みなのがポイントです。さっきのケースの「Changes not staged for commit」との違いに注目してください。
打つコマンド
git restore --staged <ファイル名>
解説
ステージ(インデックス)に上げた状態だけを取り消し、作業ディレクトリ上のファイルの中身はそのまま残ります。git addする前の状態に戻る、という認識でOKです。何かのはずみで意図しないファイルまでgit add .してしまったときによく使います。
直前のコミットメッセージのtypoに気づいた(まだpushしていない)
症状
コミットした直後に、メッセージのtypoに気づいた。「修正」と書くべきところを「習性」と書いていた、など。まだpushはしていない。
git statusを打つとこうなる
On branch feature/A
Your branch is ahead of 'origin/feature/A' by 1 commit.
(use "git push" to publish your local commits)
nothing to commit, working tree clean
作業ディレクトリはすでに綺麗で、代わりに「ahead of 'origin/...' by 1 commit」(まだpushしていないコミットが1つある)という表示が出ているのが目印です。
打つコマンド
git commit --amend
解説
エディタが立ち上がり、直前のコミットメッセージを編集できます。保存すれば、直前のコミットそのものがメッセージだけ差し替わったものに置き換わります。pushする前であれば、何度でもやり直しがきく操作です。
直前のコミットにファイルを追加し忘れた(まだpushしていない)
症状
コミットしたあとに、「あ、このファイルも一緒に入れるはずだった」と気づいた。別コミットにするほどでもない、直前のコミットに含めてしまいたい。
git statusを打つとこうなる
On branch feature/A
Your branch is ahead of 'origin/feature/A' by 1 commit.
(use "git push" to publish your local commits)
Untracked files:
(use "git add <file>..." to include in what will be committed)
src/newFile.js
nothing added to commit but untracked files present (use "git add" to track)
「ahead of ... by 1 commit」(直前のコミット)と、「Untracked files」に入れ忘れたファイルが一緒に出ていれば、このケースです。すでにgit addしていた場合は「Changes to be committed」側に出ます。
打つコマンド
git add <追加したいファイル>
git commit --amend --no-edit
解説
--no-editをつけると、コミットメッセージは変更せずにファイルだけ直前のコミットに追加できます。実態は「直前のコミットを、内容を差し替えて作り直している」だけなので、--amendはメッセージ修正にもファイル追加にも使える、と覚えておくと便利です。
直前のコミットをやり直したい、変更内容は手元に残したい
症状
コミットした内容そのものを見直したい。1つのコミットに詰め込みすぎた、コミット単位を分割し直したい、など。ただしせっかく書いたコードは消したくない。
git statusを打つとこうなる
On branch feature/A
Your branch is ahead of 'origin/feature/A' by 1 commit.
(use "git push" to publish your local commits)
nothing to commit, working tree clean
先ほどの「typoに気づいた」ケースと同じく、作業ディレクトリは綺麗で「ahead by 1 commit」だけが出ている状態です。見た目は同じでも、やりたいこと(メッセージだけ直すか、中身から作り直すか)で使うコマンドが変わります。
打つコマンド
git reset --soft HEAD~1
解説
直前のコミットが「なかったこと」になりますが、そのコミットで加えた変更はステージされた状態のまま作業ディレクトリに残ります。そこから改めてgit addを選び直して、コミットを分割し直したり、メッセージを書き直したりできます。
ちなみに「ステージも外して、ただの未コミット変更にしたい」場合は--softの代わりに--mixed(デフォルト)を使います。
git reset HEAD~1
直前のコミットごと、変更内容も全部なかったことにしたい
症状
ローカルでコードをいじりまくった結果、動くはずのものが動かなくなった。どこから間違えたのか分からない。直前のコミットの内容ごと、全部なかったことにしたい。
git statusを打つとこうなる
On branch feature/A
Your branch is ahead of 'origin/feature/A' by 1 commit.
Changes not staged for commit:
modified: src/App.js
modified: src/utils.js
modified: src/config.js
no changes added to commit (use "git add" and/or "git commit -a")
「ahead by 1 commit」(pushしていないコミットがある)に加えて、「Changes not staged for commit」にいじり回した未コミットの変更もぶら下がっている、という2段構えの状態です。
打つコマンド
git reset --hard HEAD~1
解説
作業ディレクトリ・ステージ・直前のコミットが、まとめて1つ前のコミット時点の状態に戻ります。名前のインパクトほど怖い操作ではなく、実態は「基準点を1つ前に合わせ直す」だけです。ここでの一番大事な注意点は、まだpushしていないコミットに対して使うということです。すでに他の人がpullしているコミットに対してこれを使うと、履歴がズレて混乱を招きます。
万が一「間違えてHEAD~1じゃなくてHEAD~3を打ってしまった」というような場合でも、次の項目のgit reflogで戻れます。
rebaseかresetをしたら、あったはずのコミットが消えた
症状
git rebase -iやgit reset --hardを打ったら、さっきまであったはずのコミットがgit logから消えている。「これ、もう戻せないのでは」と青ざめている。
git statusを打つとこうなる
On branch feature/fix
nothing to commit, working tree clean
ここが少し紛らわしいところなのですが、git statusは「working tree clean」と、いたって普通の顔で表示されます。**このケースを見分けているのはgit statusではなくgit log**です。「さっきまで見えていたコミットがgit logの一覧から消えた」というのがこのケースの目印になります。
打つコマンド
git reflog
解説
自分のローカルでHEADが移動した履歴が、時系列で全部並んで出てきます。
a1b2c3d HEAD@{0}: rebase (finish): returning to refs/heads/feature/fix
d4e5f6g HEAD@{1}: rebase (pick): 修正3
h7i8j9k HEAD@{2}: rebase (pick): 修正2
l0m1n2o HEAD@{3}: rebase (start): checkout HEAD~3
p3q4r5s HEAD@{4}: commit: 修正3(元のコミット)
消えたと思っていたコミットのハッシュは、大抵rebaseやresetを打つ前の行に残っています。あとはそのハッシュを指定して戻すだけです。
git reset --hard p3q4r5s
Gitは基本的にコミットをすぐには本当に消しません。ブランチという「目印」を失っても、コミット自体はしばらく(デフォルトで30日ほど)ゴミ箱の中に残っています。何か消えたと思ったら、まずこれを打つ癖をつけておくと安心です。
ブランチを間違えて削除してしまった
症状
もう使わないと思ったブランチをgit branch -Dで消したら、実は消してはいけないブランチだった。あるいはマージ前のブランチを間違えて強制削除してしまった。
git statusを打つとこうなる
On branch main
nothing to commit, working tree clean
これもgit status自体は普通の表示のままです。目印になるのはgit branch(ブランチ一覧)を打ったときに、さっきまであったはずのブランチ名が一覧から消えていることです。
git branch
打つコマンド
git reflog
解説
消えたのは「ブランチという名前の目印」だけで、コミット自体はreflogにまだ残っています。出力の中から、消したブランチの最後のコミットに該当するハッシュを探してください(直前に該当ブランチで作業していたコミットメッセージが手がかりになります)。見つけたら、そのハッシュから同じ名前でブランチを作り直せば復活します。
git branch 復活させたいブランチ名 <見つけたハッシュ>
ブランチという目印を失っている分、git reset --hardのケースよりハッシュ探しに一手間かかりますが、コミット自体が消えているわけではないので慌てなくて大丈夫です。
rebase中、コンフリクトが辛すぎて元に戻りたい
症状
git rebase -iを始めたら、想定より複雑なコンフリクトが次々出てきて、直しても直しても終わらない。画面には見慣れないメッセージが表示され続け、「今どういう状態なんだ」と分からなくなってきた。
git statusを打つとこうなる
interactive rebase in progress; onto a1b2c3d
Last commands done (2 commands done):
pick d4e5f6g 修正2
pick h7i8j9k 修正3
Next command to do (1 remaining command):
pick l0m1n2o テスト追加
(use "git rebase --edit-todo" to view and edit)
You are currently rebasing branch 'feature/fix' on 'a1b2c3d'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: src/utils.js
「interactive rebase in progress」「You are currently rebasing branch」という表示が出ていれば、rebaseの真っ最中です。ご丁寧にgit status自体が「(use "git rebase --abort" to check out the original branch)」とヒントを出してくれています。
打つコマンド
git rebase --abort
解説
rebaseを開始する前の状態に、そのまま綺麗に戻ります。コンフリクト解消のために触っていた内容も含めて全部リセットされます。rebaseは途中でいつでもやめられる操作です。迷ったら一旦--abortして、コンフリクトの少ない単位に区切ってからやり直す、というのはよくある立ち回りです。
mergeが訳の分からないことになった、元に戻りたい
症状
git mergeを実行したらコンフリクトが大量発生し、どのファイルをどう直せばいいのか収拾がつかなくなった。マージ自体をなかったことにして出直したい。
git statusを打つとこうなる
On branch feature/A
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: src/App.js
both modified: src/utils.js
no changes added to commit (use "git add" and/or "git commit -a")
「You have unmerged paths.」と出ていれば、まさにmergeのコンフリクト解消中です。こちらも「(use "git merge --abort" ...)」とヒントが表示されます。
打つコマンド
git merge --abort
解説
rebase --abortのmerge版です。merge開始前の状態にそのまま戻ります。コンフリクト解消中のファイルの中身も元通りになるので、一旦落ち着いてから、差分を小さくする・別のブランチ構成にするなど作戦を立て直してから再挑戦できます。
間違ったブランチにコミットしてしまった(まだpushしていない)
症状
feature/Aで作業しているつもりが、実はmainのまま作業してコミットしてしまっていた。まだpushはしていない。
git statusを打つとこうなる
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
(use "git push" to publish your local commits)
nothing to commit, working tree clean
一番上の「On branch」に意図していないブランチ名(この例ではmain)が出ていて、かつ普段そのブランチにローカル専用のコミットが乗らないはずなのに「ahead of ... by 1 commit」が出ている、という組み合わせが目印です。
打つコマンド
git branch feature/A
git reset --hard HEAD~1
git switch feature/A
解説
まず今のコミットを含んだ状態のまま、正しいブランチ名(feature/A)を今の位置に作ります。そのあと、間違えていた元のブランチ(この例ではmain)だけをコミット前の状態に戻します。最後に正しいブランチへ切り替えれば完了です。
複数コミットを間違えたブランチに積んでしまっていた場合は、cherry-pickを使う方法もあります。
git switch feature/A
git cherry-pick <積んでしまったコミットのハッシュ>
git switch main
git reset --hard HEAD~1
正しいブランチ側に該当コミットをcherry-pick(コミットの複製)した上で、間違えていたブランチ側からreset --hardで取り除く、という流れです。
pushしたコミットの内容を取り消したい、もうみんなが見ている
症状
pushしてしまったコミットに問題があると分かった。ただ、すでに他のメンバーがpullしている可能性がある。reset --hardで消して再pushするのは危険な気がする。
git statusを打つとこうなる
On branch feature/A
Your branch is up to date with 'origin/feature/A'.
nothing to commit, working tree clean
これまでのケースと違い、「ahead of ...」ではなく「up to date with」になっているのがポイントです。つまり問題のコミットは自分の手元だけでなく、すでにリモート(みんなが見る場所)まで届いてしまっている状態です。
打つコマンド
git revert <取り消したいコミットのハッシュ>
解説
過去のコミットを消すのではなく、その変更内容を打ち消す新しいコミットを追加します。履歴は「コミットA→コミットB(Aを打ち消す)」という形でそのまま残るので、他のメンバーの手元の履歴と矛盾しません。共有済みのコミットを取り消したいときは、履歴を書き換えるresetではなく、履歴を足すrevertを使うのが基本です。
rebaseで整理した後pushしたら拒否された
症状
git rebase -iで自分のコミット履歴を整理し直した後、いつも通りgit pushしようとしたら「リモートの履歴と合わないのでpushを拒否されました」と言われた。
git statusを打つとこうなる
On branch feature/A
Your branch and 'origin/feature/A' have diverged,
and have 3 and 2 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)
nothing to commit, working tree clean
「have diverged」(手元とリモートの履歴が分岐している)という表示が出ていれば、これはrebaseで履歴を書き換えた後にありがちなサインです。ここで案内されるgit pullをそのまま実行すると余計なマージコミットができてしまうことが多いので、素直には従わないほうが無難です。
打つコマンド
git push --force-with-lease
解説
rebaseはコミット履歴そのものを書き換える操作なので、単純なgit pushでは拒否されます。ここで安易にgit push --forceを打つと、自分がpullした後に他のメンバーが同じブランチへ加えていた変更を、気づかないまま消してしまう恐れがあります。
--force-with-leaseは、「自分が最後に取得したリモートの状態」と「今実際のリモートの状態」を比較し、変わっていれば失敗してくれる安全装置つきのforce pushです。他のメンバーが自分より先に何かpushしていた場合はエラーになって止まるので、事故を未然に防げます。
pushしたあとのgit push --forceは、このチートシートの中で唯一「ローカルの話では済まない、チーム全体に影響する」操作です。ここだけは他の項目より緊張して臨んでください。
気づいたら見慣れない「detached HEAD」という表示が出ている
症状
git checkout <コミットハッシュ>やgit checkout <タグ名>をした後、「You are in 'detached HEAD' state」というメッセージが出た。この状態のまま何かコミットしてしまったが、これはどのブランチの上にいるのか分からない。
git statusを打つとこうなる
HEAD detached at a1b2c3d
nothing to commit, working tree clean
一番上が「On branch 〇〇」ではなく「HEAD detached at 〇〇」になっているのが目印です。ブランチ名が出ていない、というのがこの状態の特徴です。
打つコマンド
git switch -c 新しいブランチ名
解説
detached HEADとは、「どのブランチにも属さない、コミット単体を指している」状態のことです。この状態でコミットを重ねてしまっても、コミット自体はちゃんと存在しています。ただし、このままブランチを切り替えると、そのコミット群への参照が失われやすくなります。
上のコマンドで今のコミット位置から新しいブランチを生やせば、通常のブランチとして続きの作業ができます。うっかりブランチを切り替えてしまってコミットを見失った場合も、git reflogから該当のハッシュを探して同様にブランチを生やせば復旧できます。
git stashした内容、どこにあるか忘れた
症状
作業を中断するときにgit stashで退避させた記憶はあるが、その後何をstashしたか忘れてしまった。あるいは、git stashしたまま数日放置していた。
git statusを打つとこうなる
On branch feature/A
nothing to commit, working tree clean
ここも少し紛らわしいのですが、git statusにはstashの中身は出てきません。作業ディレクトリは綺麗なままなので、「何もしていない」ように見えてしまいます。stashしたかどうか・何をstashしたかは、次のgit stash listで確認する必要があります。
打つコマンド
git stash list
解説
これまでstashした内容が一覧で出てきます。
stash@{0}: WIP on feature/A: p3q4r5s 機能Aの実装途中
stash@{1}: WIP on main: a1b2c3d 調査用の一時変更
中身を確認してから戻したい場合は、該当のstashを指定して取り出します。
git stash pop stash@{0}
popは取り出すと同時にstash一覧からも削除します。一覧に残したまま中身だけ適用したい場合はapplyを使ってください。
git stash apply stash@{0}
stashもreflogと同じく、Gitが「すぐには消さずに残してくれる」保険の1つです。放置していても急に消えることはないので、見つけたら落ち着いて中身を確認しましょう。
まとめ
今回挙げた症状には、ある共通点があります。pushする前の話であれば、ほぼ全部やり直しがきくということです。reflog・stash listはその代表格で、「消えた」と思ったものの多くは、実はまだGitのどこかに残っています。
唯一、本当に緊張して臨むべきなのはgit push --forceのようにチーム全体に影響するコマンドです。逆に言えば、それ以外の「詰んだ」は大抵詰んでいません。まずは自分が今どの状況にいるかを、この表から探してみてください。