git stash save で一時退避した変更を、誤って git stash clear で消してしまったときの回復法

一年くらい前から git を使い始め、ここ半年くらいは毎日の開発に git を使っています。昨日 git stash という機能を使っている時に失敗してしまい、何人かの方にアドバイスいただくことによって無事回復することが出来たので、感謝の印として、そして運悪く同じ問題に遭遇してしまった人たち(私もまたやるかも)へのメモとして記しておきます。

御託はいいから、早く回復法を知りたい人のためのまとめ

$ git fsck | awk '/dangling commit/ {print $3}'
候補の sha1 がいくつか出てくる(長く開発していると、結構多く候補が出てきます)
$ git show --summary 候補のsha1
一つ一つの sha1 の内容を確認
$ git cherry-pick -n -m1 見つけたsha1

いきさつ

私の作業のやりかたでは、 タスク毎にブランチを切って、タスクが終了したら master (svn の trunk のような立ち位置のブランチ) 、もしくはその時の開発の mainline (開発の中心となっているブランチ) にマージします。つまり、git 上で開発している時間の 95% くらいは、ブランチ上で過ごすことになります。多い日には一日に 4 本くらいのブランチを作成してはマージしています。master 上で開発は行わず、 master からは主に完成した機能のリリースのみを行います。


git stash という機能は、まだコミットするまでには至らない変更が手元にあるものの、別ブランチで緊急作業(例えば再リリース)が必要になったときなどに、手元の変更を一時退避しておき (git stash save) 、別の仕事が終わったときに退避した作業状態を復元できる (git stash pop) というかなり便利な機能です。


詳しくは、これらのリンクをどうぞ。


で、昨日も git stash を使いました。git stash を使うときは、たいてい急いでいるときです。昨日は master からのリリースに軽微な間違いがあり、すぐ修正しつつリリースしてからまた作業に戻ってきたいという局面でした。git stash save して手元の作業を退避してから master に戻ってリリースを行い、タスクブランチに戻ってきたときに git stash pop するつもりが (ヒストリから保管したからかもしれませんが) git stash clear してしまいました。


思わず Twitter にポストしたら、直後に @walf443 さんから救いのポストが。

git gcとかしてなければ、git fsckで頑張れば発掘できたような気もする

http://twitter.com/walf443/status/1461741919


おおっと思い、git fsck stash でググってみたら、私と同じうっかりものが何人かいました。


上記リンクや man git-fsck などを見ていくつかの方法を検討しました。途中で @n_iwamatsu さん、@a_matsuda さんからもアドバイスをもらいました。ありがとうございます。


結果として、私は以下の方法で復旧を行うことが出来ました。(なお、この復旧方法は「まだ git gc していなければ」という条件つきのようです)

$ git fsck | awk '/dangling commit/ {print $3}'
候補の sha1 がいくつか出てくる(長く開発していると、結構多く候補が出てきます)
$ git show --summary 候補のsha1
一つ一つの sha1 の内容を確認
$ git cherry-pick -n -m1 見つけたsha1


git fsck で宙ぶらりんになっている変更の sha1 をリストアップし、 git show でその変更の中身を確認し、目的の sha1 を見つけたら git cherry-pick で適用する、という流れです。


手元で実際に再現してみました

小さなプロジェクトを作って、今回の状況を再現してみました。

$ mkdir hatena
$ cd hatena
$ echo 'one' >> all.txt
$ echo 'hello one' >> one.txt
$ git init
Initialized empty Git repository in /home/takuto/work/git-sandbox/hatena/.git/
$ git add .
$ git commit -m 'initial'
Created initial commit db17996: initial
 2 files changed, 2 insertions(+), 0 deletions(-)
 create mode 100644 all.txt
 create mode 100644 one.txt
$ git status
# On branch master
nothing to commit (working directory clean)
$ git checkout -b hoge_task
Switched to a new branch "hoge_task"
$ echo 'two' >> all.txt
$ echo 'hello two' >> two.txt
$ git status
# On branch hoge_task
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#
#       modified:   all.txt
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#       two.txt
no changes added to commit (use "git add" and/or "git commit -a")
$ git add two.txt
$ git status
# On branch hoge_task
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       new file:   two.txt
#
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#
#       modified:   all.txt
#
$ git add all.txt
$ git status
# On branch hoge_task
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       modified:   all.txt
#       new file:   two.txt
#
$ git fsck
$ git stash list
$ git stash save
Saved working directory and index state "WIP on hoge_task: db17996... initial"
HEAD is now at db17996 initial
$ git stash list
stash@{0}: WIP on hoge_task: db17996... initial
$ git status
# On branch hoge_task
nothing to commit (working directory clean)
$ git fsck
$ git checkout master
Switched to branch "master"
$ echo '# DO SOME WORK on master'
# DO SOME WORK on master
$ git checkout hoge_task
Switched to branch "hoge_task"
$ git stash list
stash@{0}: WIP on hoge_task: db17996... initial
$ git fsck
$ echo '# clear stash accidentally'
# clear stash accidentally
$ git stash clear
$ git stash list
$ git status
# On branch hoge_task
nothing to commit (working directory clean)
$ echo '# owata \(^o^)/'
# owata \(^o^)/
$ git fsck
dangling commit 8d657f4ff71f216db0a2eaeaa9dc79bdab132bfd
$ git fsck | awk '/dangling commit/ {print $3}'
8d657f4ff71f216db0a2eaeaa9dc79bdab132bfd
$ for ref in `git fsck | awk '/dangling commit/ {print $3}'`
> do
> git show --summary $ref
> done
commit 8d657f4ff71f216db0a2eaeaa9dc79bdab132bfd
Merge: db17996... e441314...
Author: Takuto Wada <takuto@....>
Date:   Tue Apr 7 13:01:28 2009 +0900

    WIP on hoge_task: db17996... initial

$ echo '# FOUND the lost sha1'
# FOUND the lost sha1
$ git show 8d657f4ff71f216db0a2eaeaa9dc79bdab132bfd
commit 8d657f4ff71f216db0a2eaeaa9dc79bdab132bfd
Merge: db17996... e441314...
Author: Takuto Wada <takuto@....>
Date:   Tue Apr 7 13:01:28 2009 +0900

    WIP on hoge_task: db17996... initial

$ git status
# On branch hoge_task
nothing to commit (working directory clean)
$ git stash list
$ git cherry-pick -n -m1 8d657f4ff71f216db0a2eaeaa9dc79bdab132bfd
Finished one cherry-pick.
$ git status
# On branch hoge_task
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       modified:   all.txt
#       new file:   two.txt
#
$ git fsck
dangling commit 8d657f4ff71f216db0a2eaeaa9dc79bdab132bfd
$ git stash list
$ 

感じたこと、教訓など

  • 作業は落ち着いて
  • 大抵の場合、同じ問題に先にぶつかった人がどこかにいる。Google に入れる検索語にたどり着けばなんとかなる
  • Twitter すばらしい
  • 検索語のヒントを教えてもらうだけでも非常に助かる!
  • Git は単なる差分管理システムではなく、まさにオブジェクトデータベースなのだと感じた。この感覚は昨年 git 勉強会に参加してメンテナの hamano さんのお話を伺ったときに感じたことそのものでもありました。(git 勉強会の動画は id:takagimasahiro さんの日記が詳しいです)