如何在 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)。
    • 也有人干脆回退到上一个稳定版本,而不是去追查单个坏提交。

用于部分提交的工具与工作流

  • CLI 与 GUI/TUI 之间的分歧很大:
    • CLI 支持者:git add -p/--patch,以及 git resetgit checkout 的相关用法;它们因速度快、普及度高、并且能强迫更仔细地审查而受到赞扬。像“split”和“edit”这样的选项允许更细粒度的暂存,甚至可以编辑单行。
    • 批评者:觉得 git add -p 的交互式界面令人困惑、缺乏上下文,不如 GUI 里的按行选择。
  • 提到的热门替代方案:
    • TUI/CLI:lazygittigstgitgit guigit-colagit-crecordmagit(Emacs)、vim-fugitive
    • IDE/GUI:VS Code(“stage selected ranges”,但过去有 CRLF bug)、JetBrains changelists、GitHub Desktop、SourceTree、Sublime Merge。
  • 还有几个人指出,在现代 IDE 里,部分暂存很容易且符合人体工学,而且不同工具适合不同工作流。

更广泛的质量与速度张力

  • 有人认为,“高生产力”往往意味着以代码和历史质量为代价去快速交付。
  • 也有人强调“糟糕”的流程和代码会带来长期成本,一旦被广泛采用,就很难替换。