
git stash の使い方 - 作業を一時退避する pop と apply の違い、消した stash の復旧まで
当サイトは Amazon.co.jp を宣伝しリンクすることで紹介料を得る手段を提供する、Amazonアソシエイト・プログラムの参加者です。価格・在庫はリンク先の最新情報をご確認ください。
作業の途中で「先に別のブランチを直してほしい」と頼まれたり、git pullしようとして手元の変更が邪魔だと言われたりしたときに使うのがgit stashです。コミットするほどまとまっていない変更を一時的に退避し、ワーキングツリーをきれいな状態に戻せます。便利な一方で、「未追跡のファイルが退避されていなかった」「popしたらコンフリクトした」「dropしてから必要だったと気づいた」といったつまずきも多いコマンドです。この記事では、stashが何を退避して何を残すのかを最初に整理し、基本のサブコマンド、オプションごとの退避範囲、コンフリクト時の対処、消してしまったstashの復旧、内部の仕組みまでを順に見ていきます。仕様の説明はGitの公式マニュアルとリリースノートを出典にし、出力例はgit version 2.52.0の一時リポジトリで実際に実行した結果です。
まず結論 - サブコマンドの早見表
| コマンド | 役割 |
|---|---|
git stash(git stash pushと同じ) | 未コミットの変更を退避し、ワーキングツリーとインデックスをHEADの状態に戻す |
git stash list | 退避した一覧を表示する |
git stash show | 退避した内容の差分を表示する(-pでパッチ形式) |
git stash pop | 退避した内容を戻し、成功したら一覧から削除する |
git stash apply | 退避した内容を戻す。一覧には残す |
git stash drop | 一覧から1件削除する |
git stash clear | 一覧をすべて削除する |
git stash branch | 退避した時点のコミットから新しいブランチを作り、そこへ戻す |
覚えておきたい点は次の3つです。
- 既定で退避されるのは追跡しているファイルの変更だけ。未追跡のファイルは
-uを付けないと退避されない popは「戻して削除」、applyは「戻すだけ」。popでもコンフリクトしたときは削除されないdropやclearで消したstashは、通常の手段では戻せない。ただし公式マニュアルにgit fsckを使った復旧の方法が載っている
git stashが退避するもの
公式マニュアルはgit stashを、「ワーキングディレクトリとインデックスの現在の状態を記録しつつ、きれいなワーキングディレクトリに戻りたいときに使う」コマンドだと説明しています。ローカルの変更を保存し、ワーキングディレクトリをHEADのコミットに一致する状態へ戻します。
ワーキングツリー・インデックス・HEADという3つの領域の関係は、git resetとrevertとrestoreの違いで整理しています。stashはこのうち、ワーキングツリーとインデックスの「まだコミットしていない差分」を別の場所に保存する操作です。
ファイルの状態ごとの扱いは、次のとおりです。
| ファイルの状態 | git stash | git stash -u | git stash -a |
|---|---|---|---|
| 追跡ファイルの変更(未add) | 退避する | 退避する | 退避する |
| addした変更(インデックス) | 退避する | 退避する | 退避する |
| 未追跡のファイル | 残る | 退避する | 退避する |
.gitignoreで無視しているファイル | 残る | 残る | 退避する |
実際に、未addの変更(app.txt)、add済みの変更(lib.txt)、未追跡のファイル(new.txt)がある状態で試します。
$ git status --short
M app.txt
M lib.txt
?? new.txt
$ git stash
Saved working directory and index state WIP on main: 3cfb4b0 initial commit
$ git status --short
?? new.txt追跡ファイルの変更は2つとも消え、未追跡のnew.txtはそのまま残りました。「stashしたのにブランチを切り替えたらファイルが付いてきた」という場合は、そのファイルが未追跡だったということです。
変更が何もないときは、何も作られません。未追跡のファイルしかない場合も、オプションなしでは同じ結果になります。
$ git stash
No local changes to save基本の流れ - 退避して、確認して、戻す
退避する(push)
git stashはgit stash pushの省略形です。メッセージを付けない場合、一覧には「WIP on ブランチ名: コミット」という形で記録されます。
$ git stash
Saved working directory and index state WIP on main: 3cfb4b0 initial commit一覧を見る(list)
$ git stash list
stash@{0}: WIP on main: 3cfb4b0 initial commitstash@{0}が最新で、古いものほど番号が大きくなります。新しく退避するたびに、既存のエントリの番号は1つずつ後ろへずれます。git statusに--show-stashを付けると、件数だけを確認できます。
$ git status --show-stash
On branch main
nothing to commit, working tree clean
Your stash currently has 2 entries中身を見る(show)
git stash showは、退避した内容と、退避した時点のコミットとの差分を表示します。既定では変更量の要約(diffstat)で、-pを付けるとパッチ形式になります。
$ git stash show
app.txt | 1 +
lib.txt | 1 +
2 files changed, 2 insertions(+)
$ git stash show -p
diff --git a/app.txt b/app.txt
index a29bdeb..f32ed49 100644
--- a/app.txt
+++ b/app.txt
@@ -1 +1,2 @@
line1
+edit
diff --git a/lib.txt b/lib.txt
index 626799f..599309c 100644
--- a/lib.txt
+++ b/lib.txt
@@ -1 +1,2 @@
v1
+staged公式マニュアルによると、showはgit diffが受け付ける書式のオプションを受け取ります。--name-onlyでファイル名だけを表示したり、末尾に番号を付けて別のエントリを見たりできます。
$ git stash show --name-only
app.txt
$ git stash show --stat 1
lib.txt | 1 +
1 file changed, 1 insertion(+)毎回パッチ形式で見たい場合は、設定stash.showPatchをtrueにします。
戻す(pop)
$ git stash pop
On branch main
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.txt
modified: lib.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
new.txt
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (d4631245a3a22379bcce981c606f9316297b8897)
$ git stash list変更が戻り、最後の行でエントリが削除されたことが分かります。このDroppedの行に出るハッシュは、あとで説明する復旧の手がかりになります。
popとapplyの違い
公式マニュアルはapplyを「popと同じだが、stashの一覧から状態を削除しない」と説明しています。違いはこの1点です。
| コマンド | 変更を戻す | 一覧から削除する |
|---|---|---|
git stash pop | する | 成功したときだけ削除する |
git stash apply | する | しない |
2件退避した状態で、古いほう(stash@{1})をapplyしてみます。
$ git stash list
stash@{0}: On main: fix: lib
stash@{1}: On main: fix: app
$ git stash apply 'stash@{1}'
On branch main
Changes not staged for commit:
modified: app.txt
...
$ git stash list
stash@{0}: On main: fix: lib
stash@{1}: On main: fix: appapplyのあとも2件とも残っています。同じ変更を複数のブランチで試したいときや、戻した結果に自信がないときはapplyが向いています。不要になったらgit stash dropで消します。
$ git stash pop 1
...
Dropped refs/stash@{1} (6f4ce019a79dffb9c4b5aeae4536963880d9e4d4)
$ git stash drop
Dropped refs/stash@{0} (ccc191a9935be9e59a62aba6cba35f5bd11d8e8d)git stash pop 1の1はstash@{1}の省略形です。この書き方はGit 2.11のリリースノートに記載があります。番号を省略した場合は、pop、apply、drop、showのいずれも最新のstash@{0}が対象です。
NOTE
zshなどでは波括弧がシェルに解釈されることがあるので、'stash@{1}'のように引用符で囲むか、番号だけの書き方を使うと確実です。
addしていた状態も戻したいなら --index
最初の例をよく見ると、退避する前はlib.txtがadd済み(M が左側)でしたが、popのあとは未addの変更として戻っています。既定では、インデックスの状態までは復元されません。
addしていた状態ごと戻したいときは、--indexを付けます。
$ git status --short
M app.txt
M lib.txt
$ git stash
Saved working directory and index state WIP on main: 368b86f initial commit
$ git stash pop --index
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: lib.txt
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.txt
Dropped refs/stash@{0} (c7ed02366c015e7347cdaf3218e62e2c1273b2ea)
$ git status --short
M app.txt
M lib.txt公式マニュアルには、コンフリクトがある場合は--indexでの復元が失敗することがあると書かれています。Git 2.52のリリースノートには、設定stash.indexを有効にするとpopとapplyが--index付きとして動作するという記載があります(この設定は手元では未実行です)。
退避する範囲を変えるオプション
-u で未追跡のファイルも退避する
-u(--include-untracked)を付けると、未追跡のファイルも退避され、ワーキングツリーからは取り除かれます。公式マニュアルの表現では、退避したあとgit cleanで片付けられます。
$ git stash push -u -m "wip: with untracked"
Saved working directory and index state On main: wip: with untracked
$ git status --short --ignored
!! debug.lognew.txtは退避され、.gitignoreで無視しているdebug.logだけが残りました。なお、git stash showは既定では未追跡のファイルを表示しません。含めて見たいときは--include-untracked、未追跡のものだけを見たいときは--only-untrackedを付けます。
$ git stash show
app.txt | 1 +
lib.txt | 1 +
2 files changed, 2 insertions(+)
$ git stash show --include-untracked
app.txt | 1 +
lib.txt | 1 +
new.txt | 1 +
3 files changed, 3 insertions(+)未追跡のファイルしかない状態でも、-uを付ければ退避できます。
$ git stash
No local changes to save
$ git stash -u
Saved working directory and index state WIP on main: 368b86f initial commit
$ git stash show --only-untracked
only-untracked.txt | 1 +
1 file changed, 1 insertion(+)-a で無視しているファイルも退避する
-a(--all)は、未追跡のファイルに加えて、無視しているファイルも退避します。
$ git stash push -a -m "wip: all"
Saved working directory and index state On main: wip: all
$ git status --short --ignored
$ git stash pop
...
$ git status --short --ignored
M app.txt
M lib.txt
?? new.txt
!! debug.logWARNING
-aは、ビルド成果物や依存パッケージのディレクトリ、.envのようなローカル設定まで退避してワーキングツリーから取り除きます。巨大なディレクトリがあると時間もかかります。ほとんどの場面では-uで足ります。無視設定の仕組みは.gitignoreが効かない時の原因と対処で扱っています。
-m でメッセージを付ける
既定の「WIP on main: ...」では、数が増えるとどれが何の作業だったか分からなくなります。-m(--message)で説明を付けておくと、一覧で見分けられます。
$ git stash push -m "fix: app" app.txt
Saved working directory and index state On main: fix: app
$ git stash list
stash@{0}: On main: fix: appメッセージを付けると、表示は「WIP on」ではなく「On ブランチ名: メッセージ」になります。この違いは、あとで説明する復旧のコマンドに影響します。
パスを指定して一部だけ退避する
git stash pushの末尾にパスを渡すと、そのファイルの変更だけを退避します。一致しないファイルの変更は、ワーキングツリーに残ります。
$ git stash push -m "fix: app" app.txt
Saved working directory and index state On main: fix: app
$ git status --short
M lib.txt
?? new.txt
$ git stash push -m "fix: lib" -- lib.txt
Saved working directory and index state On main: fix: libパスの指定は、Git 2.13のリリースノートに「git stash pushがpathspecを受け取る」と記載されている機能です。pushを省略したgit stashの形では、--のあとにしかパスを書けません。公式マニュアルは、その理由を「サブコマンドの打ち間違いで意図しないstashが作られるのを防ぐため」としています。
-p で変更の一部だけ退避する
-p(--patch)は、git add -pと同じ対話形式で、退避する変更のかたまり(hunk)を選びます。次の例では、app.txtの2か所の変更のうち、1つ目だけをyで退避しています。
$ git stash push -p
diff --git a/app.txt b/app.txt
index a82bf91..0af8103 100644
--- a/app.txt
+++ b/app.txt
@@ -1,4 +1,4 @@
-line1
+line1 changed
line2
line3
line4
(1/2) Stash this hunk [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? y
@@ -9,4 +9,4 @@ line8
line9
line10
line11
-line12
+line12 changed
(2/2) Stash this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]? n
Saved working directory and index state WIP on main: 368b86f initial commit
$ git diff
...
-line12
+line12 changed2つ目の変更だけがワーキングツリーに残りました。公式マニュアルによると、--patchは--keep-indexを暗黙に有効にします(--no-keep-indexで打ち消せます)。
--staged でaddした変更だけ退避する
-S(--staged)は、addした変更だけを退避します。公式マニュアルは、「コミット先が現在のブランチではなくstashになる点を除けば、通常のgit commitに近い」と説明しています。Git 2.35のリリースノートに追加の記載があります。
$ git status --short
M app.txt
M lib.txt
$ git stash push --staged -m "staged only"
Saved working directory and index state On main: staged only
$ git status --short
M app.txt
$ git stash show
lib.txt | 1 +
1 file changed, 1 insertion(+)作業中に、関係のない修正を思いついて手を入れてしまったときに便利です。その修正だけをgit addして--stagedで退避しておけば、あとで別のブランチに持っていけます。
--keep-index でaddした変更を手元に残す
-k(--keep-index)は、addした変更をワーキングツリーとインデックスに残したまま、stashを作ります。
$ git status --short
M app.txt
M lib.txt
$ git stash push --keep-index -m "keep index"
Saved working directory and index state On main: keep index
$ git status --short
M lib.txt
$ git stash show
app.txt | 1 +
lib.txt | 1 +
2 files changed, 2 insertions(+)--stagedとは逆の用途で、「これからコミットする内容だけでテストが通るか」を確かめたいときに使います。注意したいのは、git stash showの出力のとおり、stashの中にはadd済みの変更も含まれていることです。手元に残るかどうかが変わるだけで、退避される内容が減るわけではありません。
| オプション | stashに入るもの | 手元に残るもの |
|---|---|---|
| なし | 未addの変更とaddした変更 | 未追跡のファイル |
--staged | addした変更だけ | 未addの変更、未追跡のファイル |
--keep-index | 未addの変更とaddした変更 | addした変更、未追跡のファイル |
複数のstashを扱う
stashはブランチに紐づかない
stashの一覧は、リポジトリ全体で1つです。mainで退避した変更を、別のブランチで戻せます。
$ git stash
Saved working directory and index state WIP on main: 368b86f initial commit
$ git switch -c feature
Switched to a new branch 'feature'
$ git stash pop
On branch feature
Changes not staged for commit:
modified: lib.txt
...
Dropped refs/stash@{0} (3712cb1d87d39e60838a3b8b0e8c1060fa71bb3e)「間違えてmainで作業を始めてしまった」ときは、この流れで変更を正しいブランチへ移せます。一覧に表示される「on main」は、退避した時点のブランチ名の記録であり、戻す先を制限するものではありません。
stashからファイルを1つだけ取り出す
stashの実体はコミットなので(後述)、コミットを指定できるコマンドにそのまま渡せます。エントリ全体を戻さずに、特定のファイルだけを取り出すこともできます。
$ git restore --source='stash@{0}' -- app.txt
$ git status --short
M app.txtこの方法は、ファイルの内容をstashの時点のものに置き換えます。popやapplyのようなマージは行われないので、そのファイルに手元の変更があれば上書きされます。
dropとclear
git stash dropは1件、git stash clearはすべてのエントリを削除します。どちらも確認なしで実行されます。
git stash drop 'stash@{1}' # 指定した1件を削除
git stash clear # すべて削除公式マニュアルはclearについて、削除されたエントリは刈り込み(prune)の対象になり、復旧できなくなる可能性があると注意しています。
popでコンフリクトしたとき
退避したあとに同じ箇所を変更するコミットが入ると、戻すときにコンフリクトします。
$ git stash pop
Auto-merging app.txt
CONFLICT (content): Merge conflict in app.txt
On branch main
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: app.txt
no changes added to commit (use "git add" and/or "git commit -a")
The stash entry is kept in case you need it again.
$ git stash list
stash@{0}: On main: wip: body最後の行のとおり、コンフリクトした場合、popでもstashは削除されません。公式マニュアルにも、コンフリクトで失敗した場合は一覧から削除されないこと、手で解決したあと自分でgit stash dropを実行する必要があることが書かれています。このとき終了ステータスは1でした。
ファイルには、通常のマージと同じ形式の目印が入ります。「Updated upstream」が現在のブランチ側、「Stashed changes」がstash側です。
title
<<<<<<< Updated upstream
body from commit
=======
body from stash
>>>>>>> Stashed changes解決して進める
ファイルを編集して目印を取り除き、git addで解決済みにします。マージと違ってコミットは不要です。git addした内容はインデックスに入るので、addした状態にしたくなければgit restore --stagedで戻します。最後に、残っているstashを手動で削除します。
# app.txt を編集してコンフリクトを解決したあと
$ git add app.txt
$ git status --short
M app.txt
$ git restore --staged app.txt
$ git stash drop
Dropped refs/stash@{0} (7dacf726f44ccfb79e6511a61e49ab8d7662db69)git stash dropを忘れると、戻し終えたはずのエントリが一覧に残り続けます。あとで間違えてもう一度popしないよう、解決が済んだら消しておきます。
いったんやめる
解決をあきらめて元に戻したいときは、ワーキングツリーをリセットします。stashは残っているので、あとでやり直せます。
$ git reset --hard
HEAD is now at 451b4da body v4
$ git stash list
stash@{0}: On main: wip: v3WARNING
git reset --hardは、stashと関係のない未コミットの変更もすべて破棄します。popする前に手元に別の変更があった場合は、それも失われます。実行前にgit statusで確認してください。
git stash branch で確実に戻す
コンフリクトを避けたいときは、git stash branchが使えます。公式マニュアルによると、stashを作った時点のコミットから新しいブランチを作って切り替え、そこへ変更を戻します。退避したときと同じ土台の上に戻すので、コンフリクトは起きません。成功すると、そのstashは削除されます。
$ git stash branch topic/wip
Switched to a new branch 'topic/wip'
On branch topic/wip
Changes not staged for commit:
modified: app.txt
...
Dropped refs/stash@{0} (c93d922156b37879bf41d9d6e70a03d5e3aa05ae)
$ git log --oneline -1
ed33571 change bodymainは451b4daまで進んでいましたが、新しいブランチはstashを作った時点のed33571から始まっています。ここでコミットしてから、通常のマージやリベースでmainに合わせれば、落ち着いてコンフリクトを解決できます。マージとリベースの選び方はmergeとrebaseの使い分けを参照してください。
未追跡のファイルがぶつかる場合
-uで退避した未追跡のファイルと同じ名前のファイルが手元にあると、そのファイルは戻せません。
$ git stash pop
Already up to date.
note.txt already exists, no checkout
error: could not restore untracked files from stash
...
The stash entry is kept in case you need it again.この場合もstashは残ります。手元のファイルを別名に変えるか削除してから、もう一度popします。
同じファイルに手元の変更がある場合
stashが触るファイルに未コミットの変更があると、コンフリクト以前に、戻す処理そのものが拒否されます。
$ git stash pop
error: Your local changes to the following files would be overwritten by merge:
app.txt
Please commit your changes or stash them before you merge.
Aborting
...
The stash entry is kept in case you need it again.手元の変更を先にコミットするか、それも別のstashとして退避してから戻します。
消してしまったstashを復旧する
公式マニュアルには「誤ってclearまたはdropしたstashエントリの復旧」という節があります。冒頭には、誤って削除したstashは通常の安全機構では復旧できないと書かれたうえで、リポジトリにまだ残っていて到達できなくなったstashの一覧を得る方法として、次のコマンドが示されています。
git fsck --unreachable |
grep commit | cut -d\ -f3 |
xargs git log --merges --no-walk --grep=WIPgit fsck --unreachableで、どの参照からもたどれないオブジェクトを列挙し、そのうちのコミットをgit logに渡しています。stashはマージコミットの形をしているので--mergesで絞り込み、既定のメッセージに含まれる「WIP」で検索する、という組み立てです。
メッセージを付けたstashは --grep=WIP に一致しない
ここに落とし穴があります。-mでメッセージを付けたstashは「On main: メッセージ」という形になり、「WIP」を含みません。実際に-m "wip: important"で作ったstashをdropしてから公式のコマンドを実行すると、何も表示されませんでした(--grepは既定で大文字と小文字を区別します)。
--grep=WIPを外すと、見つかります。
$ git stash drop
Dropped refs/stash@{0} (8e3d79f7822d5e870701772f9567ee0817ed1cb9)
$ git fsck --unreachable | grep commit | cut -d\ -f3 | xargs git log --merges --no-walk --oneline
c93d922 On main: wip: v3
8e3d79f On main: wip: important
3b2baa9 On main: with note
7dacf72 On main: wip: body過去にpopやdropで消えたstashも並ぶので、メッセージと日時から目的のものを探します。--onelineを外せば日時も表示されます。
見つけたコミットを戻す
ハッシュが分かれば、git stash applyにそのまま渡せます。公式マニュアルに、applyはpopと違い、stash pushやstash createで作られたコミットであれば何でも指定できると書かれています。
$ git stash apply 8e3d79f7822d5e870701772f9567ee0817ed1cb9
On branch main
Changes not staged for commit:
modified: lib.txt
...一覧に戻したい場合はgit stash storeを使います。
$ git stash store -m "recovered: important" 8e3d79f7822d5e870701772f9567ee0817ed1cb9
$ git stash list
stash@{0}: recovered: importantdropやpopを実行した直後であれば、ターミナルに残っているDropped refs/stash@{0} (ハッシュ)の行からハッシュを拾うのがいちばん早い方法です。
いつまで復旧できるか
消したstashのコミットは、どの参照からもたどれないオブジェクトになります。公式マニュアルによると、git gcは既定で2週間より古い到達不能オブジェクトを削除の対象にします(gc.pruneExpireで変更できます)。gcがいつ実行されるかは環境によるので、「2週間は必ず残る」とも「2週間で必ず消える」とも言えません。気づいた時点ですぐに復旧してください。
stashの内部表現
仕組みを知っておくと、ここまでの挙動が理解しやすくなります。Gitのオブジェクトや参照の基本はGitの内部構造(オブジェクトとpackfile)の解説で扱っています。
refs/stashとreflog
公式マニュアルによると、最新のstashはrefs/stashという参照に保存され、それより古いものはこの参照のreflogにあります。stash@{1}という書き方は、reflogの通常の記法そのものです。
$ cat .git/refs/stash
b46c42f872fccfe318526a0f337e8cd976510c12
$ git reflog show stash
b46c42f stash@{0}: On main: second
424b0bd stash@{1}: On main: first
$ git show-ref
451b4daa0b652e51a824d60ebe6cb014d9ced863 refs/heads/main
b46c42f872fccfe318526a0f337e8cd976510c12 refs/stashstashの一覧は「スタック」と説明されることが多いですが、実体は1つの参照とそのreflogです。dropはreflogのエントリを1行消す操作に当たります。
通常のブランチのreflogは既定で90日(到達できないものは30日)で期限切れになりますが、refs/stashは扱いが異なります。Git 2.52.0のソースコード(reflog.c)には「設定されていなければstashは期限切れにしない」というコメント付きの処理があり、一覧に残っているstashが時間の経過だけで消えることはありません。
stashコミットの親構成
stashの1件は、複数のコミットで表現されています。公式マニュアルの「DISCUSSION」の節には、次の図が載っています。
.----W
/ /
-----H----IHは退避した時点のHEAD、Iはインデックスの状態を記録したコミット、Wはワーキングツリーの状態を記録したコミットです。WはHとIの2つを親に持つマージコミットの形をしていて、refs/stashが指すのはこのWです。
$ git cat-file -p 'stash@{0}'
tree c8f285e56f868cf9e9ab14c85342d524b276af88
parent 451b4daa0b652e51a824d60ebe6cb014d9ced863
parent df0ce126f47917d2f19acca82198368d57558743
author Sample User <sample@example.com> 1794704400 +0900
committer Sample User <sample@example.com> 1794704400 +0900
On main: second
$ git log --graph --oneline -4 'stash@{0}'
* b46c42f On main: second
|\
| * df0ce12 index on main: 451b4da body v4
|/
* 451b4da body v4
* ed33571 change body-uや-aを付けて退避した場合は、未追跡のファイルを記録したコミットが3番目の親として加わることを、手元で確認しました。
$ git log --format='%h %p %s' -1 'stash@{0}'
f1daec2 3cfb4b0 bda16aa 98bbee6 On main: wip: with untracked
$ git log --graph --oneline -5 'stash@{0}'
*-. f1daec2 On main: wip: with untracked
|\ \
| | * 98bbee6 untracked files on main: 3cfb4b0 initial commit
| * bda16aa index on main: 3cfb4b0 initial commit
|/
* 3cfb4b0 initial commit
$ git ls-tree -r --name-only 'stash@{0}^3'
new.txtこの構造から、次のことが説明できます。
--indexでaddの状態を復元できるのは、インデックスが別のコミット(I)として保存されているからgit stash branchがコンフリクトなしで戻せるのは、土台のコミット(H)が記録されているから- 復旧のコマンドが
--mergesで絞り込むのは、stashがマージコミットの形をしているから git stash showが未追跡のファイルを既定で表示しないのは、それが3番目の親に分かれているから
git stash saveは非推奨
古い解説記事にはgit stash save "メッセージ"という書き方が出てきます。公式マニュアルはsaveを「git stash pushに置き換えられた非推奨のもの」と明記していて、Git 2.16のリリースノートにも非推奨になったことが記載されています。
違いは、saveがパスの指定を受け取れない点です。オプション以外の引数は、すべてメッセージとして連結されます。
| 書き方 | メッセージ | パス指定 |
|---|---|---|
git stash save "message" | 引数として渡す | できない |
git stash push -m "message" | -mで渡す | できる |
Git 2.52.0の時点ではsaveもまだ動作し、警告も表示されませんでした。
$ git stash save "old style message"
Saved working directory and index state On main: old style message新しく覚えるならpushです。エイリアスやスクリプトにsaveが残っている場合は、push -mに書き換えておくと安心です。エイリアスの設定はよく使うGitコマンドとエイリアスで紹介しています。
リリースノートで導入の時期を確認できた機能を、まとめておきます。
| バージョン | 内容 |
|---|---|
| Git 1.5.3 | git stashの追加 |
| Git 1.7.7 | --include-untracked |
| Git 1.8.4 | git rebase --autostash |
| Git 2.9 | git pull --rebase --autostash |
| Git 2.11 | git stash apply 4のような番号だけの指定 |
| Git 2.13 | git stash pushのパス指定 |
| Git 2.16 | git stash saveの非推奨化 |
| Git 2.27 | git merge --autostash |
| Git 2.35 | --staged |
| Git 2.51 | git stash exportとgit stash import、設定pull.autostash |
| Git 2.52 | 設定stash.index |
-m、--keep-index、--patch、--all、git stash branchの導入バージョンは、今回リリースノートで確認していないため未確認です。
自動で退避するautostash
「退避して、操作して、戻す」という流れを、コマンドの側で自動的に行う仕組みがあります。
rebase --autostash
git rebaseは、未コミットの変更があると実行を拒否します。--autostashを付けると、開始前に一時的なstashを作り、終了後に戻します。
$ git rebase main
error: cannot rebase: You have unstaged changes.
error: Please commit or stash them.
$ git rebase --autostash main
Created autostash: 35b9dd6
Rebasing (1/1)
Applied autostash.
Successfully rebased and updated refs/heads/feature.
$ git status --short
M lib.txt
$ git stash list成功した場合、一時的なstashは一覧に残りません。設定rebase.autoStashをtrueにすると、毎回付けなくても有効になります。
merge --autostash と pull --autostash
git mergeとgit pullにも同じオプションがあります。
$ git merge --autostash --no-edit topic
Updating 1429041..2e09dbe
Created autostash: ad86863
Fast-forward
topic.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 topic.txt
Applied autostash.
$ git pull --rebase
error: cannot pull with rebase: You have unstaged changes.
error: Please commit or stash them.
$ git pull --rebase --autostash
Created autostash: 3c66241
Current branch main is up to date.
Applied autostash.対応する設定はmerge.autoStashとrebase.autoStashです。公式マニュアルによると、pull.autostashを設定した場合はgit pullではそちらが優先され、merge.autostashとrebase.autostashは無視されます。
WARNING
公式マニュアルは--autostashについて、「操作が成功したあとのstashの適用で、単純ではないコンフリクトが起きることがあるので注意して使うこと」と書いています。自動で戻す段階でコンフリクトした場合の細かい挙動は、今回は検証していません。
git switch と stash
git switchは、切り替え先と食い違うファイルに手元の変更があると、切り替えを拒否します。エラーメッセージが、コミットかstashを勧めています。
$ git switch main
error: Your local changes to the following files would be overwritten by checkout:
feature.txt
Please commit your changes or stash them before you switch branches.
Aborting切り替え先と食い違わないファイルの変更であれば、stashしなくてもそのまま持ち越されます。
なお、Git 2.55のリリースノートには、git checkout -mがローカルの変更を保存するためにstashを作るようになったという記載があり、執筆時点のgit switchのマニュアル(2.56.0)でも、-m(--merge)の説明が「衝突するローカルの変更を切り替え前に自動でstashし、切り替え後に再適用する」という内容になっています。手元の環境は2.52.0のため、この新しい挙動は未実行です。
別のマシンへ持っていくexportとimport
stashはローカルリポジトリのrefs/stashにあるだけなので、通常のgit pushでは送られません。Git 2.51のリリースノートには、stashのエントリを交換するための形式が定義され、git stash exportとgit stash importが追加されたと記載されています。
公式マニュアルによると、exportはstashを、通常のfetchとpushで転送できるコミットの連なりに変換します。Git公式のFAQに載っている手順のとおりに試しました。
$ git stash list
stash@{0}: On main: wip: app
stash@{1}: On main: wip: lib
$ git stash export --to-ref refs/heads/stashes
$ git push origin stashes
To ../origin.git
* [new branch] stashes -> stashes別のクローン側で取得して、取り込みます。
$ git fetch origin +stashes:stashes
* [new branch] stashes -> stashes
* [new branch] stashes -> origin/stashes
$ git stash import stashes
$ git stash list
stash@{0}: On main: wip: app
stash@{1}: On main: wip: libimportは既存の一覧に追加する動作で、置き換えたい場合は先にclearするよう公式マニュアルに書かれています。FAQは、--include-untrackedで退避したstashには機密情報を含むファイルが入り得るので注意するよう呼びかけています。pushすれば、その内容がリモートに送られるためです。
worktreeや一時コミットとの使い分け
stashは「短時間の退避」に向いた道具です。Pro Gitも、作業が中途半端な状態でブランチを切り替えたいが、まだコミットはしたくない、という場面の解決策としてstashを紹介しています。一方で、退避が長引く場合は別の方法のほうが向いています。
| 方法 | 向いている場面 | 注意点 |
|---|---|---|
git stash | 数分から数時間の割り込み、pullの前の退避、ブランチを間違えたときの移動 | 数が増えると何の作業か分からなくなる。pushされない |
| 一時コミット(WIPコミット) | 日をまたぐ中断、別のマシンで続きをやりたいとき | あとでgit resetや--amendで整える必要がある |
git worktree | 作業を止めずに別のブランチを並行して開きたいとき | ディレクトリが増える。依存パッケージの導入などが別途必要になることがある |
一時コミット
変更をそのままコミットしておき、再開するときに取り消す方法です。ブランチに属するので、どの作業の続きかが明確で、pushすればバックアップにもなります。
$ git commit -am "WIP: lib"
[main 8684429] WIP: lib
1 file changed, 1 insertion(+)
# 再開するとき
$ git reset HEAD~1
Unstaged changes after reset:
M lib.txtgit commit -aは未追跡のファイルを含めないので、必要なら先にgit addします。git resetの動きはgit resetとrevertとrestoreの違いで解説しています。一時コミットを共有ブランチにpushしてしまうと取り消しが難しくなるので、自分の作業ブランチで使ってください。
git worktree
git worktreeは、1つのリポジトリに複数のワーキングツリーを持たせる機能です。作業中のディレクトリには触れずに、別のディレクトリで別のブランチを開けます。
$ git worktree add ../hotfix -b hotfix main
Preparing worktree (new branch 'hotfix')
HEAD is now at 2e09dbe topic: add file
$ git worktree list
/tmp/work/demo 2e09dbe [main]
/tmp/work/hotfix 2e09dbe [hotfix](git worktree listのパスは短く書き換えています。)
手元で確認したところ、追加したワーキングツリーでgit stash listを実行すると、元のワーキングツリーで作ったstashがそのまま表示されました。stashの一覧はワーキングツリーごとではなく、リポジトリで共有されています。
場面別の早見表
| やりたいこと | コマンド | 備考 |
|---|---|---|
| 作業を一時的に退避する | git stash | 未追跡のファイルは残る |
| 未追跡のファイルも退避する | git stash -u | 無視しているファイルは残る |
| 説明を付けて退避する | git stash push -m "説明" | 一覧で見分けやすくなる |
| 特定のファイルだけ退避する | git stash push -- app.txt | Git 2.13以降 |
| 変更の一部だけ退避する | git stash push -p | 対話形式で選ぶ |
| addした変更だけ退避する | git stash push --staged | Git 2.35以降 |
| addした変更を残して退避する | git stash push --keep-index | stashにはadd済みの変更も入る |
| 一覧を見る | git stash list | stash@{0}が最新 |
| 中身を見る | git stash show -p | 番号を付けると別のエントリ |
| 戻して削除する | git stash pop | コンフリクト時は削除されない |
| 戻すが残しておく | git stash apply | 複数のブランチで試すとき |
| addの状態ごと戻す | git stash pop --index | 既定では未addとして戻る |
| ブランチを間違えたので移す | git stashのあとgit switchしてgit stash pop | stashはブランチに紐づかない |
| コンフリクトせずに戻す | git stash branch topic/wip | 退避時のコミットからブランチを作る |
| 1つのファイルだけ取り出す | git restore --source='stash@{0}' -- app.txt | 手元の変更は上書きされる |
| 1件削除する | git stash drop | 番号を省略すると最新 |
| すべて削除する | git stash clear | 確認なしで実行される |
| 消したstashを探す | git fsck --unreachableとgit log --merges --no-walk | 見つけたらgit stash apply ハッシュ |
| 変更があるままrebaseする | git rebase --autostash | 終了後に自動で戻る |
| 別のマシンへ持っていく | git stash export --to-refとgit stash import | Git 2.51以降 |
よくあるつまずき
未追跡のファイルが退避されない
既定では、追跡しているファイルの変更しか退避されません。新しく作ったファイルも含めたいときは-uを付けるか、先にgit addしてから退避します。
popしたらaddの状態が消えた
既定のpopとapplyは、すべてを未addの変更として戻します。addの状態も戻したいときは--indexを付けます。
popしたのに一覧に残っている
コンフリクトしたか、戻す処理が失敗したかのどちらかです。出力の最後に「The stash entry is kept in case you need it again.」と表示されていたはずです。解決したあと、git stash dropで手動で削除します。
どのstashが何か分からない
git stash listの説明が「WIP on main」ばかりになると、見分けがつきません。git stash show -p 番号で中身を確認できますが、最初から-mでメッセージを付けておくのが確実です。数が増えてきたら、退避ではなく一時コミットやブランチに切り替える合図です。
番号がずれて別のstashを消した
新しく退避するたびに番号が1つずつずれるので、少し前に確認した番号はもう別のエントリを指しているかもしれません。dropやpopに番号を渡す前に、直前でgit stash listを確認します。間違えて消した場合は、出力に表示されたDroppedの行のハッシュを使ってgit stash apply ハッシュで戻せます。
公式の復旧コマンドで見つからない
前述のとおり、-mでメッセージを付けたstashは「WIP」を含まないので、--grep=WIPに一致しません。--grepを外すか、付けたメッセージの一部で検索します。
stashをpushしたつもりになっている
git stash pushの「push」は、stashの一覧に積むという意味で、リモートへのgit pushとは関係ありません。stashはローカルにしかなく、マシンが壊れれば失われます。残しておきたい作業は、コミットしてリモートにpushするのが基本です。
まとめ
git stashは、追跡ファイルの未コミットの変更(ワーキングツリーとインデックス)を退避し、HEADの状態に戻す。未追跡のファイルは-u、無視しているファイルまで含めるなら-aが必要popは戻して削除、applyは戻すだけ。addの状態も復元するなら--indexを付けるpushにパスや-pを渡せば一部だけ、--stagedならaddした変更だけを退避できる。--keep-indexはaddした変更を手元に残すが、stashには含まれるpopでコンフリクトしてもstashは消えない。解決後にgit stash dropするか、git stash branchで退避時のコミットから戻すdropやclearで消したstashは、git fsck --unreachableでコミットを探し、git stash applyにハッシュを渡して戻す。-mを付けたstashは公式の例の--grep=WIPに一致しない- stashの実体は
refs/stashとそのreflogで、1件はHEADとインデックスのコミットを親に持つマージコミット git stash saveは非推奨で、git stash push -mを使う- 長引く中断には一時コミット、並行作業には
git worktreeのほうが向いている
stashを安全に使うこつは、短く使うこととメッセージを付けることの2つです。一覧が数件を超えてきたら、ブランチとコミットに切り替えることを検討してみてください。
参考
- git-stash(Git公式マニュアル)
- Git Tools - Stashing and Cleaning(Pro Git)
- git-rebase(Git公式マニュアル。--autostash)
- git-merge(Git公式マニュアル。--autostash)
- git-pull(Git公式マニュアル。--autostash)
- git-config(Git公式マニュアル。stash.showPatch、pull.autostashなど)
- git-switch(Git公式マニュアル)
- git-worktree(Git公式マニュアル)
- git-fsck(Git公式マニュアル)
- git-gc(Git公式マニュアル)
- gitfaq(Git公式FAQ。stashのexportとimport)
- reflog.c(Git 2.52.0のソースコード。refs/stashの期限の扱い)
- Git 1.5.3 リリースノート
- Git 1.7.7 リリースノート
- Git 1.8.4 リリースノート
- Git 2.9.0 リリースノート
- Git 2.11.0 リリースノート
- Git 2.13.0 リリースノート
- Git 2.16.0 リリースノート
- Git 2.27.0 リリースノート
- Git 2.35.0 リリースノート
- Git 2.51.0 リリースノート
- Git 2.52.0 リリースノート
- Git 2.55.0 リリースノート


