【Git】直前コミットを取り消す手順を解説

【Git】直前コミットを取り消す手順を解説:状況に応じた最適な方法と注意点

ソフトウェア開発においてバージョン管理システムGitは不可欠なツールです。コミットは開発の節目を記録する重要な操作ですが、時には「しまった!」「間違えた!」と直前のコミットを取り消したくなることがあります。コミットメッセージを間違えた、ファイルを追加し忘れた、意図しないファイルをコミットしてしまった、そもそも直前の変更自体を無かったことにしたいなど、様々な状況が考えられます。

幸いなことに、Gitには直前のコミットを取り消すための複数の方法が用意されています。しかし、それぞれの方法は挙動や結果が異なり、状況に応じて使い分ける必要があります。誤った方法を選択すると、意図しないデータの損失や、共同開発している他のメンバーとの間で混乱を引き起こす可能性もあります。

本記事では、Gitで直前のコミットを取り消すための主要な方法を、その原理、具体的な手順、実行例、そして注意点を含めて詳細に解説します。約5000語というボリュームで、単なるコマンド紹介にとどまらず、Gitの内部的な仕組みに触れながら、なぜそのコマンドを使うのか、使うとどうなるのかを深く掘り下げていきます。これを読めば、あなたはどのような状況でも自信を持って直前コミットの取り消し操作を行えるようになるでしょう。

はじめに:なぜ直前コミットを取り消すのか?

Gitにおけるコミットは、プロジェクトの特定時点でのスナップショットを記録する操作です。開発者はコードの追加、修正、削除といった作業が一区切りついた際に、その変更内容をコミットとして保存します。これにより、過去の任意の状態に戻ったり、変更履歴を追跡したり、他の開発者とコードを共有したりすることが可能になります。

しかし、人間が作業する上でミスはつきものです。以下のような状況で、直前のコミットを取り消したいと考えることは珍しくありません。

  • コミットメッセージを間違えた: スペルミスがある、情報が不十分、別のコミットのメッセージを使ってしまったなど。
  • ファイルを追加し忘れた: コミットに含めるべき変更ファイルの一部を git add し忘れた。
  • 間違えてファイルをコミットした: まだ作業途中のファイルや、コミットすべきではない一時ファイルなどを誤って含めてしまった。
  • 変更内容自体を無かったことにしたい: 直前のコミットに含まれる変更内容が、そもそも誤っていたり、不要になったりした。
  • 直前のコミットを複数に分割したい: 一つのコミットに含めるには大きすぎる変更をまとめてコミットしてしまったので、分割してコミットし直したい。

これらの状況に対処するために、Gitはいくつかのコマンドを提供しています。特に直前のコミットは、まだ作業の記憶が新しく、他の開発者がそのコミットを元にした作業を開始している可能性も低いため、比較的容易に取り消したり修正したりできることが多いです。

直前コミットを取り消す操作には、主に以下の3つのアプローチがあります。

  1. 直前のコミットを「修正」する (git commit --amend): 直前のコミットに、追加の変更を含めたり、コミットメッセージを変更したりして、新しいコミットで置き換えます。これは「取り消す」というより「やり直す」「上書きする」イメージに近いですが、結果として直前のコミットは履歴から見えなくなります。
  2. 直前のコミットを「無かったこと」にする (git reset): HEAD(現在のブランチが指すコミット)を直前のコミットの親に移動させます。これにより、直前のコミットはブランチの先端から外れます。この操作は、インデックスやワークツリーの状態をどうするかによって、さらにいくつかのバリエーションがあります。
  3. 直前のコミットの変更を「打ち消す」 (git revert): 直前のコミットで行われた変更を逆方向に実行する新しいコミットを作成します。元のコミット自体は履歴に残ります。

これらの方法を理解し、状況に応じて適切に使い分けることが、Gitを効率的かつ安全に使う上で非常に重要です。特に、履歴を書き換える操作(git commit --amendgit reset)は、既にリモートリポジトリにプッシュして他の開発者と共有しているコミットに対して行うと問題を引き起こす可能性があるため、注意が必要です。

本記事では、まずこれらの操作を理解するために不可欠なGitの基本概念をおさらいし、その後、上記3つのアプローチそれぞれについて、詳細な解説と具体的な手順、そして使い分けのポイントを説明します。

Gitの基本概念のおさらい:HEAD, インデックス, ワークツリー

Gitのコミット取り消し操作を正しく理解するには、Gitが内部的にどのように状態を管理しているかを知る必要があります。特に重要な概念は以下の3つです。

  • ワークツリー (Working Tree / Working Directory):
    これはあなたが実際にファイルを作成したり編集したりする場所です。ローカルファイルシステムのディレクトリ構造そのものを指します。lsdir コマンドで見えるファイルやフォルダがワークツリーの内容です。あなたがコードを編集する際、変更はまずこのワークツリーで行われます。
  • インデックス (Index / Staging Area):
    次にコミットされる変更内容を一時的に置いておく場所です。ワークツリーでの変更を git add コマンドを使ってインデックスに登録します。インデックスは、次に作成されるコミットに含めたい変更を「ステージング」するための領域と考えると良いでしょう。インデックスに登録された変更だけが、次の git commit コマンドでコミットとして記録されます。インデックスの状態は、git status コマンドである程度確認できます(”Changes to be committed” のセクション)。
  • HEAD:
    現在作業しているブランチの最新コミットを指すポインタです。ほとんどの場合、HEADは現在のブランチ名(例:maindevelop)を指しており、そのブランチ名が最新のコミットを指しています。つまり、HEADは「現在のブランチの、最新のコミット」を意味します。git log コマンドで表示されるコミット履歴の一番上が、通常HEADが指しているコミットです。

