← 返回博客

逐行staging:精确添加你想要的内容

feature tutorial

为什么精细化 Staging 很重要

一个精心构建的 Git 历史讲述着一个故事。每个 commit 代表一个单独的逻辑变更:一个 bug 修复、一次重构、一个新功能。但现实中的开发很少那么整洁。你修复了一个 bug,然后注意到附近的一个拼写错误,然后调整了一些格式,然后开始了一个小的重构——全都在同一个文件里。如果你一次性将整个文件 staging 并 commit,你会得到一个混合了不相关变更的 commit。这使得代码审查更困难,使用 bisect 追踪 bug 更困难,也使 commit 历史作为文档的价值降低。

精细化 staging 让你可以将工作拆分为有意义的 commit,即使变更在同一个文件中交织在一起。你不是一次性 commit 所有东西,而是精确选择哪些变更属于同一组。

Staging 的三个层级

文件级 staging

最基本的层级。你用 git add 将整个文件 staging:

git add src/auth.ts src/utils.ts

当每个文件只包含一个逻辑变更时,这样做是可以的。但一旦文件包含多个不相关的修改,文件级 staging 就太粗糙了。

Hunk 级 staging

Git 可以将文件的变更拆分为"hunk"——由未更改的代码分隔的连续修改行块。使用 git add -p(补丁模式),Git 逐个展示每个 hunk,并询问是否要 staging:

git add -p src/auth.ts

对于每个 hunk,你选择 y(staging)、n(跳过)、s(拆分为更小的 hunk)或 e(手动编辑)。这是对文件级 staging 的重大改进,但有局限性。Hunk 由邻近性定义:如果两个不相关的变更靠得很近(间隔不到三行上下文),Git 会将它们合并为一个 hunk。分离它们需要手动编辑补丁,这既繁琐又容易出错。

行级 staging

行级 staging 是最精确的选项。你不是处理文件或 hunk,而是选择要 staging 的单独行。想要 staging 第12行和第15行但不要第14行?可以做到。这让你完全控制每个 commit 包含什么,无论文件中的变更有多接近。

在命令行中,行级 staging 意味着用 git add -pe 选项手动编辑补丁。你需要理解 unified diff 格式,小心地修改补丁,并希望不会引入错误。它可以工作,但不是大多数开发者想要经常做的事。

GitSquid 中的行级 Staging

GitSquid 使行级 staging 变得可视化且简单直接。当你选择一个修改过的文件时,diff 查看器会显示每一个更改的行。要 staging 特定行:

  • 点击单行来选择它。
  • Cmd+Click(Windows/Linux 上为 Ctrl+Click)选择多个单独的行。
  • Shift+Click 选择一个行范围。

选择了想要的行之后,将它们 staging。只有这些行被添加到索引中。其余的作为未 staging 的修改留在你的工作目录中,准备包含在不同的 commit 中。

同样的工作流程也可以反向使用。如果你已经 staging 了想要从 staging 区域移除的行,可以在已 staging 的 diff 中选择它们,只取消这些行的 staging。

底层工作原理

当你 staging 单独的行时,GitSquid 在服务器端生成一个只包含选定变更的补丁。然后使用 Git 的补丁机制将此补丁应用到索引。通过在服务器端处理补丁生成而不是在客户端操作 diff,结果是可靠的,并且与 Git 期望的完全匹配。不会出现手动编辑补丁时可能遇到的格式问题或偏移错误。

实际示例

将 bug 修复与重构分离

你正在进行重构并注意到一个 bug。因为修复方法很明显,所以你立即修复了它。现在你的文件包含两种类型的变更:重构和 bug 修复。通过行级 staging,你只选择 bug 修复的行,用清晰的消息如"fix null check in auth validation"进行 commit,然后单独 commit 重构部分。结果是两个干净、可审查的 commit,而不是一个混乱的 commit。

将调试代码与生产代码分离

你在调试时在实际代码更改旁边添加了一些 console.log 语句。与其试图记住哪些行是调试的、哪些是真实的,你可以直观地只选择生产变更,staging 并 commit。调试行留在你的工作目录中,你可以继续使用它们或稍后丢弃。

为审查准备部分 commit

你有大量的变更,但只有部分工作准备好了接受审查。行级 staging 让你可以 commit 完成的部分,同时保持进行中的代码未 commit。你的 pull request 保持聚焦,审查者不必费力翻阅不完整的代码。

保持 Commit 的原子性

原子性 commit 是包含单个完整逻辑变更的 commit。它通过测试,不破坏构建,可以独立理解。原子性 commit 不仅仅是整洁的问题。它们有实际的好处:

  • 代码审查。审查者可以独立理解每个 commit。一个标题为"fix input validation"且只涉及验证逻辑的 commit 很容易审查。一个标题为"various changes"且涉及验证、样式和配置的 commit 就不是了。
  • Bug 追踪。当出现问题时,git bisect 可以精确定位引入问题的 commit。原子性 commit 使结果有意义:你找到的是导致 bug 的具体变更,而不是一堆不相关修改的杂烩。
  • Revert。如果需要 revert 一个 commit,原子性 commit 可以干净地 revert。混合的 commit 可能迫使你 revert 实际上想保留的变更。
  • 历史即文档。几个月后,当有人阅读 commit 历史以理解一段代码存在的原因时,带有清晰消息的原子性 commit 讲述了一个有用的故事。

行级 staging 是使原子性 commit 变得实用的工具。没有它,从混乱的实际开发中创建干净的 commit 需要纪律和手动努力。有了它,过程就像选择属于同一组的行一样简单。

最好的 commit 是那些恰好做一件事的 commit。精细化 staging 给你控制权来实现这一点,即使你的工作目录在讲述不同的故事。