git stash の使い方 - 作業を一時退避する pop と apply の違い、消した stash の復旧まで

git stash の使い方 - 作業を一時退避する pop と apply の違い、消した stash の復旧まで

作成日:
読了:約51分
更新日:

当サイトは 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 stashgit stash -ugit 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 commit

stash@{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: app

applyのあとも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.log

new.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.log

WARNING

-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 changed

2つ目の変更だけがワーキングツリーに残りました。公式マニュアルによると、--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した変更未追跡のファイル
--stagedaddした変更だけ未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: v3

WARNING

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 body

mainは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=WIP

git 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: important

dropや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/stash

stashの一覧は「スタック」と説明されることが多いですが、実体は1つの参照とそのreflogです。dropはreflogのエントリを1行消す操作に当たります。

通常のブランチのreflogは既定で90日(到達できないものは30日)で期限切れになりますが、refs/stashは扱いが異なります。Git 2.52.0のソースコード(reflog.c)には「設定されていなければstashは期限切れにしない」というコメント付きの処理があり、一覧に残っているstashが時間の経過だけで消えることはありません。

stashコミットの親構成

stashの1件は、複数のコミットで表現されています。公式マニュアルの「DISCUSSION」の節には、次の図が載っています。

       .----W
      /    /
-----H----I

Hは退避した時点の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.3git stashの追加
Git 1.7.7--include-untracked
Git 1.8.4git rebase --autostash
Git 2.9git pull --rebase --autostash
Git 2.11git stash apply 4のような番号だけの指定
Git 2.13git stash pushのパス指定
Git 2.16git stash saveの非推奨化
Git 2.27git merge --autostash
Git 2.35--staged
Git 2.51git 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: lib

importは既存の一覧に追加する動作で、置き換えたい場合は先に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.txt

git 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.txtGit 2.13以降
変更の一部だけ退避するgit stash push -p対話形式で選ぶ
addした変更だけ退避するgit stash push --stagedGit 2.35以降
addした変更を残して退避するgit stash push --keep-indexstashにはadd済みの変更も入る
一覧を見るgit stash liststash@{0}が最新
中身を見るgit stash show -p番号を付けると別のエントリ
戻して削除するgit stash popコンフリクト時は削除されない
戻すが残しておくgit stash apply複数のブランチで試すとき
addの状態ごと戻すgit stash pop --index既定では未addとして戻る
ブランチを間違えたので移すgit stashのあとgit switchしてgit stash popstashはブランチに紐づかない
コンフリクトせずに戻す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 importGit 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つです。一覧が数件を超えてきたら、ブランチとコミットに切り替えることを検討してみてください。

参考

.gitignore が効かない時の原因と対処 - 追跡済みファイルと書き方の落とし穴を確認する

.gitignore が効かない時の原因と対処 - 追跡済みファイルと書き方の落とし穴を確認する

約40分

.gitignoreに書いたのにファイルが無視されない時の原因を、追跡済みファイル・パターンの書き方・否定パターンの制約・別の無視設定・文字コードの5つに分けて整理します。git rm --cachedで追跡を外した後に他の人の作業ツリーで起きること、スラッシュや**の意味、!で再包含できない条件、.git/info/excludeとcore.excludesFileの優先順位、git check-ignore -vでの切り分け、assume-unchangedとskip-worktreeが向かない理由まで、公式マニュアルと実機の出力で解説します。

git reset と revert と restore の違い - コミットを安全に取り消す選び方

git reset と revert と restore の違い - コミットを安全に取り消す選び方

約34分

git reset・git revert・git restoreの違いを、ワーキングツリー・インデックス・HEADのどこを動かすかで整理します。resetの--soft/--mixed/--hard/--keep/--merge、revertの複数コミット指定とマージコミットの-m、revertしたマージを再度マージする時の落とし穴、restoreの--staged/--sourceとcheckoutの関係、push済みの判断と--force-with-lease、場面別の早見表、reflogとORIG_HEADでの復旧と保持期間の既定値まで、公式ドキュメントと実機の出力で解説します。

Git の merge と rebase の使い分け - 履歴の形・黄金律・やり直し方

Git の merge と rebase の使い分け - 履歴の形・黄金律・やり直し方

約8分

Git の merge と rebase の違いを実務目線で整理します。マージコミットで分岐を残す merge と、コミットを付け替えて直線的にする rebase の仕組み、fast-forward と --no-ff と squash の違い、「公開済みブランチを rebase しない」黄金律、git rebase -i での squash / fixup、コンフリクト処理、git pull --rebase、そして reflog や ORIG_HEAD でのやり直しまで、Git 公式ドキュメントを一次ソースにまとめます。

楽天市場のおすすめPR / 楽天アフィリエイト