これらの関係を簡単にまとめると以下のようになります。

  1. あなたはワークツリーでファイルを編集する。
  2. 編集した内容を次のコミットに含めるために git add <ファイル名>インデックスに登録する(ステージングする)。
  3. インデックスに登録された内容を git commitコミットとして永続的に記録する。
  4. 新しいコミットが作成されると、HEADがその新しいコミットを指すようになる(そして、HEADが指しているブランチもその新しいコミットを指すようになる)。
ワークツリー (作業場所)
    ↓ git add
インデックス (次にコミットする内容を準備)
    ↓ git commit
リポジトリ (コミット履歴を保管)
    ↑ HEAD (現在のブランチの最新コミットを指す)

直前のコミットを取り消す操作は、主にこのHEADポインタの位置を移動させたり、インデックスやワークツリーの状態を変更したりすることによって実現されます。

git status コマンドは、ワークツリーとインデックスの状態を確認するのに役立ちます。

  • “Changes to be committed”: インデックスにステージングされている変更。これが次のコミットに含まれます。
  • “Changes not staged for commit”: ワークツリーで変更されたが、まだ git add されていない変更。
  • “Untracked files”: Gitの管理下にない、ワークツリー内の新しいファイル。

これらの概念を頭に入れておくことで、これから説明する各種コマンドの挙動をより深く理解できるようになります。

方法1: 直前のコミットを「修正」する (git commit --amend)

最も頻繁に直前コミットを取り消したい状況の一つは、「コミットメッセージを間違えた」または「コミットに含めるべきファイルを忘れた/間違えて含めた」といった、直前のコミットの内容を少し修正したい場合です。このような場合に最適なのが git commit --amend コマンドです。

git commit --amend は「直前のコミットを修正する」と表現されますが、実際には直前のコミットを上書きして、新しいコミットを作成します。ただし、この新しいコミットは直前のコミットと同じ親を持ち、結果的に履歴上は直前のコミットが「無かったこと」になったように見えます。

この方法が適している状況:

  • 直前のコミットのコミットメッセージだけを修正したい。
  • 直前のコミットに含めるべきファイルを git add し忘れたので、追加したい。
  • 直前のコミットに誤って含めてしまったファイルを、削除した上でコミットし直したい。
  • 直前のコミットに含まれる変更内容の一部を微調整したい(例:typo修正)。

原理:

git commit --amend を実行すると、Gitは現在のインデックスの状態と、直前のコミットの親を元にして、新しいコミットを作成します。そして、現在のブランチのHEADを、この新しいコミットに移します。これにより、元の直前コミットはどのブランチからも直接参照されなくなり、履歴から「消えた」ように見えます(実際にはGitオブジェクトとしてしばらく残りますが、通常の方法ではアクセスできなくなります)。

もし git commit --amend を実行する前にインデックスに変更を追加 (git add) していなければ、新しいコミットの内容は元の直前コミットの内容に、メッセージ変更を加えただけになります。インデックスに変更を追加していた場合は、その追加された変更が新しいコミットに含まれます。

詳細な手順と実行例:

ここでは、いくつかのシナリオで git commit --amend を使う手順を説明します。

シナリオ1-1:コミットメッセージだけを修正したい

ファイルを変更したり追加したりする予定はなく、単に直前のコミットのメッセージだけを修正したい場合です。

  1. 何もファイルを変更せず、何もインデックスに追加しない状態で git commit --amend を実行します。
    bash
    git commit --amend
  2. デフォルトのエディタが起動し、直前のコミットメッセージが表示されます。
  3. メッセージを修正し、エディタを保存して閉じます。
  4. Gitが新しいコミットを作成し、直前のコミットを置き換えます。

実行例:

“`bash

最初のコミットを作成 (メッセージに typo があるとする)

echo “Initial content.” > file1.txt
git add file1.txt
git commit -m “Add intial file” # ‘initial’ を ‘intial’ と間違えた

履歴を確認

git log –oneline

出力例:

a1b2c3d Add intial file

コミットメッセージを修正したい

git commit –amend

エディタが開く。メッセージを以下のように修正して保存:

Add initial file

履歴を再度確認

git log –oneline

出力例:

e4f5g6h Add initial file # コミットハッシュが変わっていることに注目

“`

git log --oneline を見ると、コミットハッシュが変わり、メッセージが修正された新しいコミットができていることがわかります。元のコミット(a1b2c3d)は履歴から消えています。

シナリオ1-2:ファイルを追加し忘れたので、直前コミットに追加したい

直前にコミットした後に、コミットに含めるべきだった別のファイルを編集したり作成したりしたことに気づいた場合です。

  1. 追加し忘れたファイル(または修正したファイル)を git add します。
    “`bash
    # 新しいファイルを作成し、ステージングする
    echo “More content.” > file2.txt
    git add file2.txt

    あるいは、修正したファイルをステージングする

    (file1.txt を修正したとする)

    git add file1.txt

    2. `git commit --amend` を実行します。bash
    git commit –amend
    “`
    3. エディタが開きます。通常は既存のメッセージをそのまま使うか、必要に応じて修正します。保存して閉じます。
    4. Gitは、直前のコミットの内容 + インデックスに追加した新しい変更 を含んだ新しいコミットを作成します。

実行例:

“`bash

コミットを作成

echo “Content for file1.” > file1.txt
git add file1.txt
git commit -m “Add file1”

履歴を確認

git log –oneline

出力例:

a1b2c3d Add file1

file2.txt を追加し忘れたことに気づく

echo “Content for file2.” > file2.txt
git add file2.txt # 追加し忘れたファイルをステージング

直前コミットに file2.txt を追加して修正

git commit –amend

エディタが開く。メッセージはそのまま ‘Add file1’ で保存。

履歴を再度確認

git log –oneline

出力例:

e4f5g6h Add file1 # コミットハッシュが変わっている

新しいコミットが file1.txt と file2.txt の両方を含んでいることを確認

git show HEAD –name-only

出力例:

commit e4f5g6h…

Author: …

Date: …

Add file1

file1.txt

file2.txt

“`

