GitHub 贡献图是一张按周排列的日期网格,每列 7 天,一年大约 53 列。我写 time_assassin 的出发点很简单:既然每个格子对应一天,能不能先选好日期,再让 Git 在这些日期产生提交。
两个时间戳
Git 的每个提交有两个时间戳,author date 和 committer date,默认都取自系统时钟。要控制它们,可以修改提交参数,也可以直接修改系统时间。我选择了后者:在 PowerShell 里用 Set-Date 把系统时间设到目标日期,之后正常执行 commit,两个时间戳会一起被改写,无论 GitHub 按哪个口径统计,结果都落在同一天。
这个做法会影响整台机器的时间,我也清楚这一点。项目定位是一个小工具,使用时需要管理员权限,脚本结束后会通过 w32tm /resync 把时间同步回来。
网格与日期偏移
网格的计算比看起来麻烦。1 月 1 日不一定落在第一列第一行,需要根据它在星期中的位置计算偏移。第 iw 列第 id 行对应的日期,距 1 月 1 日有 7 * iw + id - offset 天;负数落在空白格,超过当年天数则落在范围之外。平年 365 天加上最大偏移 6 是 371 格,正好等于 53×7;闰年 366 天加偏移 6 需要 372 格,最后一天会被截掉。
这个问题是在测试 2024 年时发现的:12 月 31 日的格子点不亮。原因是列数被写死成了 53。我没有修改它,因为工具主要服务于我自己使用的年份,但这是网格计算里一个明确的边界情况。
日期累加用的是固定 24 小时:
function addDay(dt: Date, dn: number) {
return new Date(dt.getTime() + dn * 24 * 60 * 60 * 1000)
}
在有夏令时的时区,这种方式会产生漂移。我所在的时区没有夏令时,所以没有遇到问题,但这并不意味着实现是通用的。
页面交互与导出
页面上的每个日期渲染成一个 Layui 按钮,title 属性保存 YYYY-MM-DD,点击后在灰色和绿色之间切换。选中的日期可以导出为 JSON,下次导入后继续编辑。生成脚本时,每个日期对应三条操作:设置系统时间、向 checkpoint.log 追加一行、执行 git add 和 git commit,最后统一 push。
生成的脚本
checkpoint.log 是必要的:如果工作区没有变化,第二次 commit 会直接失败。时间固定为 11:45:14,是为了避开午夜边界,减少时区和夏令时导致日期偏移的可能。脚本开头先执行 net start w32time,因为时间服务未启动时 Set-Date 可能不生效。
使用范围
运行这个脚本会向仓库历史写入大量以过去日期为时间戳的提交,而且这些提交会一直保留。我只在一个专门用于画图的仓库里使用它,没有把它用在真实项目上。
后来才知道的
后来我注意到 Git 本身支持用 GIT_AUTHOR_DATE 和 GIT_COMMITTER_DATE 为单次提交指定时间,不需要修改系统时钟。已经完成的版本没有改动。如果当时知道这两个变量,实现会简单一些,也不会影响整台机器的时间。
回顾
现在回看这个项目,主要涉及三部分内容:贡献图的网格布局、Git 的双时间戳,以及系统时钟与 NTP 的相互影响。代码不复杂,但每个部分都有需要确认的边界。