Gitコマンドでコミットを取り消す方法(reset/revert解説)

Gitコマンドでコミットを取り消す方法(reset/revert解説)

はじめに:Gitにおける「取り消し」という概念

ソフトウェア開発におけるバージョン管理システムは、コードの変更履歴を記録し、管理するための不可欠なツールです。Gitは、その中でも特に広く使われている分散型バージョン管理システムです。Gitを使うことで、開発者は安心してコードの変更を加えたり、過去の状態に戻したり、複数の開発者と協力して作業を進めたりすることができます。

しかし、開発を進める上で、意図しない変更を加えてしまったり、間違ったコミットをしてしまったりすることはよくあります。このような場合に必要となるのが、「コミットを取り消す」操作です。Gitには、コミットを取り消すためのいくつかの方法が用意されていますが、その中でも代表的なものが git resetgit revert です。

これらのコマンドは、どちらもコミットを取り消す目的で使用されますが、その振る舞いや履歴への影響が大きく異なります。誤ったコマンドを選択してしまうと、予期しない結果を招いたり、場合によっては共同開発者に迷惑をかけてしまったりする可能性もあります。そのため、それぞれのコマンドがどのように機能し、どのような状況で使うべきかを正しく理解しておくことは、Gitを効果的かつ安全に使う上で非常に重要です。

この記事では、Gitにおけるコミットの取り消しについて、特に git resetgit revert に焦点を当てて、その詳細なメカニズム、使い方、そして適切な使い分けについて、初心者にも分かりやすく解説します。これらのコマンドをマスターすることで、Gitを使った開発作業における「困った!」を減らし、より自信を持ってコードの変更を管理できるようになるでしょう。

まずは、Gitがどのように変更履歴を管理しているのか、基本的な概念から復習しましょう。この基礎理解が、resetrevert の挙動を深く理解するための土台となります。

Gitの基本的な概念の復習

git resetgit revert の詳細に入る前に、Gitの基本的な概念、特にコミット、HEAD、インデックス、ワーキングツリーの関係について再確認しておきましょう。これらの概念は、これらのコマンドがどのように動作するのかを理解するために不可欠です。

コミットとは?

Gitにおけるコミットは、プロジェクトの特定時点におけるスナップショットのようなものです。ファイル群の特定の状態を記録し、一意のハッシュ値(SHA-1など)で識別されます。各コミットは通常、その親となるコミットへのポインタを持ち、これらが連なってコミット履歴という一本の線(または分岐したツリー)を形成します。コミットには、誰が、いつ、どのような意図で変更を加えたのかを示すコミットメッセージが含まれます。

HEAD、インデックス、ワーキングツリー

Gitは、プロジェクトの状態を管理するために、主に以下の3つの領域を使用します。

  1. ワーキングツリー (Working Tree / Working Directory)

    • 開発者が実際にファイルを作成したり編集したりする作業ディレクトリです。
    • ここにあるファイルは、Gitの管理下にあるファイルだけでなく、Gitに追跡されていない新しいファイル(untracked files)や、Gitの管理下にあるが変更が加えられたファイル(modified files)などが混在しています。
  2. インデックス (Index / Staging Area)

    • 次にコミットされる変更内容を準備するための「上演エリア」です。
    • ワーキングツリーでの変更のうち、次にコミットに含めたい変更を git add コマンドを使ってインデックスに追加します。
    • インデックスは、コミットされるスナップショットの内容を一時的に保持します。
  3. HEAD

    • 現在作業しているブランチの先端、つまり最新のコミットを指す特別なポインタです。
    • 通常は現在のブランチ名(例: main, develop)を介して、そのブランチの最新コミットを指しています。
    • git commit コマンドを実行すると、HEADが指すコミットを親として新しいコミットが作成され、HEADはその新しいコミットに移動します。

これらの関係を図で表すと、以下のようになります。

+-----------------+
| ワーキングツリー| (実際のファイル編集)
+-----------------+
^
| git add (変更をインデックスに追加)
|
+-----------------+
| インデックス | (次にコミットする準備)
+-----------------+
^
| git commit (インデックスの内容をコミットとして記録)
|
+-----------------+
| HEAD | (現在のブランチの最新コミットを指す)
+-----------------+
|
| (過去のコミット履歴へ)
v
+-----------------+
| コミット履歴 | (過去のスナップショット群)
+-----------------+

