1. gitの「元に戻す」が分かりにくい理由
開発中にファイルの変更を元に戻すため、git restoreを使う機会がありました。
これまでも、次のコマンドを見たり、実際に使ったりしたことはあります。
git restore
git reset
git revert
しかし、正直なところ、それぞれの違いや仕組みを完全に理解して使っていたわけではありません。
- ファイルの変更を消したいときは
restore - commitを戻したいときは
reset - push後は
revert
というように、用途を何となくで使っていました。
この機会にちょっと調べてみました。
それぞれは操作する対象や履歴の扱い方が異なります。
- Working Treeを変更する
- Indexを変更する
- HEADやブランチが指すコミットを変更する
- 過去の変更を打ち消す新しいコミットを作る
この違いを理解しないまま使うと、必要な変更まで消してしまったり、他のメンバーと共有している履歴を書き換えてしまったりする可能性があります。
restore・reset・revertをなんとなーく扱うのではなく、gitの状態がどのように変化するのかを確認しながら整理をしたいと思います。
この記事のゴールは、コマンドを実行する前に次のことを予測できるようになることです。
- Gitのどの状態が変更されるのか
- ファイルの変更内容は残るのか
- コミット履歴はどのように変わるのか
- 他のメンバーと共有済みの履歴にも使えるのか
先に結論
大まかには、変更がどの段階まで進んでいるかによって、使用するコマンドが変わります。
| 間違いに気づいた段階 | やりたいこと | 主なコマンド |
|---|---|---|
| ファイルを編集した後 | 編集内容を破棄する | git restore <file> |
git addした後 |
ステージから外す | git restore --staged <file> |
| commitした後 | ローカルコミットを取り消す | git reset |
| 他のメンバーと共有した後 | 履歴を残して変更を打ち消す | git revert |
ただし、この表だけでは「なぜそのコマンドを選ぶのか」までは分かりません。
ここから、git内部の状態を確認しながら詳しく見ていきます。
2. restore・reset・revertを理解するための3つの場所
3つのコマンドを理解するためには、まず次の3つを区別する必要があります。
- Working Tree
- Index
- HEAD
Working Tree
Working Treeは、現在手元で編集しているファイルの状態です。
例えば、commit済みのsample.txtに次の内容があったとします。
Hello
エディタで次のように変更します。
Hello
git
この時点では、追加したgitという変更はWorking Treeにだけ存在します。
まだgit addしていないため、次のcommitには含まれません。
HEAD Hello
Index Hello
Working Tree Hello + git
Index
Indexは、次のcommitに含める内容を保持する場所です。
一般的には「ステージング領域」と呼ばれます。
git add sample.txt
を実行すると、Working Treeにあるsample.txtの内容がIndexへ登録されます。
HEAD Hello
Index Hello + git
Working Tree Hello + git
ここで重要なのは、git addによってファイルそのものがWorking TreeからIndexへ移動するわけではないことです。
Working Treeにあるファイルの内容を、次のcommitに含める候補としてIndexへ登録しています。
HEAD
HEADは、現在作業している場所を示す参照です。
通常は、現在チェックアウトしているブランチを通して、そのブランチの先頭コミットを指しています。
例えば、次の履歴があるとします。
A --- B --- C
↑
main / HEAD
この場合、mainブランチとHEADはコミットCを指しています。
ここでcommitを実行します。
git commit
すると、Indexに登録されている内容から新しいコミットDが作られます。
その後、mainブランチの先頭がDへ進み、HEADも現在地としてDを参照します。
A --- B --- C --- D
↑
main / HEAD
変更が進む流れ
通常の開発では、変更は次の順番で進みます。
ファイルを編集
↓
Working Treeが変わる
↓
git add
↓
Indexに変更が登録される
↓
git commit
↓
Indexの内容から新しいコミットが作られる
↓
ブランチの先頭が新しいコミットへ進む
restore・reset・revertの違いは、この流れのどこを操作するかによって説明できます。
3. git restore:ファイルの状態を復元する
git restoreは、主にWorking TreeやIndexにあるファイルの状態を復元するコマンドです。
重要なのは、単に「前の状態へ戻る」と捉えるのではなく、別の場所にある内容を使って、対象を上書きすると理解することです。
編集した内容を破棄する
次の状態を考えます。
HEAD 変更前
Index 変更前
Working Tree 変更後
まだgit addしていない変更を破棄する場合は、次のコマンドを使用します。
git restore sample.txt
このコマンドでは、基本的にIndexに登録されている内容を使って、Working Treeのsample.txtを復元します。
実行前
HEAD 変更前
Index 変更前
Working Tree 変更後
git restore sample.txt
↓
実行後
HEAD 変更前
Index 変更前
Working Tree 変更前
HEADとIndexは変更されません。
Working TreeだけがIndexと同じ内容になり、Working Treeにしか存在しなかった変更は破棄されます。
Gitはcommitされた内容やIndexに登録された内容を管理していますが、Working Treeだけに存在する変更を破棄した場合、その変更をGitから必ず復元できるとは限りません。
そのため、すぐにgit restoreを実行するのではなく、まず破棄される差分を確認します。
git diff -- sample.txt
表示された差分を確認し、次の点を判断します。
- 必要な修正が含まれていないか
- 残しておきたいコードまで削除されないか
- 本当に破棄して問題のない変更か
差分を確認し、破棄しても問題ないと判断できてから、次のコマンドを実行します。
git restore sample.txt
特に、1つのファイルに必要な変更と不要な変更が混在している場合は注意が必要です。
ファイル全体をrestoreすると、不要な変更だけでなく、残したかった変更まで破棄される可能性があります。
git addを取り消す
次に、ファイルを変更し、git addした後の状態を考えます。
HEAD 変更前
Index 変更後
Working Tree 変更後
変更を次のcommit対象から外したい場合は、次を実行します。
git restore --staged sample.txt
この場合、通常はHEADの内容を使ってIndexを復元します。
実行前
HEAD 変更前
Index 変更後
Working Tree 変更後
git restore --staged sample.txt
↓
実行後
HEAD 変更前
Index 変更前
Working Tree 変更後
Working Treeの変更内容は残っています。
つまり、git restore --stagedはファイルの編集内容を削除するコマンドではありません。
変更内容を残したまま、その変更を次のcommit候補から外すコマンドです。
restoreで覚えるべきこと
通常のgit restoreと、--stagedを付けた場合では、変更する対象が異なります。
git restore sample.txt
Index ─────────→ Working Tree
git restore --staged sample.txt
HEAD ─────────→ Index
この関係を理解すると、2つのコマンドを別々に丸暗記する必要がなくなります。
-
git restore <file>はWorking Treeを対象にする -
git restore --staged <file>はIndexを対象にする
ただし、どちらも対象の内容を別の状態で上書きする操作です。
実行する前に、何を復元元にして、Working TreeとIndexのどちらを変更するのかを確認する必要があります。
4. git reset:ブランチの位置と状態を過去のコミットへ合わせる
git resetは、git restoreよりも操作範囲が広いコマンドです。
commitを指定して実行するresetでは、現在のブランチが指す位置を移動します。
そのうえで、指定したモードによってIndexやWorking Treeも変更します。
次の履歴を考えます。
A --- B --- C
↑
main / HEAD
コミットCを取り消し、mainブランチの先頭をBへ戻す場合、次のように指定します。
git reset HEAD~1
HEAD~1は、現在のコミットから1つ前のコミットを表しています。
ただし、コミットCの変更内容をどこまで残すかは、resetのモードによって異なります。
以降の例では、コミットCを作成した後に、新しい編集やgit addをしていない状態を前提とします。
--soft
git reset --soft HEAD~1
--softでは、ブランチが指すコミットだけを移動します。
IndexとWorking Treeは変更しません。
ブランチ Bへ移動
Index Cの変更を残す
Working Tree Cの変更を残す
コミットCはブランチの履歴から外れますが、その変更内容はステージされた状態で残ります。
そのため、次のような場面で使用できます。
- コミットメッセージを修正してcommitし直したい
- 直前のcommitに含める内容を少し調整したい
- commitだけを取り消し、すぐに再commitしたい
--mixed
git reset --mixed HEAD~1
--mixedはデフォルトのモードなので、オプションを省略することもできます。
git reset HEAD~1
--mixedでは、ブランチが指す位置を移動し、Indexも移動先のコミットに合わせます。
Working Treeの内容は残ります。
ブランチ Bへ移動
Index Bの状態へ合わせる
Working Tree Cの変更を残す
コミットとgit addが取り消され、変更内容だけがWorking Treeに残ります。
そのため、次のような場面で使用できます。
- commitに含めるファイルを選び直したい
- 変更内容を見直してから、もう一度
git addしたい - commitとステージングを取り消し、編集内容だけ残したい
--hard
git reset --hard HEAD~1
--hardでは、ブランチ・Index・Working Treeのすべてを移動先のコミットに合わせます。
ブランチ Bへ移動
Index Bの状態へ合わせる
Working Tree Bの状態へ合わせる
コミットだけでなく、IndexとWorking Treeにある追跡対象ファイルの変更も破棄されます。
3つのモードを比較すると、次のようになります。
| モード | ブランチ | Index | Working Tree |
|---|---|---|---|
--soft |
移動する | 変更しない | 変更しない |
--mixed |
移動する | 移動先に合わせる | 変更しない |
--hard |
移動する | 移動先に合わせる | 移動先に合わせる |
操作範囲が徐々に広がると考えると分かりやすくなります。
--soft
ブランチだけ
--mixed
ブランチ + Index
--hard
ブランチ + Index + Working Tree
git reset --hardを実行する前に確認する
git reset --hardは、Working TreeとIndexを指定したコミットの状態に合わせるため、未commitの変更を失う可能性があります。
そのため、すぐに実行するのではなく、少なくとも次のようなコマンドを使って現在の状態を確認してからのほうが安全です。
git status
git diff
git diff --staged
git log --oneline
それぞれ、確認する目的が異なります。
git status
git status
変更中のファイル、ステージ済みのファイル、未追跡のファイルなど、リポジトリ全体の状態を確認します。
まずはここで、現在どのような変更が存在しているのかを把握します。
git diff
git diff
Working Treeにあり、まだIndexへ登録されていない変更を確認します。
reset --hardによって失いたくない修正が含まれていないかを確認します。
git diff --staged
git diff --staged
Indexへ登録されている変更を確認します。
次のcommitに含める予定だった変更の中に、残しておきたいものがないかを確認します。
git log --oneline
git log --oneline
現在のコミット履歴と、reset後の移動先を確認します。
HEAD~1が本当に戻りたいコミットなのか、別のコミットを誤って指定していないかを確認します。
これらの結果を確認したうえで、次の点を判断します。
- Working Treeに残したい変更がないか
- Indexに残したい変更がないか
- reset先として指定するコミットが正しいか
- 対象のコミットが他のメンバーと共有されていないか
- 変更を破棄しても問題ないか
すべて確認し、変更を破棄しても問題ないと判断できた場合に実行します。
git reset --hard HEAD~1
--hardは、単に「状態をきれいにするためのコマンド」ではありません。
どの変更が消えるのかを確認し、その変更を失っても問題ないと判断したうえで使用するコマンドとして扱う必要があります。
resetはなぜ共有後に注意が必要なのか
resetでブランチの先頭を過去へ移動すると、そのブランチから見える履歴が変わります。
例えば、次の状態からBへresetしたとします。
reset前
A --- B --- C
↑
main / HEAD
reset後
A --- B
↑
main / HEAD
C
コミットCのオブジェクトがその瞬間に完全削除されるわけではありませんが、mainブランチの履歴からは外れます。
自分だけが使用しているローカルブランチであれば、commitをやり直す方法として利用できます。
しかし、すでにpushされ、他のメンバーがコミットCを含む履歴を参照している場合は注意が必要です。
ローカルでresetすると、ローカルブランチとリモートブランチの履歴が一致しなくなります。
その状態をforce pushなどでリモートへ反映すると、他のメンバーが参照している履歴を書き換えることになります。
そのため、他のメンバーと共有済みのコミットを取り消す場合は、次に説明するgit revertが選択肢になります。
5. git revert:履歴を残して変更を打ち消す
git revertは、指定したコミットを履歴から削除するコマンドではありません。
対象のコミットが加えた変更と反対の変更を作り、それを新しいコミットとして記録します。
次の履歴を考えます。
A --- B --- C
↑
main / HEAD
コミットCの変更を打ち消す場合は、次を実行します。
git revert <CのコミットID>
すると、コミットCを打ち消す新しいコミットDが作成されます。
A --- B --- C --- D
↑
main / HEAD
Dには、Cが加えた変更を打ち消す内容が記録されています。
例えば、コミットCで次の行を追加したとします。
Git
Cをrevertすると、新しいコミットDには、その行を削除する変更が記録されます。
重要なのは、コミットCが消えていないことです。
履歴には次の両方が残ります。
-
Cで変更を追加した -
DでCの変更を打ち消した
このため、他のメンバーと共有している履歴でも、何が起きたのかを後から追跡できます。
resetとの違い
resetとrevertの大きな違いは、履歴の扱い方です。
resetでは、ブランチが指す位置を過去へ移動します。
reset前
A --- B --- C
↑
main
reset後
A --- B
↑
main
C
一方、revertでは過去のコミットを残したまま、新しいコミットを追加します。
revert前
A --- B --- C
revert後
A --- B --- C --- D
↑
Cを打ち消すコミット
そのため、基本的には次のように使い分けられます。
- 自分だけが使用しているローカルコミットをやり直す
→resetを検討する - 他のメンバーと共有済みのコミットを取り消す
→revertを検討する
ただし、「push済みなら必ずrevert」「push前なら必ずreset」という意味ではありません。
判断の中心は、対象の履歴を他のメンバーがすでに参照しているかどうかです。
また、revert対象のコミット以降に関連する変更が加えられている場合は、単純に元の状態へ戻らなかったり、コンフリクトが発生したりすることがあります。
revertも、実行前に対象コミットの内容と、その後の変更を確認してから実行する必要があります。
6. 3つのコマンドをどう選ぶか
ここまでの内容を整理します。
| コマンド | 主な対象 | 変更内容の扱い | コミット履歴の扱い |
|---|---|---|---|
restore |
Working Tree・Index | 残す場合と破棄する場合がある | ブランチの位置は移動しない |
reset |
ブランチ・Index・Working Tree | モードによって異なる | ブランチの位置を移動する |
revert |
過去のコミットが加えた変更 | 反対の変更を作る | 新しいコミットを追加する |
コマンドを選ぶ前に、次の順番で考えます。
1. 変更はcommitされているか
まだcommitされていない場合は、Working TreeかIndexを操作します。
まだcommitしていない
├─ Working Treeの変更を破棄する
│ → git restore
│
└─ Indexから変更を外す
→ git restore --staged
2. 変更内容を残したいか
commitを取り消す場合でも、変更内容をどこに残すかによってresetのモードが変わります。
変更をステージしたまま残す
→ git reset --soft
変更をWorking Treeに残す
→ git reset --mixed
変更も含めて破棄する
→ git reset --hard
3. 履歴は他のメンバーと共有されているか
他のメンバーが参照している履歴であれば、履歴を書き換える操作は慎重に判断します。
自分だけが使用しているローカル履歴
→ resetを検討する
他のメンバーと共有済みの履歴
→ revertを検討する
実行前に確認する
変更を戻すコマンドを実行する前に、現在の状態を確認します。
git status
Working Treeにある未ステージの変更を確認します。
git diff
Indexに登録されている変更を確認します。
git diff --staged
コミット履歴と、現在のブランチの位置を確認します。
git log --oneline
これらのコマンドは、ただ実行すればよいわけではありません。
表示された内容を確認し、次のことを判断するために使用します。
- 現在の変更がWorking Tree・Index・コミット履歴のどこにあるか
- 残しておきたい変更が含まれていないか
- どの状態まで戻そうとしているのか
- 対象の履歴が他のメンバーと共有されていないか
- 実行後に失われる変更を理解しているか
gitで変更を戻すときに危険なのは、コマンドを知らないことだけではありません。
現在の変更がどこにあり、実行後に何が残るのか分からないまま、戻すコマンドを実行することです。
7. まとめ
restore・reset・revertは、すべて変更を戻す場面で使われますが、操作している対象と履歴の扱い方は異なります。
-
restore
Working TreeやIndexにあるファイルの状態を復元する -
reset
ブランチの位置を移動し、モードに応じてIndexやWorking Treeも変更する -
revert
過去の変更を打ち消す新しいコミットを作る
最初は、次の流れで考えると整理しやすくなります。
まだcommitしていない
↓
restoreを検討する
commitしたが、他のメンバーと共有していない
↓
resetを検討する
他のメンバーと共有している
↓
revertを検討する
ただし、コマンド名やタイミングだけで判断するのではなく、実行前に次の点を確認することが重要です。
- 変更はWorking Tree・Index・コミット履歴のどこにあるか
- 変更内容を残したいか、破棄したいか
- 対象の履歴は他のメンバーと共有されているか
- 実行すると、どの状態が変更されるのか
- 失われる可能性のある変更を確認したか
この5点を確認できれば、restore・reset・revertを何となく使うのではなく、実行結果を予測しながら選べるようになります。
ローカルであったり、確認をきちんとおこなえばresetも怖いものではありませんね。
せっかく学んだので使える時にgit restoreをどんどん使っていこうかと思います。