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 优化你的工作流

作者头像GitLab阅读 9 分钟

无论你之前是否使用过 Git Stash、正在使用它,还是对替代工作流感到好奇,这篇文章都适合你。我们将深入探讨 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 命令显示你现有的 Stash,从最新的开始,编号从 0 开始。在本例中,我们看到一个 Stash:

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

切换分支时使用 Stash

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

在切换分支时存储更改有一些缺点:

  • 创建 Stash 后,你可能会完全忘记它的存在并重复工作。
  • 很容易忘记哪个分支属于哪个 Stash。每当 Stash 中的更改建立在未合并的功能分支之上时,你可能难以取消 Stash 这些更改,除非你在正确的分支上。
  • 如果在此期间对分支进行了更多更改,或者分支可能已被重新整理,则可能难以将 Stash 重新应用到分支。
  • Stash 不会备份到服务器上。当你的本地副本消失时(例如,如果存储库被删除或硬盘出现故障),你的更改就会丢失。

切换分支的替代工作流

与其使用 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 之前重新整理分支。这将重新整理整个分支,包括 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
  • 你可以指定在何处写入问候语。
  • 问候语将根据一天中的时间而有所不同。

在这种情况下,我们希望分别提交每个功能更改。在许多情况下,你可以使用 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 的所有更改来解决冲突。在本例中,theirs 是 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"

搞定!你最终得到三个提交的历史记录,每个提交一次添加一项功能更改。

总结

Stash 有各种用例。我不建议在切换分支时使用它来保存更改。相反,我建议进行可以轻松推送和管理的临时提交。我使用别名来简化此工作流,并使其不易出错。另一方面,Stash 非常适合将大型相关的提交分解成较小的单个提交。考虑到这一点,你可以维护更清晰的项目历史记录,并确保你的工作始终得到备份和整理。

希望你喜欢阅读。如果你对这篇文章中每个提交中使用的测试感兴趣,可以在 https://gitlab.com/toon/greetings 访问此示例项目。

关于作者

Toon Claes 是 GitLab 的高级后端工程师,拥有 C & C++ 以及 Web 和移动开发背景。他对机械键盘充满热情,一直在寻找完美的键盘。作为一名忠实的 GNU Emacs 用户,Toon 主动参与社区,并喜欢使用 Org 模式进行他的项目。

这是一篇由 GitLab 赞助的文章。GitLab 是一个全面的基于 Web 的 DevSecOps 平台,提供 Git 存储库管理、问题跟踪、持续集成和部署管道功能。它既有开源版本,也有专有版本,旨在涵盖整个 DevOps 生命周期,使其成为寻求单一平台来管理代码和操作数据的团队的热门选择。

关注 MDN 最新动态

订阅 MDN 新闻通讯,不错过任何关于最新 Web 开发趋势、技巧和最佳实践的更新。