これで、直前のコミットが file1.txtfile2.txt の両方を含む新しいコミットに置き換わりました。

シナリオ1-3:誤って含めてしまったファイルを、直前コミットから削除したい

直前にコミットした際に、コミットすべきではないファイル(例:一時ファイル、IDEの設定ファイルなど)を誤って git add してコミットしてしまった場合です。

  1. 誤ってコミットしてしまったファイルをワークツリーから削除するか、.gitignore に追加してGitの追跡から除外します。
    “`bash
    # 誤ってコミットしたファイルをワークツリーから削除する場合
    # rm path/to/wrong_file.log

    誤ってコミットしたファイルがまだワークツリーにある場合で、次回以降無視したい場合

    git rm –cached path/to/wrong_file.log # インデックスから削除するだけで、ワークツリーには残す

    echo “path/to/wrong_file.log” >> .gitignore

    2. インデックスの状態を、含めたいファイルだけに調整します。誤ってコミットしたファイルがインデックスから削除されていることを確認します。必要であれば、`git add` で含めたいファイルを再度ステージングします。bash

    間違えて含めてしまったファイルがインデックスに残っている場合は、インデックスから取り除く

    git reset HEAD path/to/wrong_file.log # これでインデックスからそのファイルが外れる

    3. インデックスの状態が意図した通りになっていることを `git status` で確認します。
    4. `git commit --amend` を実行します。
    bash
    git commit –amend
    “`
    5. エディタでメッセージを確認・修正し、保存して閉じます。
    6. Gitは、現在のインデックスの状態 を含んだ新しいコミットを作成します。

実行例:

“`bash

file1.txt と誤って log ファイルをコミットしてしまう

echo “Content for file1.” > file1.txt
echo “Debug log…” > debug.log
git add file1.txt debug.log
git commit -m “Add file1 and debug log” # debug.log を含めてしまった

履歴を確認

git log –oneline

出力例:

a1b2c3d Add file1 and debug log

debug.log はコミットすべきではなかった

debug.log をインデックスから外し、今後無視するように設定

git reset HEAD debug.log # インデックスから外す (ワークツリーには残る)
echo “debug.log” >> .gitignore # 今後無視する

インデックスの状態を確認 (debug.log が Changes to be committed にないことを確認)

git status

出力例:

On branch main

Your branch is ahead of ‘origin/main’ by 1 commit.

(use “git push” to publish your local commits)

Changes not staged for commit:

(use “git add …” to update what will be committed)

(use “git restore …” to discard changes in working directory)

.gitignore

debug.log

Untracked files:

(use “git add …” to include in what will be committed)

nothing added to commit but untracked files present (use ‘git add’ to include in commit)

.gitignore をコミットに追加することを忘れずにステージング

git add .gitignore

インデックスの状態を再度確認 (file1.txt がステージングされていることを確認 – これはamendの時に自動でインデックスから元のcommit内容がロードされるので不要な確認だが、理解のために)

git status

出力例:

On branch main

Your branch is ahead of ‘origin/main’ by 1 commit.

(use “git push” to publish your local commits)

Changes to be committed:

(use “git restore –staged …” to unstage)

.gitignore

Changes not staged for commit:

(use “git add …” to update what will be committed)

debug.log # debug.log はワークツリーにあるがステージングされていない

Amend コマンド実行。この時、現在のインデックス(.gitignore)と直前コミット(file1.txt)の内容が合わさる点に注意。

通常、–amend は直前コミットの内容をインデックスにロードしてから、現在のインデックスの状態を新しいコミットに反映します。

なので、誤って含めたファイルをインデックスから外す(git reset HEAD )だけで十分です。

その後、新しく含めたいファイル(.gitignore)をgit addしておけばOK。

git commit –amend

エディタが開く。メッセージを修正するなら修正して保存。例: “Add file1 and ignore debug log”

履歴を再度確認

git log –oneline

出力例:

e4f5g6h Add file1 and ignore debug log

新しいコミットが file1.txt と .gitignore だけを含んでいることを確認

git show HEAD –name-only

出力例:

commit e4f5g6h…

Author: …

Date: …

Add file1 and ignore debug log

.gitignore

file1.txt # file1.txt は前のコミットから引き継がれる形で含まれる

``
**補足:**
–amendを実行する際、Gitは直前のコミットの内容を一時的にインデックスにロードします。その後、インデックスの現在の状態(git addしてステージングしたファイルや、git reset HEAD でステージングから外したファイルの状態)を元に新しいコミットを作成します。したがって、誤って含めてしまったファイルをコミットから外すには、そのファイルをgit reset HEAD でインデックスから外した上で–amend` を実行するのが正しい手順です。

git commit --amend の注意点:

  • コミットハッシュが変わる: git commit --amend は元のコミットを上書きするため、新しいコミットは元のコミットとは異なる新しいハッシュ値を持つことになります。
  • 履歴を書き換える操作である: これは非常に重要な点です。git commit --amend はブランチの先端の履歴を書き換える操作です。
  • リモートにpush済みのコミットには基本的に使わない: もし git commit --amend を行うコミットを既にリモートリポジトリに git push している場合、ローカルとリモートで履歴が食い違ってしまいます。この食い違いを解消するには、通常 git push --force または git push --force-with-lease を使う必要がありますが、これは他の開発者がその古いコミットを元に作業を進めている場合に、その開発者の履歴を壊してしまう可能性があります。共同開発しているリポジトリでは、基本的にpush済みのコミットに対して git commit --amend を使うのは避けるべきです。どうしても必要な場合は、チームメンバーと連携を取り、細心の注意を払って行いましょう。ローカルだけで作業している場合や、まだ誰とも共有していないコミットに対しては安全に使用できます。

