把 bug tracker 塞进 git 仓库,同步和冲突怎么解决?我把 git-bug 拆开实测了一遍

上周五(9 月 25 日)Hacker News 首页出现了一个讲 git-bug 的帖子------"嵌在 git 里的分布式 bug tracker",这两天涨到了 353 分。它能翻红有个前置事件------9 月 22 日发了 v0.11.0,这是项目 16 个月来的第一次发版,一口气带上了近 300 个 commit。更硬的背书来自 kernel.org 那边:b4 的维护者 Konstantin Ryabitsev 在今年的 Kernel Recipes 上演示了 b4 和 cgit 对 git-bug 的支持。一个做内核邮件列表工作流的元老,给一个 bug tracker 站台,这比星数有说服力多了。

git-bug 的思路一句话能说完:issue 不开独立服务,就存成普通的 git 对象,push/pull 走你现有的 git remote,仓库里不多一个文件。这个说法我在 HN 上刷到过不止一次,但"git 对象当数据库"具体长什么样、两个克隆离线各写各的之后合并会不会打架,这些全是别人嚼过的结论。我决定自己装一遍,把 refs 里的数据拆开看,顺手把同步和冲突的坑都踩一遍。

第一坑在我自己的机器上

安装没什么可说的,release 页面下 tar.gz 解压即用,我拿的是 git-bug_0.11.0_linux_amd64.tar.gz,解出来一个 34M 的静态二进制(Go 1.27.1 编译,不挑系统库)。git-bug version 输出 v0.11.0,build hash 都在。建好测试仓库、提交了 README,然后执行 git-bug user new 创建身份,它当场崩了------不是报错,是 SIGBUS:

bash 复制代码
unexpected fault address 0x7f0fa1283040
fatal error: fault
[signal SIGBUS: bus error code=0x2 addr=0x7f0fa1283040 pc=0xadc08a]
go.etcd.io/bbolt/internal/common.(*Meta).Txid(...)
go.etcd.io/bbolt@v1.4.0/internal/common/meta.go:128
github.com/blevesearch/bleve/v2/index/scorch.(*Scorch).persistSnapshot(...)

堆栈往下翻,是 git-bug 在第一次跑的时候要建本地缓存(bleve 全文索引,底层 bbolt 存储),mmap 映射文件时挂的。我的测试仓库一开始放在云盘挂载目录------980M 的 tmpfs,机器又只有 1.9G 内存、零 swap,bbolt 把索引文件 mmap 进内存再写入,物理页兜不住。同样的命令挪到本地磁盘(overlay,9.7G 空闲)一次就过了。

这个坑和 git-bug 关系不大,但值得记一笔:它首次运行要建全文索引缓存,对小内存机器有实际要求,1G 内存的 VPS 大概率撞得上。建缓存在这台 1 核机花了几秒,仓库大了这个成本会放大------我没测大数据量,不下结论。

拆开看:一条 bug 在 git 里长什么样

环境跑通之后,我建了第一个 bug(标题"登录接口500",正文描述 redis 超时),然后去看它到底存成了什么。git for-each-ref 一眼就能看到门道:

bash 复制代码
98bda0d commit  refs/bugs/e1f0f64fb8550becb731f1d3139cb1109518f779e3a0b07750ef32fe51456e9b
920d4ddb commit  refs/heads/main
c5ffee3 commit
refs/identities/ed8301622df3f79042b4be21570fa6cfc6963571e001fed04f1251724ba59809

每个 bug 是 refs/bugs/ 下的一个引用,指向一个货真价实的 commit;我的身份(identity)也同理挂在 refs/identities/。git status 完全干净,工作区没有新增文件;这些 ref 用 git 自己的传输协议就能推拉------"不开独立服务"的底气就在这。再往里拆。git cat-file -p 看这个 commit,有个反直觉的细节:author 和 committer 都是空的 <>。作者信息不放在 commit 的常规字段里,而是藏在 tree 中的一个 blob 里:

sql 复制代码
100644 blob e69de29 create-clock-2
100644 blob e69de29 edit-clock-2
100644 blob f562e40 ops100644 blob e69de29 version-4

