git stashを使いこなす:-pで部分的なstashと誤ってdropしたstashの復元

Git tutorial - IT technology blog
Git tutorial - IT technology blog

あの日、決済機能を修正していたところで、約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をstash
  • n——スキップ、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しなければならない状況が不満に感じるようになる。

Share: