開発ブログ

WWWクリエイターズが送る、Git、CSS、HTML、コマンドライン、Macの便利機能など、開発に関する役立ち情報発信します。気まぐれに更新。

git commit の取り消し&やり直し、完全攻略。

最終更新:2017-10-05 by Joe

git commit の取り消しや、やり直し操作に関する方法をまとめました。Git はどんなコミットでもすべてを記録しています。間違えたら、いつでも昔の記録からもとに戻せばいいのです。

直前のコミットを取り消して、やり直す

git commitを実行した後に、そのコミットをやり直す場合は、インデックスと作業ツリーの状態をどの状態に戻すかをコントロールできます。

[1] git commit 実行を取り消す

下記のコマンドで、直前に行ったの git commit の実行を取り消す事ができます。

// 直前のコミットを取り消す
git reset --soft HEAD^

この「git reset --soft」は、HEAD(=現在のブランチの先頭)を、その一つ前のコミットの状態、すなわちHEAD^(緑色) にリセットするコマンドです。このコマンドの実行後は、ブランチの先頭を示す参照である HEAD が、もともとHEAD^(緑色)の位置に移動します。作業ツリーとインデックスの状態(紫色)は影響をうけず、そのままの状態です。

実行前:

git reset --soft HEAD^ の実行後:

git reset --soft

「HEAD^」は「HEAD」の一つ前のコミットを意味しますので、これにより、コミットを実行する前に戻ります。これは、すなわち「作業ツリーの状態をインデックスにステージしたが、まだ git commit を実行していない状態」に戻れます。