方法2: 直前のコミットを「無かったこと」にする (git reset)

直前のコミットそのものをブランチの履歴から切り離したい場合、つまりコミット自体を無かったことにしたい場合に使うのが git reset コマンドです。git reset は、HEADポインタ(そして現在のブランチ)を、指定したコミット(今回は直前コミットの親)に移動させるコマンドです。

git reset コマンドにはいくつかのオプションがあり、これによってHEADの移動に加えて、インデックスとワークツリーの状態をどうするかを制御できます。直前コミットを取り消す場合は、通常 git reset HEAD~1 と指定します。HEAD~1 は「HEADが現在指しているコミットの親」という意味です。つまり、「直前のコミットの、そのまた直前のコミット」を指します。

git reset HEAD~1 の主要なオプションは以下の3つです。

  • --soft: HEADを移動する だけ。インデックスとワークツリーの状態は変更しない。
  • --mixed (デフォルト): HEADを移動し、インデックスを移動先のコミットの状態に戻す。ワークツリーは変更しない。
  • --hard: HEADを移動し、インデックスとワークツリーの両方を移動先のコミットの状態に戻す。

これらのオプションは、直前のコミットで行った変更内容を、取り消し後にどう扱いたいかに応じて使い分けます。

原理:

git reset HEAD~1 は、現在のブランチが指しているコミットを、直前のコミットの親(つまりHEAD~1)に強制的に変更します。これにより、直前のコミットは現在のブランチの履歴から切り離されます。

そして、指定されたオプションに応じて、インデックスとワークツリーの状態が以下のように変化します。

  • --soft: HEADはHEAD~1に移動しますが、インデックスの状態とワークツリーの状態は、リセット前のHEAD(直前コミット)があった時の状態(つまり、直前コミットで行われた変更がすべてワークツリーにあり、かつインデックスにもステージングされている状態)のままになります。
  • --mixed (デフォルト): HEADはHEAD~1に移動し、インデックスはHEAD~1が指すコミットの状態に戻されます。ワークツリーは変更されません。結果として、直前コミットで行われた変更内容はすべてワークツリーに残っていますが、インデックスからは外された(アンステージされた)状態になります。
  • --hard: HEADはHEAD~1に移動し、インデックスもワークツリーもHEAD~1が指すコミットの状態に完全に一致させます。結果として、直前コミットで行われた変更内容はすべてワークツリーからも完全に削除され、失われます。

各オプションの詳細な手順と実行例:

ここでは、それぞれのオプションを使った git reset HEAD~1 の手順を説明します。

まず、基準となるコミットをいくつか作成しておきます。

“`bash

初期コミット

echo “Line 1” > file.txt
git add file.txt
git commit -m “Add initial file” # コミットA (HEAD~1 の親)

最初の変更をコミット

echo “Line 2” >> file.txt
git add file.txt
git commit -m “Add line 2” # コミットB (これが取り消したい直前コミット、今回の HEAD~1)

さらに変更をコミット

echo “Line 3” >> file.txt
git add file.txt
git commit -m “Add line 3” # コミットC (これが現在の HEAD)

履歴を確認

git log –oneline

出力例:

c1c1c1c Add line 3 # HEAD, コミットC

b2b2b2b Add line 2 # HEAD~1, コミットB (取り消したい対象)

a3a3a3a Add initial file # HEAD~2, コミットA (reset –hard, –soft, –mixed の移動先)

“`

現在のHEADは「コミットC (Add line 3)」です。私たちは「コミットC」を取り消して、「コミットB (Add line 2)」の状態に戻したいとします。

シナリオ2-1: 直前コミットCを無かったことにしたいが、変更内容(”Line 3″の追加)はステージングされた状態に戻したい -> git reset HEAD~1 --soft

  1. git reset HEAD~1 --soft を実行します。
    bash
    git reset HEAD~1 --soft
  2. GitはHEADポインタを「コミットB」に移動させます。インデックスとワークツリーは「コミットC」があった時の状態(つまり、”Line 3″が追加されており、その変更がステージングされている状態)のままになります。
  3. 履歴を確認します。
    bash
    git log --oneline

    出力例:
    bash
    b2b2b2b Add line 2 # HEAD がここに移動
    a3a3a3a Add initial file

    「コミットC」が履歴から消えました。
  4. ステータスを確認します。
    bash
    git status

    出力例:
    bash
    On branch main
    Your branch is ahead of 'origin/main' by 1 commit.
    # (use "git push" to publish your local commits) # この行は環境による
    Changes to be committed:
    (use "git restore --staged <file>..." to unstage)
    modified: file.txt

    git status を見ると、直前コミットCで行われた変更(”Line 3″の追加)が “Changes to be committed”、つまりインデックスにステージングされた状態で残っていることがわかります。ワークツリーの file.txt も “Line 3” が含まれた状態です。

使いどころ:
直前のコミットは不要だったが、その変更内容は次のコミットに含めたい(または、他の変更とまとめて再コミットしたい)場合に適しています。git commit --amend に似ていますが、--soft の場合はコミットメッセージ編集のエディタは開きませんし、現在のインデックスの状態を完全に引き継ぎます。直前コミットを複数に分割し直したい場合などにも、まず --soft でリセットし、変更をインデックスから外して(git reset HEAD または git restore --staged)、改めて git add -p などで分割してコミットしていく、といった使い方ができます。

シナリオ2-2: 直前コミットCを無かったことにしたいが、変更内容(”Line 3″の追加)はワークツリーには残したいが、ステージングは解除したい -> git reset HEAD~1 --mixed (またはオプションなし)

