Medium 好讀版點此。
在 Day 16 文章中,我們示範了用 git stash 暫存進度,但不會形成 commit。在這個單元,我們將深入探討 git stash 指令對 .git/ 資料夾造成的影響,藉此觀察 git stash 指令到底把進度暫存到哪。
先在 main 跟 feature 都各建立一個 commit。
main 分支建立 commit:echo "Hello, world" > hello_world.txt
把檔案加到預存區:
git add hello_world.txt
形成一個 commit:
git commit -m "Initial commit"
feature 分支也建立 commit:feature 分支:git branch feature
切換到 feature 分支:
git switch feature
在 feature 分支建立與 main 分支上同名的檔案:
echo "Feature version" > hello_world.txt
把檔案加到預存區:
git add hello_world.txt
在 feature 分支上形成一個 commit:
git commit -m "Feature change"
目前資料夾結構如下:
.git/
├── hooks/
├── info/
│
├── logs/
│ ├── refs/
│ │ └── heads/
│ │ ├── feature
│ │ └── main
│ └── HEAD
│
├── objects/
│ ├── 9e/ # tree物件
│ │ └── 206bdfe0f75cf9011bc37c3a77528b50993a7d
│ ├── a5/ # blob物件
│ │ └── c19667710254f835085b99726e523457150e03
│ ├── cc/ # commit物件
│ │ └── 745c806c5dabd2ca4bbdc10e9fa2a60245cff3
│ ├── ce/ # commit物件
│ │ └── 7da55f5b7e61fe3954f39bf9cf18845b5b8a44
│ ├── db/ # blob物件
│ │ └── c69068b98a226af5471ad0543d5ad3316b4cb7
│ ├── e5/ # tree物件
│ │ └── 9896eb1f48ef9c75e675692e23966e5ba7cdfb
│ └── info/
│ └── pack/
│
├── refs/
│ ├── heads/
│ │ ├── feature # 指向cc745c806c5dabd2ca4bbdc10e9fa2a60245cff3
│ │ └── main # 指向ce7da55f5b7e61fe3954f39bf9cf18845b5b8a44
│ └── tags/
│
├── COMMIT_EDITMSG
├── config
├── description
├── HEAD
└── index
圖示如下:
先切回 main 分支:
git switch main
在 main 分支上做改動:
echo "Main version" > hello_world.txt
切換到 feature 分支前,先做 git stash:
這時候 .git/ 資料夾長什麼樣子呢?
.git/
├── hooks/
├── info/
│
├── logs/
│ ├── refs/
│ │ ├── heads/
│ │ │ ├── feature
│ │ │ └── main
│ │ └── stash # 內文為0000000000000000000000000000000000000000 56aa497609b28445020882744648ce5964ee0805 Ralph <ralph@ralphmail.com> 1755743231 +0800 WIP on main: ce7da55 Initial commit
│ └── HEAD
│
├── objects/
│ ├── 4b/ # 新tree物件
│ │ └── 17fc94a51a7fd3dfddf8e223352cce1a5b21c2
│ ├── 9e/
│ │ └── 206bdfe0f75cf9011bc37c3a77528b50993a7d
│ ├── 50/ # 新blob物件
│ │ └── 55b9198f7069db185ec0f12534e44bfd5c9623
│ ├── 56/ # 新commit物件
│ │ └── aa497609b28445020882744648ce5964ee0805
│ ├── a5/
│ │ └── c19667710254f835085b99726e523457150e03
│ ├── cc/
│ │ └── 745c806c5dabd2ca4bbdc10e9fa2a60245cff3
│ ├── ce/
│ │ └── 7da55f5b7e61fe3954f39bf9cf18845b5b8a44
│ ├── db/
│ │ └── c69068b98a226af5471ad0543d5ad3316b4cb7
│ ├── dd/ # 新commit物件
│ │ └── 7b0929cbfede635b9d7eb63c2a1f579087c0bc
│ ├── e5/
│ │ └── 9896eb1f48ef9c75e675692e23966e5ba7cdfb
│ └── info/
│ └── pack/
│
├── refs/
│ ├── heads/
│ │ ├── feature
│ │ └── main
│ ├── tags/
│ └── stash # 指向56aa497609b28445020882744648ce5964ee0805
│
├── COMMIT_EDITMSG
├── config
├── description
├── HEAD
├── index
└── ORIG_HEAD # 指向ce7da55f5b7e61fe3954f39bf9cf18845b5b8a44
居然多了好幾個物件!再透過 git cat-file 指令檢視,看起來好像多了一個 commit...哦不,是兩個 commit:
這樣看起來,連同 refs/ 中多出來的 stash 這個參考,好像可以畫成以下結構:
所以真的有多兩個 commit 嗎?我們再用 git log 看看:
沒有新的 commit!所以這兩個多出來的 commit 到底是什麼?
我們先下以下指令觀察:
git log refs/stash
得到的結果如下,這樣與這次 stash 有關的三個 commit 都出來了,由下而上包含:
cd7da55...:在 git stash 前最後的 commit。dd7b092...:在 git stash 當下,預存區 index 的快照。56aa497...:stash 這個 commit,上代包含另外兩個 commit。
以目前來說,工作目錄是我有做的更動,因此最終指向的 blob 物件內容為 "Main Version"。
實際打開 .git/logs/refs/stash 看到的紀錄,跟 Day 11 介紹 git commit 之後 logs/ 裡存的內容十分相似,依序為上代雜湊碼 0000000...、自己的雜湊碼 56aa497...、作者資訊 Ralph...、commit 訊息 WIP on main...:
0000000000000000000000000000000000000000 56aa497609b28445020882744648ce5964ee0805 Ralph <ralph@ralphmail.com> 1755743231 +0800 WIP on main: ce7da55 Initial commit
現在切回 feature 分支:
git switch feature
再更改 feature 分支上的檔案:
echo "Feature second version" > hello_world.txt
這次做 git add:
git add hello_world.txt
這時候再做 git stash 呢?
此時 .git/ 資料夾長這樣:
.git/
├── hooks/
├── info/
│
├── logs/
│ ├── refs/
│ │ ├── heads/ # 沒有新的commit,因此這邊無變化
│ │ │ ├── feature
│ │ │ └── main
│ │ └── stash # 多一行56aa497609b28445020882744648ce5964ee0805 2881edecc2e2c832858db96df2d29abc511d3a93 Ralph <ralph@ralphmail.com> 1755748433 +0800 WIP on feature: cc745c8 Feature change
│ └── HEAD
│
├── objects/
│ ├── 4b/
│ │ └── 17fc94a51a7fd3dfddf8e223352cce1a5b21c2
│ ├── 9e/
│ │ └── 206bdfe0f75cf9011bc37c3a77528b50993a7d
│ ├── 28/ # 新commit物件
│ │ └── 81edecc2e2c832858db96df2d29abc511d3a93
│ ├── 50/
│ │ └── 55b9198f7069db185ec0f12534e44bfd5c9623
│ ├── 56/
│ │ └── aa497609b28445020882744648ce5964ee0805
│ ├── 85/ # 新blob物件
│ │ └── a9b9543ee7fdc04fee7d31a56519e594337ca3
│ ├── a5/
│ │ └── c19667710254f835085b99726e523457150e03
│ ├── b9/ # 新tree物件
│ │ └── bd12ed57019858943deb7b65df681942fdab8e
│ ├── cc/
│ │ └── 745c806c5dabd2ca4bbdc10e9fa2a60245cff3
│ ├── ce/
│ │ └── 7da55f5b7e61fe3954f39bf9cf18845b5b8a44
│ ├── db/
│ │ └── c69068b98a226af5471ad0543d5ad3316b4cb7
│ ├── dd/
│ │ └── 7b0929cbfede635b9d7eb63c2a1f579087c0bc
│ ├── e5/
│ │ └── 9896eb1f48ef9c75e675692e23966e5ba7cdfb
│ ├── e6/ # 新commit物件
│ │ └── 747c3633027d55a67eb1d52e1fd36c12719d42
│ └── info/
│ └── pack/
│
├── refs/
│ ├── heads/
│ │ ├── feature
│ │ └── main
│ ├── tags/
│ └── stash # 內文為2881edecc2e2c832858db96df2d29abc511d3a93
│
├── COMMIT_EDITMSG
├── config
├── description
├── HEAD
├── index
└── ORIG_HEAD # 內文仍為cc745c806c5dabd2ca4bbdc10e9fa2a60245cff3
根據多次 git cat-file 指令:
可繪製出下列關係圖,新的 stash 在上半部:
而如果再下 git log refs/stash 呢?
最下面兩個為 main 跟 feature 分支上最新的 commit,往上 e6747c3... 是預存區經 git stash 形成的 commit、最上面的 2881ede... 則是工作目錄經 git stash 形成的 commit。
值得留意的是,在 main 分支上,因為沒有 git add 就直接 git stash,所以索引的 commit 仍是指向前一次 git add 做出的 tree 物件(下圖紫色圈圈);在 feature 分支上,在 git stash 前有 git add,因此索引的 commit 是指向經 git stash 之後才出現的 tree 物件(下圖橘色圈圈):
現在切回 main 分支:
git switch main
以 git stash list 觀察目前有哪些 stash:
在 main 分支上的 stash 是「上上一個」,因此為 stash@{1},接著把下列指令把該暫存工作狀態拿回來:
git stash pop stash@{1}
這時 .git/ 資料夾變成:
.git/
├── hooks/
├── info/
│
├── logs/
│ ├── refs/
│ │ ├── heads/
│ │ │ ├── feature
│ │ │ └── main
│ │ └── stash # 變成0000000000000000000000000000000000000000 2881edecc2e2c832858db96df2d29abc511d3a93 Ralph <ralph@ralphmail.com> 1755748433 +0800 WIP on feature: cc745c8 Feature change
│ └── HEAD
│
├── objects/ # 無變化,做出來的物件不會消失
│ ├── ...
│ └── info/
│ └── pack/
│
├── refs/
│ ├── heads/
│ │ ├── feature
│ │ └── main
│ ├── tags/
│ └── stash # 仍為2881edecc2e2c832858db96df2d29abc511d3a93
│
├── COMMIT_EDITMSG
├── config
├── description
├── HEAD
├── index
└── ORIG_HEAD # 內文仍為cc745c806c5dabd2ca4bbdc10e9fa2a60245cff3
在 /logs/refs/stash 中,變成只有下面這條:
0000000000000000000000000000000000000000 2881edecc2e2c832858db96df2d29abc511d3a93 Ralph <ralph@ralphmail.com> 1755748433 +0800 WIP on feature: cc745c8 Feature change
在 main 分支上的 stash commit 紀錄消失了,2881ede... 變成首個 stash 有關的 commit。
在 main 分支上下 git add 與 git commit 後,切回 feature 分支:
接著用以下指令,把 feature 分支上的 stash 抓回來:
git stash pop stash@{0}
此時 .git/ 資料夾變成:
.git/
├── hooks/
├── info/
│
├── logs/
│ ├── refs/
│ │ └── heads/ # stash檔案消失
│ │ ├── feature
│ │ └── main
│ └── HEAD
│
├── objects/ # 無變化
│ ├── ...
│ └── info/
│ └── pack/
│
├── refs/ # stash檔案消失
│ ├── heads/
│ │ ├── feature
│ │ └── main
│ └── tags/
│
├── COMMIT_EDITMSG
├── config
├── description
├── HEAD
├── index
└── ORIG_HEAD # 內文仍為cc745c806c5dabd2ca4bbdc10e9fa2a60245cff3
/logs/refs 跟 refs/ 的 stash 檔案都消失了!但是之前因為兩次 git stash 做出來的物件全部都還在。
使用 git stash 將檔案暫存時,一共會生成以下 git 物件:
index 暫存狀態的 commit 物件,上代為對應分支上最新的 commit 物件。stash 這項參考指向此,上代包含 3. 中代表預存區 index 的 commit 物件,以及對應分支上最新的 commit 物件。由 3. 生成的 commit 物件會指向最近一次放在預存區 index 對應的 tree 物件,如果是經 git add 才 git stash,會指向經 git stash 之後才生成之 tree 物件;如果未經 git add 即 git stash,則會指向原本就已經存在的 tree 物件。
當以 git stash pop 把暫存狀態抓回來後,.git/refs 與 .git/logs/refs 的 stash 檔案會把被重新拉回去的暫存狀態資訊移除,但因為 git stash 指令生成的 git 物件不會消失。