Gitコマンドでコミットを取り消す方法(reset/revert解説)
はじめに:Gitにおける「取り消し」という概念
ソフトウェア開発におけるバージョン管理システムは、コードの変更履歴を記録し、管理するための不可欠なツールです。Gitは、その中でも特に広く使われている分散型バージョン管理システムです。Gitを使うことで、開発者は安心してコードの変更を加えたり、過去の状態に戻したり、複数の開発者と協力して作業を進めたりすることができます。
しかし、開発を進める上で、意図しない変更を加えてしまったり、間違ったコミットをしてしまったりすることはよくあります。このような場合に必要となるのが、「コミットを取り消す」操作です。Gitには、コミットを取り消すためのいくつかの方法が用意されていますが、その中でも代表的なものが git reset
と git revert
です。
これらのコマンドは、どちらもコミットを取り消す目的で使用されますが、その振る舞いや履歴への影響が大きく異なります。誤ったコマンドを選択してしまうと、予期しない結果を招いたり、場合によっては共同開発者に迷惑をかけてしまったりする可能性もあります。そのため、それぞれのコマンドがどのように機能し、どのような状況で使うべきかを正しく理解しておくことは、Gitを効果的かつ安全に使う上で非常に重要です。
この記事では、Gitにおけるコミットの取り消しについて、特に git reset
と git revert
に焦点を当てて、その詳細なメカニズム、使い方、そして適切な使い分けについて、初心者にも分かりやすく解説します。これらのコマンドをマスターすることで、Gitを使った開発作業における「困った!」を減らし、より自信を持ってコードの変更を管理できるようになるでしょう。
まずは、Gitがどのように変更履歴を管理しているのか、基本的な概念から復習しましょう。この基礎理解が、reset
や revert
の挙動を深く理解するための土台となります。
Gitの基本的な概念の復習
git reset
や git revert
の詳細に入る前に、Gitの基本的な概念、特にコミット、HEAD、インデックス、ワーキングツリーの関係について再確認しておきましょう。これらの概念は、これらのコマンドがどのように動作するのかを理解するために不可欠です。
コミットとは?
Gitにおけるコミットは、プロジェクトの特定時点におけるスナップショットのようなものです。ファイル群の特定の状態を記録し、一意のハッシュ値(SHA-1など)で識別されます。各コミットは通常、その親となるコミットへのポインタを持ち、これらが連なってコミット履歴という一本の線(または分岐したツリー)を形成します。コミットには、誰が、いつ、どのような意図で変更を加えたのかを示すコミットメッセージが含まれます。
HEAD、インデックス、ワーキングツリー
Gitは、プロジェクトの状態を管理するために、主に以下の3つの領域を使用します。
-
ワーキングツリー (Working Tree / Working Directory)
- 開発者が実際にファイルを作成したり編集したりする作業ディレクトリです。
- ここにあるファイルは、Gitの管理下にあるファイルだけでなく、Gitに追跡されていない新しいファイル(untracked files)や、Gitの管理下にあるが変更が加えられたファイル(modified files)などが混在しています。
-
インデックス (Index / Staging Area)
- 次にコミットされる変更内容を準備するための「上演エリア」です。
- ワーキングツリーでの変更のうち、次にコミットに含めたい変更を
git add
コマンドを使ってインデックスに追加します。 - インデックスは、コミットされるスナップショットの内容を一時的に保持します。
-
HEAD
- 現在作業しているブランチの先端、つまり最新のコミットを指す特別なポインタです。
- 通常は現在のブランチ名(例:
main
,develop
)を介して、そのブランチの最新コミットを指しています。 git commit
コマンドを実行すると、HEADが指すコミットを親として新しいコミットが作成され、HEADはその新しいコミットに移動します。
これらの関係を図で表すと、以下のようになります。
+-----------------+
| ワーキングツリー| (実際のファイル編集)
+-----------------+
^
| git add (変更をインデックスに追加)
|
+-----------------+
| インデックス | (次にコミットする準備)
+-----------------+
^
| git commit (インデックスの内容をコミットとして記録)
|
+-----------------+
| HEAD | (現在のブランチの最新コミットを指す)
+-----------------+
|
| (過去のコミット履歴へ)
v
+-----------------+
| コミット履歴 | (過去のスナップショット群)
+-----------------+
git reset
や git revert
は、これらの要素(特にHEAD、インデックス、そしてワーキングツリー)に様々な影響を与えることで、コミットを取り消したり、過去の状態に戻ったりする操作を実現します。
git reset
によるコミットの取り消し
git reset
コマンドは、主にHEADが指すコミットを過去の特定のコミットに移動させるために使用されます。これは、コミット履歴を「なかったこと」にする、つまり履歴を書き換える操作にあたります。reset
は非常に強力なコマンドであり、その使い方によっては作業内容が失われる可能性があるため、慎重に使う必要があります。
git reset
コマンドの基本的な形式は以下の通りです。
bash
git reset [--soft | --mixed | --hard | --merge | --keep] [<コミット>]
<コミット>
は、HEADを移動させたいターゲットとなるコミットを指定します。指定しない場合は、デフォルトでHEAD(現在のコミット)がターゲットとなりますが、実際にはHEADをHEADにリセットすることはあまり意味がないため、通常は HEAD~1
のように相対的な指定や、コミットのハッシュ値など、過去のコミットを指定します。
例:
– HEAD~1
: HEADの1つ前のコミット
– HEAD~2
: HEADの2つ前のコミット
– abcdef1
: ハッシュ値が abcdef1
で始まるコミット
– origin/main
: リモートリポジトリの main
ブランチの先端コミット
git reset
の挙動は、指定するオプション(--soft
, --mixed
, --hard
など)によって大きく異なります。これらのモードは、HEADを移動させた後に、インデックスとワーキングツリーをどのように扱うかを制御します。
git reset --soft <コミット>
このモードは、最も「柔らかい」リセットです。
-
挙動:
- HEADを移動します: HEADポインタを指定した
<コミット>
に移動させます。これにより、現在のブランチの先端が指定コミットになります。 - インデックスとワーキングツリーは変更しません: HEADが移動する前の状態(つまり、リセット対象となったコミット群の変更内容)が、そのままインデックス(ステージングエリア)に残ります。ワーキングツリーのファイル内容も変更されません。
- HEADを移動します: HEADポインタを指定した
-
結果:
- リセット対象となったコミット群は、もはや現在のブランチの履歴の一部ではなくなります(ただし、
git reflog
を使えば復旧可能です)。 - インデックスには、リセット対象となったコミット群で加えられたすべての変更が、ステージングされた状態で残っています。
- ワーキングツリーのファイル内容も、リセット前と変わりません。
- リセット対象となったコミット群は、もはや現在のブランチの履歴の一部ではなくなります(ただし、
-
使用例:
- 直前のコミットを間違えてしまったが、その変更内容は維持したまま、コミットメッセージを修正したり、一部の変更を追加/削除したりして、もう一度コミットしたい場合。
- コミットを複数に分割したい場合(例えば、大きな変更を1つのコミットにしてしまったが、論理的な塊ごとに分けたい場合)。
-
具体的な手順(例:直前のコミットを取り消してやり直す):
“`bash
状況:直前のコミット(例:Commit D)を取り消したい
コミット履歴:A -> B -> C -> D (HEAD)
直前のコミットを取り消すが、変更はインデックスに残す
git reset –soft HEAD~1
結果確認
git log –oneline => A -> B -> C (HEAD)
git status => Changes to be committed: (Commit D の変更がステージングされている)
必要に応じて変更を修正(ワーキングツリーでファイルを編集)
git add <修正したファイル>
新しいコミットメッセージでコミットし直す
git commit -m “新しい、より適切なコミットメッセージ”
“`この操作により、元のCommit Dは履歴から消え、新しいコミットに置き換えられます。インデックスに作業内容が残るので、すぐに新しいコミットを作成できます。
git reset --mixed <コミット>
(デフォルト)
このモードは、git reset
のデフォルトの動作です。--mixed
オプションを省略した場合も、このモードが適用されます。
-
挙動:
- HEADを移動します:
--soft
と同様に、HEADポインタを指定した<コミット>
に移動させます。 - インデックスをリセットします: インデックスの内容を指定した
<コミット>
の状態に戻します。つまり、リセット対象となったコミット群で加えられた変更は、インデックスから削除されます。 - ワーキングツリーは変更しません: ワーキングツリーのファイル内容は、リセット前と変わりません。インデックスからは外れますが、ファイルそのものは残っています。
- HEADを移動します:
-
結果:
- リセット対象となったコミット群は、履歴から消えます。
- インデックスは空になります(正確には、指定コミット時点のインデックス状態に戻ります)。
- リセット対象となったコミット群で加えられたすべての変更は、インデックスからは削除されますが、ワーキングツリーには「変更されたファイル」として残ります(unstageされた状態)。
-
使用例:
- 直前のコミットを間違えてしまったので、その変更内容をインデックスから外し、ワーキングツリーで作業状態に戻して、ファイルの内容を修正したり、再度ステージングし直したりしたい場合。
- いくつかのコミットを取り消し、それらの変更をすべて破棄するのではなく、ワーキングツリーに残して確認・再利用したい場合。
-
具体的な手順(例:直前のコミットを取り消して作業状態に戻す):
“`bash
状況:直前のコミット(例:Commit D)を取り消し、変更をワーキングツリーに戻したい
コミット履歴:A -> B -> C -> D (HEAD)
直前のコミットを取り消し、変更はインデックスから外しワーキングツリーに残す
git reset HEAD~1 # –mixed はデフォルトなので省略可
結果確認
git log –oneline => A -> B -> C (HEAD)
git status => Changes not staged for commit: (Commit D の変更がunstaged状態で表示される)
git diff => Commit D で加えられた変更内容が表示される
必要に応じて変更を修正(ワーキングツリーでファイルを編集)
git add <修正したファイル>
git commit -m “再度コミット”
“`
この操作により、元のCommit Dは履歴から消え、その変更はワーキングツリーに戻されます。インデックスがクリアされるため、変更内容を再確認してからステージングし直す必要があります。
git reset --hard <コミット>
このモードは、最も「破壊的な」リセットです。使用には十分な注意が必要です。
-
挙動:
- HEADを移動します:
--soft
,--mixed
と同様に、HEADポインタを指定した<コミット>
に移動させます。 - インデックスをリセットします: インデックスの内容を指定した
<コミット>
の状態に完全に一致させます。 - ワーキングツリーをリセットします: ワーキングツリーのファイル内容を指定した
<コミット>
の状態に完全に一致させます。これにより、リセット対象となったコミット群で加えられたすべての変更や、まだコミット/ステージングされていなかったワーキングツリーでの作業内容が、問答無用で破棄されます!
- HEADを移動します:
-
結果:
- リセット対象となったコミット群は、履歴から消えます。
- インデックスとワーキングツリーは、指定した
<コミット>
の時点のプロジェクトの状態と完全に一致します。 - リセット対象となったコミット以降に加えられた、コミットされていない作業内容は全て失われます。
-
警告:
git reset --hard
は、ローカルのワーキングツリーにあるステージされていない(untracked/modifiedだがgit add
していない)変更も、ステージされている変更も、全て破棄します。このコマンドを実行する前に、失われては困る変更がないか必ず確認してください。- 一度失われた変更を元に戻すのは困難です(ただし、
git reflog
を使ってコミット自体は復旧できる可能性があります)。
-
使用例:
- 実験的な変更をいくつか行ったが、全て破棄して完全に過去の特定のコミットの状態に戻りたい場合。
- ワーキングツリーとインデックスの状態を、直近のコミットの状態と完全に一致させたい場合(ただし、これは
git restore .
やgit clean -fdx
と組み合わせて行う方が安全な場合もあります)。
-
具体的な手順(例:直前のコミットとその後の未コミット変更を全て破棄する):
“`bash
状況:直前のコミット(例:Commit D)とその後のワーキングツリーでの変更を全て破棄したい
コミット履歴:A -> B -> C -> D (HEAD)
ワーキングツリーには未コミットの変更があるとする
直前のコミットとその後の全ての変更を破棄
git reset –hard HEAD~1
結果確認
git log –oneline => A -> B -> C (HEAD)
git status => clean (未コミットの変更も全て消えている)
ワーキングツリーのファイル内容 => Commit C の時点に戻っている
“`
この操作は、本当に「そのコミット以降のことは全て無かったことにしたい」という場合にのみ使用してください。特に、まだコミットしていない重要な作業内容がある場合は、必ず事前にコミットするか、
git stash
などで一時的に退避させてください。
その他のモード(--merge
, --keep
)
--merge
と --keep
は、reset
対象のコミットと現在の状態の間にコンフリクトが発生しうる場合に、それをどう扱うかを指定するモードですが、これらはあまり一般的ではありません。
--merge
: 現在のローカルの変更を保持しようとしながら、指定コミットにリセットします。コンフリクトが発生した場合は、Gitがマージコンフリクトとして示します。--keep
:reset
対象のコミットへの切り替えによって、ローカルでステージされていない変更が上書きされる場合、リセットを中止します。
これらのモードは特定の状況で使用されるため、まずは --soft
, --mixed
, --hard
の3つを理解することが重要です。
git reset
とリモートリポジトリ
git reset
は、あくまでローカルリポジトリのHEADポインタを移動させ、ローカルのインデックスやワーキングツリーを操作するコマンドです。リモートリポジトリ(GitHub, GitLabなど)には直接影響を与えません。
しかし、git reset
でローカルのブランチ履歴を過去に巻き戻した後で、そのブランチをリモートにプッシュしようとすると問題が発生します。ローカルの履歴がリモートより短くなっているため、Gitは「fast-forward」ではないプッシュだと判断し、デフォルトではプッシュを拒否します。
このような場合、強制的にプッシュする (git push --force
または git push --force-with-lease
) 必要が出てきます。
bash
git push origin <ブランチ名> --force
警告: git push --force
は非常に危険な操作です!
- リモートリポジトリ上の履歴を、ローカルの履歴で上書きします。
- もし他の開発者が、強制プッシュする前のリモートの状態から作業を開始していた場合、その開発者のローカルリポジトリは古い履歴に基づいています。そこに新しいコミットを追加してプッシュしようとすると、履歴の不整合が発生し、複雑なマージやリベースが必要になったり、最悪の場合その開発者の作業内容が失われたりする可能性があります。
- 共有リポジトリや、複数の開発者が利用しているブランチ(例:
main
,develop
)に対してgit reset --hard
を実行し、その後git push --force
するのは絶対に避けるべきです。これは他の開発者の作業を台無しにする可能性があります。
したがって、git reset
(特に --hard
)は、基本的にローカルの個人用ブランチや、まだリモートにプッシュしていないコミットに対してのみ使用することが推奨されます。
git reflog
の活用:失われたコミットの救世主
「git reset --hard
を実行してしまって、大事な変更が消えてしまった!」――このようなパニックに陥った場合でも、Gitには git reflog
という強力な味方がいます。
git reflog
(reference logs) は、リポジトリ内のHEADやブランチなどの参照が過去にどこを指していたか、その操作履歴を記録しています。つまり、あなたがいつ、どのコミットにチェックアウトしたか、リセットしたか、マージしたか、リベースしたかなどの履歴が、ローカルに保存されています。
git log
がコミット自身の親子関係を辿る履歴であるのに対し、git reflog
はHEADなどのポインタがどのように移動したかの履歴です。git reset --hard
でコミット履歴から消えたように見えても、そのコミット自体はGitオブジェクトとしてリポジトリ内にしばらく(デフォルトで90日間)残っています。reflog
は、その残っているコミットへのポインタを提供してくれます。
-
git reflog
の使い方:
bash
git reflog
実行すると、以下のような出力が得られます。abcdef1 (HEAD -> main) HEAD@{0}: commit: Fix bug in feature X
2345678 HEAD@{1}: reset: moving to HEAD~1
9876543 HEAD@{2}: commit: Implement feature Y
fedcba9 HEAD@{3}: checkout: moving from develop to main
...各行はHEADの過去の状態を示しており、
HEAD@{n}
の形式で参照できます。例えばHEAD@{0}
はHEADの現在の位置、HEAD@{1}
は1つ前の位置、という具合です。ここには、コミットメッセージや、その操作(commit, reset, checkoutなど)が表示されます。 -
reset --hard
で消えたコミットを復旧する方法:git reflog
を実行し、reset --hard
を実行する前の、失われたコミット群の先端だったコミットを探します。そのコミットのハッシュ値(上記の例の9876543
など)やHEAD@{n}
の参照を見つけます。- 失われたコミットに戻るには、そのコミットに対して
git checkout
またはgit reset
を実行します。- 特定のコミットの状態を確認したいだけなら:
bash
git checkout 9876543
これで、そのコミットの detached HEAD 状態になります。 - 失われたコミット群を現在のブランチの先端として復旧したい場合:
bash
git reset --hard 9876543
これにより、現在のブランチのHEAD、インデックス、ワーキングツリーが、失われたコミットの状態に戻ります。
- 特定のコミットの状態を確認したいだけなら:
git reflog
はgit reset --hard
の「破壊性」に対する重要なセーフティネットです。しかし、reflog
の情報は一定期間で期限切れとなり消去されるため、完全に永続的ではありません。また、Git以外のファイルシステムレベルでの削除などには対応できません。あくまで、Gitの操作で参照が失われたコミットを復旧するためのツールです。
git reset
の注意点とベストプラクティス
- ローカルの個人用ブランチで使用する:
git reset
は履歴を書き換えるため、特にgit reset --hard
は他の開発者に影響を与えないローカルの個人用ブランチでのみ使用するのが安全です。 - プッシュ済みのコミットには使用しない: リモートにプッシュ済みのコミットに対して
reset
を行うと、git push --force
が必要になり、共有リポジトリで問題を引き起こす可能性があります。プッシュ済みのコミットを取り消したい場合は、後述するgit revert
を検討してください。 --hard
の使用は慎重に:git reset --hard
は未コミットの作業内容を完全に破棄します。実行前にgit status
でワーキングツリーに重要な変更が残っていないか必ず確認してください。もし残っている場合は、コミットするかgit stash
で一時退避させるなどの対応が必要です。git reflog
を知っておく: 万が一の事態に備え、git reflog
の使い方を理解しておきましょう。
まとめると、git reset
はローカルブランチのHEADポインタを移動させ、履歴を書き換えるコマンドです。特に --soft
, --mixed
, --hard
モードは、インデックスとワーキングツリーの扱いが異なり、その破壊性が異なります。ローカルでの作業やり直しには便利ですが、リモートへの影響やデータ損失の危険性には注意が必要です。
git revert
によるコミットの取り消し
git revert
コマンドは、git reset
とは全く異なる方法でコミットを取り消します。revert
は履歴を書き換えるのではなく、取り消したいコミットで加えられた変更内容を打ち消すような、新しいコミットを作成することで「取り消し」を実現します。
-
基本概念:
- 指定したコミットの変更内容(差分)を逆方向に適用し、その結果を新しいコミットとして記録します。
- 元のコミット自体は履歴に残ります。履歴を削除したり書き換えたりすることはありません。
- 非破壊的な操作であり、共有リポジトリでも安全に使用できます。
-
git revert <コミット>
の挙動:- 指定した
<コミット>
の変更内容(差分)を計算します。 - その変更内容を逆方向に適用しようとします。
- 逆方向の変更を適用した結果、ワーキングツリーとインデックスが更新されます。
- その変更内容を、新しいコミットとしてインデックスにステージングし、コミットメッセージの編集画面を開きます。
- コミットメッセージを確認・編集し、コミットを確定すると、指定コミットの変更を打ち消す新しいコミットが現在のブランチの先端に追加されます。
- 指定した
-
結果:
- 元のコミットは履歴にそのまま残ります。
- 元のコミットの変更を打ち消す、逆の変更内容を持つ新しいコミットが履歴に追加されます。
- プロジェクトのファイル内容は、指定した
<コミット>
が適用される前の状態に戻ります。
-
使用例:
- リモートにプッシュ済みのコミットに含まれる機能や修正を取り消したい場合。
- 過去の特定のコミットの変更だけを安全に取り消したい場合(そのコミットより後の他のコミットは維持したい場合)。
- 履歴を書き換えたくない、あるいは書き換えることができない共有ブランチで作業している場合。
-
具体的な手順(例:過去の特定のコミットを取り消す):
“`bash
状況:master ブランチにプッシュ済みの Commit B の変更を取り消したい
コミット履歴:A -> B -> C -> D (HEAD on master)
Commit B の変更を打ち消す新しいコミットを作成
git revert B
コミットメッセージ編集画面が開くので、確認/編集して保存
デフォルトメッセージ例: “Revert ‘Commit B message’ This reverts commit .”
コミット確定後、履歴を確認
git log –oneline => A -> B -> C -> D -> E (HEAD on master)
ここで E は Commit B の変更を打ち消す Revert コミット
“`
この操作により、履歴は
A -> B -> C -> D -> E
となり、Commit B 自体は履歴に残りますが、Commit E がその変更を打ち消すため、プロジェクトのファイル内容は Commit B が適用される前の状態に戻ります。
複数のコミットを revert
する
git revert
は、単一のコミットだけでなく、複数のコミットをまとめて取り消すこともできます。これには、コミットの範囲を指定します。
-
範囲指定:
bash
git revert <コミット範囲>
<コミット範囲>
は、例えばHEAD~3..HEAD
のように指定します。これは「HEADから遡って3つ前までのコミット」を意味します。git revert HEAD~3..HEAD
と実行すると、指定された範囲のコミットが、古い方から順番にrevertされます。各コミットがrevertされるたびに、デフォルトでは新しいコミットが一つずつ作成されます。- 例:
A -> B -> C -> D (HEAD)
の履歴でgit revert HEAD~2..HEAD
(C
とD
をrevert) を実行すると、まずC
を打ち消すコミットが作成され、次にD
を打ち消すコミットが作成されます。結果A -> B -> C -> D -> E (Revert C) -> F (Revert D)
となります。
- 例:
-
順番にrevertする場合の注意点(コンフリクト):
複数のコミットを順番にrevertする際、後のコミットが前のコミットで行われた変更に依存していたり、同じ箇所を変更していたりする場合、コンフリクトが発生する可能性があります。コンフリクトが発生したら、手動で解消し、git add
してgit revert --continue
またはgit commit
を実行して revert 処理を続ける必要があります。
revert
のオプション
-
-n
または--no-commit
:
git revert -n <コミット>
このオプションを付けると、revertによる変更をワーキングツリーとインデックスに適用しますが、新しいコミットは作成しません。revertしたい複数のコミットがある場合に、それらを個別のrevertコミットにするのではなく、まとめて一つのコミットとして記録したい場合に便利です。- 例:
git revert -n HEAD~2..HEAD
これにより、HEAD~2..HEAD
の範囲のコミット全ての逆方向の変更がワーキングツリーとインデックスにステージングされますが、まだコミットはされません。その後、自分でgit commit
を実行して、それらの変更をまとめて一つのコミットとして記録できます。
- 例:
-
-e
または--edit
:
git revert -e <コミット>
(デフォルト)
このオプションはデフォルトの挙動であり、revertコミットを作成する際にコミットメッセージの編集画面を開きます。 -
--continue
,--abort
,--quit
:
複数のコミットをまとめてrevertしている途中でコンフリクトが発生し、それを解消した後、処理を再開 (--continue
)、中止 (--abort
)、または終了 (--quit
) するために使用します。
git revert
のメリットとデメリット
-
メリット:
- 非破壊的: 履歴を書き換えません。元のコミットはそのまま残ります。
- 安全: 共有リポジトリやプッシュ済みのコミットに対して安全に使用できます。他の開発者の履歴に影響を与えません。
- 意図が明確: revert コミット自体が「このコミットの変更を取り消した」という記録になるため、履歴を見たときに意図が分かりやすいです。
-
デメリット:
- 履歴が複雑になる可能性がある: 取り消したいコミットの数だけ新しいコミットが追加されるため、revertを頻繁に行うとコミット履歴が長くなり、追いかけにくくなることがあります。
- コンフリクト: revert対象のコミットより後のコミットで同じファイルが変更されている場合、コンフリクトが発生する可能性があります。
- 「なかったこと」にはならない: あくまで変更を打ち消すだけであり、元のコミット自体は履歴から消えません。完全に履歴を消したい場合は
reset
が必要になります(ただし、安全性の問題があります)。
まとめると、git revert
は履歴を書き換えることなく、新しいコミットとして変更の打ち消しを記録するコマンドです。共有リポジトリで安全に過去の変更を取り消したい場合に最適な方法です。
reset
とrevert
の比較と使い分け
git reset
と git revert
は、どちらも「コミットを取り消す」という目的に使われますが、その根本的なアプローチと履歴への影響が大きく異なります。どちらを使うべきかは、状況、特に以下の点を考慮して判断する必要があります。
- 目的: コミットを「なかったこと」にしたいのか、それともコミットの「変更を打ち消したい」のか。
- 履歴への影響: 履歴を書き換えても良いか、それとも履歴を追加してでも元の履歴を保持したいか。
- 対象のコミット: 取り消したいコミットがローカルだけのものか、それとも既にリモートにプッシュされているか。
- 破壊性: 作業内容が失われるリスクを許容できるか。
これらの観点から両者を比較してみましょう。
特徴 | git reset |
git revert |
---|---|---|
基本操作 | HEADを過去のコミットに移動させる(履歴を書き換える) | 変更を打ち消す新しいコミットを作成する(履歴に追加する) |
履歴への影響 | 過去に移動させたコミット以降の履歴を削除/変更する | 元の履歴を保持し、その上に新しいコミットを追加する |
非破壊性 | --hard モードは破壊的(データ損失あり得る) |
非破壊的(元の履歴・データは失われない) |
使用シーン | ローカルブランチでの作業のやり直し、実験的な変更の破棄 | プッシュ済みコミットの取り消し、共有ブランチでの作業 |
リモート | リモートにプッシュ済みの場合は push --force が必要(危険) |
通常の push で安全に反映できる |
「取り消し」の定義 | 「そのコミットは存在しなかった」 | 「そのコミットで行われた変更を元に戻す」 |
使い分けの考え方フローチャート (イメージ)
-
取り消したいコミットは、既にリモートリポジトリにプッシュされていますか?
- はい: 基本的には
git revert
を使用してください。 履歴を書き換えるgit reset
+git push --force
は、他の開発者に迷惑をかけるリスクが非常に高いです。revert
は非破壊的で、共有環境で安全です。 - いいえ(ローカルリポジトリのみの変更ですか)? → 2へ
- はい: 基本的には
-
直前のコミットだけを取り消したいですか? それとも、過去の任意のコミットを取り消したいですか?
- 直前のコミットだけ: → 3へ
- 過去の任意のコミット: → 4へ
-
直前のコミットを取り消して、その変更内容をどうしたいですか?
- 変更内容をステージングされた状態に戻して、すぐにコミットし直したい:
git reset --soft HEAD~1
- 変更内容をワーキングツリーに戻して、ファイル内容を修正してからステージング/コミットし直したい:
git reset --mixed HEAD~1
(またはgit reset HEAD~1
) - 変更内容を完全に破棄して、直前のコミットの前の状態に戻りたい:
git reset --hard HEAD~1
(注意: 未コミットの変更も失われます)
- 変更内容をステージングされた状態に戻して、すぐにコミットし直したい:
-
過去の任意のコミットを取り消したいが、そのコミット以降の履歴(他のコミット)は残したいですか?
- はい:
git revert <取り消したいコミット>
- いいえ(そのコミット以降の全ての変更を破棄して、そのコミットの時点に完全にリセットしたいですか)?
- はい:
git reset --hard <対象コミット>
(注意: 対象コミット以降の全ての履歴と未コミット変更は失われます)
- はい:
- はい:
このフローチャートは一般的な目安です。状況によっては他の要素も考慮する必要がありますが、基本的な判断基準として役立つでしょう。最も重要なのは、リモートにプッシュ済みかどうか、そして履歴を書き換えても問題ないかどうかです。
よくあるシナリオと具体的なコマンド例
ここでは、実際の開発で遭遇しがちな具体的なシナリオを取り上げ、それぞれの場合にどのコマンド(reset
または revert
)をどのように使うのが適切か、具体的なコマンド例とともに解説します。
現在のコミット履歴を以下と仮定します。
* abcd123 (HEAD -> feature/my-new-feature) Implement feature X part 3
* efgh456 Implement feature X part 2
* ijkl789 Implement feature X part 1
* 0123456 (origin/main, main) Initial feature setup
あなたが feature/my-new-feature
ブランチで作業しているとします。
シナリオ1:直前のコミット(abcd123
)を間違えたので、変更をワーキングツリーに戻してやり直したい
- 状況:
Implement feature X part 3
コミットに含まれるコードに問題が見つかった。このコミットの内容は維持しつつ、修正を加えてもう一度コミットしたい。ただし、ステージング状態ではなく、ワーキングツリーで自由に編集したい。 - 目的: 直前のコミットを取り消し、その変更をワーキングツリーに戻す。
- 適切なコマンド:
git reset --mixed HEAD~1
または単にgit reset HEAD~1
(mixedはデフォルト) - 理由: ローカルブランチの直前のコミットを取り消すだけで、変更内容は残したいからです。ワーキングツリーに戻すことで、ファイル内容を自由に修正できます。
“`bash
実行コマンド
git reset HEAD~1
実行後の状態
コミット履歴:
* efgh456 (HEAD -> feature/my-new-feature) Implement feature X part 2
* ijkl789 Implement feature X part 1
* 0123456 (origin/main, main) Initial feature setup
git status を確認すると、abcd123 コミットで加えられた変更が “Changes not staged for commit:” として表示されます。
その後、ファイルを修正し、git add でステージングし直し、新しいコミットを作成します。
git add .
git commit -m “Fix issue and complete feature X part 3”
“`
シナリオ2:直前のコミット(abcd123
)を間違えたが、ステージング状態は維持してコミットメッセージだけ変更したい
- 状況: コードは正しいが、
Implement feature X part 3
のコミットメッセージが不適切だった。コミットメッセージだけを修正したい。変更内容はステージングされたままにしておきたい。 - 目的: 直前のコミットを取り消し、その変更をインデックス(ステージングエリア)に残す。
- 適切なコマンド:
git reset --soft HEAD~1
- 理由: コミットメッセージだけを変えたい、あるいは変更内容をそのまま再度コミットしたい場合に、インデックスに作業内容を残す
--soft
が適しています。
“`bash
実行コマンド
git reset –soft HEAD~1
実行後の状態
コミット履歴:
* efgh456 (HEAD -> feature/my-new-feature) Implement feature X part 2
* ijkl789 Implement feature X part 1
* 0123456 (origin/main, main) Initial feature setup
git status を確認すると、abcd123 コミットで加えられた変更が “Changes to be committed:” として表示されます。
その後、新しいコミットメッセージでコミットします。
git commit -m “より適切な Feature X part 3 のコミットメッセージ”
Tips: 直前のコミットメッセージを修正するだけなら git commit –amend を使う方が一般的で簡単です。
git commit –amend -m “新しいコミットメッセージ”
reset –soft HEAD~1 は、コミットメッセージだけでなく、ステージングされた内容も変更してコミットし直したい場合に有効です。
“`
シナリオ3:実験的なコミットをいくつか行ったが、全て破棄して過去の特定のコミット(ijkl789
)に戻りたい
- 状況:
ijkl789
以降にefgh456
とabcd123
の2つのコミットを作成したが、これらは実験的なものであり、その後のワーキングツリーでの作業も含めて全て無かったことにしたい。完全にijkl789
の時点の状態に戻りたい。 - 目的: 指定したコミット以降の全ての履歴と未コミットの変更を完全に破棄する。
- 適切なコマンド:
git reset --hard ijkl789
- 理由: 指定コミット以降の全ての変更を完全に破棄し、ファイルシステムの状態もその時点に戻したいからです。これは最も破壊的な操作です。
“`bash
実行コマンド
git reset –hard ijkl789
実行前の警告:
git status を実行し、失われては困る未コミットの変更がないことを必ず確認してください。
もしある場合は、git stash などで一時的に退避させてください。
実行後の状態
コミット履歴:
* ijkl789 (HEAD -> feature/my-new-feature) Implement feature X part 1
* 0123456 (origin/main, main) Initial feature setup
git status を確認すると “nothing to commit, working tree clean” と表示されます。
ワーキングツリーのファイル内容は、ijkl789 コミットの時点に戻っています。
abcd123 と efgh456 コミット、および reset 前の未コミット変更は失われます。
“`
シナリオ4:masterブランチにプッシュ済みの、過去の特定のコミット(efgh456
)の変更だけを取り消したい
- 状況:
main
ブランチは既にリモートにプッシュされており、その中のefgh456
コミットに問題があることが判明した。このコミットの変更だけを取り消したいが、それより後のコミット(abcd123
など、他の開発者が既に利用している可能性がある)は維持したい。 - 目的: 履歴を書き換えることなく、特定の過去のコミットの変更を打ち消す新しいコミットを作成する。
- 適切なコマンド:
git revert efgh456
- 理由: リモートにプッシュ済みであり、他の開発者に影響を与えずに安全に特定のコミットの変更を取り消したいからです。
revert
は履歴に新しいコミットを追加する非破壊的な操作です。
“`bash
まず、master ブランチに移動します(このシナリオでは main ブランチと仮定)
git checkout main
実行コマンド
git revert efgh456
コミットメッセージ編集画面が開くので、デフォルトメッセージを確認/編集して保存します。
例: Revert “Implement feature X part 2”
This reverts commit efgh456.
実行後の状態
コミット履歴:
* 567890 (HEAD -> main) Revert “Implement feature X part 2” (新しいコミット)
* abcd123 Implement feature X part 3
* efgh456 Implement feature X part 2
* ijkl789 Implement feature X part 1
* 0123456 Initial feature setup
efgh456 コミットは履歴に残ったまま、その変更を打ち消す新しいコミット 567890 が追加されました。
これをリモートに push することで、efgh456 の変更だけが取り消された状態を共有できます。
git push origin main
“`
シナリオ5:複数の過去のコミット(efgh456
と abcd123
)をまとめて revert したい
- 状況:
efgh456
とabcd123
の両方のコミットに含まれる変更を取り消したい。これらはmain
ブランチにプッシュ済みであり、安全に取り消したい。個別に2つの revert コミットを作成しても良いが、可能であれば1つの revert コミットにまとめたい。 - 目的: 複数の指定コミットの変更を打ち消す新しいコミットを作成する。まとめて1つのコミットにしたい場合は
-n
オプションを使用する。 - 適切なコマンド:
git revert efgh456 abcd123
(個別に revert) またはgit revert -n efgh456 abcd123
(まとめて revert) または範囲指定git revert -n HEAD~2..HEAD
(現在のHEADがabcd123の場合) - 理由: 複数のコミットの変更を取り消したいが、履歴を書き換えるのは避けたいからです。
“`bash
状況:main ブランチにいると仮定
コミット履歴:A -> B -> C -> D (HEAD on main) B=efgh456, C=abcd123 と仮定
方法1:個別に revert コミットを作成する
Gitは指定したコミットを古い方から順番に処理します
git revert efgh456 abcd123
-> まず efgh456 を revert するコミットのメッセージ編集画面が開く
-> 次に abcd123 を revert するコミットのメッセージ編集画面が開く
結果:A -> B -> C -> D -> E (Revert B) -> F (Revert C)
方法2:-n オプションを使ってまとめて1つのコミットにする
Gitは指定したコミットを新しい方から逆順に処理し、変更をワーキングツリー/インデックスに適用します
git revert -n abcd123 efgh456
または、現在のHEADが abcd123 なら範囲指定で
git revert -n HEAD~2..HEAD
実行後、変更はステージングされますがコミットはされません。
git status を確認すると、efgh456 と abcd123 の変更を打ち消す内容が “Changes to be committed:” にあります。
その後、自分でまとめてコミットします
git commit -m “Revert changes introduced in feature X parts 2 and 3”
結果:A -> B -> C -> D -> E (Revert B and C combined)
“`
複数のコミットをまとめてrevertする場合、-n
オプションを使うと履歴がスッキリするというメリットがありますが、コンフリクトが発生した場合はまとめて解決する必要があります。個別にrevertする場合は、コミットごとにコンフリクトを解決できますが、履歴が長くなります。状況に応じて選択してください。
Gitにおけるその他の取り消し・修正操作(補足)
Gitには reset
や revert
以外にも、変更を取り消したり修正したりするための便利なコマンドやオプションがいくつかあります。これらも知っておくと、より柔軟にGitを使いこなすことができます。
-
git commit --amend
:- 用途: 直前のコミットを修正する。具体的には、直前のコミットを「なかったこと」にして、現在のインデックスの内容と新しいコミットメッセージ(あるいは元のメッセージを編集したもの)で新しいコミットを作成し直します。
- 挙動: 実質的には
git reset --soft HEAD~1
を実行してから、インデックスの内容でコミットし直すのと似ています。ただし、--amend
は一つのコマンドで完結します。 - 注意点:
reset
と同様、直前のコミットのハッシュ値が変わります。もしそのコミットを既にリモートにプッシュしている場合は、push --force
が必要になり、他の開発者に影響を与える可能性があります。したがって、--amend
も基本的にローカルの未プッシュのコミットに対してのみ使用すべきです。 -
例:
“`bash
# 直前のコミットにいくつかの変更を追加したい
git add <追加したいファイル>
git commit –amend –no-edit # メッセージを編集せずにコミットに追加直前のコミットメッセージだけを変更したい
git commit –amend # メッセージ編集画面が開く
“`
-
git restore
/git checkout
:- 用途: ワーキングツリーまたはインデックスの特定のファイルの変更を取り消す。
git restore <ファイル名>
: ワーキングツリーでの<ファイル名>
の変更を、インデックスまたはHEAD(インデックスにない場合)の状態に戻します。未ステージの変更を取り消すのに使います。git restore --staged <ファイル名>
: インデックスでの<ファイル名>
の変更(ステージングされた変更)を、HEADの状態に戻します。つまり、ステージングを取り消します (git reset HEAD <ファイル名>
と同じ効果)。git checkout <ファイル名>
: 以前はワーキングツリーの変更取り消しにも使われましたが、現在はgit restore
が推奨されています。ワーキングツリーでの変更を、指定したコミット(デフォルトはHEAD)の状態に戻します。git checkout .
: ワーキングツリーの全ての未ステージの変更を取り消し、HEADの状態に戻します。(これも現在はgit restore .
が推奨)git checkout <コミット> <ファイル名>
: 指定した<コミット>
時点の<ファイル名>
をワーキングツリーに持ってきます。- 注意点: これらのコマンドはファイル単位での操作であり、コミット全体を取り消す
reset
やrevert
とは目的が異なります。特にgit restore
やgit checkout <ファイル名>
は、ワーキングツリーでの未コミットの変更を破棄するため、実行前に確認が必要です。
-
git clean
:- 用途: Gitに追跡されていないファイル(untracked files)を削除する。
- 注意点: 非常に強力で、一度削除したファイルは通常の方法では復旧できません。
- 例:
bash
git clean -n # 削除されるファイルを事前に確認(重要!)
git clean -f # untracked files を強制的に削除
git clean -fdx # untracked files と ignore されているファイルも削除
-
git stash
:- 用途: 作業中の変更(ワーキングツリーとインデックスにあるもの)を一時的に保存し、ワーキングツリーをクリーンな状態に戻す。
- 挙動: 未コミットの変更をスタッシュに積み上げ、いつでも後からその変更を元に戻すことができます。
- 使用例: 作業途中で緊急のバグ修正が必要になったが、今の変更はまだコミットしたくない場合など。
- 例:
bash
git stash save "WIP: <作業内容のメモ>" # 作業内容をスタッシュに保存
git stash list # スタッシュの一覧を表示
git stash apply # 最新のスタッシュを適用(スタッシュは残る)
git stash pop # 最新のスタッシュを適用し、スタッシュから削除 git reset --hard
を実行する前に、もし未コミットの変更を残しておきたい場合は、git stash
で一時的に退避させておくのが安全です。
-
git rebase -i
(インタラクティブなリベース):- 用途: コミット履歴を編集する。コミットの並べ替え、結合 (squash)、分割、削除、コミットメッセージの変更などをインタラクティブに行えます。
- 挙動: 指定したコミット以降の履歴を一時的に剥がし、編集を加えた上で、元のコミットの上に新しいコミットとして積み直します。
- 注意点:
reset
と同様に履歴を書き換える操作です。リモートにプッシュ済みのコミットに対して行うと問題を引き起こします。 - 例: 直近3つのコミットを編集したい場合
bash
git rebase -i HEAD~3
これによりエディタが開き、編集可能なコミットのリストが表示されます。ここでpick
をdrop
に変更することでコミットを削除するなど、様々な操作が可能です。 reset
やrevert
よりも複雑ですが、複数のコミットをまとめて整理したい場合に非常に強力なツールです。特定のコミットを履歴から完全に削除したい場合は、rebase -i
でdrop
を使う方法もあります(ただしこれも履歴書き換えなので注意)。
これらのコマンドは、それぞれ異なる目的と影響範囲を持っています。状況に合わせて適切なコマンドを選択することが、Gitでの作業効率と安全性を高める鍵となります。
まとめ
この記事では、Gitにおけるコミットの取り消し方法として、主に git reset
と git revert
という2つのコマンドに焦点を当てて詳しく解説しました。
-
git reset
は、ローカルブランチのHEADポインタを過去のコミットに移動させることで、履歴を書き換えるコマンドです。特に--soft
,--mixed
,--hard
というモードがあり、それぞれHEAD、インデックス、ワーキングツリーへの影響が異なります。--soft
: HEADのみ移動、インデックス/ワーキングツリーは維持。変更はステージング状態に残る。直前のコミットメッセージ修正や再コミットに便利。--mixed
(デフォルト): HEAD移動、インデックスリセット、ワーキングツリーは維持。変更はunstage状態でワーキングツリーに残る。直前のコミットを取り消し、作業状態に戻すのに便利。--hard
: HEAD移動、インデックス/ワーキングツリーも指定コミットの状態にリセット。破壊的であり、未コミットの変更を含むリセット対象以降の作業内容は全て失われる。完全に過去の状態に戻りたい場合に使うが、非常に慎重に。reset
は履歴を書き換えるため、リモートにプッシュ済みの共有ブランチに使用することは推奨されません。git push --force
が必要になり、他の開発者に問題を引き起こす可能性があります。ローカルの個人用ブランチで使うのが安全です。git reflog
を使うと、reset --hard
などで失われたように見えるコミットも復旧できる可能性があります。
-
git revert
は、取り消したいコミットの変更内容を逆方向に適用し、それを新しいコミットとして履歴に追加するコマンドです。- 元のコミット自体は履歴に残ります。履歴を書き換えることはありません。
- 非破壊的であり、リモートにプッシュ済みの共有ブランチでも安全に使用できます。
- 特定の過去のコミットの変更だけを取り消したいが、それ以降の他のコミットは維持したい場合に最適です。
- 複数のコミットを一度にrevertすることも可能ですが、コンフリクトに注意が必要です。
-n
オプションで複数の revert をまとめて1つのコミットにすることもできます。
最も重要な使い分けの原則は、「その操作はリモートにプッシュ済みか?」そして「履歴を書き換えても安全か?」です。
- リモートにプッシュ済みのコミットを取り消したい →
git revert
- ローカルの未プッシュのコミットを取り消し、履歴を書き換えたい →
git reset
(状況に応じて –soft, –mixed, –hard を選択)
git reset --hard
は特に破壊性が高いため、実行前に必ず git status
で失われては困る変更がないか確認し、必要であれば git stash
で一時退避させる習慣をつけましょう。
また、git commit --amend
、git restore
/ git checkout
、git clean
、git stash
、git rebase -i
など、Gitには他にも変更の管理や取り消しに関連する便利なコマンドがあります。これらを理解し、適切な場面で使い分けることで、Gitでの開発作業はよりスムーズで安全になります。
Gitの操作、特に履歴を書き換えるものは、最初は難しく感じたり、恐れを感じたりするかもしれません。しかし、これらのコマンドの仕組みを理解し、まずはローカルの実験用リポジトリで練習を重ねることで、自信を持って使えるようになります。git reflog
はあなたの強力なセーフティネットとなりますので、その使い方もしっかり覚えておきましょう。
Gitの「取り消し」操作をマスターし、より効率的で堅牢なバージョン管理を目指しましょう。
FAQ (よくある質問)
Q: git reset --hard
で失ったデータは本当に戻せないのですか?
A: ファイルシステム上から削除されたデータ自体をGitが復旧させることはできません。しかし、git reset --hard
によってコミット履歴から「見えなくなった」コミットに含まれる変更内容は、git reflog
を使って復旧できる可能性が高いです。reflog
にはHEADが指していた過去のコミットの履歴が記録されているため、reset
する前のコミットを見つけ出し、そのコミットに対して再度 git reset --hard
や git checkout
を実行することで、その時点の状態に戻ることができます。ただし、reflog
の情報は永続的ではなく、一定期間(デフォルトで90日)を過ぎると削除されることに注意が必要です。また、まだコミットしていなかった(git add
すらしていなかった)変更は reflog
の対象外ですので、完全に失われます。
Q: git revert
したコミットを再度 revert
したらどうなりますか?
A: 例えば、A -> B -> C
という履歴で git revert B
を実行し、A -> B -> C -> D (Revert B)
となったとします。この状態で git revert D
(Revert B コミット) を実行するとどうなるか? git revert D
は、コミット D(つまり B の変更を打ち消す変更)の逆方向の変更を適用します。D は B の変更を打ち消すものだったので、D の逆方向の変更は、元の B の変更と同じ変更になります。結果として、B の変更内容が再びプロジェクトに適用され、新しいコミットが作成されます。つまり、A -> B -> C -> D (Revert B) -> E (Revert D)
となり、E は元の B と同じコード変更をもたらすコミットとなります。これは意図しない結果になることが多いので、通常は revert したコミットを再度 revert するのではなく、新しい変更としてコードを記述し直す方が推奨されます。
Q: git reset
と git checkout
の違いは何ですか?
A: どちらも指定したコミットの状態に切り替えることができますが、その影響範囲と目的が異なります。
* git reset <コミット>
は、現在のブランチのHEADを <コミット>
に移動させ、必要に応じてインデックスやワーキングツリーをその状態に合わせます。特に --hard
以外のモードでは、リセット対象となったコミットの変更内容をインデックスやワーキングツリーに残すことができます。これは履歴を書き換える操作であり、ブランチの先端を過去に戻すことが主な目的です。
* git checkout <コミット>
または git checkout <コミット> <ファイル名>
は、指定した <コミット>
時点のファイル内容をワーキングツリーに持ってきます。ブランチ名を指定しない <コミット>
へのチェックアウトは、「detached HEAD」状態になり、どのブランチにも属さない状態になります。これは特定の過去の状態を確認したり、そこからファイルを一時的に取り出したりすることが主な目的であり、通常は履歴を書き換えることはありません(detached HEAD で新しいコミットを作成すると、後でそのコミットにアクセスしにくくなるという影響はありますが)。ファイル名を指定した場合は、ワーキングツリーのそのファイルだけが変更され、インデックスやHEADには影響を与えません。
簡単に言えば、reset
は「ブランチの先端を過去に戻す」、checkout
は「過去の状態や特定のコミットのファイルを確認・取得する」といったニュアンスで使い分けることが多いです(ただし、git checkout
はブランチ切り替えなど他の機能も持っており、Git 2.23以降はファイルの状態復旧には git restore
が推奨されています)。
これらのFAQが、あなたのGitコマンドへの理解をさらに深める助けになれば幸いです。