ops` 这个 blob 是一段 JSON:

json 复制代码
{"author":{"id":"ed830162..."},"ops":[{"type":1,"timestamp":1790569569,
"nonce":"AnndhUnHCfMGcU8IkPL8YDZ3Fk4=","title":"登录接口500",
"message":"手机号+验证码登录偶发500,日志看是redis超时","files":null}]}

剩下三个空 blob 才是精髓:create-clock-2、edit-clock-2 是逻辑时钟(Lamport clock)标记,数字就是计数值。bug 的历史不是 commit 链堆出来的,而是"每个操作生成一个新 commit,tree 里放操作日志和时钟计数"。分布式合并不比内容,比时钟------后面冲突实验会看到它怎么动。

clone 下来 bug 是空的

这是 HN 讨论区吵得最凶的一个点,我亲手验证了:git clone 一份仓库进去跑 git-bug bug ls,列表是空的,git for-each-ref 数 refs/bugs 是 0 个。原因一句话讲清:git 默认的 fetch refspec 只有 refs/heads/*:refs/remotes/origin/*,refs/bugs 和 refs/identities 根本不在拉取范围内。解法是 git-bug 自己封装的 push/pull:

vbnet 复制代码
$ git-bug pull originMerging data ...
ed83016: new
e1f0f64: new

它会显式去 fetch refs/bugs/* 和 refs/identities/*,然后进自己的合并逻辑,把新对象和已有数据归并(Merging data 后面的 new/updated 就是归并结果)。也就是说协作双方照常用 git push/git pull 同步代码,issue 用 git-bug push/git-bug pull 单独同步------两套动作,一个 remote。CI、镜像、备份脚本如果只认 refs/heads,这些 bug 数据它们是看不见的,这在团队里落地之前得先想清楚。

两个克隆离线各写各的,合并不打架

真正的重头戏是离线并发。我搭了个最小拓扑:一个 bare 仓库当 origin,两个克隆 A、B 分别指向它。A 端先给 bug 加一条评论"我看看redis慢日志"(-F - 从 stdin 读内容),不 push。B 端此刻对 A 的评论一无所知,也建了自己的身份 tester-b,加了条"复现了,iOS端必现",然后 push 到 origin。A 端再 pull:

vbnet 复制代码
Merging data ...
ae2102d: new
e1f0f64: updated

A 端的评论列表里,两条评论按时间排好了队,一条不少;git-bug bug show 里 participants 也自动汇总成 tester、tester-b 两个人。全程没有任何"冲突请手工处理"的提示------对评论这种只增不改的操作,合并就是无脑并集,Lamport 时钟负责排序。这正是邮件列表工作流的形状:两个人各自离线干活,回头一同步,谁也不用等谁。

同时改标题,最后听谁的

评论合并太温柔了,我想要点刺激的:A、B 两端同时改同一个 bug 的标题。A 端改成"登录接口500 已定位redis慢查询",B 端在不知情的情况下改成"iOS 登录接口500必现"。先 push、pull 一轮之后,奇怪的事情出现了:A 端显示 A 的标题,B 端显示 B 的标题,两边对不上。

我第一反应是"CRDT 翻车了"。盯着操作复盘了一下才发现是我急了:A 端 pull 到 B 的标题编辑后,合并动作发生在 A 端本地------合并结果只存在于做了合并的那一端,A 端没有把结果 push 回去,origin 里的 refs/bugs 还是 B 的版本,B pull 回来的自然还是自己的标题。补上这一环,A 端 push、B 端再 pull:

kotlin 复制代码
Merging data ...
e1f0f64: updated

B 端标题变成了"登录接口500 已定位redis慢查询",和 A 端一致。再拆 refs 看,两端的 edit-clock 都收敛到同一个值 6(两个身份各自从 2 起步,各自编辑加一次,合并后再累加)。谁的标题赢、时钟怎么计的,源码里的裁决规则我没细翻,从行为看它是确定性的:同样的操作序列,两边最终拿到同一个答案,这比"谁赢"重要得多。中间态不一致这件事则要当作使用常识记住------同步完一轮不代表全局收敛,得有人把合并结果推回去。

最后称一下分量

最后量了一下成本:这条 bug 从创建、两条离线评论、两次标题编辑到合并,全部历史 7 个 commit,松散对象 21 个,git count-objects 打包后 3.50 KiB。bare origin 仓库整包 372K。对个人项目来说,这个开销约等于不存在。顺带一提,git-bug webui 能起带 GraphQL API 的本地界面,termui 也有终端版,交互层不依赖 GitHub。

我自己的判断是:它不适合替代 Jira 或 GitHub Issues 当团队的唯一 issue 系统------没有权限模型,refs/bugs 的可见性跟着仓库走,这套边界在多人组织里不够用。但它的 sweet spot 很清楚:不推远程的本地仓库、个人项目、离线环境、以及任何"issue 跟着代码走"的场合。kernel.org 那边把它接进内核贡献流程,图什么不难猜:数据在 git 协议里,不依赖第三方服务。

这次实测的完整脚本和命令序列我在本地跑通后整理成了一版,贴在文末,复制即可复现。

bash 复制代码
#!/usr/bin/env bash
# git-bug 双端离线同步实验,可复现
# 环境:Linux, git 2.34.1, git-bug v0.11.0 (git-bug_0.11.0_linux_amd64.tar.gz)
set -e
GB=/path/to/git-bug   # 解压出的二进制路径

mkdir origin.git && cd origin.git && git init -q --bare && cd ..
git clone -q origin.git repo && cd repo
git config user.name tester && git config user.email t@t.local
echo "# demo" > README.md && git add . && git commit -qm init

$GB user new -n tester -e t@t.local --non-interactive
$GB bug new -t "登录接口500" -m "手机号+验证码登录偶发500" --non-interactive
$GB bug ls
git for-each-ref                          # refs/bugs/ refs/identities/
git ls-tree -r refs/bugs/<bug完整哈希>     # ops / create-clock-2 / edit-clock-2 / version-4

cd .. && git clone -q origin.git repo-b && cd repo-b
git config user.name tester-b && git config user.email b@t.local
$GB pull origin                           # clone 不带 refs/bugs,必须显式拉
$GB user new -n tester-b -e b@t.local --non-interactive
echo "复现了,iOS端必现" | $GB bug comment new <bug完整哈希> -F - --non-interactive
$GB push origin

cd ../repo
echo "我看看redis慢日志" | $GB bug comment new <bug完整哈希> -F - --non-interactive
$GB pull origin                           # 双端评论自动共存
$GB bug title edit <bug完整哈希> -t "登录接口500 已定位redis慢查询" --non-interactive

cd ../repo-b
$GB bug title edit <bug完整哈希> -t "[iOS] 登录接口500必现" --non-interactive
$GB push origin
cd ../repo && $GB pull origin             # A 端完成合并,结果只在 A 本地
$GB push origin                           # 关键:合并结果要推回去
cd ../repo-b && $GB pull origin           # B 端标题收敛为 A 的版本
git ls-tree -r refs/bugs/<bug完整哈希>     # edit-clock 两端一致
相关推荐
L·S·P7 小时前
25MB 管理 100+ 种数据库:开源轻量客户端 DBX 介绍与上手
数据库·mysql·开源·database·dbx
m4Rk_9 小时前
【论文阅读】Agent 记忆机制(83):Inside Out——用可演化 PersonaTree 构建 Agent 的核心长期记忆
论文阅读·人工智能·学习·开源·github
Experience-摆渡11 小时前
一个开源免费的本地AI抠图工具 unbagrnd:断网也能用的背景移除
人工智能·开源
resh_people11 小时前
开源鸿蒙平台 KMP_CMP 三方库「WhatIf」适配全流程
华为·开源·harmonyos
分布式存储与RustFS12 小时前
模型 checkpoint 放对象存储:分片上传、断点续传与残留清理
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
m4Rk_12 小时前
【论文阅读】Agent 记忆机制(81):EMR——用情景记忆避免 Agent 在多步推理中反复绕圈
论文阅读·人工智能·学习·开源·github
金字塔頂の蝸牛12 小时前
每周GitCode开源项目推荐
开源·软件工程·ai编程
weixin_4045512414 小时前
开源 LLM 可观测平台深度比较:Langfuse、Phoenix、Helicone、Opik 与 MLflow
开源·llm
302wanger15 小时前
一份用"工程师思维"写的人生指南:472 条建议,每条都标了证据等级
开源