Gitで間違った実行してしまったコミットや、現在編集中のファイルの内容を削除して、最新のコミットの状態に戻りたいときがあります。
そんな時に非常に便利なコマンドにgit resetがあります。
git resetはとても便利ですが、あまりに強力で注意しなければいけないこともあります。
ここでは、git resetの使い方や注意点、各オプション–soft, –hard, –mixed、やオプションなしで実行した場合の違いなどについて実例でまとめています。
git resetとは何か?使い方
git resetは現在のブランチの最新のコミットから、指定したコミットの上までをごっそり削除する超強力なコマンドです。
すなわち、指定したコミットまでコミット履歴を遡るということです。
主な使い方は3つあります。
指定したコミットまで戻る
指定したコミットまで戻る場合は引数でコミット番号のみを指定します。
git reset <コミット>
コミットの指定方法には、ハッシュ値のコミット番号やHEAD, @, タグ、HEAD@{n}や@@{n}が使えます。
HEADの省略形である、@ を使うことが一般的です。
また、変更したファイルを残すか、全て削除するかをオプションで指定する必要があります(後述)。
指定したファイルのみ指定したコミットの状態に戻すコマンド
指定したファイルのみ指定したコミットの状態に戻す場合は、引数でコミット番号とファイルパスの相対パスを指定します。
git reset <コミット> <ファイルパス>
実例
例えば、「app/controllers/api/pj1/clients_controller.rb」というファイルを現時点より3つ前のコミットの状態に戻したい場合は以下のようにします。
$ git reset @^^^ app/controllers/api/pj1/clients_controller.rb
Unstaged changes after reset:
M app/controllers/api/pj1/clients_controller.rb
git resetの注意点
git resetの注意点は大きく3つあります。
オプションにより変更中のファイルを残すかどうかが変わる
git resetには「–soft」「–mixed」「–hard」の3つのオプションがあり、どれを選択したかで、コミットを遡ったときに、現在のブランチの作業内容を残すかどうかが変わります。(後述)
コミットログから消えてなくなる
1つ目は、指定したコミットまで戻るため、それより新しいコミット履歴がごっそりなくなります。git logで参照しても表示されなくなります。
ただしGitから完全に消えるわけではなく、Gitコマンドの操作履歴にはどんな処理をしたかの記録が残っているので、復活させることはできます。(復活方法については後述しています)
複数人の共同開発レポジトリでは使わない
2つ目はGithubのリモートレポジトリなどで複数人による開発をしている場合には基本的に使ってはいけないということです。
コミット履歴は過去のコミットに全て紐づいています。このため、過去のコミット履歴が変わると、その後のコミットのコミット番号も変わります。
git resetでコミットを戻してリモートレポジトリにプッシュしてしまうと、他の人たちのコミット履歴とズレてエラーが発生します。
もちろんgit resetを使ってはいけないというわけではありません。git resetは非常に便利なコマンドなので、自分のローカルのみの作業やリモートレポジトリより先に進んでいるコミットで使用する分には問題ありません。
git resetの重要なオプション
git resetには3つの重要なオプションがあります。「–soft」「–mixed」「–hard」です。
それぞれで何をどこまで消すのかが変わってきます。まとめると以下のようになっています。
オプション | ステージ前ファイル (git add前) | インデックスされたファイル (git add後、commit前) | 未追跡 (untracked) |
---|---|---|---|
–soft | 残る | 残る | 残る |
–mixed(デフォルト) | 残る | 削除 | 残る |
–hard | 削除 | 削除 | 残る |
インデックスファイルやワークツリーなどすべてを消去し、真っさらな過去に戻るりたい場合は「–hard」を使用します。
現在の作業中のファイルを修正したい場合は残したいファイルに応じて「–mixed」か「–soft」を使います。
オプション無しでgit resetを実行した場合は「–mixed」オプションを付けた処理と同じになります。
なお、いずれのオプションにおいても未追跡(untracked)のファイルは残ります。
–softオプション
–softオプションとは何か?使い方
–softオプションはgit resetを実行する時点でのはファイルの変更内容を全て残すコマンドです。
現在作業中の内容が残り、git resetしたことで生じた差分も残ります。
git reset --soft <コミット>
git reset --soft <コミット> <ファイルパス>
git reset –softの実例
例えば、git statusが以下のように、git addしたファイル「docker-compose.yml」とgit add前のファイル「clients_controller.rb」、未追跡のファイル「test.html.erb」の3つがあるとします。
$ git status
On branch test
Your branch is ahead of 'origin/aa' by 1 commit.
(use "git push" to publish your local commits)
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: docker-compose.yml
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: app/controllers/api/pj1/clients_controller.rb
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/views/layouts/test.html.erb
コミットの履歴は以下のようになっています。
$ git log --oneline
c9c53c7 (HEAD -> test) comment out
0701d9d (second/aa, origin/aa, aa) [U]docker-compose add webpack port 3035
a90d4ef [U]content-security-policy(CSP) enable webpack-dev-server
6adca49 [A]destroyメソッド & Modal追加
61d3b4b [A]ClientEdit.vue
00093d1 [F]ClientNewからClientForm.vueを切り出し
この状態で、3つ前のコミット「6adca49 [A]destroyメソッド & Modal追加」までgit reset –softを使って戻ります。
3つ前のコミットを指定するには「@^^^」を使います。(もしくは、@~~~ やHEAD^^^など)
$ git reset --soft @^^^
実行しても何も表示されません。git logで履歴を確認します。
$ git log --oneline
6adca49 (HEAD -> test) [A]destroyメソッド & Modal追加
61d3b4b [A]ClientEdit.vue
00093d1 [F]ClientNewからClientForm.vueを切り出し
すると、「c9c53c7」「0701d9d」「a90d4ef」の3つのコミットが削除され、現在のブランチの最新のコミットを指すHEAD->が、指定したコミット「6adca49」になっているのがわかります。
この状態でgit statusを確認すると以下のようになります。
$ git st
On branch test
Your branch is behind 'origin/aa' by 2 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: app/controllers/api/pj1/clients_controller.rb
modified: config/initializers/content_security_policy.rb
modified: docker-compose.yml
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: app/controllers/api/pj1/clients_controller.rb
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/views/layouts/test.html.erb
もともとあった、git addしたファイル「docker-compose.yml」とgit add前のファイル「clients_controller.rb」、未追跡のファイル「test.html.erb」はそのままです。
新たに、git add後のファイルに「content_security_policy.rb」と「clients_controller.rb」が追加されていることがわかります。
つまり、コミットを戻しはしたけど、ファイルの変更内容はgit reset –softを実行した当時の状態のままで、かつ、コミットを戻ったことで生まれたファイルの変更をステージのファイルとして保持しています。
–hardオプション
–hardオプションとは何か?使い方
–hardオプションはgit resetを実行する時点でのはファイルの変更内容を全て削除して、完全に指定したコミットの状態に戻るコマンドです。(未追跡ファイルは残ります)
git reset --hard <コミット>
git reset --hard <コミット> <ファイルパス>
git reset –hardの実例
例えば、git statusが以下のように、git addしたファイル「docker-compose.yml」とgit add前のファイル「clients_controller.rb」、未追跡のファイル「test.html.erb」の3つがあるとします。
$ git status
On branch test
Your branch is ahead of 'origin/aa' by 1 commit.
(use "git push" to publish your local commits)
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: docker-compose.yml
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: app/controllers/api/pj1/clients_controller.rb
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/views/layouts/test.html.erb
コミットの履歴は以下のようになっています。
$ git log --oneline
c9c53c7 (HEAD -> test) comment out
0701d9d (second/aa, origin/aa, aa) [U]docker-compose add webpack port 3035
a90d4ef [U]content-security-policy(CSP) enable webpack-dev-server
6adca49 [A]destroyメソッド & Modal追加
61d3b4b [A]ClientEdit.vue
00093d1 [F]ClientNewからClientForm.vueを切り出し
この状態で、3つ前のコミット「6adca49 [A]destroyメソッド & Modal追加」までgit reset –hardを使って戻ります。
3つ前のコミットを指定するには「@^^^」を使います。(もしくは、@~~~ やHEAD^^^など)
$ git reset --hard @^^^
HEAD is now at 6adca49 [A]destroyメソッド & Modal追加
git resetが実行され、最新のコミットを指すHEADが移動しました。git logで履歴を確認します。
$ git log --oneline
6adca49 (HEAD -> test) [A]destroyメソッド & Modal追加
61d3b4b [A]ClientEdit.vue
00093d1 [F]ClientNewからClientForm.vueを切り出し
すると、「c9c53c7」「0701d9d」「a90d4ef」の3つのコミットが削除され、現在のブランチの最新のコミットを指すHEAD->が、指定したコミット「6adca49」になっているのがわかります。
この状態でgit statusを確認すると以下のようになります。
$ git status
On branch test
Your branch is behind 'origin/aa' by 2 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/views/layouts/test.html.erb
nothing added to commit but untracked files present (use "git add" to track)
もともとあったgit addしたファイル「docker-compose.yml」とgit add前のファイル「clients_controller.rb」がなくなっていることがわかります。
なおGit未追跡のファイル「test.html.erb」は–hardをしてもそのまま残ります。
オプション無し(–mixedオプション)
–mixedオプション(オプション無し)とは?使い方
git resetのオプションが無い場合は–mixedオプションと同じ処理になります。
git resetを実行する時点での最新のコミットとリセット後のファイルを比較して差分が生じたファイルをgit add前(ステージ前)として残します。(未追跡ファイルは残ります)
git reset <コミット>
git reset <コミット> <ファイルパス>
git reset (–mixed)の実例
例えば、git statusが以下のように、git addしたファイル「docker-compose.yml」とgit add前のファイル「clients_controller.rb」、未追跡のファイル「test.html.erb」の3つがあるとします。
$ git status
On branch test
Your branch is ahead of 'origin/aa' by 1 commit.
(use "git push" to publish your local commits)
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: docker-compose.yml
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: app/controllers/api/pj1/clients_controller.rb
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/views/layouts/test.html.erb
コミットの履歴は以下のようになっています。
$ git log --oneline
c9c53c7 (HEAD -> test) comment out
0701d9d (second/aa, origin/aa, aa) [U]docker-compose add webpack port 3035
a90d4ef [U]content-security-policy(CSP) enable webpack-dev-server
6adca49 [A]destroyメソッド & Modal追加
61d3b4b [A]ClientEdit.vue
00093d1 [F]ClientNewからClientForm.vueを切り出し
この状態で、3つ前のコミット「6adca49 [A]destroyメソッド & Modal追加」までオプション無しのgit resetを使って戻ります。
3つ前のコミットを指定するには「@^^^」を使います。(もしくは、@~~~ やHEAD^^^など)
$ git reset @^^^
Unstaged changes after reset:
M config/initializers/content_security_policy.rb
git resetが実行され、「Unstaged changes after reset」ステージ前のファイルが生成されたことがわかります。git logで履歴を確認します。
$ git log --oneline
6adca49 (HEAD -> test) [A]destroyメソッド & Modal追加
61d3b4b [A]ClientEdit.vue
00093d1 [F]ClientNewからClientForm.vueを切り出し
すると、「c9c53c7」「0701d9d」「a90d4ef」の3つのコミットが削除され、現在のブランチの最新のコミットを指すHEAD->が、指定したコミット「6adca49」になっているのがわかります。
この状態でgit statusを確認すると以下のようになります。
$ git status
On branch test
Your branch is behind 'origin/aa' by 2 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
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: config/initializers/content_security_policy.rb
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/views/layouts/test.html.erb
no changes added to commit (use "git add" and/or "git commit -a")
もともとあったgit addしたファイル「docker-compose.yml」とgit add前のファイル「clients_controller.rb」がなくなっていることがわかります。
代わりに、git add前のファイルに「content_security_policy.rb」が新たに追加されています。
なおGit未追跡のファイル「test.html.erb」は–hardをしてもそのまま残ります。
現在の変更内容を全て削除する方法
現在の変更内容を全て削除するコマンド
最新のコミット状態から、あれこれとコードをいじって、git addしたりしなかったり(コミット前)という状況が発生するのは普通の作業です。
このときに、「変更内容を一掃して最新のコミットに戻りたい」という状態になることがあります。
そのときに、git reset –hardを使って、最新のコミット状態に戻ることができます。
引数で最新のコミット「@」を指定するだけです。
git reset --hard @
実例
例えば以下のように、git addしたファイルや、git add前のファイルが複数あるとします。
$ git st
On branch test
Your branch is behind 'origin/aa' by 2 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: app/views/layouts/test.html.erb
modified: config/initializers/content_security_policy.rb
modified: docker-compose.yml
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: app/views/layouts/test.html.erb
この状態において、これらの変更をまとめて消したいときは、git reset --hard @
を実行します。
$ git reset --hard @
HEAD is now at 6adca49 [A]destroyメソッド & Modal追加
すると以下のように最新のコミットにHEADが移動します。
git statusで確認すると、編集中だったファイルがステージおよびステージ前も含めて一掃されていることがわかります。
$ git status
On branch test
Your branch is behind 'origin/aa' by 2 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working tree clean
削除したコミットを復活させる方法
削除したコミットを復活させるコマンド
削除したコミットを復活させるには、git reflogというコマンドと、git resetを合わせて使います。
git reflogとは全てのブランチのGitコマンドの履歴を表示するコマンドです。
各コマンドにはコマンド履歴がHEAD@{n}として割り振られています。
間違ったgit resetを実行したときのHEAD@{n}を指定して、git resetを実行すると、コミットがgit resetを実行する前の状態に戻ります。
#コマンド履歴の一覧を確認
$ git reflog
#コミットを戻す
$ git reset <コマンド履歴を指定>
なお、HEAD@{n}以外にも@@{n}で指定することができます。
注意点:コミット番号とHEAD@{n}は違う
コミット履歴の番号はHEAD@{n}とは異なるものです。nの数値が違っても、コミット番号が同じ場合もあります。
129ba79 (image_tag) HEAD@{4}: checkout: moving from fix to image_tag
129ba79 (image_tag) HEAD@{5}: commit: [A]home.index ホバーの画像切り替え
checkoutしてブランチを移動しただけなど、指しているコミット履歴の内容は同じなので、git resetするときに、git reflogで表示されるコミット番号でも指定できます。
実例
例えば以下のような状態のコミット履歴があるとします。
$ git log --oneline
c9c53c7 (HEAD -> test) comment out
0701d9d (second/aa, origin/aa, aa) [U]docker-compose add webpack port 3035
a90d4ef [U]content-security-policy(CSP) enable webpack-dev-server
6adca49 [A]destroyメソッド & Modal追加
61d3b4b [A]ClientEdit.vue
00093d1 [F]ClientNewからClientForm.vueを切り出し
git statusは以下のようになっています。
$ git status
On branch test
Your branch is ahead of 'origin/aa' 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: app/controllers/api/pj1/clients_controller.rb
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/views/layouts/test.html.erb
no changes added to commit (use "git add" and/or "git commit -a")
この時に、git reset --soft @^^^
を実行して、以下のようなログとgit statusの状態になったとします。
$ git log --oneline
6adca49 (HEAD -> test) [A]destroyメソッド & Modal追加
61d3b4b [A]ClientEdit.vue
00093d1 [F]ClientNewからClientForm.vueを切り出し
$ git status
On branch test
Your branch is behind 'origin/aa' by 2 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: app/controllers/api/pj1/clients_controller.rb
modified: config/initializers/content_security_policy.rb
modified: docker-compose.yml
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/views/layouts/test.html.erb
このgit resetを取り消して元の状態に戻るには、まずgit reflogでgit reset直前のコミット番号を探します。
$ git reflog
6adca49 (HEAD -> test) HEAD@{0}: reset: moving to @^^^
c9c53c7 HEAD@{1}: reset: moving to @^
9e8f510 HEAD@{2}: revert: Revert "[A]destroyメソッド & Modal追加"
c9c53c7 HEAD@{3}: reset: moving to @^
6b26abb HEAD@{4}: revert: Revert "[A]destroyメソッド & Modal追加"
c9c53c7 HEAD@{5}: commit: comment out
0701d9d (second/aa, origin/aa, aa) HEAD@{6}: reset: moving to @^
2d9d637 HEAD@{7}: revert: Revert "[A]destroyメソッド & Modal追加"
0701d9d (second/aa, origin/aa, aa) HEAD@{10}: rebase (finish): returning to refs/heads/test
すると一番上に「6adca49 (HEAD -> test) HEAD@{0}: reset: moving to @^^^」でreset: moving to @^^^
を実行したという記録があるのがわかります。
この直前に戻りたいのでその下のコミット「c9c53c7」もしくは「HEAD@{1}」を指定して、git resetを実行します。
そのコミット以降に編集したコミット前のファイルの変更内容も残したい場合は「–soft」オプションをつけます。
$ git reset HEAD@{1}
Unstaged changes after reset:
M app/controllers/api/pj1/clients_controller.rb
以上でgit resetの取り消しが完了です。
git logを確認すると消したはずのコミットが復活していることがわかります。
$ git log --oneline
c9c53c7 (HEAD -> test) comment out
0701d9d (second/aa, origin/aa, aa) [U]docker-compose add webpack port 3035
a90d4ef [U]content-security-policy(CSP) enable webpack-dev-server
6adca49 [A]destroyメソッド & Modal追加
61d3b4b [A]ClientEdit.vue
00093d1 [F]ClientNewからClientForm.vueを切り出し
git statusを確認すると以下、間違ったgit resetを実行する前の状態に戻っています。
$ git status
On branch test
Your branch is ahead of 'origin/aa' 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: app/controllers/api/pj1/clients_controller.rb
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/views/layouts/test.html.erb
no changes added to commit (use "git add" and/or "git commit -a")
以上で復活完了です。