--mixedgit reset のデフォルトのオプションです。したがって、単に git reset HEAD~1 と実行した場合も同じ結果になります。

  1. git reset HEAD~1 --mixed を実行します。
    bash
    git reset HEAD~1 --mixed # または git reset HEAD~1
  2. GitはHEADポインタを「コミットB」に移動させます。インデックスは「コミットB」が指す状態に戻ります(つまり、直前コミットCで行われた変更はインデックスから削除されます)。ワークツリーは変更されず、「コミットC」があった時の状態(”Line 3″が含まれている)のままになります。
  3. 履歴を確認します。
    bash
    git log --oneline

    出力例:
    bash
    b2b2b2b Add line 2 # HEAD がここに移動
    a3a3a3a Add initial file

    「コミットC」が履歴から消えました。
  4. ステータスを確認します。
    bash
    git status

    出力例:
    bash
    On branch main
    Your branch is ahead of 'origin/main' by 1 commit.
    # (use "git push" to publish your local commits) # この行は環境による
    Changes not staged for commit:
    (use "git add <file>..." to update what will be committed)
    (use "git restore <file>..." to discard changes in working directory)
    modified: file.txt

    git status を見ると、直前コミットCで行われた変更(”Line 3″の追加)が “Changes not staged for commit”、つまりワークツリーには残っているが、インデックスからは外された(アンステージされた)状態で表示されています。ワークツリーの file.txt には “Line 3” が含まれています。

使いどころ:
直前のコミットは不要だったが、その変更内容はワークツリーに残しておき、改めて変更内容を確認・修正したり、一部だけをステージングしてコミットし直したりしたい場合に適しています。これが git reset の最も一般的な使い方と言えるでしょう。

シナリオ2-3: 直前コミットCを無かったことにしたい。変更内容(”Line 3″の追加)もすべて破棄したい -> git reset HEAD~1 --hard

!!! 警告 !!!
git reset --hard破壊的な操作です。インデックスとワークツリーの両方が指定したコミットの状態に強制的に上書きされるため、直前コミットで行われた変更内容はすべて失われます。失われた変更内容を簡単に元に戻すことはできません(git reflog を使えば戻せる可能性はありますが、保証されません)。このコマンドを使う際は、本当に変更内容を破棄してしまって良いかを十分に確認してください。

  1. git reset HEAD~1 --hard を実行します。
    bash
    git reset HEAD~1 --hard

    通常、確認メッセージは表示されません。実行するとすぐに指定されたコミットの状態に戻されます。
  2. GitはHEADポインタを「コミットB」に移動させます。インデックスもワークツリーも、「コミットB」が指す状態に完全に一致させられます。
  3. 履歴を確認します。
    bash
    git log --oneline

    出力例:
    bash
    b2b2b2b Add line 2 # HEAD がここに移動
    a3a3a3a Add initial file

    「コミットC」が履歴から消えました。
  4. ステータスを確認します。
    bash
    git status

    出力例:
    bash
    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

    git status を見ると、ワークツリーがクリーンな状態になっていることがわかります。これは、直前コミットCで行われた変更(”Line 3″の追加)がワークツリーからも完全に削除され、「コミットB」があった時の状態に戻されたことを意味します。ワークツリーの file.txt を確認すると、”Line 3″ が消えているはずです。

使いどころ:
直前のコミットが完全に間違いで、その変更内容も一切不要になった場合にのみ使用します。例えば、まだ実験段階の変更を誤ってコミットしてしまい、その変更自体を完全に破棄したい場合などです。繰り返しになりますが、このコマンドは非常に危険ですので、使用には最大限の注意が必要です。 迷ったら --soft--mixed を使いましょう。

git reset の注意点:

  • 履歴を書き換える操作である: git reset は、git commit --amend と同様にブランチの先端の履歴を書き換える操作です。元の直前コミットはブランチの履歴から切り離されます。
  • リモートにpush済みのコミットには基本的に使わない: git commit --amend と同じ理由で、既にリモートにpushしているコミットに対して git reset を使うのは避けるべきです。履歴の食い違いが発生し、他の共同開発者に影響を与える可能性があります。どうしても必要な場合は git push --force などが必要になりますが、これも非常に危険です。
  • --hard オプションはデータの損失につながる: --hard オプションはワークツリーの変更を強制的に破棄するため、失いたくない変更が含まれていないか十分に確認してから実行してください。
  • HEAD~1 以外の指定も可能: 今回は直前コミットを取り消すために HEAD~1 を使いましたが、git reset は任意のコミットを指定できます (git reset <commit_hash>)。例えば、特定のコミットまで一気に戻りたい場合などに使用します。ただし、遡るコミット数が増えるほど、履歴書き換えやデータ損失のリスクも高まります。

方法3: 直前のコミットの変更を「打ち消す」 (git revert)

これまでの git commit --amendgit reset は、直前のコミットを履歴から「消す」ことで取り消しを実現していました。しかし、時には履歴を書き換えることなく、直前コミットで行われた変更を「取り消したい」場合があります。このような場合に最適なのが git revert コマンドです。

git revert <commit> コマンドは、指定したコミットで行われた変更内容との変更を行う新しいコミットを作成します。元のコミット自体は履歴にそのまま残ります。

この方法が適している状況:

  • 直前のコミットを既にリモートリポジトリにpushしており、他の開発者がそれを元に作業を進めている可能性がある。履歴を書き換えると混乱を招くため、履歴を残したい。
  • 直前コミットは間違いだったが、その間違いだったという事実自体も履歴に残しておきたい。
  • 非破壊的に変更を取り消したい。

原理:

git revert HEAD (または git revert <直前コミットのハッシュ>) を実行すると、Gitは指定したコミット(今回は直前コミット)の変更内容を解析し、その変更を打ち消すための逆の変更セットを作成します。例えば、元のコミットでファイルに一行を追加していた場合、revertコミットではその一行を削除します。元のコミットでファイルを削除していた場合、revertコミットではそのファイルを復活させます。

