Git における、git commit の取り消し方法や、やり直し操作に関する方法をまとめました。Git はどんなコミットでもすべてを記録していますので、一度間違えたとしても、いつでも昔の記録からもとに戻せ事が保証されています。
取り消しや、やり直し方法をマスターすれば、バージョン管理ツールとしてのメリットを最大限享受することができます。
目次
git commit の取り消し方法、6選
コミットした直後に「あっ、この変更入れ忘れた!」「あっ、いらないファイルを混ぜてコミットしちゃった!」など、確認不足による間違いは、時間に追われるエンジニアに非常にしばしば発生します。そのような間違いコミットは、まだpushしていなければ、容易に取り消したり、その後コミットをやり直すことができます。
コミットの取り消しにまつわるケースを、下記の6種類に分けてご紹介します。
- 直前の git commit 実行を取り消す
- 過去のコミット作成の直後に戻す
- 間違いコミットの打ち消しコミットを作る
- 直前コミットを上書きする
- 過去のコミットを歴史から削除する
- 過去の「あの時」に戻す
それでは、見ていきます。
1直前の git commit 実行を取り消す
「git reset <branch>」は、現在のブランチの先頭(HEAD)を、指定のコミットに強制的にリセットするためのコマンドです。「--soft」オプションを付けると、作業ツリーとインデックスをそのままにして、ブランチの先頭のみを変更します。(デフォルトではmixedモードとなり、インデックスをも更新します)
下記の操作により「git commit 」の実行の直前に戻すことができます。
// 直前のコミット操作を取り消す
git reset --soft HEAD^
下記のように、--softモードであれば、コミットを取り消した後も、インデックスと作業ツリーの状態はそのまま残りますので、その後、修正を追加してコミットをやり直すことが可能です。
このように、HEAD 参照の書き換えだけで留めてくれますので、取り消したあとに、作業ツリーに追加修正し、再コミットが行いやすい特徴があります。
逆に、もしインデックスも作業ツリーの変更も同時に取り消したいのであれば、「--hard」オプションを利用できます。これは次のセクションでカバーします。
2過去のコミットの直後に戻す
「git reset 」は「--hard」オプションを付けることにより、HEADのみでなく、インデックス、および作業ツリー上の変更もすべて取り消してくれます。これにより、一つ前のコミットを作成した直後に戻る事ができます。
// 直前のコミット作成の直後に戻す
git reset --hard HEAD^
下記は、git commit --hard HEAD^ の実行イメージです。
変更内容ががすべて消し飛びますので、何もなかった事にできます。逆に、インデックス、作業ツリーに、必要な変更が残っていないことに注意して下さい。commit か、stash していない変更内容はあとで取り返す事ができません。
複数のコミットを一度に取り消す
「git reset」を使えば、直前のコミットだけでなく、いかなるコミットの状態にリセットすることができます。下記は--hard オプションを使って、過去のコミット直後に戻る様子です。いかなるコミットを指定しても構いません。指定したコミットに、HEADを強制的に差し戻します。
// 現在のブランチ先頭を、<commit>の状態に戻す
git reset --hard <commit>
3間違いコミットの打ち消しコミットを作る
この方法は、正確には、コミットの取り消しではありませんが、言うなれば「変更内容の取り消すような反対の変更を含んだ新しいコミットを作成する」コマンドです。git revert により作成されたコミットを「revertコミット」「打ち消しコミット」と呼ぶことがあります。
// 直前の変更を打ち消すコミットを作成 (2つは同義) git revert git revert HEAD
このrevert コミットは、バージョン管理上、単なる1つの新しいコミットですので、ブランチの過去の履歴に影響を与えません。ですので、Pushしてもコンフリクトする事がありません。
蛇足ではありますが、git revert で注意が必要なのは、マージコミットを取り消す場合です。git revert での取り消しについて、詳しくはこちらをご覧ください。
4直前コミットを上書きする
こちらは「直前のコミットを上書きする」コマンドです。git commit に「--amend」オプションをつければ、直接的に直前のコミットを修正することができます。
// 直前のコミットを修正する。
git commit --amend
実際は、現在のHEADが指すを破棄して、新しいコミットが作成されます。
コミットの取り消しの弊害?Pushする時は要注意!
amend 操作で注意が必要なのは、git commit --amend 操作でコミットの識別子(SHA-1)が更新されますので、バージョン管理システム上は「古いコミットを破棄し、新しいコミットで置き換えた」という扱いになります。
つまり、もし古いコミットをすでにPushしてしまっているのであれば、新しいコミットを作成してそれをPushした時、リモート側の古いコミット履歴と食い違いが生じるので、必ず衝突(コンフリクト)を引き起こします。このようなコンフリクトの解決方法は、この記事の後半の「コミットの取り消しをリモートに反映する方法」をご覧ください。
5 過去のコミットを歴史から削除する
git rebase は、ブランチの歴史を修正するためのコマンドで、ブランチ履歴の整理のために、よく使うのgit コマンドの1つです。「i」は「interactive(インタラクティブ)」の略です。
例えば、下記のように取り消したいコミットの直前を指定します。あくまで「リベース」を実行していますので、「どのコミットを取り消したいか?」ではなく、「どのコミットに対して、リベースをかけるか」を引数で指定する点を意識して下さい。
// 現在のブランチを、<commit> までリベース。 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 」のいずれかに変更して、保存すれば、まとめて書き換えてくれます。もし取り消したいコミットがあれば、「drop」もしくは「d」を記述します。
ただし、過去のコミットの破棄は、その後、そのコミットに依存する変更を含むすべてに影響を与えます。そのような場合は、このリベース処理の途中で、何度もコンフリクトの解消をするように促される事になります。「直前のコミットの取り消し」と異なり「過去の特定のコミットの取り消し」は、歴史上大きな影響を及ぼす可能性があるといえるでしょう。
一般的に、git rebase -i は、「s(コミットを直前のコミットに統合)」「f(sと一緒。ログも破棄)」などを利用して、ログをキレイに整える目的で利用することが多いでしょう。
6過去の「あの時」に戻す。「取り消し」の取り消し
少し込み入ったパターンですが、git commit 取り消し操作を間違えてしまった、必要だったコミットが履歴に見当たらなくなってしまうことがあります。
なんだか全部もう何もかも嫌になって、「あの頃からすべてやり直せたら・・」という気もちになる時、そんなときは「git reflog」が便利です。git reflog は、HEADを含む「参照」の移動をすべて記録していますので、過去の好きなタイミングに帰ることができるのです。
// いまのブランチのすべてのコミット歴史を表示 git reflog
デフォルトではHEADの移動をすべて表示します。
fc1fd0f HEAD@{0}: reset: moving to HEAD
fc1fd0f HEAD@{1}: commit: Fix the bug.
82f1826 HEAD@{2}: commit: Fix the bug.
7a52e59 HEAD@{3}: cherry-pick : Fix the layout.
ba43eb4 HEAD@{4}: commit: Fix the bug.
c57472a HEAD@{5}: commit: Add the related css props.
88e418c HEAD@{6}: commit: Narrow the margin between paragraphs
7d8611b HEAD@{7}: commit: Update the title tag.
8226a0d HEAD@{8}: commit: Add the adobe affilicate tag.
HEAD@{xx}というのは、過去何番目のHEADの状態かということですね。
元に戻したいポイントがわかれば、あとは、git reset --hard で強制的に巻き戻します。
// 指定のコミットに、ブランチ状態を強制上書き。全部取り消し。
git reset --hard HEAD@{14}
git reflogすごいですね。git reflog を使った操作はこちら:
git reflog のマニュアルです。
【補足】取り消しをリモートに push して反映する方法
上記で、紹介したやり方でローカルのブランチ歴史を書き直した場合、もし修正前をすでに git push してしまっているのであれば、修正後の push は完全にコンフリクトします。
その場合、下記の「-f」オプションを付けて push することで、強制上書きできます。
// 強制的にpushして、リモートの履歴を上書きする git push -f origin <branch name>
注意としては、チームのみんなが参照しているブランチ(masterなど)にこれを行わない用にして下さい。他の人全員が、次回のpush でコンフリクトすることになるので、大変な迷惑になると思います。一人で開発しているレポジトリであれば、気楽に行ってしまって良いかもしれません。
こちらに git checkout について補足します。
補足:git checkoutとの振る舞いの違い
コミットの取り消しにおける、よくある勘違いのひとつが「git checkout <commit>」によって状態を戻そうとしてしまうことです。git checkout では、引数にブランチ名でなくコミットを指定すると、「匿名ブランチ(anonymous branch)」を自動で作成しチェックアウト、そして、その匿名ブランチの先頭をその指定したコミットとする」といった振る舞いになります。
逆に、git checkoutの引数にコミットとファイルパスの組み合わせ指定すれば、インデックスと作業ツリーにそのファイル状態を配置することができます。これはgit checkout のもう一つの機能です。しっかりと仕様を理解して、混乱を避けるようにしましょう。
補足をもう一つ、ご興味があれあば。
補足:git reset のモードについて
上記の通り、git reset --softは、HEADだけを書き換えますが、引数なしのデフォルト(mixed)モードでは、HEADとインデックスを同時に、また、hardモードでは、さらに作業ツリーを含めて、すべて指定された状態に上書きするのでした。
soft モード
mixed モード(引数なしのデフォルト)
hard モード
git reset のモードをまとめます。
モード指定 | 動作 |
git reset --soft | 現在のブランチの先頭だけをリセット。(インデックス、作業ツリーはそのまま) |
git reset (git reset --mixed) |
現在のブランチの先頭と、インデックスをリセット |
git reset --hard | 現在のブランチの先頭、インデックスも作業ツリーも全部リセット |
mixed モードは、インデックスを書き換えますので、再コミットを作りたい時は、git add をつかったステージングをやり直す必要があります。
また、hard モードにおいては、作業ツリーの変更は、stashなど別の方法で記録していない限り、取り返す事が絶対にできなくなります。これはくれぐれもご注意下さい。怖い時はとりあえずcommitや、stashしておけば、保存されますので、安心かもしれません。
git reset はこちらにも詳しくまとめています。ぜひご一読下さい。
参考情報
git commitの取り消し、やり直し、歴史の変更に関しては、仕様を理解しておくと滞りなく開発をすすめられるでしょう。特に、忙しい開発において間違いは必ず発生するので、ここの操作でまごつかずにスムーズに解決し、開発に集中できるようになっておくと良いと思います。
下記は関連記事です。
また、コミット前の「git add」に関する取り消しはぜひこちらをご覧ください。