使用 Git stash 最佳化您的工作流程
無論您是第一次使用 Git stash,已經在使用它,還是對替代工作流程感到好奇,這篇文章都適合您。我們將深入探討暫存的用例,討論它的一些弊端,並介紹一種更安全、更方便地管理未提交程式碼的替代方法。讀完這篇文章,您將更好地理解如何有效地使用 stash,並發現改進工作流程的不同策略。
什麼是 Git stash?
您可能聽說過 git stash。這是一個 Git 內建命令,可用於存放未提交的本地更改。例如,當您的工作目錄中有已修改的檔案(通常稱為“髒”狀態)時,git status 可能會顯示類似以下內容:
$ git status
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: main.go
no changes added to commit (use "git add" and/or "git commit -a")
當您想儲存這些更改,但又不想將它們提交到當前分支時,可以改為將它們暫存:
$ git stash
Saved working directory and index state WIP on main: 821817d some commit message
這將清理您的工作目錄。
$ git status
On branch main
nothing to commit, working tree clean
git stash list 命令會顯示您現有的暫存,從最新的開始,依次編號為 0。在此示例中,我們看到一個暫存:
$ git stash list
stash@{0}: WIP on main: 821817d some commit message
切換分支時的暫存
Git stash 最常見的用例是,在切換分支以處理其他事情之前,您想臨時存放任何正在進行的程式碼。例如:
$ git status
On branch feature-a
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: lib/feature-a/file.go
no changes added to commit (use "git add" and/or "git commit -a")
$ git stash
Saved working directory and index state WIP on feature-a: fd25af5 start feature A
$ git switch feature-b
# ... start working of feature B
切換分支時暫存更改存在一些缺點:
- 建立暫存後,您可能會完全忘記它的存在,從而導致重複工作。
- 很容易忘記一個暫存屬於哪個分支。每當暫存中的更改建立在尚未合併的特性分支之上時,除非您位於正確的分支上,否則可能很難恢復這些暫存的更改。
- 如果在此期間該分支上發生了更多更改,或者該分支可能已被 rebase,則可能難以將暫存重新應用到該分支。
- 暫存不會在伺服器上進行備份。當您的本地副本消失時(例如,儲存庫被刪除或硬碟發生故障),您的更改就會丟失。
切換分支的替代工作流程
與使用 Git stash 存放本地更改不同,可以考慮將其提交到分支。這些提交將是臨時的,您應該在提交訊息中清楚地說明這一點,例如透過將其標題設為“WIP”。您可以透過執行以下命令來完成此操作:
git add .
git commit -m "WIP"
# or 'git commit -mWIP'
稍後,當您返回該分支並看到最後一個提交的標題為“WIP”時,您可以使用以下命令回滾它:
git reset --soft HEAD~
這將從當前分支中刪除最後一個提交,但保留工作目錄中的更改。為了使此過程更方便,您可以為它設定兩個 別名:
git config --global alias.wip '!git add -A && git commit -mWIP'
git config --global alias.unwip '!git reset --soft $(git log -1 --format=format:"%H" --invert-grep --grep "^WIP$")'
這些別名添加了兩個 Git 子命令:
git wip:此命令會暫存所有本地更改(包括未跟蹤的檔案),並將一個標題為“WIP”的提交寫入當前分支。git unwip:此命令使用git log從當前分支的尖端查詢一個標題不是“WIP”的提交。然後使用--soft重置到該提交,將更改保留在工作目錄中。
現在,當您有本地更改並需要切換分支時,只需輸入 git wip,更改就會儲存在當前分支中。如果您在一個特性分支上,並且您的團隊工作流程可以接受重寫這些分支的歷史記錄,那麼您甚至可以推送該分支以備份這些更改。稍後,當您返回該分支時,可以鍵入 git unwip 繼續處理這些更改。如果您的工作流程允許,您可以在使用 git unwip 之前 rebase 該分支。這將 rebase 整個分支,包括 WIP 更改,以確保您正在處理目標分支的最新版本。
git unwip 命令設計用於在任何分支上工作。如果當前分支的尖端沒有 WIP 提交,則什麼都不會發生。如果尖端有多個提交,它們都會被撤銷。如果 WIP 提交之上有一個非 WIP 提交,它將不會被回滾。您需要手動解決這個問題。
警告:由於 git wip 會提交所有未跟蹤的檔案,請確保包含敏感資訊的檔案都已新增到您的 .gitignore 檔案中。否則,它們將成為 Git 歷史記錄的一部分,您可能會意外地將它們推送到所有人都可以訪問的遠端倉庫。
何時使用 Git stash
如前所述,Git stash 不適合在切換分支時使用。Git stash 更好的用例是分解提交。
關於所謂的“提交衛生”已經有很多討論,並且有很多不同的觀點。如果每次提交都能講述自己的故事,那麼它會非常有益。每次提交都只做一個功能性更改,最好附帶一個寫得好的提交訊息。在更小的提交的工作流程中,程式碼審查者可以逐個提交併逐步理解故事,這將更容易。如果需要,它還可以使您能夠回滾一小部分更改。
想象一下您有一個 Go 專案,以下是您開始時的示例:
package main
import "fmt"
func Greet() {
fmt.Print("Hello world!")
}
func main() {
Greet()
}
當您執行此程式碼時,它會列印“Hello world!”。出於各種原因,您需要重構它。進行了一系列更改後,您會得到:
package main
import (
"fmt"
"io"
"os"
"time"
)
var now = time.Now
func Format(whom string) string {
greeting := "Hello"
if h := now().Hour(); 6 < h && h < 12 {
greeting = "Good morning"
}
return fmt.Sprintf("%v %v!", greeting, whom)
}
func Greet(w io.Writer) {
fmt.Fprint(w, Format("world"))
}
func main() {
Greet(os.Stdout)
}
主要功能保持不變,但有一些功能更改:
- 您可以指定要問候的
whom。 - 您可以指定要寫入問候語的
where。 - 問候語將根據一天中的時間而有所不同。
在這種情況下,我們希望單獨提交這些功能性更改。在許多情況下,您可以使用 git add -p 來一次暫存一小部分程式碼,但在這種情況下,更改過於交織。這時 git stash 就派上用場了。在下面的步驟中,我們將使用它作為備份,其中我們將最終結果儲存在 stash 中,應用它,然後撤銷當前功能性更改不需要的更改。由於最終結果儲存在 stash 中,我們可以為要進行的每個提交重複此過程。
讓我們來看看:
git stash push --include-untracked
這會將所有本地更改儲存在 stash 中,然後您可以開始將更改分解為單獨的提交。--include-untracked 選項還將包含從未提交過的檔案,如果您添加了新檔案,這將很有用。
現在我們可以開始處理第一個提交了。鍵入 git stash apply 將更改從 stash 恢復到您的本地工作目錄:
$ git stash apply
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: main.go
no changes added to commit (use "git add" and/or "git commit -a")
在您喜歡的編輯器中開啟 main.go,並對其進行修改,使其包含新增 whom 的更改。這可能看起來像:
package main
import (
"fmt"
)
func Greet(whom string) string {
return fmt.Sprintf("Hello %v!", whom)
}
func main() {
fmt.Print(Greet("world"))
}
在此過程中,您可以丟棄所有不需要的更改,因為最終結果已安全地儲存在 stash 中。這意味著您可以修改程式碼,使其能夠正確編譯並確保測試透過這些更改。當您滿意後,可以像往常一樣提交這些更改:
git add .
git commit -m "allow caller to specify whom to greet"
我們可以為下一個提交重複這些步驟。鍵入 git stash apply 開始。不幸的是,這可能會導致衝突:
$ git stash apply
Auto-merging main.go
CONFLICT (content): Merge conflict in main.go
Recorded preimage for 'main.go'
On branch main
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: main.go
no changes added to commit (use "git add" and/or "git commit -a")
解決衝突超出了本文的範圍,但在這種情況下,有一種快速恢復 stash 中的更改的方法可能效果很好:
git restore --theirs .
git restore --staged .
讓我們看看它的作用。git restore --theirs 命令會告訴 Git 透過採用他們的(theirs)所有更改來解決衝突。在這種情況下,their 是 stash,它將從中應用更改。git restore --staged . 命令將取消暫存這些更改,這意味著它們不再新增到索引中,並且在下次鍵入 git commit 時會被忽略。
現在您可以再次開始修改程式碼,最終可能會得到類似以下的內容:
package main
import (
"fmt"
"io"
"os"
)
func Greet(w io.Writer, whom string) {
fmt.Fprintf(w, "Hello %v!", whom)
}
func main() {
Greet(os.Stdout, "world")
}
在這裡,您可以重複常用命令來編寫另一個提交:
git add .
git commit -m "allow caller to specify where to write the greeting to"
對於最後的提交,只需執行:
git stash apply
git checkout --theirs .
git reset HEAD
git add .
git commit -m "use different greeting in the morning"
您就完成了!您最終得到了三個提交歷史,每個提交一次新增一個功能更改。
總結
Stashing 有各種用例。我不建議將其用於切換分支時儲存更改。我更推薦進行臨時的、易於推送和管理的提交。我使用別名來簡化此工作流程並減少錯誤。另一方面,stashing 非常適合將大的、相關的提交分解成更小的、獨立的提交。考慮到這一點,您可以維護一個更乾淨的專案歷史記錄,並確保您的工作始終得到備份和組織。
希望您閱讀愉快。如果您對本文中每個提交使用的測試感興趣,可以訪問 https://gitlab.com/toon/greetings 處的示例專案。
關於作者
Toon Claes 是 GitLab 的高階後端工程師,擁有 C & C++ 以及 Web 和移動開發背景。他對機械鍵盤充滿熱情,並一直在尋找最完美的鍵盤。作為一名忠實的 GNU Emacs 使用者,Toon 積極參與社群活動,並喜歡為他的專案使用 Org mode。
這是一篇由 GitLab 贊助的文章。 GitLab 是一個全面的基於 Web 的 DevSecOps 平臺,提供 Git 儲存庫管理、問題跟蹤、持續整合和部署流水線功能。它有開源和專有版本,旨在覆蓋整個 DevOps 生命週期,使其成為團隊尋找單一平臺來管理程式碼和運營資料的熱門選擇。