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 生命周期,使其成为团队寻找单一平台来管理代码和运营数据的热门选择。