如何在 Git 中提交文件的一部分
把改动拆成小而专注的 Git 提交,被认为对调试、代码审查和安全回滚至关重要,但许多开发者要么把整个 PR squash 掉,要么把无关的修改混在一起。评论者比较了强制原子、始终可用提交的工作流,与偏好每个 PR 只保留一个提交历史的做法,并讨论了命令行工具如 `git add -p` 与 GUI 和 IDE 集成(VS Code、JetBrains、Magit、lazygit 等)在暂存文件部分内容上的优劣。总体来看,这场讨论强调了,掌握部分提交和有意识地塑造历史,是专业软件开发中核心却常被忽视的一项技能。
提交粒度与良好历史记录的价值
- 许多人认为,把改动拆分成小而连贯的提交被低估了,但实际上非常有用,尤其是在调试和回滚故障时。
- 也有人指出,在实践中,一些团队很少回滚单个提交;他们往往回退部署,或者再写一个新的“修复”提交。
- 很多人强烈认为,精心构造好的提交(以及学好 Git)是一项被低估的技能,它与软件质量和“先发出来再说”式生产力之间的更广泛取舍有关。
Squash 与增量提交,以及 revert
- 一派更喜欢在每个 PR 上使用 squash merge:
- PR 是审查单位,而不是每个中间提交。
- 这样会得到线性的、可二分定位(bisectable)的历史记录,并且不会有损坏的中间状态。
- Squash 有助于去掉“修个 typo / 其实能跑了”这类噪音提交,并重写成一条更好的提交信息。
- 另一派则偏好不 squash 的原子提交:
- 每个提交都应该能构建并通过测试,这样更便于 bisect 和细粒度回滚。
- Squash 可能会丢失重要上下文(例如文件移动和重构),也会让认真排查 bug 更困难。
- 有人指出,如果 squash 之后变成难以阅读的一大坨,那这个 PR 可能本来就太大,或者混杂了不同关注点。
- 关于合并策略的另一条讨论:
- 有人建议在功能分支上使用 rebase,但在主分支上使用 merge commit,这样既能保留按 PR 分组的提交,也能保留线性的逐提交历史(
--first-parent与--no-merges)。 - 也有人干脆回退到上一个稳定版本,而不是去追查单个坏提交。
- 有人建议在功能分支上使用 rebase,但在主分支上使用 merge commit,这样既能保留按 PR 分组的提交,也能保留线性的逐提交历史(
用于部分提交的工具与工作流
- CLI 与 GUI/TUI 之间的分歧很大:
- CLI 支持者:
git add -p/--patch,以及git reset和git checkout的相关用法;它们因速度快、普及度高、并且能强迫更仔细地审查而受到赞扬。像“split”和“edit”这样的选项允许更细粒度的暂存,甚至可以编辑单行。 - 批评者:觉得
git add -p的交互式界面令人困惑、缺乏上下文,不如 GUI 里的按行选择。
- CLI 支持者:
- 提到的热门替代方案:
- TUI/CLI:
lazygit、tig、stgit、git gui、git-cola、git-crecord、magit(Emacs)、vim-fugitive。 - IDE/GUI:VS Code(“stage selected ranges”,但过去有 CRLF bug)、JetBrains changelists、GitHub Desktop、SourceTree、Sublime Merge。
- TUI/CLI:
- 还有几个人指出,在现代 IDE 里,部分暂存很容易且符合人体工学,而且不同工具适合不同工作流。
更广泛的质量与速度张力
- 有人认为,“高生产力”往往意味着以代码和历史质量为代价去快速交付。
- 也有人强调“糟糕”的流程和代码会带来长期成本,一旦被广泛采用,就很难替换。