whyso — git blame 看不到的那部分

git blame 能告诉你谁、何时、改了什么。whyso 告诉你为什么改。


问题

想知道一行代码为什么长这个样子,我们能做的无非是看 git blame 和提交记录。

$ git blame internal/handler/page.go
a3f1b2c (parkjunwoo 2026-03-08) func CreatePage(c *gin.Context) {

谁改的、什么时候改的、改了什么——都有。为什么改——没有。

“在提交信息里写原因不就行了?“现实是这样的:

fix: update handler
refactor: clean up
wip

写好提交信息是一种自律。不管团队约定定得多严,凌晨三点修 bug 的时候,很少有人会想着"让我认真把这次变更的背景和理由记录在提交信息里”。

但在 AI 编程时代,情况不同了。变更的理由已经被记录下来了。


Claude Code 的会话数据

Claude Code 把所有对话以 JSONL 文件的形式保存在 ~/.claude/projects/ 下。每条记录包含用户消息、AI 回复、tool_use(Write、Edit、Bash)调用,以及将它们串联起来的 parentUuid 链。

用户:"开始实现 whyso validate 命令"
  → AI:Write internal/crosscheck/crosscheck.go
    → AI:Edit internal/crosscheck/crosscheck.go
      → AI:"初始化 crosscheck 包 — 验证 SSaC↔OpenAPI operationId 匹配"

文件为什么被创建、为什么被修改,都留在对话本身里。比提交信息更丰富,也不需要刻意去写。只要进行了对话,就会自动留下记录。

whyso 解析这些数据,提取每个文件的变更历史。


看见文件的一生

file: internal/crosscheck/crosscheck.go
created: 2026-03-08T14:23:01Z
history:
  - timestamp: 2026-03-08T14:23:01Z
    session: 09351222-d7be-41fe-994f-87c2d7067e5d
    user_request: "开始实现 whyso validate 命令"
    answer: "初始化 crosscheck 包 — 验证 SSaC↔OpenAPI operationId 匹配"
    tool: Write

  - timestamp: 2026-03-09T10:15:33Z
    session: 4e9b4e5e-3a50-43f2-be6e-e5db228ecc3b
    user_request: "加上验证 x-sort 列是否存在于 DDL 中"
    answer: "新增 x-sort/x-filter 列 → DDL 交叉验证"
    tool: Edit

  - timestamp: 2026-03-10T09:41:22Z
    session: b2e43b4f-cb21-4286-975d-1eb9de8a16c0
    user_request: "也加上 Func spec 的交叉验证"
    answer: "新增 @call ↔ Func spec 参数数量/类型交叉验证"
    tool: Edit

为什么被创建、经历了哪些请求、如何演化——一个文件的完整一生一目了然。


为什么需要它

代码审查的空白

在审查 PR 时,总会遇到"这个改动为什么要做?“的时刻。提交信息里没有原因,就得去问作者。作者记不清了,就得重新读代码来推断。

在 AI 编程环境下,这个问题的答案就在会话数据里。whyso 把那个答案取出来。

入职培训

新加入团队的成员理解代码库最快的方式,就是知道"这段代码为什么变成现在这样”。git log 是变更的时间线,文档是当前状态的快照。whyso 的文件级历史展示了两者之间的那部分——意图的链条

自我记录

就算是独立开发,也会遇到三个月前为什么这样写已经想不起来的时候。whyso 能还原过去的自己和 AI 之间的对话。提交信息里没写的上下文,就在那里。


与 git blame 的关系

whyso 不是要替代 git blame,而是补充它。

git blamewhyso
O
何时OO
改了什么O(行级别)O(tool_use 级别)
为什么改△(提交信息)O(用户请求 + AI 说明)
数据来源git 历史Claude Code 会话

如果说 git blame 是基于提交的"正式记录”,那 whyso 就是基于对话的"工作日志"。正式记录里没写进去的上下文,留在了工作日志里。


AI 读取自己记录的结构

whyso 最有趣的使用场景,是AI 自己读取,而不是人。

Claude Code 在会话结束后会忘掉一切。下一个会话打开同一个项目,昨天为什么那样写、考虑过哪些方案又放弃了、用户在什么背景下提出的请求——全都没了。只能靠读代码来推断。

想象一下在 CLAUDE.md 里加一行:

修改文件前务必先执行 `whyso history <file>`。如有历史记录,读完再修改。

这一行改变了什么:

知道哪些变更不能回退

打算修改某个文件,读着读着发现有段代码看起来很奇怪。光看当前状态会觉得"是不是写错了?“想改掉。但如果 whyso 历史里写着"用户明确要求这个结构,理由不是性能而是可读性”,就不会去动它。

不再重新提议已经放弃的方案

上一个会话里有过"试试 A 方案?"→“不,用 B"的对话,新会话的 AI 不知道这件事,于是又提议 A。对用户来说很烦。有了 whyso,就能知道哪个方向已经讨论过并被否定了。

接续连贯的工作上下文

说"帮我继续昨天的重构”,现在只能看 diff 来推断。有了 whyso 历史,就能直接读到昨天会话里的用户请求和 AI 的判断依据。不再是推断,而是基于事实的连续作业。

记录跨会话累积

有了这个结构,whyso 的历史会随着会话的积累而不断增长。这个会话里修改的原因,也会传递给下一个会话的 AI。会话间没有记忆的问题,以文件为单位得到了解决。不需要额外的记忆系统。

whyso 里记录的是 AI 做的事。但 AI 不记得那些事。自己创建的记录,对自己来说最有用的时刻。 这就是 whyso 与 AI 编程工具结合时独特的价值所在。

对人来说,whyso 是"自动留下好提交信息的工具"。对 AI 来说,whyso 是跨越会话的记忆


使用方法

安装

go install github.com/park-jun-woo/whyso/cmd/whyso@latest

查询文件历史

# 单个文件
whyso history README.md

# 整个目录
whyso history internal/ --all

# 镜像文件结构输出
whyso history . --all --output .file-histories/

# JSON 格式
whyso history README.md --format json

查看会话列表

whyso sessions

工作原理

parentUuid 链回溯

JSONL 中的每条记录都通过 uuidparentUuid 相互关联。从 AI 修改文件的 tool_use 开始,沿 parentUuid 链向上追溯,就能到达触发这次修改的用户最初请求。

user message (uuid: A)          ← "加上验证 x-sort 列是否存在于 DDL 中"
  → assistant tool_use (parentUuid: A)
    → tool_result (parentUuid: B)
      → assistant Edit (parentUuid: C)  ← 这次修改的原因 = A

中间夹着 tool_result 类型的消息,因此只要提取 type == "user"content 为字符串的记录,就能找到真正的用户请求。

设计决策

只追踪 Write/Edit。 Claude Code 的 Write 和 Edit tool_use 明确包含文件路径和变更内容。Bash 命令的文件操作(rmmvcp)通过启发式方法检测,但 Write/Edit 是主要追踪对象。Claude Code 在修改文件时被设计为使用 Write/Edit,因此仅凭这两个工具就能捕获大部分变更。

包含子代理。 Claude Code 处理复杂任务时有时会创建子代理。子代理的会话保存在父会话目录下。whyso 也会解析这些子代理会话,构建完整的历史。

增量更新。 使用 --output 选项输出到目录时,已经解析过的会话会被跳过。即使会话增长到数百个,也不会每次都重新全量解析。

数据

用 whyso 追踪 whyso 项目自身的开发过程,结果如下:

项目数值
会话数17
Write 调用660
Edit 调用1,415
Bash 文件操作206
变更的唯一文件数480

17 个会话中,提取出了 480 个文件的变更历史。每个文件都记录着"为什么被创建、为什么被修改"。


局限性

  • 仅支持 Claude Code。 其他 AI 编程工具的会话数据格式不同。目前只支持 Claude Code 的 JSONL 格式。
  • 只追踪文本文件。 Write/Edit tool_use 针对的是文本文件。图片、二进制文件的变更不会被追踪。
  • 需要本地存在会话数据。 ~/.claude/projects/ 下必须有会话文件。如果已删除或在其他机器上操作,那部分记录就不存在了。

总结

在与 AI 协作写代码的时代,变更的原因不再只能依赖提交信息。对话本身就是记录,可以从那份记录中自动提取每个文件的变更历史。

如果说 git blame 展示的是"谁、何时、改了什么",那 whyso 展示的就是"为什么改"。

代码:github.com/park-jun-woo/whyso