あとは、必要に応じて修正を加え、git commit をやり直す事ができます。(この操作は体感的には「git commit --amend」とよく似ています。

 

[2] git commit でなく、git addも一緒に取り消す

あなたが望めば、git reset の引数なし(=mixedモード)を使えば、インデックスも同時にリセットすることができます。これによって、変更内容を作業ツリーにとどめたまま、「addとcommit 両方をやり直す」という操作が実現できます。

// 直前のコミットをやり直す (git add も含めてやり直す)
git reset HEAD^

// 同義
git reset --mixed HEAD^

実行後、インデックスも同時に、HEAD^(緑色)の状態にリセットされます。この状態から、git addgit commitをそれぞれやり直すことができます。

git reset HEAD^ の実行後:

git reset --mixed

この方法であれば、addを含めてやり直すことで、コミットを小さく何回かに分けてコミットを作り直す事ができます。ついついまとめていっしょくたにしてしまったコミットも、やり直しの際にコミットを分割して、履歴をキレイに作り直したりできます。

[3] 直前の git commit をやり直す

コミットをしたけど、ちょっとしたミス(タイプミス、ログの消し忘れ、など)があって、修正してコミットし直したい時、直前のコミット内容を修正します。これは、前述の git reset --soft のあとに、git commit を実行するのとよく似ていますが、こちらは直前のコミットを上書きするようにコミットを作る、という動作です。

// 直前のコミット内容を修正する
git commit --amend

もし直前の間違ったコミットをすでに、Pushしてしまってた後であれば、リモートにプッシュしたコミット内容と、ローカルで修正したコミット内容で、歴史に食い違いが生じる事になります。このため、amend操作のあと、改めてpushすると必ずコンフリクトします。これの対応方法は後述します。

コミット自体を完全に取り消す

さて、上述の方法は「コミットのやり直し」についてでしたが、こちらは完全に取り消して、無かったことにする方法です。

直前のコミットを無かったことに・・・。

コミットを作成したけど、そもそも不要な変更だった・・・といった場合など、不要になったコミットを完全に削除し、「完全になかったことに・・」したい時の操作です。

// 直前のコミットを完全に取り消す
git reset --hard HEAD^

しくみは単純ですが、git reset --hard <commit> は、現状のブランチの先頭(branch headと呼びます)を強制的に指定のコミットに合わせるコマンドです。作業ツリー、インデックス、ブランチの先頭(とブランチ履歴)が、全てを指定したコミットに強制的に一致させします。この例では、ブランチの先頭は HEAD ですので、その直前のコミット、すなわち HEAD^ に強制一致させます。

実行後は、インデックスと作業ツリー両方が、もともとの HEAD^(緑色)の状態になる事に注意して下さい。

git reset --hard

上記で分かるように、「git reset --hard xxxx」を行うと、作業ツリーの内容もろとも強制上書きされて、作業ツリーとインデックスに残されていた変更は吹き飛び、消えてしまいます。

これらのコミットされていない変更は、stashなど別の方法で記録していない限り、取り返す事が絶対にできなくなります。これはくれぐれもご注意下さい。なんだか怖い時はとりあえずcommitや、stashしておけば、安心かもしれません。

git reset --hard のやり直し

もしも誤ってgit reset --hard して、必要だったコミットが履歴から消えてしまった・・。失ったコミット取り返したい、すなわち「やり直しをやり直したい(?)」といった方には、git reflog という手段で復元できます。この方法は、一度でもコミットして、変更が何らかの形で記録されていれば、利用可能です。こちらの記事をご覧ください:

git reset について補足

このように、git resetはコミットの取り消しや、やり直し作業に非常に有効なコマンドです。下記に簡単にreset のモードを比較します。

モード指定 動作
git reset --soft 現在のbranchの先頭だけをリセット。(インデックス、作業ツリーはそのまま)
git reset
(git reset --mixed)
現在のbranchの先頭と、インデックスをリセット
git reset --hard 現在のbranchの先頭、インデックスも作業ツリーも全部リセット

git reset のモードについては、こちらの記事により詳しく解説しています。ぜひご覧ください。

 

昔のコミットの状態に戻す

現在のブランチの状態を強制的に、昔の特定のコミットの状態に戻します。上述の git reset --hardを利用します。

// 特定のコミット状態に戻る
git reset --hard <commit>

現在のブランチの先頭(HEAD)を指定したコミットに移動します。その指定コミット移行のコミットはブランチの歴史からなくなります。(必要に応じて、git reflogから参照して、取り戻せます)

git checkoutとの振る舞いの違い

よくある勘違いが「git checkout <commit>」によって戻そうとしてしまうことです。git checkout では、引数にブランチ名でなくコミットを指定すると、「匿名ブランチ(anonymous branch)」を自動で作成しチェックアウト、そして、その匿名ブランチの先頭をその指定したコミットとする」とう振る舞いになります。コミットをブランチから削除するようなコマンドではありません。git checkoutはあくまでブランチを移動するためコマンドなのです。

コミットした変更内容を、別のブランチに移動する

こちらは応用編です。一度作成したコミットを取り消して、そのコミットの変更内容を別のブランチに移動します。

[1] 新しい別ブランチにコミット移動する

さて、コミットすべきブランチを間違えてしまいました。その変更を単純に取り消しするのではなく、新しいブランチにコミットを移動してしまいます。

// (1) 現在のコミットで、新しい別ブランチを新しく作ってチェックアウト。
git checkout -b new-branch

// (2) コミットを取り消すため、さっきのブランチに戻る
git checkout original-branch

// (3) 間違ったコミットを取り消す。
git reset --hard HEAD^

(1)により、コミットをブランチ new-branch に保存しておきます。(3)により、original-branchの先頭のコミットを消去します。この操作は、new-branchの歴史はそのままですので、変更内容自体は別のブランチに保存したままoriginal-branchの歴史を書き直せるというわけです。

履歴はこんな感じになったでしょうか?

* refs/heads/new-branch (新しい変更を保持している)
* HEAD -> refs/heads/original-branch (コミット前のoriginal-branch)
*

[2] 既存の別ブランチにコミットを移動する

移動先が既存のブランチでなくても、要領は同じです。こちらでも、git reset --hard も利用できますが、あえて別のコマンド、cherry-pickを使って移動を実現してみます。

// (1) 既存のanother-branch ブランチをチェックアウト
git checkout another-branch

// (2) 間違ったコミットをチェッリーピック
git cherry-pick <wrong-commit>

// (3) 間違ったコミットのブランチに戻り、コミットを取り消し
git checkout original-branch
git reset --hard HEAD^

Pushしたコミットを削除する

さて、ここまでは、ローカルでの取り消し作業の話でした。それが、一度、リモートにPush してしまうと、リモートに変更が反映され、もしそれがチームで共有ブランチであれば、そのコミットを参照している他のチームメンバーの事を考える必要があります。

このようなケースの対応方法において、2つの方針を紹介します。

[1] git revertで「打ち消しコミット」を作る

間違ったコミットをpushしてしまったとき・・、これは、基本的には、完全な取り消しは諦めて、 git revert コマンドを使って「打ち消しコミット(revert commitと呼ばれます)」を作成して下さい。

git revert 
git revert HEAD // 上と同義

git revert はgit commit 自体を取り消したり、消去したりわけではなく、変更を「打ち消す」ようなコミットを新しく作成するコマンドです。git log をして履歴を見ると、過ちのコミットがしっかりと歴史に(コミット履歴に)刻まれてしまいます。ちょっとなんだか気持ち悪いのも事実です。

補足ですが、git revert で注意が必要なのは、マージコミットを取り消す場合です。git revert での取り消しについて、詳しくはこちらをどうぞ:

[2] pushしたコミットを履歴から削除する

もし、完全に一人で開発していたり、深夜残業してて「よし、絶対自分しか作業していないぞ、しめしめ・・」という時に、こっそり直したい時・・。「リモートの歴史を強制的に書き換える」方法があります。

// さっきのコミットを訂正。(amendでも、reset softでも、なんでも)
git commit . --amend 

// 強制的にpushして、リモートレポジトリのブランチを上書きする。えいや〜
git push -f origin <branch name>

繰り返しですが、チームで共有しているブランチに対してこれやると普通は怒られますので、不安な人は必ずチームの先輩に確認してからやって下さい。

特にチームメーンバーが同じリモートレポジトリを使っているのであれば、突然の取り消しは混乱を招く可能性があります。(あなたがさっきfetch してブランチアウトした元のコミットが突然消えてしまったら、きっと混乱するはずです。)

間違った操作を全部取り消して、やり直す

commit やマージを色々まちがえてしまって、全部もう何もかも嫌になって、イチからやり直したい時があります。せっかちな私は、何度これにお世話になったかわかりません。

// いまのブランチのすべてのコミット歴史を表示
git reflog 

// 指定のコミットに、ブランチ状態を強制上書き。全部取り消し。
git reset --hard HEAD@{14}

HEADを含む、参照(refs)に変更があったすべての記録を記録しています。HEAD@{xx}というのは、過去何番目のHEADの状態かということですね。git reflogすごいです。

git reflog を使った操作はこちら:

昔のコミットを歴史から消し去る

これも、よく使うのgit コマンドです。現在のブランチで、指定したコミットまでを一気にリベースします。

git rebase -i <commit>

こんな感じで、テキストエディタで編集する用にファイルが立ち上がるので

pick 6fcde18 Replace the news link.
pick 460b0b8 Set public false for the news.
pick de9f1d2 Fix the bug .
pick 4b0346d Remove news.

# Rebase b4792e2..4b0346d onto b4792e2 (4 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit

「pick」って書いてある部分を「p, r, e, s, f ,x ,d 」のいずれかに変更して、保存すれば、まとめて書き換えてくれます。これも便利なコマンどです。だいたい使うのは「s(コミットを直前のコミットに統合)」「f(sと一緒。ログも破棄)」ですね。

書き直した履歴を、強制的に push する

紹介したやり方でブランチ歴史を書き直した場合、もし修正前をすでにpushしてしまっているのであれば、修正後のpushは完全にコンフリクトします。その場合、下記のコマンドで強制上書きできます。

// 強制的にpushして、リモートの履歴を上書きする
git push -f origin <branch name>

繰り返しになりますが、チームのみんなが参照しているブランチ(masterなど)にこれを行わない用にして下さい。他の人全員が、次回のpush でコンフリクトすることになるので、すごい迷惑になると思います。

さて、gitの取り消し操作に関しては、以上です。筆者はいつも間違いやすいので、上記で紹介したコマンドはよく使う物ばかりです。

参考リンク

git commitの取り消し、やり直し、歴史の変更に関しては、いつでもできるようになっておくべきでしょう。間違いは必ず発生するので、ここの操作でまごつかずにスムーズに解決し、開発に集中できるようになっておくと良いと思います。

また、コミット前の「git add」に関する取り消しはぜひこちらをご覧ください。

 

閉じる