この逆の変更セットを適用した結果を新しいコミットとして作成し、現在のブランチの先端に通常のコミットとして追加します。

コミットA -- コミットB (直前コミット) -- コミットC (HEAD)
                                 ↓ git revert コミットC
コミットA -- コミットB -- コミットC -- コミットD (Cの変更を打ち消すrevertコミット, 新しいHEAD)

詳細な手順と実行例:

直前のコミットCを取り消したいとします。

  1. git revert HEAD を実行します。HEAD は現在のブランチの最新コミット(取り消したい直前コミット)を指します。
    bash
    git revert HEAD
  2. Gitはrevertコミットのコミットメッセージを編集するためのエディタを開きます。デフォルトのメッセージは「Revert “<元のコミットメッセージ>”\n\nThis reverts commit <元のコミットハッシュ>.」のようになっています。通常はこのまま保存して閉じますが、必要であれば修正することも可能です。
  3. エディタを保存して閉じると、Gitが新しいrevertコミットを作成します。
  4. 履歴を確認します。
    bash
    git log --oneline

    先ほどの例で作成した「コミットC (Add line 3)」を取り消すrevertコミットを作成した場合の履歴は以下のようになります。
    bash
    d4d4d4d Revert "Add line 3" # HEAD がここに移動
    c1c1c1c Add line 3 # 直前コミットはそのまま残っている
    b2b2b2b Add line 2
    a3a3a3a Add initial file

    新しいコミット(d4d4d4d)が追加され、「コミットC (c1c1c1c)」は履歴に残ったままです。新しいrevertコミットの内容を確認すると、「コミットC」で追加した”Line 3″が削除されていることがわかります。

実行例:

先ほどの例で作成した「コミットC (Add line 3)」がある状態から開始します。

“`bash

履歴を確認

git log –oneline

出力例:

c1c1c1c Add line 3 # HEAD

b2b2b2b Add line 2

a3a3a3a Add initial file

直前コミットを取り消すrevertコミットを作成

git revert HEAD

エディタが開く。デフォルトメッセージ (“Revert ‘Add line 3′”) で保存。

履歴を再度確認

git log –oneline

出力例:

d4d4d4d Revert “Add line 3” # HEAD がここに移動

c1c1c1c Add line 3 # 元のコミットは残っている

b2b2b2b Add line 2

a3a3a3a Add initial file

ワークツリーの状態を確認

git status

出力例:

On branch main

Your branch is ahead of ‘origin/main’ by 2 commits. # コミットCとRevertコミットが追加された

(use “git push” to publish your local commits)

nothing to commit, working tree clean # ワークツリーは「コミットB」の状態に戻っている

file.txt の内容を確認 (コミットBの状態、つまり “Line 3” が消えている)

cat file.txt

出力例:

Line 1

Line 2

“`

これで、元のコミットCは履歴に残しつつ、その変更内容が打ち消された新しいコミットDが作成されました。ワークツリーの状態はコミットBの時点に戻っています。

git revert の注意点:

  • 履歴を書き換えない(非破壊的): これが最大の利点です。新しいコミットを追加するだけで、既存の履歴を変更しないため、既にpush済みのコミットに対して安全に使用できます。共同開発環境では、コミットを取り消す場合の推奨手段となります。
  • 取り消しの事実が履歴に残る: コミットが間違いだったという事実自体がrevertコミットとして履歴に残ります。
  • コンフリクトが発生する可能性がある: もし取り消したいコミット以降に、そのコミットと同じファイルや行に対して別の変更が加えられている場合、revert操作中にコンフリクト(競合)が発生する可能性があります。この場合、手動でコンフリクトを解消してからコミットを完了させる必要があります。
  • HEAD は単一のコミットを指す: git revert HEAD~1 のように指定することも可能ですが、通常は git revert HEAD で直前のコミットを取り消します。もしさらに前の複数のコミットを取り消したい場合は、複数の git revert コマンドを実行するか、git revert <commit1> <commit2> ... のように複数のコミットをまとめて指定します。

各方法の比較と使い分け

ここまで見てきたように、直前コミットを取り消す操作にはいくつかの方法があり、それぞれに利点と欠点があります。どの方法を選択すべきかは、現在の状況と、操作後の状態をどうしたいかによって異なります。

以下に、各方法の比較と使い分けのガイドラインを示します。

操作 コマンド例 履歴の書き換え ワークツリーの状態 インデックスの状態 適している状況 注意点
直前コミット修正 git commit --amend する 変更なし(直前コミット適用後の状態 + 現在の変更) 現在の状態(git add された変更) メッセージ修正、ファイル追加・削除、直前コミットの微調整(ローカルのみ) コミットハッシュが変わる。push済みコミットへの適用は危険。共同開発では基本的に避ける。
コミット無かったことに git reset HEAD~1 --soft する 変更なし(直前コミット適用後の状態) 変更なし(直前コミット適用後の変更がステージング) コミット自体は不要だが、変更内容はステージング済みで残し、すぐに再コミットしたい コミットハッシュが変わる。push済みコミットへの適用は危険。共同開発では基本的に避ける。
コミット無かったことに git reset HEAD~1 --mixed する 変更なし(直前コミット適用後の状態) リセット先のコミット(HEAD~1)の状態(ステージング解除) コミット自体は不要だが、変更内容はワークツリーに残し、改めて整理・コミットしたい コミットハッシュが変わる。push済みコミットへの適用は危険。共同開発では基本的に避ける。
コミット無かったことに git reset HEAD~1 --hard する リセット先のコミット(HEAD~1)の状態(変更破棄) リセット先のコミット(HEAD~1)の状態(インデックスクリア) 直前コミットが完全に間違いで、変更内容もすべて破棄したい(ローカルのみ) 破壊的な操作。ワークツリーの変更がすべて失われる。push済みコミットへの適用は危険。最も危険な方法
変更を打ち消す git revert HEAD しない 新しいコミット適用後の状態(変更が打ち消される) 新しいコミット適用後の状態(ワークツリーと一致) 履歴を書き換えずに変更を取り消したい。push済みコミットを取り消したい。共同開発。 新しいコミットが追加される。コンフリクト発生の可能性あり。

