あの日、決済機能を修正していたところで、約8ファイルが中途半端な変更状態だった。上司から「ログインバグを急いで直して、ユーザーがログインできない」とメッセージが来た。git stashして、mainブランチに切り替えて、修正して、戻ってきた——すると、そのstashにはローカルテスト用のconfigファイルや、決済とは全く関係ないデバッグ用のconsole.logも含まれていた。適用したら、不必要な場所でconflictが発生した。
基本的なgit stashの使い方は誰でも知っている。しかし、チームで6ヶ月実際に使ってみて、知らないと問題に直面しやすい3つのテクニックを発見した:-pで部分的なstash、混乱しないようにstashに名前を付ける方法、そして誤ってdropしたstashの復元方法。
Gitはstashをどのように保存するか——どこかに「隠す」わけではない
このメカニズムを理解してこそ、dropした後でもstashを復元できる理由、そしてなぜ素早く行動しなければならないかが説明できる。Gitは変更をどこかに「隠す」わけではない——git stashするたびに、Gitはrefs/stashに2〜3の隠しcommitを作成する:
- staged changesのためのcommit
- unstaged changesのためのcommit
- untracked filesのためのcommit(
-uを使用した場合)
# stashリストを表示
git stash list
# stash@{0}: WIP on feature/payment: a3f2b1c Add payment form
# stash@{1}: WIP on main: 9d8c4e5 Fix login bug
# applyする前にstashの変更内容を表示
git stash show -p stash@{0}
このdangling commitは、garbage collectionが実行されるまでGitのobject databaseに残り続ける。だから復元できる——そしてだから素早く行動しなければならない。
git stash -p——変更を部分的にstash
まさに先ほどの不当なconflictがきっかけで-pを探すことになった。このフラグ(--patchの略)は、stashするかworking treeに残すかを、各”hunk”——小さな変更の塊——ごとに選択できる。
git stash -p
Gitは各hunkを表示して確認を求める:
diff --git a/src/payment.js b/src/payment.js
@@ -45,7 +45,12 @@ function validateCard(card) {
+ if (!card.cvv) {
+ throw new Error('CVV required');
+ }
Stash this hunk [y,n,q,a,d,s,?]?
よく使う選択肢:
y——このhunkをstashn——スキップ、working treeに残すs——hunkをさらに分割する(大きなhunkに非常に便利)q——終了、選択済みの内容を適用?——フルヘルプを表示
実際の例:決済機能のコードを書きながら、デバッグ用のconsole.logも数行追加していた。git stash -pを使えば、デバッグ部分だけstashして、機能部分を作業継続のために残しておける——全ファイルをstashする必要はない。
stashへの名前付けと特定ファイルのstash
混乱しないように名前を付ける
名前なしのstashを数週間使った後、自分のリストはこんな状態になっていた:
stash@{0}: WIP on feature/payment: a3f2b1c ...
stash@{1}: WIP on feature/payment: a3f2b1c ...
stash@{2}: WIP on feature/payment: a3f2b1c ...
どれがどれかわからない。-mを使ってからは:
git stash push -m "payment: validate CVV logic"
git stash push -m "debug: temp console.log for card parser"
ずっとわかりやすくなった:
stash@{0}: On feature/payment: debug: temp console.log for card parser
stash@{1}: On feature/payment: payment: validate CVV logic
特定ファイルのstash
-pで各hunkを確認するのではなく、1〜2ファイルだけstashしたい場合:
git stash push -m "only payment validator" -- src/payment.js src/validators/card.js
untracked filesを含める
デフォルトではgit stashは新しく作成したファイル(untracked)を無視する。-uを追加して一緒にstashするには:
git stash push -u -m "include new files too"
git fsckでdropしたstashを復元する
あの時、誤ってgit stash dropしてしまい、stashには約2時間分のコードが入っていた——完全に消えたと思った。実はそうではなかった。それがこれを学んだきっかけだ。
stashは隠しcommitだ。Gitがまだgarbage collectionを実行していないため、dangling commitはまだobject databaseに残っている:
# 参照を持たないすべての「dangling」commitを検索
git fsck --lost-found | grep commit
出力:
dangling commit a1b2c3d4e5f67890...
dangling commit f9e8d7c6b5a43210...
目的のcommitを見つけるために各commitを確認:
git show a1b2c3d4e5f67890
見つかったら、適用する:
git stash apply a1b2c3d4e5f67890
重要な注意点:復元できる時間は無制限ではない。Gitは定期的にgarbage collectionを実行する——デフォルトでは2週間後またはloose objectsの数が約6,700を超えたとき——その後dangling commitは完全に消える。数時間以内に気づいたから復元できた。
予防のコツ:dropする前にhashを保存しておく:
# dropする前にstashのhashを取得
git rev-parse stash@{0}
# a1b2c3d4e5f67890... ← 保存しておく
git stash drop stash@{0}
# 数時間以内に後悔した場合:
git stash apply a1b2c3d4e5f67890
チームでの実際のワークフロー
8人のチームで、このルールを適用する前は週に3〜5回の「不合理な」conflict——誤ったstashや間違った場所へのapplyから生じるconflict——を数えていた。全員で合意してから:常にstashに名前を付け、混在した変更には-pを使い、dropする前にhashをコピーする——その数は0〜1に減った。
最もよく使う2つのパターンがある:
Pattern 1:stash + 素早くbranchを作成して実験する
# featureの途中で、アイデアを素早くテストしたい
git stash push -m "feature/payment in progress"
git checkout -b experiment/try-new-approach
# うまくいかなかったので、戻る
git checkout feature/payment
git stash pop
Pattern 2:変更を正しいbranchに移動する
# 専用branchではなくmainでコードしてしまった
git stash push -m "oops、feature branchでやるべきだった"
git checkout -b feature/new-feature
git stash pop
これは少なくとも週1回は使う。
その他の参考コマンド
# stashをapplyするがリストに残す(popのように削除しない)
git stash apply stash@{1}
# stashから直接新しいbranchを作成
git stash branch new-branch-name stash@{0}
# stashリスト全体を削除
git stash clear
まとめ
-pの存在を知る前に1年以上Gitを使っていた。ドキュメントには書いてあるが、基本的なチュートリアルでは触れていない——だから多くのユーザーが見過ごしてしまう。git fsckは、誤ってdropしたstashを必死に探していたとき、Stack Overflowのスレッドで偶然読んだ。
git stash -pを試してみて——慣れると、commitしたくないデバッグコードが数行あるだけで全ファイルをstashしなければならない状況が不満に感じるようになる。
