一张热力图,七年没有答案

注:本文由 gpt-5.6-sol 撰写

一张热力图,七年没有答案

事情起于一个看似微不足道的细节。

一个开发者从周一写到周四,留下四个 commit。星期五,他把它们统一 push 到自己的 Gitea。随后打开个人主页,却发现前四天一片空白,只有星期五亮起一个绿色方格。

Gitea 记住了代码到达服务器的时间,却没有展示代码真正写下的日期。


持续七年的问题

这个问题最早可以追溯到 2019 年。

Issue #8178:Heatmap: Don't count commits by other authors 指出,镜像仓库里的 commit 会被算到仓库所有者名下,真实作者反而没有贡献。

随后几年,相同问题以不同形式反复出现:

人们反复询问:为什么 heatmap 统计 push,而不统计 commit?为什么连续工作数日,最后只亮起一天?

维护者的回答长期保持一致:Gitea 的 heatmap 记录平台活动。评论、评审、创建 Issue、推送代码,都是 action。系统知道谁在什么时间执行了 push,却没有一份现成且可信的 commit 贡献账本。

维护者 @lafriks 代表了这种谨慎。他反复提醒:Git 历史会被 force push 改写,默认分支会变化,仓库可能被删除、导入或镜像。

若按 commit 统计,系统必须知道作者、提交者和推送者分别是谁,还要识别 fork、重复 commit 与组织成员资格。一个绿色方格背后,可能是对整台服务器所有仓库的长期扫描。


一个新贡献者决定解决它

2026 年 1 月,第一次参与 Gitea 贡献的 @fjamesprice 决定解决这个问题。

他提交了 PR #36469:Use commit author date for heatmap instead of push date

最初的方案很自然:保存 commit author date,让 heatmap 使用这个日期。

随后,问题迅速扩大。

如果一次 push 含有三十天的 commit,系统应该生成三十条 action,还是另外建立一张表?

前一种方案会让动态列表出现大量相似记录。后一种方案则会带来数据库迁移、索引、数据清理与一致性问题。

这个 PR 改了又改,最终累计:

  • 19 个 commit
  • 12 个文件
  • 334 行新增
  • 25 条普通评论
  • 9 条 review comment

维护者 @silverwind 一度认为新的独立表方案在架构上成立。

维护者 @wxiaoguang 则提醒,action 表本来已有数百万行和性能压力,heatmap 不应与它产生新的依赖。

@lafriks 的意见更加明确:force push、错误合并、镜像复制和仓库导入都可能制造大量需要重新分析的 commit。这些边界情况会让系统付出远高于功能价值的复杂度,现有方案反而更加合理。

@wxiaoguang 同意了这一判断。

最终,@fjamesprice 主动关闭 PR。

他在留言里写道,这是自己的第一个 PR。虽然没有被采用,但仍然是一次很好的学习经历。

故事原本应该到此结束。


几个小时后,Silverwind 改变了判断

关闭 PR 后不久,@silverwind 查阅了 GitHub 的贡献统计规则。

他发现,GitHub 的贡献图确实依据 commit author、默认分支和 author date 计算,而非 push date。

于是,他重新开启了 Issue #36471:Heatmap shows commits on push date instead of commit author date

他写道:

GitHub 确实统计 commit。从根本上看,贡献应该按照 commit 计算。

随后,他给出了一套新的方向:

  • 每天由后台任务扫描默认分支
  • 依据已验证邮箱识别作者
  • 使用 author date 归属日期
  • fork 不计入贡献
  • 将过去一年的结果写入独立表
  • 评论、Issue 和评审等活动继续从 action 表读取

@fjamesprice 又写出一份新的设计。

@silverwind 表示认可,并认为每天凌晨执行一次完整计算是可以接受的。

@lafriks 继续追问:

  • force push 后已经消失的 commit 如何处理?
  • 默认分支变化后,旧数据如何更新?
  • 仓库删除后,历史记录如何清理?
  • 非组织成员的 commit 是否应该出现?
  • PR review 等非代码活动如何保留?
  • 同一个 commit 被复制到多个仓库时,是否会重复计算?

争论至此已经发生变化。

大家争论的已不再是“要不要修一个日期”,而是“什么才算贡献”。


WxiaoGuang 把旧设计写进源码

两个月后,@wxiaoguang 提交了 PR #37195:Add comment for the design of user activity time

这个 PR 没有改变任何程序行为。

它只在源码里加入了一段醒目的注释:

// HINT: USER-ACTIVITY-PUSH-COMMITS:
// it only uses the doer's action time,
// it doesn't use git commit's time.

这段注释的含义非常明确:

当前 heatmap 使用行动者的 action time,不使用 git commit time;这是有意设计。

这并非一次中性的文档整理。

此前,@wxiaoguang 已经在 PR #36469 中表示,既然这个问题已有结论,就应该把设计写入代码,避免后来者继续投入不被接受的尝试。

PR #37195 随后获得 @bircni@lunny 两名维护者批准,并由 @wxiaoguang 合并。

六天后,@wxiaoguang 引用这段注释,关闭了 Issue #36471

在他看来,争论已经结束:heatmap 记录的是 action,不是 Git 历史。


六小时后,Issue 再次打开

@wxiaoguang 关闭 Issue 六小时后,@silverwind 又把它重新打开。

他在评论中写道:

This is still a major behaviour difference from GitHub that must be fixed, in my opinion. Keep open.
Pushes are not contributions, commits are.

这仍然是 Gitea 与 GitHub 之间重大的行为差异。这个问题必须修复,Issue 应该保持开放。
Push 不是贡献,commit 才是。

他还指出,这个问题并不只影响 heatmap,也会影响 “top contributors”等其他功能。

从那以后,Issue #36471 一直保持开放,并带有 proposal/accepted 标签。

但那段宣告“当前行为属于有意设计”的注释,至今仍留在源码中。


Silverwind 为什么不亲自实现?

有人或许会问:@silverwind 既然如此坚持,而且拥有 merge 权限,为什么不亲自完成?

因为 merge 权限并不等于个人决定权。

Gitea 的常规 PR 需要两名 maintainer 批准。如果 @silverwind 自己编写,他仍然需要另外两名维护者认可,而公开讨论中恰好存在强烈的保留意见。

他后来完成了 PR #36622:Load heatmap data asynchronously,解决了 heatmap 查询阻塞页面的问题,却没有承担贡献模型的全面改造。

这也说明,他愿意处理 heatmap 的性能问题,但重新定义“贡献”需要的不只是代码,还需要维护者之间形成共同认可。


新的希望,新的争议

如今,现任 Gitea 技术监督委员会成员 @lunny 正在开发 PR #37209:Implement DB-backed contributor stats with queued updates and UI sidebar contributors

这个 PR 试图建立新的 contributor stats 数据层:

  • 按仓库保存贡献
  • 按作者识别用户
  • 按 author date 记录日期
  • 默认分支变化时重新计算
  • force push 时重新扫描仓库

它或许会成为未来 heatmap 的基础。

但这项工程同样庞大,目前涉及数十个文件和数千行代码,也面临数据库长期增长、force push 一致性与历史重算成本等争议。


一张绿色方格背后的真正问题

七年过去,Gitea 仍然知道谁在今天按下了 push,却无法让所有维护者共同认可:昨天写下的 commit,应该怎样成为昨天的贡献。

这场争论表面上围绕一张热力图,实质上争夺的是一个定义:

软件项目究竟应该记录人的操作,还是记录人的创造?