Git-knife:像编辑表格一样修改提交信息、作者和日期
文章摘要
git-knife 的定位很明确:一个用来编辑 git 提交元数据的桌面 GUI,可编辑的字段包括提交信息、作者日期、提交者日期、作者姓名/邮箱和提交者姓名/邮箱,界面呈现为一张可以直接编辑的表格。它的口号是「把你的 git 历史捅成想要的形状」,工具名和刀的比喻贯穿整个项目。
作者对市场空白的判断写得相当清楚:现有的 GUI(GitKraken、Sublime Merge、Fork、lazygit)在改写提交信息和重排提交上做得很好,但它们把提交日期当成事实上不可变的东西,也不暴露任意提交的提交者日期和作者身份;而那些能改写这些元数据的工具(git-filter-repo、git rebase 的环境变量技巧、git commit-tree)没有 GUI。git-knife 填的就是这个交集:一个能批量、安全地编辑每一个字段的干净 GUI。README 里附了一张详细的对比表格,逐项标注各工具是否支持改写作者日期、提交者日期、作者身份以及批量正则查找替换。
它的实现策略是一个重要卖点:它从不重新实现 git,而是 shell out 调用系统的 git CLI,用 git commit-tree 重建提交,并复用每个提交原本的 tree 对象,因此文件内容可证明地永远不会被改变。技术栈是 Tauri v2 + Rust + Node/pnpm。核心代码组织为:src-tauri/src/git.rs 是唯一 spawn git 的地方;commits.rs 负责 open_repo 和 list_commits;rewrite.rs 提供 preview_edits 和 apply_edits,从最早被编辑的提交一路到 tip 通过 commit-tree 重建链条,然后保存一个备份 ref 并用「对旧 tip 做 compare-and-swap」的方式移动分支;backup.rs 负责列出并恢复备份。
MVP 已实现的功能包括:打开仓库并按 ref 选择任意本地分支来查看/编辑,不需要 checkout,所以工作区完全不受影响;批量查找替换(字面量或正则,带 $1 反向引用),可以选择作用在哪些字段上,典型场景是把所有提交里的旧邮箱换成新邮箱;应用前预览每一处变更;每次改写前自动创建备份 ref 并支持一键恢复;当改写会触及已推送的历史时给出警告;签名感知——给已签名的提交打徽章、警告改写会剥离签名、并可以用你配置的密钥重新签名(如果开启重签但没配密钥,会在动任何 ref 之前安全地失败);支持跨 merge 编辑,重建完整提交图并保留每个 merge 的父节点。尚未实现的是重排、squash、drop,以及暂存区/分支/远端管理。
README 还花了不少篇幅讲改写后的操作规范:编辑一个提交会改变它以及之后每个提交的哈希,本地分支与远端因此分叉,普通 push 会被拒绝,应该用 git push --force-with-lease——它会在远端自上次 fetch 后有变动时拒绝推送,从而避免悄无声息地覆盖同事的提交。改写共享历史后,其他人需要 fetch 加 git reset --hard(会丢弃本地独有提交,需要先协调)。撤销方面,应用内的 Backups 面板可一键恢复改写前的 tip,命令行则可以用 git for-each-ref refs/knife-backup 找到旧 tip 再 reset。
一个有意思的透明性设计:默认情况下 git-knife 会给每个被改写的 tip 提交附上一条公开披露的 note,放在自己专属的 notes ref 上(refs/notes/git-knife),因此不会碰你正常的 notes,在普通 git log 里不可见但完全可发现——你可以用 git notes --ref=git-knife show <commit> 读它,或者用 git for-each-ref refs/notes/git-knife 检查「这个仓库是否被 git-knife 编辑过」。README 强调这里没有隐藏编码,随时可以在界面上关掉,也可以用 git update-ref -d 彻底清除。改写策略本身在 git 层面有一个验证脚本,复现完整的 commit-tree 流程并断言内容 diff 为空。
HN 评论精华
这条帖子拿到 165 分,讨论呈现出三条清晰的主线:一是「谁真的需要改写提交日期和作者」——结果收到了一批相当具体的真实用例;二是围绕签名提交与共享历史的安全性质疑,作者当场发版本响应;三是一场规模不小、与工具本身几乎无关的关于「LLM 写的代码和文档」的争吵。
- iamcoder18 问出了最实际的问题:「这很酷,但真的有人需要改写提交的作者或日期吗?」回复相当踊跃。simonw 给了三个具体场景:(1)做爬虫项目时希望提交日期匹配数据实际变化的时间,从 Internet Archive 之类的来源重建出来——他举了自己重建 Tim Berners-Lee 最初那个浏览器历史的项目为例;(2)把一个 git 仓库拆成两个,希望保留最终落到新仓库的那些文件的提交历史(作者和日期),相当于只重放原仓库中某个目录的历史;(3)偶尔在整理和合并开源贡献者的 PR 时搞砸了,导致工作被错误地记在自己名下,他会修正提交把功劳还给正确的人。dgunay 提供了一个非常当下的用例:「某个 agent 不知为何动了我的 git config,我不得不回头修提交的作者身份」,以及「我基于别人的工作创建了 PR,所以把大部分提交重新署名到他们名下」——但他补充说自己从没需要动过日期。f1shy 说自己经常因为终端配置错误搞乱作者,而且在不同项目(私人和工作)之间切换需要不同身份。icase 则贡献了本帖最诚实的一条:「有啊,当我想让它看起来像我没有拖延一整周、而是一天之内实现了整个功能的时候。」
- Defletter 的用例最出人意料,也最能说明这个工具的价值:他为一个模拟议会项目找这样的工具「找了将近十年」,甚至前一天还在搜。他们决定用 git 来存储法律,用提交代表议会法案本身。问题在于必须保证每个提交都有正确的元数据——他们完全不在乎提交哈希的保持;而如果不小心漏了一条法律需要用交互式 rebase 插回历史中,它不应该把后续所有提交的日期重置为当前时间戳。本质上他们想把 git 当历史档案用。TiddoLangerak 提醒说原生 git 里提交有两个日期,作者日期记录内容何时被撰写、不随 rebase 改变,只有提交日期会变,所以正确的元数据其实已经在那里了,而且更精确。Defletter 的回应精彩地解释了为什么两个日期都需要能改:「作者日期是它被提交到议会的日期,提交日期是御准(Royal Assent)的日期。同理还有公投被发起的日期与结果被认证的日期。」ClikeX 也提到类似的场景(Legalize 项目把所有法律记录成 git 结构,每次明确的变更是一个带正确时间戳的提交)。
- mellosouls 提出了道德层面的保留:「嗯,这似乎是在把一件你通常不该做的事变得容易。」作者回应说意图主要是推送前的本地清理,比如批量修正错误的作者邮箱、或者修掉来自时钟不准的机器的日期。Carrok 的类比得到了广泛认同:「就像刀一样啊?一般我们不想把人切开,但当我们需要这么做的时候,比如手术,一把锋利的刀确实有帮助。」nkrisc 补充说从名字就能看出作者知道这一点——刀用错了很危险,用对了极其有用。slashdave 则调侃:「表格?那是剁肉刀,不是手术刀。」
- lrvick 提出了最有分量的技术批评:「注意:这在使用多作者签名提交的仓库上不会也不可能工作。签名的 git 历史是不可变的,而未签名的 git 历史是一个供应链攻击向量。」不过他也承认,对于单作者的 WIP 分支在提 PR 前做清理,这个工具是有用的。作者当场响应并发布了 v0.2.0:签名提交现在会在改写前被标记警告,并且可以用你的密钥重新签名重建后的提交。lrvick 的回复很有风度:「这对个人用例确实有帮助,有意思!不过如果历史里有多个作者还是不行,但我猜这个工具用得最多的场景就是作者清理自己的 PR。感谢在这类事情上倾听意见!」
- NichoPaolucci 挑出了 README 里那句「它从不重新实现 git——它 shell out 调用系统 git CLI 并用 git commit-tree 重建提交,复用每个提交原本的 tree,所以文件内容可证明地永远不会改变」,评论道「幸好 LLM 注意到了这点——我还担心这东西会重新实现 git 呢」。作者回应说 shell out 调用 git 并复用原始 tree 对象是刻意的设计,意味着即使他在别处搞砸了,文件内容也不可能被改变。wavemode 则岔出一句题外话:「provably(可证明地)和 probably(大概)只差一个字母,这一直让我觉得好笑,而且我经常把它们看混。」
- whateveracct 的一句「为什么现在没人能自己做事了?全是 LLM 垃圾」引爆了本帖最长的一条支线。danudey 的回应比较有代表性:他用 Claude 写了很多平时抽不出时间做的小工具,比如一个
ghCLI 扩展,给它一个 PR 号和多个分支,它会把该 PR cherry-pick 到那些分支上,或者告诉他哪些分支不行——「对管理多个发布分支很方便,但也没方便到值得我花大量时间和精力好好做一遍」。他强调自己没有用 Claude 替代真正的技能(研究、解决问题),只是替代那些「花几个小时读 API 文档写样板代码」的部分。whateveracct 澄清自己的不满其实主要是文档:「但如果你要分享这些工具——哪怕只是发个 gist——为什么不自己写给人看的英文部分呢?」gritzko 的回应很实在:「那恰恰是难的部分。你得连贯地把一切解释清楚,同时适配读者的思维框架。没人会无缘无故做这件事。那是地狱般的工作。」这条支线还引出了 saghm 一句略带讽刺但站得住的话:「考虑到你上面写的英文并没有让人看清你只在抱怨那一部分,或许值得考虑:无论怎么写,自然语言里的精确性都很难。」 - rozab 提到了一个被广泛共鸣的 LLM 怪癖:模型在被纠正后总是坚持要大声说明「没做什么」。「让它改掉代码里某个蠢东西之后,它会在代码里留一条注释吹嘘自己没做那个蠢事,对未来的读者来说就像麦片盒上贴着『不含石棉!』的标签。」evan_ 说自己最近留了无数条本质上是「这看起来不错,但删掉 80% 的注释」的 PR review。aozgaa 观察到一个更麻烦的效应:「在我的经验里这些注释表现得像一种 prompt injection。LLM 犯了一个概念性错误,把它写进注释,然后后续的 agent 就照着犯同样的错。」不过 orisho 提出了一个反向观点:如果在这个代码库上工作的人大多用同一个模型,这些注释其实很有用——它们在模型下次最需要上下文的地方告诉「未来的自己」:尽管你倾向于这个解法,它是错的,原因如下。「这些注释对你我没用,但对 LLM 有用。就像你给另一个人留的、帮他避开陷阱的注释,只不过这是 LLM 的陷阱,不是人的。」vips7L 的反驳很干脆:「如果 LLM 真像你们很多人声称的那么好,它们就不需要这些注释。它们会读代码然后知道做了什么,不需要关于没做什么的注释。」
- 关于截图,本帖有一段有趣的插曲。beart 说「看起来截图是别人显示器的翻拍照片,我在想为什么不用 print screen,不知为何这让我很不想碰这个项目」,Conscat 附和说「我本来以为是半透明窗口,读了这条才反应过来,那个弯曲太明显了,哇」。作者澄清那确实是截图,弯曲感来自 Zorin 的窗口透明效果。skinfaxi 直接跑了 exiftool 贴出元数据证明 Software 字段是 gnome-screenshot,并评论这个质疑「毫无意义且不正确」。yboris 则提了一个更有用的建议:请把截图放在 README 最前面——「我不太愿意 clone 一个仓库、花时间在上面,结果发现项目看起来很烂、我根本不想用。截图能节省别人的时间。」作者照做了。
- 功能请求方面,hootz 想要一个 TUI 版的表格编辑器;vivzkestrel 列了一串愿望清单:拖放式的 rebase UI、能改变提交顺序、能改变某个历史提交里包含哪些文件。jauntywundrkind 分享了自己最近在另外两个帖子里为
git rebase -i说好话时正好把它比作电子表格,觉得这个项目的出现「很有缘分」,并说自己现在很享受 jj,但git rebase -i的表格隐喻依然是赢家,把它扩展、加倍下注是好事。unqueued 赞赏这个工具使用 git-notes 并在自己的命名空间里创建备份分支,但希望它更轻量一些,并推荐了 git-revise。f1shy 问 magit 里有没有类似的东西,作者反问 magit 是否支持批量的作者/日期编辑,还是只能 reword。