使い分けのフローチャート(思考プロセス):

  1. そのコミットは既にリモートリポジトリに git push しましたか?

    • はい: git revert を使うのが最も安全です。履歴を書き換える amendreset は、他の開発者に影響を与える可能性があるため避けるべきです。どうしても履歴を書き換えたい場合は、チームメンバーと緊密に連携し、git push --force-with-lease などの危険なコマンドを使う覚悟が必要です(通常非推奨)。
    • いいえ (ローカルだけのコミット): 次の質問へ。
  2. 直前のコミットの内容を少しだけ修正したい(メッセージだけ、ファイル追加漏れなど)ですか?それとも、コミットそのものを無かったことにしたいですか?

    • 少し修正したい: git commit --amend が最も簡単です。ファイルを修正・git add してから git commit --amend を実行します。
    • コミットそのものを無かったことにしたい: 次の質問へ。
  3. 直前のコミットで行った変更内容は、リセット後にどうなっていてほしいですか?

    • ステージングされた状態で残したい: git reset HEAD~1 --soft を使います。リセット後すぐに git commit を実行すれば、同じ変更内容で新しいコミットが作成できます。
    • ワークツリーには残したいが、ステージングは解除したい: git reset HEAD~1 --mixed (デフォルト) を使います。リセット後、変更内容はワークツリーに残っているので、確認・修正後に改めて必要なファイルだけを git add してコミットできます。
    • すべて破棄したい: git reset HEAD~1 --hard を使います。ただし、これは非常に危険な操作です。本当に変更内容が不要であることを何度も確認してから実行してください。

この思考プロセスに従うことで、状況に最も適したコマンドを選択できます。

もし間違えてしまったら?: git reflog の活用

git reset --hard を実行して変更内容を失ってしまったり、意図しない状態になってしまったりした場合でも、諦めるのはまだ早いです。Gitには git reflog という非常に便利なコマンドがあり、これが「元に戻す」ための救世主となることがあります。

git reflog (reference logs) は、HEADが過去にどのコミットを指していたか、つまりあなたが行った操作(コミット、リセット、マージ、リベースなど)によってHEADがどのように移動したかの履歴を記録しています。これは、ブランチの履歴には表示されない、ローカルリポジトリ内でのHEADの動きのログです。

git reflog の使い方:

ターミナルで単に git reflog と実行します。

bash
git reflog

出力例:

bash
c1c1c1c (HEAD -> main) HEAD@{0}: commit: Add line 3
b2b2b2b HEAD@{1}: commit: Add line 2
a3a3a3a HEAD@{2}: commit (initial): Add initial file

各行はHEADの過去の状態を表しています。

  • HEAD@{0}: 現在のHEADの位置。
  • HEAD@{1}: 1つ前の操作を実行する前のHEADの位置。
  • HEAD@{n}: n個前の操作を実行する前のHEADの位置。

それぞれの行の先頭にあるハッシュ値(c1c1c1c, b2b2b2b, a3a3a3a)は、その時点でのHEADが指していたコミットのハッシュです。

git reset --hard で戻す手順:

もし git reset HEAD~1 --hard を実行して、直前コミット(例:コミットC)とその変更内容を失ってしまったとします。git reflog を見ると、失われたコミットCがリストに表示されているはずです。

“`bash

誤って git reset HEAD~1 –hard を実行した直後

git reflog

出力例:

b2b2b2b (HEAD -> main) HEAD@{0}: reset: moving to HEAD~1 # reset操作でb2b2b2bに移動したログ

c1c1c1c HEAD@{1}: commit: Add line 3 # resetする直前のHEAD (失われたコミットC)

b2b2b2b HEAD@{2}: commit: Add line 2 # コミットCの親

a3a3a3a HEAD@{3}: commit (initial): Add initial file

“`

この例では、HEAD@{1} が誤ってリセットする前の状態、つまり失われたコミットC (c1c1c1c) を指しています。このコミットに戻りたい場合は、git reset --hard にこの HEAD@{1} またはコミットハッシュを指定します。

bash
git reset --hard HEAD@{1} # または git reset --hard c1c1c1c

これで、HEADポインタ、インデックス、ワークツリーがすべて「コミットC」があった時の状態に戻ります。つまり、失われたはずの変更内容がワークツリーにも復活します。

注意点:

  • git reflog はローカルリポジトリにのみ記録されます。クローンし直したり、リポジトリを削除したりすると失われます。
  • git reflog のエントリは一定期間(デフォルトでは90日)後に自動的に削除されます。
  • git reflog はあくまで「HEADの移動履歴」です。過去の特定のコミットに戻れる保証はありますが、失われたファイルや変更内容が必ずしも完璧に復元できるとは限りません(特に git clean などで追跡対象外ファイルも削除した場合など)。

しかし、誤った git reset などから復旧するための強力なツールであることは間違いありません。困ったときは、まず git reflog を見てみましょう。

リモートリポジトリとの関係:push済みコミットを取り消す際の注意点

Gitは分散型バージョン管理システムです。ローカルリポジトリでの変更は、git push によってリモートリポジトリに反映され、他の共同開発者と共有されます。

ここで重要になるのが、履歴を書き換える操作(git commit --amendgit reset)を、既にリモートにpush済みのコミットに対して行うとどうなるかという問題です。

