Optimize your workflow with Git stash title over a gradient background. A git icon in the bottom-left corner. A git "branch" icon in the top-right corner.
贊助

使用 Git stash 最佳化您的工作流程

閱讀時間 8 分鐘

無論您是第一次使用 Git stash,已經在使用它,還是對替代工作流程感到好奇,這篇文章都適合您。我們將深入探討暫存的用例,討論它的一些弊端,並介紹一種更安全、更方便地管理未提交程式碼的替代方法。讀完這篇文章,您將更好地理解如何有效地使用 stash,並發現改進工作流程的不同策略。

什麼是 Git stash?

您可能聽說過 git stash。這是一個 Git 內建命令,可用於存放未提交的本地更改。例如,當您的工作目錄中有已修改的檔案(通常稱為“髒”狀態)時,git status 可能會顯示類似以下內容:

bash
$ 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")

當您想儲存這些更改,但又不想將它們提交到當前分支時,可以改為將它們暫存:

bash
$ git stash
Saved working directory and index state WIP on main: 821817d some commit message

這將清理您的工作目錄。

bash
$ git status
On branch main
nothing to commit, working tree clean

git stash list 命令會顯示您現有的暫存,從最新的開始,依次編號為 0。在此示例中,我們看到一個暫存:

bash
$ git stash list
stash@{0}: WIP on main: 821817d some commit message

切換分支時的暫存

Git stash 最常見的用例是,在切換分支以處理其他事情之前,您想臨時存放任何正在進行的程式碼。例如:

bash
$ 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”。您可以透過執行以下命令來完成此操作:

bash
git add .
git commit -m "WIP"
# or 'git commit -mWIP'

稍後,當您返回該分支並看到最後一個提交的標題為“WIP”時,您可以使用以下命令回滾它:

bash
git reset --soft HEAD~

這將從當前分支中刪除最後一個提交,但保留工作目錄中的更改。為了使此過程更方便,您可以為它設定兩個 別名

bash
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 專案,以下是您開始時的示例:

go
package main

import "fmt"

func Greet() {
	fmt.Print("Hello world!")
}

func main() {
	Greet()
}

當您執行此程式碼時,它會列印“Hello world!”。出於各種原因,您需要重構它。進行了一系列更改後,您會得到:

go
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 中,我們可以為要進行的每個提交重複此過程。

讓我們來看看:

bash
git stash push --include-untracked

這會將所有本地更改儲存在 stash 中,然後您可以開始將更改分解為單獨的提交。--include-untracked 選項還將包含從未提交過的檔案,如果您添加了新檔案,這將很有用。

現在我們可以開始處理第一個提交了。鍵入 git stash apply 將更改從 stash 恢復到您的本地工作目錄:

bash
$ 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 的更改。這可能看起來像:

go
package main

import (
	"fmt"
)

func Greet(whom string) string {
	return fmt.Sprintf("Hello %v!", whom)
}

func main() {
	fmt.Print(Greet("world"))
}

在此過程中,您可以丟棄所有不需要的更改,因為最終結果已安全地儲存在 stash 中。這意味著您可以修改程式碼,使其能夠正確編譯並確保測試透過這些更改。當您滿意後,可以像往常一樣提交這些更改:

bash
git add .
git commit -m "allow caller to specify whom to greet"

我們可以為下一個提交重複這些步驟。鍵入 git stash apply 開始。不幸的是,這可能會導致衝突:

bash
$ 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 中的更改的方法可能效果很好:

bash
git restore --theirs .
git restore --staged .

讓我們看看它的作用。git restore --theirs 命令會告訴 Git 透過採用他們的(theirs)所有更改來解決衝突。在這種情況下,their 是 stash,它將從中應用更改。git restore --staged . 命令將取消暫存這些更改,這意味著它們不再新增到索引中,並且在下次鍵入 git commit 時會被忽略。

現在您可以再次開始修改程式碼,最終可能會得到類似以下的內容:

go
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")
}

在這裡,您可以重複常用命令來編寫另一個提交:

bash
git add .
git commit -m "allow caller to specify where to write the greeting to"

對於最後的提交,只需執行:

bash
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 生命週期,使其成為團隊尋找單一平臺來管理程式碼和運營資料的熱門選擇。