git resetgit 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が移動する前の状態(つまり、リセット対象となったコミット群の変更内容)が、そのままインデックス(ステージングエリア)に残ります。ワーキングツリーのファイル内容も変更されません。
  • 結果:

    • リセット対象となったコミット群は、もはや現在のブランチの履歴の一部ではなくなります(ただし、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ポインタを指定した <コミット> に移動させます。
    • インデックスをリセットします: インデックスの内容を指定した <コミット> の状態に戻します。つまり、リセット対象となったコミット群で加えられた変更は、インデックスから削除されます。
    • ワーキングツリーは変更しません: ワーキングツリーのファイル内容は、リセット前と変わりません。インデックスからは外れますが、ファイルそのものは残っています。
  • 結果:

    • リセット対象となったコミット群は、履歴から消えます。
    • インデックスは空になります(正確には、指定コミット時点のインデックス状態に戻ります)。
    • リセット対象となったコミット群で加えられたすべての変更は、インデックスからは削除されますが、ワーキングツリーには「変更されたファイル」として残ります(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ポインタを指定した <コミット> に移動させます。
    • インデックスをリセットします: インデックスの内容を指定した <コミット> の状態に完全に一致させます。
    • ワーキングツリーをリセットします: ワーキングツリーのファイル内容を指定した <コミット> の状態に完全に一致させます。これにより、リセット対象となったコミット群で加えられたすべての変更や、まだコミット/ステージングされていなかったワーキングツリーでの作業内容が、問答無用で破棄されます!
  • 結果:

    • リセット対象となったコミット群は、履歴から消えます。
    • インデックスとワーキングツリーは、指定した <コミット> の時点のプロジェクトの状態と完全に一致します。
    • リセット対象となったコミット以降に加えられた、コミットされていない作業内容は全て失われます。
  • 警告:

    • 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 で消えたコミットを復旧する方法:

    1. git reflog を実行し、reset --hard を実行する前の、失われたコミット群の先端だったコミットを探します。そのコミットのハッシュ値(上記の例の 9876543 など)や HEAD@{n} の参照を見つけます。
    2. 失われたコミットに戻るには、そのコミットに対して git checkout または git reset を実行します。
      • 特定のコミットの状態を確認したいだけなら:
        bash
        git checkout 9876543

        これで、そのコミットの detached HEAD 状態になります。
      • 失われたコミット群を現在のブランチの先端として復旧したい場合:
        bash
        git reset --hard 9876543

        これにより、現在のブランチのHEAD、インデックス、ワーキングツリーが、失われたコミットの状態に戻ります。

    git refloggit 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 <コミット> の挙動:

    1. 指定した <コミット> の変更内容(差分)を計算します。
    2. その変更内容を逆方向に適用しようとします。
    3. 逆方向の変更を適用した結果、ワーキングツリーとインデックスが更新されます
    4. その変更内容を、新しいコミットとしてインデックスにステージングし、コミットメッセージの編集画面を開きます。
    5. コミットメッセージを確認・編集し、コミットを確定すると、指定コミットの変更を打ち消す新しいコミットが現在のブランチの先端に追加されます。
  • 結果:

    • 元のコミットは履歴にそのまま残ります。
    • 元のコミットの変更を打ち消す、逆の変更内容を持つ新しいコミットが履歴に追加されます。
    • プロジェクトのファイル内容は、指定した <コミット> が適用される前の状態に戻ります。
  • 使用例:

    • リモートにプッシュ済みのコミットに含まれる機能や修正を取り消したい場合。
    • 過去の特定のコミットの変更だけを安全に取り消したい場合(そのコミットより後の他のコミットは維持したい場合)。
    • 履歴を書き換えたくない、あるいは書き換えることができない共有ブランチで作業している場合。
  • 具体的な手順(例:過去の特定のコミットを取り消す):

    “`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 (CD を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 は履歴を書き換えることなく、新しいコミットとして変更の打ち消しを記録するコマンドです。共有リポジトリで安全に過去の変更を取り消したい場合に最適な方法です。

resetrevertの比較と使い分け

git resetgit revert は、どちらも「コミットを取り消す」という目的に使われますが、その根本的なアプローチと履歴への影響が大きく異なります。どちらを使うべきかは、状況、特に以下の点を考慮して判断する必要があります。

  1. 目的: コミットを「なかったこと」にしたいのか、それともコミットの「変更を打ち消したい」のか。
  2. 履歴への影響: 履歴を書き換えても良いか、それとも履歴を追加してでも元の履歴を保持したいか。
  3. 対象のコミット: 取り消したいコミットがローカルだけのものか、それとも既にリモートにプッシュされているか。
  4. 破壊性: 作業内容が失われるリスクを許容できるか。

これらの観点から両者を比較してみましょう。

特徴 git reset git revert
基本操作 HEADを過去のコミットに移動させる(履歴を書き換える) 変更を打ち消す新しいコミットを作成する(履歴に追加する)
履歴への影響 過去に移動させたコミット以降の履歴を削除/変更する 元の履歴を保持し、その上に新しいコミットを追加する
非破壊性 --hard モードは破壊的(データ損失あり得る) 非破壊的(元の履歴・データは失われない)
使用シーン ローカルブランチでの作業のやり直し、実験的な変更の破棄 プッシュ済みコミットの取り消し、共有ブランチでの作業
リモート リモートにプッシュ済みの場合は push --force が必要(危険) 通常の push で安全に反映できる
「取り消し」の定義 「そのコミットは存在しなかった」 「そのコミットで行われた変更を元に戻す」

使い分けの考え方フローチャート (イメージ)

  1. 取り消したいコミットは、既にリモートリポジトリにプッシュされていますか?

    • はい: 基本的には git revert を使用してください。 履歴を書き換える git reset + git push --force は、他の開発者に迷惑をかけるリスクが非常に高いです。revert は非破壊的で、共有環境で安全です。
    • いいえ(ローカルリポジトリのみの変更ですか)? → 2へ
  2. 直前のコミットだけを取り消したいですか? それとも、過去の任意のコミットを取り消したいですか?

    • 直前のコミットだけ: → 3へ
    • 過去の任意のコミット: → 4へ
  3. 直前のコミットを取り消して、その変更内容をどうしたいですか?

    • 変更内容をステージングされた状態に戻して、すぐにコミットし直したい: git reset --soft HEAD~1
    • 変更内容をワーキングツリーに戻して、ファイル内容を修正してからステージング/コミットし直したい: git reset --mixed HEAD~1 (または git reset HEAD~1)
    • 変更内容を完全に破棄して、直前のコミットの前の状態に戻りたい: git reset --hard HEAD~1 (注意: 未コミットの変更も失われます)
  4. 過去の任意のコミットを取り消したいが、そのコミット以降の履歴(他のコミット)は残したいですか?

    • はい: 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 以降に efgh456abcd123 の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:複数の過去のコミット(efgh456abcd123)をまとめて revert したい

  • 状況: efgh456abcd123 の両方のコミットに含まれる変更を取り消したい。これらは 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には resetrevert 以外にも、変更を取り消したり修正したりするための便利なコマンドやオプションがいくつかあります。これらも知っておくと、より柔軟に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 <コミット> <ファイル名>: 指定した <コミット> 時点の <ファイル名> をワーキングツリーに持ってきます。
    • 注意点: これらのコマンドはファイル単位での操作であり、コミット全体を取り消す resetrevert とは目的が異なります。特に git restoregit 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

      これによりエディタが開き、編集可能なコミットのリストが表示されます。ここで pickdrop に変更することでコミットを削除するなど、様々な操作が可能です。
    • resetrevert よりも複雑ですが、複数のコミットをまとめて整理したい場合に非常に強力なツールです。特定のコミットを履歴から完全に削除したい場合は、rebase -idrop を使う方法もあります(ただしこれも履歴書き換えなので注意)。

これらのコマンドは、それぞれ異なる目的と影響範囲を持っています。状況に合わせて適切なコマンドを選択することが、Gitでの作業効率と安全性を高める鍵となります。

まとめ

この記事では、Gitにおけるコミットの取り消し方法として、主に git resetgit 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 --amendgit restore / git checkoutgit cleangit stashgit 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 --hardgit 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 resetgit 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コマンドへの理解をさらに深める助けになれば幸いです。

コメントする

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

上部へスクロール