これらの操作は、ローカルブランチの先端が指すコミット(そしてそれ以降の履歴)を新しいコミットに置き換えてしまいます。例えば、以下のような状況を考えます。

  1. ローカルでコミットA → コミットB → コミットC (HEAD) と進める。
  2. コミットCをリモートに git push origin main する。リモートもローカルと同じ履歴になる。
  3. ローカルで git reset HEAD~1 --soft を実行し、コミットCを無かったことにする。ローカルブランチは コミットA → コミットB (HEAD) となる。コミットCはローカルブランチの履歴から外れる。
  4. ここで git push origin main しようとすると、Gitは拒否します。なぜなら、リモートの main ブランチの先端はまだコミットCを指しているのに、ローカルの main はコミットBを指しており、リモートの履歴がローカルの履歴に含まれていないためです。Gitは通常、履歴を失うようなプッシュ(ノン・ファストフォワードプッシュ)をデフォルトでは許可しません。

この食い違いを解消し、ローカルの書き換えた履歴(この例ではコミットBが先端の履歴)をリモートに強制的に反映させるには、git push --force または git push --force-with-lease コマンドを使用する必要があります。

“`bash
git push origin main –force

またはより安全なオプション

git push origin main –force-with-lease
“`

git push --force の危険性:

git push --force は、リモートブランチの履歴をローカルブランチの履歴で無条件に上書きします。これは、もし他の開発者があなたが git push --force する前にリモートの最新コミット(この例ではコミットC)を取得して、それを元に新しいコミット(コミットD)を作成し、先にリモートにプッシュしていた場合、その開発者がプッシュしたコミットDをあなたのローカル履歴で上書きしてしまい、そのコミットがリモートから消えてしまうという深刻な問題を引き起こす可能性があります。他の開発者は次に git pull した際に大きなコンフリクトに直面したり、自身のコミットが消えて困惑したりすることになります。

git push --force-with-lease:

--force-with-lease は、--force より少し安全な選択肢です。これは、リモートブランチの履歴が、あなたがローカルで履歴を書き換える前に取得した時点から変更されていない場合にのみ、強制プッシュを許可します。もしあなたがローカルで履歴を書き換えている間に他の誰かが同じブランチにプッシュしていた場合、--force-with-lease はプッシュを拒否し、履歴の食い違いが発生していることを教えてくれます。これにより、他人の作業を誤って上書きしてしまうリスクを減らすことができます。

“`bash

推奨される強制プッシュの方法

git push origin main –force-with-lease
“`

共同開発における推奨事項:

共同開発しているリポジトリでは、基本的にリモートにpush済みのコミットに対して履歴を書き換える操作(git commit --amend, git reset)は避けるべきです。 もし誤ってpushしてしまったコミットを取り消したい場合は、git revert を使うのが最も安全で、他の開発者との間で混乱を招きません。git revert は新しいコミットを追加するだけなので、履歴の整合性が保たれ、強制プッシュは必要ありません。

例外的に、自分しか使っていないブランチ であれば、push済みでも git commit --amendgit reset を使用し、git push --force-with-lease でリモートに反映させても問題ありません。また、pushした直後で、他の誰もそのコミットを取得していないことが確実な場合 に限り、チームの合意があれば履歴を書き換えて強制プッシュすることも可能ですが、非常にリスクの高い行為であるという認識を持つ必要があります。

まとめると、リモートにpush済みの直前コミットを取り消す必要がある場合は、まず git revert を検討する のが共同開発におけるベストプラクティスです。履歴の書き換えが必要な場合は、その影響範囲とリスクを十分に理解し、特に共有ブランチでは細心の注意を払い、できれば避けるべきです。

まとめ

Gitで直前のコミットを取り消す操作は、開発ワークフローにおいて頻繁に発生する可能性のある作業です。しかし、その方法は一つではなく、状況に応じて最適なコマンドを選択する必要があります。

本記事では、直前コミットを取り消す主要な3つのアプローチについて、その原理、手順、実行例、そして注意点を詳細に解説しました。

  1. git commit --amend: コミットメッセージの修正や、ファイル追加漏れなど、直前コミットの内容を「修正」したい場合に適しています。新しいコミットで元のコミットを上書きするため、履歴を書き換える操作です。
  2. git reset HEAD~1: 直前コミットそのものをブランチの履歴から「無かったこと」にしたい場合に利用します。--soft, --mixed, --hard のオプションにより、インデックスとワークツリーをどう扱うかを選択できます。これも履歴を書き換える操作であり、特に --hard は破壊的で注意が必要です。
  3. git revert HEAD: 直前コミットの変更内容を「打ち消す」新しいコミットを作成します。元のコミットは履歴に残るため、履歴を書き換えない非破壊的な操作です。push済みのコミットを取り消す場合や、共同開発環境では最も安全な方法として推奨されます。

これらのコマンドの使い分けは、以下の点を考慮して判断します。

  • 対象のコミットがリモートにpush済みか、ローカルのみか。 (push済みなら git revert を優先)
  • 履歴を書き換えたいか、残したいか。 (書き換えるなら amend or reset, 残すなら revert)
  • 直前コミットの変更内容をどうしたいか(ステージング、ワークツリーに残す、完全に破棄)。 (git reset のオプションで選択)

もしこれらの操作で意図しない状態になってしまった場合でも、慌てずに git reflog コマンドを使ってHEADの移動履歴を確認し、過去の状態に戻せる可能性があることを覚えておきましょう。

Gitのこれらの強力な機能を理解し、適切に使いこなすことで、より効率的で安全な開発ワークフローを確立できます。特に、共同開発における履歴書き換えのリスクを常に意識し、迷ったときは非破壊的な git revert を選択するという原則を持つことが重要です。

この詳細な解説が、あなたがGitでのコミット取り消し操作を行う上で役立てば幸いです。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール