每个开发者迟早都会问的问题
上个月还是好好的,你很确定。侧边栏对得整整齐齐,日期解析器能正确处理时区,按钮也是恰到好处的蓝色。然后一份 bug 报告砸了过来,你盯着一个有两年历史的文件,试图回答一个看似简单的问题:这是什么时候变的?哪个 commit 是罪魁祸首?
紧接着就是第二个问题:是谁改的,为什么要改?commit 信息也许能解释,作者也许还记得。但你得先找到那个 commit,而脑力体操正是从这里开始的。
CLI 工具箱,以及它的短板
调查一个文件的历史所需要的一切,Git 其实都已具备。这些工具强大且久经考验,摩擦不在于任何单条命令,而在于你必须把多少条命令串联起来。
git log --follow
git log --follow --oneline -- src/styles/sidebar.css
这条命令会列出所有改动过该文件的 commit,即使文件被重命名过也能追踪。作为起点它没有问题,但它给你的只是 SHA 和 commit 信息,而不是内容。想看文件在某个时间点的实际模样,你需要下一条命令。
git show SHA:path
git show a3b4c5d:src/styles/sidebar.css
这条命令会输出该 commit 时文件的完整内容。要比较两个版本,就得运行两次再对比输出,或者使用 git diff a3b4c5d 9f2e1d0 -- src/styles/sidebar.css。然后对每一个候选 commit 重复这个过程。如果有三十个 commit 改动过这个文件,你就得复制粘贴 SHA 好一阵子,还要全程在脑子里记住各版本之间的差异。
git blame
git blame src/styles/sidebar.css
blame 会为每一行标注最后修改它的 commit。当可疑的那一行还在文件里,而你想知道是谁写的,它非常出色。但当回归问题来自一行已被删除的代码,或者某行最近的一次改动只是无害的格式调整,反而掩盖了你真正关心的那次变更时,它就没那么管用了。于是你只能一遍又一遍地运行 git blame SHA^ -- path,一层一层地剥开历史。
git bisect
bisect 会在整个历史上执行二分查找,定位破坏了某个可测试行为的 commit。当"坏了"意味着构建失败或测试变红时,bisect 正是对的工具,任何 GUI 功能都无法取代它。但对于"这条 CSS 规则是什么时候改的?"这类问题,它就是杀鸡用牛刀了。你不需要在每一步都 checkout 并重新构建项目,你只是想看看一个文件随时间的变化而已。
所以,对于"看着这个文件演变"这个需求,CLI 的答案是:用 log 列出 commit,用 show 输出每个版本,手动对每一对做 diff,并把所有状态都记在脑子里。每条命令都没问题,有问题的是这套工作流。
文件时间轴:可视化的答案
GitSquid 在 v2.6 中推出了文件时间轴,并且包含在免费版中。思路很简单:与其在脑子里逐个 commit 重建文件的演变,不如直接在屏幕上看着它发生。
右键即可打开
右键点击任意文件,选择显示历史。文件历史视图随即打开,完全聚焦于这一个文件。
每个 commit 一个刻度
文件查看器上方有一条时间轴:每一个改动过该文件的 commit 对应一个刻度。文件的一生就这样横向铺开。一个只有三个 commit 的文件看起来很平静;而一个有六十个刻度的文件,在你读任何一行代码之前,就已经告诉你它的变动有多频繁。
拖动滑块浏览版本
拖动滑块,查看器会随着拖动实时更新。这正是这个功能的核心:你不是在离散的快照之间跳转,还要在脑中比对差异,而是亲眼看着文件发生变化。第一次拖完全程之后,来回拖动几乎是即时响应,所以只要把历史过一遍,你就可以在可疑区域附近自由来回,逐步锁定发生变化的那个 commit。
悬停查看谁改了什么
把鼠标悬停在任意刻度上,就能看到那个历史时刻的作者和 commit 信息。"是谁在什么时候写的"这个问题,不用离开时间轴,也完全不用运行 blame,就有了答案。
按下播放键
点击 ▶,GitSquid 会自动播放整段历史。第一次浏览一个陌生文件时,这是建立直觉最快的方式:你会看到结构出现、成长、被重构。这就是读三十个 diff 和看一段延时摄影之间的区别。
Snapshot 或 Diff
头部的 Snapshot / Diff 开关可以在两种视图间切换:文件在该 commit 时的完整内容,或者经典的与父 commit 对比的 diff。Snapshot 模式回答"文件当时长什么样?",Diff 模式回答"这个 commit 到底改了什么?"。追查回归问题通常从 Snapshot 模式开始,在 Diff 模式中结束。
跳转到某个版本
一旦找到你关心的版本,右侧的三个操作能带你更进一步:复制 SHA 把 commit 哈希放进剪贴板,随处可用;打开 commit 跳转到完整的 commit,让你看到除了这个文件之外还改了什么;当你需要真正运行旧代码时,Checkout detached 会把工作目录精确切换到那个 commit。
实战演练:定位一个 CSS 回归
来看一个具体的例子。应用的侧边栏丢了 padding,没人知道是从什么时候开始的。用文件时间轴来追查的过程是这样的:
- 右键
sidebar.css→ 显示历史。时间轴显示出横跨八个月的 24 个刻度。 - 从最新往最旧拖动,同时盯着 padding 规则。在大约三分之二处,规则发生了肉眼可见的变化。第一遍过完之后拖动是即时的,于是你在那个区域附近来回拨动滑块,直到锁定数值翻转的那个刻度。
- 悬停在那个刻度上。是一位队友,三周前,在一个标注为布局重构的 commit 里改的。谜底解开了一半。
- 切换到 Diff。开关显示出与父 commit 的 diff:padding 被合并成了简写属性,转换过程中丢了一个值。回归问题就在这里。
- 打开 commit。完整的 commit 显示,这次批量改动一共触及了十二个文件,正是那种容易在 review 中被放过的变更。如果你的团队还在争论这类变更究竟应该以什么方式进入历史,可以看看 rebase 与 merge 之争。
- 复制 SHA 并修复。找出肇事 commit 之后,你可以 revert 它、向前修复,或者用 cherry-pick 绕过它。哪种情况适合哪种方案,我们的指南如何撤销一个 Git commit有详细介绍。
总耗时:几分钟。不用背任何 SHA,也不用在终端回滚记录里做考古。
什么时候 blame 或 bisect 仍然是对的工具
时间轴并不能取代工具箱里的其他工具,假装它能取代是不诚实的:
- 用 blame:当你已经盯着某一行,想就地知道它的最后作者时。对于一行最近没被重新格式化过的代码,blame 仍然是最短路径。
- 用 bisect:当症状是"构建坏了"或"某个测试失败",而你不知道哪个文件是元凶时。bisect 搜索的是整个仓库的历史,时间轴检查的是单个文件,它们回答的是不同的问题。
- 用时间轴:当问题围绕单个文件的演变时。它什么时候变的,谁改的,改之前长什么样。
看着你自己的代码演变
文件时间轴包含在 GitSquid 的免费版中:无需账号,没有遥测,在 macOS、Windows 和 Linux 上原生运行。打开一个仓库,右键点击那个你一直好奇的文件,按下播放键。
下载 GitSquid 免费版,让你的下一次回归追查拥有一条时间轴,而不是一串 SHA 列表。