Gerrit 的文档里有这样一条命令1:
git fetch https://gerrit.googlesource.com/gerrit refs/changes/70/98070/meta \ && git log -p FETCH_HEAD它拉下来的不是代码,而是 Gerrit 自己项目里第 98070 号 change(Gerrit 对“一份待评审的修改”的叫法)的评审记录。git log 打出来的每个提交,都是一次评审动作:谁加了评审人、谁投了什么票、什么时候被合并。Gerrit 的单元测试逐字断言了这种提交的格式,其中一个长这样2:
Update patch set 1
Patch-set: 1Change-id: I…Subject: Change subjectBranch: refs/heads/masterCommit: …Reviewer: Gerrit User 1 <1@gerrit>CC: Gerrit User 2 <2@gerrit>Label: Code-Review=-1, 1_1_1_code_review__1_1Label: Verified=+1, 1_1_1_verified_1_2一次 Code-Review -1 和一次 Verified +1,就是一个 Git 提交。看到这里,我们大概会冒出一串疑问:一个代码评审系统,为什么要把投票写成提交?这些提交挂在哪个引用上,是在什么时候、由谁写进去的?每次评审都多出几个引用,一个活跃的项目最后会有多少引用,Git 扛得住吗?引用里既有代码又有评审数据,还有别人没发出去的草稿,权限又怎么划?
《从 git clone 说起》里讲 Gerrit 的那一节,只走到“push 到 refs/for/main 之后多出了两个引用”。这篇接着往下挖,从 Gerrit 为什么要自己实现 Git 服务端说起。
一、Git 服务端的选择:为什么是 JGit
现象:Gerrit 不调用 git
GitLab 的 Gitaly 处理 clone 和 push 时,最终会去执行 git upload-pack、git receive-pack 这些 Git 自带的程序。Gerrit 不一样:clone、fetch、push 都在它自己的 JVM 里完成,不去调用 git 程序。干活的是 JGit,一个用纯 Java 重新实现的 Git 库,提供对象读写、打包,以及服务端的 UploadPack(服务 fetch)和 ReceivePack(服务 push)。这和 Gerrit 的出身有关。按官方的说法,Gerrit 最早是给 Rietveld(Google App Engine 上的 Python 评审工具)打的一组补丁,为 Android 开源项目服务;到 2.x 才整个改写成跑在 Java 容器里的程序3。不过语言只解释了“为什么是 Java”,没有解释“为什么不在 Java 外面套一个 git 二进制”。
第一次尝试:在 git 外面挂钩子
最直觉的做法,是让 Git 自己的 receive-pack 收 push,Gerrit 通过钩子插手。Git 在更新引用之前会调用 pre-receive 钩子,把每条命令 <old> <new> <ref> 从标准输入喂给它:
# pre-receive 读到的一行(示意)0000000000000000000000000000000000000000 612f4c1… refs/for/main问题在于这个钩子能做的事太少。文档写得很清楚:钩子以非零状态退出,所有引用都不更新;以零退出,引用就照常更新4。也就是说,钩子只能对客户端的命令说“行”或“不行”。而 Gerrit 要做的是第三种事:客户端说“把提交写到 refs/for/main”,服务端回答“好的”,实际却写到 refs/changes/34/1234/1。 用 pre-receive 放行,Git 会真的创建 refs/for/main;拒绝,客户端就看到报错。
能让钩子接管命令的 proc-receive,要到 Git 2.29 才出现5,那时 Gerrit 早已在 JGit 上运行多年。
解决:把 JGit 当库嵌进进程
JGit 是一个库,ReceivePack 的每条命令都是一个 Java 对象,带着自己的结果状态。钩子不只能拒绝,还能把某条命令直接标成“已成功”,JGit 之后就不会再碰它(下一节细讲)。同样,UploadPack 读引用时调的是一个 Repository 对象,换掉这个对象,就能决定它看到什么。图 1 是 Gerrit 里这几层的样子:
HTTP 入口 GitOverHttpServlet 直接继承 JGit 的 GitServlet,SSH 入口把 git-upload-pack 命令绑到 Upload 类;两条路最后都是 new UploadPack(...) 或 new ReceivePack(...)。Gerrit 插手的地方只有两处:传给它们的仓库先被 PermissionAwareRepositoryManager.wrap() 包了一层;ReceivePack 的 PreReceiveHook 换成了 Gerrit 自己的 ReceiveCommits(AsyncReceiveCommits.java:319-329)。
还有一个好处是存储可以替换。仓库由 GitRepositoryManager 打开,开源版默认是本地磁盘上的 FileRepository;JGit 另有一套 DfsRepository 抽象,可以把 pack 和引用放到分布式存储上。协议层不用改,换的只是最底下那一层。
JGit 让 Gerrit 可以接管 push。接管之后,Gerrit 在钩子里到底做了什么?
二、push 到 refs/for/main:一次投递的全过程
现象:push 成功了,引用却不存在
$ git push origin HEAD:refs/for/mainremote: https://gerrit.example.com/c/hello/+/1234 add lib [NEW] * [new reference] HEAD -> refs/for/main客户端看到的是 [new reference],服务器上却查不到 refs/for/main。refs/for/ 这类分支在 Gerrit 里叫 magic branch:判断方法只有一行,名字以 refs/for/ 开头就是(MagicBranch.java)。它像一个投递地址,告诉 Gerrit“这是一份待评审的提交,目标分支是 main”。
原因:命令的结果由钩子决定
JGit 的 ReceivePack.service() 按固定顺序做事:读命令,收 pack 并检查对象是否连通,调用 PreReceiveHook,执行引用更新,回报结果。关键在执行这一步:executeCommands() 只处理结果还是“未尝试”(NOT_ATTEMPTED)的命令6。
Gerrit 就利用了这一点。图 2 左边是 JGit 的流程,右边是 ReceiveCommits.processCommandsUnsafe() 在钩子里做的事7,红色是 refs/for/main 这条命令的去向:
图里没画出来的有三点。第一,refs/for/main%topic=x,r=alice 里 % 之后是选项,parseMagicBranch() 会切出来,并检查推送者能不能往目标分支上传评审。第二,magic 命令和普通命令不能混在一次 push 里。第三,写引用用的是 BatchUpdate,Gerrit 把“改引用、改评审数据、发通知”打包执行的工具。
普通 push(HEAD:refs/heads/main)其实也不走 JGit 的执行:Gerrit 在钩子里做完权限检查,同样用 BatchUpdate 自己写引用。所以交给 JGit 的仓库可以是只读的,图 1 里 newBatchUpdate() 直接抛异常。
Change-Id 和 change 编号
图 2 里有两个值来历不明:让两次 push 对上号的 Change-Id,和新分配的编号 1234。前者由客户端生成,后者由服务器发放:
Change-Id 是 commit-msg 钩子写在提交信息末尾的一个尾注(Key: value 形式的行,Git 叫 trailer)8。钩子只在没有 Change-Id 时才添加,所以 git commit --amend 改了内容、哈希变了,Change-Id 照旧,Gerrit 就把新提交认作同一个 change 的下一个 patch set。
编号存在 All-Projects 仓库 refs/sequences/changes 指向的 blob 里。每次领号都是一次引用更新,为了少写几次,进程一次领一批(默认 20 个)放在内存里发,所以并发时编号不严格递增9。push 之后,仓库里多了两个引用,一个指向代码,一个叫 meta。meta 里装的是什么?评审数据为什么要放进 Git?
三、评审数据的存储:从 ReviewDb 到 NoteDb
现象:两份存储,两套一致性
Gerrit 2.x 的评审数据存在一个 SQL 数据库里,叫 ReviewDb;代码存在 Git 仓库里。一次上传要同时改两边:Git 里多一个 patch set 引用,数据库里多几行记录。两份存储带来的麻烦,在备份文档里看得最清楚:2.x 的数据库应当和 Git 数据放在同一台机器的同一个卷上,这样文件系统快照才能同时覆盖两者10。复制也一样,把 Git 仓库推到另一台机器很容易,数据库得另想办法;而一个 patch set 引用如果到了,对应的数据库行没到,另一边看到的就是一个半成品。
解决:评审数据也是提交
Gerrit 3.0 起只剩 NoteDb 一种后端,2.16 是迁移的最后一站1。文档列的好处正对着上面的问题:所有数据在一个地方;复制和备份只要对引用做快照,分支、patch set 和指向它们的元数据就一起到了;元数据的修改以提交的形式保存,历史天然留着;插件可以加字段,不用改数据库表结构。每个 change 的评审状态放在 refs/changes/34/1234/meta 这条“分支”上,每次操作(投票、加评审人、合并)都往上追加一个提交,尾注由 ChangeUpdate.applyImpl() 拼出11,读时由 ChangeNotesParser.parseAll() 从新到旧重放12。图 4 是一个 change 从创建到合并的 meta 提交:
图里的作者只写账号号和服务器 ID,不带真名和邮箱;投票尾注后的 4c1d… 是这张票的 UUID,供后续复制投票时引用。图里没有的是草稿:没发出的草稿评论放在 All-Users 仓库的 refs/draft-comments/34/1234/<账号号>,发布评论要同时改两个仓库,这两步不是原子的;NoteDbUpdateManager 特意先写项目仓库、再删草稿,失败时最多留下一份可以被过滤掉的旧草稿13。
同样的思路贯穿整个 Gerrit:权限配置在每个项目的 refs/meta/config 分支里,账号、群组在 All-Users 的 refs/users/*、refs/groups/* 下。整个系统没有独立的业务数据库,状态就是一组 Git 仓库里的引用14。代价也很直接:一次上传至少要动两个引用,而且必须一起成功。每次操作都要原子地改好几个引用,引用又越积越多,Git 的引用存储扛得住吗?
四、引用的存储:packed-refs 的瓶颈与 reftable
现象:原子更新的代价和引用总数成正比
BatchUpdate 执行前有一句检查:引用库不支持原子批量更新,就报错 “cannot use NoteDb with a repository that does not support atomic batch ref updates”15;真正写引用时,NoteDbUpdateExecutor 也总是 setAtomic(true)16。传统的引用存储(JGit 里叫 RefDirectory)把引用放在一个个小文件(loose ref)和一个按名字排序的文本文件 packed-refs 里。原子地改几个引用,要锁住相关文件、把 packed-refs 整份读进来改好、再整份写回。类注释直说:耗时与引用总数成线性,与这次改了几个无关17。
而 Gerrit 的引用数恰恰随评审历史线性增长。JGit 的 reftable 设计文档拿 Android 举例:86.6 万个引用,packed-refs 有 62 MB,改 2 个引用也要读进 62 MB、写出 62 MB18。图 5 把同一次事务在两种格式下的做法和代价放在一起:
解决:reftable
reftable 是一种二进制引用存储格式,设计文档就放在 JGit 仓库里,JGit 的 FileRepository 和 DFS 后端都能用它;Git 2.45 也把它作为可选的引用后端收了进去19。它的文件一旦写成就不再修改:写入是往 tables.list 列出的那叠表上追加一个小表,读取从最新的表往旧查,删除写一条“墓碑”记录,小表多了由后台合并。图 6 打开 push patch set 2 时写出的那个小表:
表内的 ref 块按名字排序并做前缀压缩,块尾的 restart 表让读者可以二分查找;大文件还会有 ref index(每块最后一个名字)和 obj 块(从对象哈希反查引用)。文档明确说,这个格式假定“绝大多数引用只有一个 SHA-1 值,并且共享前缀,比如 Gerrit 的 refs/changes/”,并建议这类仓库用 64 KiB 的块。协议 v2 的 ls-refs ref-prefix refs/heads/ 这类前缀扫描,也只需读相关的块。已有仓库可以用 gerrit convert-ref-storage -p <项目> 转成 reftable20。引用数量的问题有了答案,可这些引用里有私有 change、ACL 配置,还有 All-Users 里别人的账号数据。同一个仓库,怎么让不同的人看到不同的引用?
五、引用的可见性:按权限过滤
现象:引用列表本身就是数据
Git 协议里,服务端先报出“我有哪些引用、各指向哪个哈希”,客户端再按哈希要对象。看得见 refs/changes/35/1235/1,就知道有一个 1235 号 change;拿到它的哈希,就能 fetch 到还没公开的代码。设计文档因此要求三件事:没有读权限的引用不出现在广告里;上传时拒绝基于看不见的提交;tag 只有在它指向的提交能从某个可见的非 tag 引用到达时才可见14。
第一次尝试:在广告时过滤
JGit 提供了 AdvertiseRefsHook,可以在发引用列表之前删掉一些。可列表只是读引用的入口之一。协议 v2 的 ls-refs 按前缀读,want-ref 按名字解析,客户端直接 want 一个哈希时,服务端还要检查它是不是某个可见引用的 tip(JGit 的 getTipsWithSha1())。只在广告那一刻过滤,就要保证这几条路径都不漏;这是推断,但 Gerrit 选择的做法正好绕开了这个问题。
解决:换掉引用数据库
Gerrit 交给 JGit 的仓库里,引用数据库是 PermissionAwareReadOnlyRefDatabase:getRefsByPrefix()、exactRef()、getTipsWithSha1() 都先过一遍权限过滤,写操作直接抛异常21。JGit 不管走哪条路径读引用,读到的都是过滤后的结果。判断规则在 DefaultRefFilter 和 RefVisibilityControl 里22 23,图 7 是一个例子:
规则里有两处为性能做了取舍。change 引用不逐个算权限,而是先从索引和缓存取出当前用户可见的 change 集合,再看编号在不在里面;用户对 refs/* 都有 READ 时走快路径,只单独判断 refs/meta/config。
这一层只管读。push 没有协议 v2,receive-pack 仍要先列出所有引用,大仓库会有几十 MB。Gerrit 在 push 的广告里去掉全部 refs/changes/*24;可这样客户端协商出的共同祖先会更旧,要多传服务端早有的对象。于是又补一招:把当前用户在这个项目里最近的 open change(默认 32 个,receive.advertiseOpenChangesRefs)的当前 patch set 作为 .have 行发出去。.have 的意思是“我有这个对象,但它不对应可以拉取的引用名”。广告大致长这样(示意):
385ce2c… refs/heads/main\0report-status delete-refs side-band-64k …7f3d9b2… refs/meta/config9a0b7e2… .have用户刚推过的 change 往往是下一次 push 的基础,客户端就不必重传这部分对象。源码注释也坦白,这是一个启发式办法,有待度量。评审通过以后,change 要合进目标分支。分支、change 状态、可能还有好几个相关的 change,都得一起变。合并时怎么保证它们一起变?
六、提交(submit):合并策略与原子更新
现象:合并不只是移动一个分支
在 Gerrit 里点 Submit,至少要做三件事:按项目设定的方式把代码并进分支;把 change 的状态改成 MERGED;如果它依赖同一条提交链上的前几个 change,或者属于同一个 topic,要把它们一起合进去。要是分支先动了、meta 还没写 MERGED 时服务器挂掉,就会出现“代码已经在 main 上,change 却还是 open”的状态。所以分支引用和所有 meta 引用必须放进同一次原子更新。
合并方式叫提交类型(submit type),每个项目选一种25:MERGE_IF_NECESSARY(能快进就快进,否则生成合并提交,项目没配置时默认用它)、MERGE_ALWAYS(总是生成合并提交)、REBASE_IF_NECESSARY 和 REBASE_ALWAYS(变基到分支最新提交上,真的变了基就生成一个新 patch set)、FAST_FORWARD_ONLY(只允许快进),以及忽略依赖关系、逐个 cherry-pick 的 CHERRY_PICK。
解决:一个项目一批,失败整体重试
MergeOp.merge() 先用索引算出要一起合并的 change 集合(注释提醒索引可能落后几秒),再从 NoteDb 重读一遍,剔除状态已不是 NEW 的26。然后每个 SubmitStrategyOp 算出新的分支 tip,并把 change 标成 MERGED。同一个项目的这些改动,最后进同一个 setAtomic(true) 的 BatchRefUpdate:
原子批量更新里,每条命令都带着期望的旧值,这就是 CAS(compare-and-swap,比较并交换):当前值等于旧值才写入。图 8 右边那种情况,MergeOp 用 RetryHelper 把整个 integrateIntoHistory 重做一遍,超时还按涉及的项目数放大。
跨项目的 topic 就没这么干净。BatchUpdates.execute() 对各项目依次执行27,MergeOp 的注释也承认失败时可能有一部分 change 已经合并;重试时它们被识别为 ALREADY_MERGED 跳过。这是“最终补齐”,不是跨仓库事务。引用写完之后,BatchUpdates 才去更新索引、发事件、发邮件。第一步查的是索引,第二步又回 NoteDb 重读。既然真相在 NoteDb 里,索引是干什么的?它和缓存怎么配合?
七、索引与缓存:在 NoteDb 之上查得快
现象:Git 只会按名字和哈希查找
设计文档说得很直白:Git 只支持按 SHA-1 或按引用名快速查找14。可评审系统天天要回答的是另一类问题:“我名下所有 open 的 change”“这个文件最近被哪些 change 改过”。在 NoteDb 上回答它们,只能把所有 meta 分支都重放一遍。
解决:Lucene 索引负责“找”
Gerrit 内置了四个 Lucene 索引:项目、账号、群组和 change。Lucene 是一个 Java 全文检索库,这里用来按字段查询。change 索引的每个文档存着状态、所有者、文件列表等字段,查询先在索引里找出 change 编号,再按需从 NoteDb 读取。索引是从 NoteDb 派生出来的,会落后。Gerrit 在每个文档里多存一个字段 ref_state,格式是 项目:引用名:哈希,记下建索引时读过的每个引用的值。StalenessChecker 把它和仓库里的当前值一比,就知道这份文档是不是过期了28。配置文档还提到,两次写入同时发生时存在竞争,其中一次可能没反映到索引里;打开 index.autoReindexIfStale,每次建完索引都再查一次是否过期,默认关闭29:
缓存:按哈希寻址,不用失效
从 meta 重放出一个 change 的状态并不便宜。ChangeNotesCache 的键是 (项目, change 编号, meta 引用的哈希),值是解析好的 ChangeNotesState30。它被声明成可以落盘的缓存,不过 change_notes 的磁盘存储默认关闭,平时只在内存里。键里带着哈希,意味着同一个键对应的内容永远不会变:meta 一旦追加了新提交,哈希就变了,自然命中不到旧条目。读一个 change 的流程于是变成:先 exactRef 读 meta 的当前哈希(reftable 上几十微秒),再用它查缓存,未命中才真正遍历提交链。缓存不需要失效机制,这正是“状态即引用”带来的便利。到这里,一台 Gerrit 的全部状态是一组 Git 仓库,外加可以重建的索引和缓存。如果有好几台机器呢?
八、多台机器:复制与多站点
现象:一个 JVM,一个文件系统
设计文档对单机毫不含糊:Gerrit 是一个 JVM 进程,读写一个文件系统;机器或存储坏了,客户端只会收到错误14。最简单的扩展是只读副本(replica):用 --replica 启动,只服务 Git 读操作,没有 REST API、网页和搜索31,SSH 上的 git-receive-pack 直接回错误(DefaultCommandModule.java:87-91)。
复制:状态即引用的红利
副本上的数据由 replication 插件推过去。它的 ReplicationQueue 实现了 core 的 GitReferenceUpdatedListener,引用一更新就收到通知32,默认等 15 秒、按 refs/*:refs/* 推送,NoteDb 的 meta 引用也一起推33。评审数据就是引用,所以复制 Git 仓库就等于复制了评审系统。但复制是异步的,而且索引、缓存、网页会话不在 Git 里,它根本不管。
多站点:谁有资格写这个引用
如果不止一个站点能写,异步复制就会制造脑裂(split brain):site A 确认了一次 push,还没复制给 site B 就挂了;B 不知道自己落后,继续在旧值上接受写入,两边从此分叉。multi-site 插件的设计文档34给出三条通道,见图 10:
其中关键的是全局引用库(global-refdb,可以是 Zookeeper 这类支持原子比较并设置的存储),它为每个可变引用记着最新的哈希。这一节的机制来自插件文档,不在 core 源码里;core 留下的挂点是可替换的 GitRepositoryManager 和 DynamicRefDbRepository,文档说插件正是包装 GitRepositoryManager 来检查每次 Git 交互。插件还要求先迁移到 NoteDb,这是推断:只有所有状态都在引用里,按引用做 CAS 的全局库才管得住全部写入。
小结
回到开头那条 git log,它背后的问题链是这样的:
- 为什么用 JGit? Git 的
pre-receive只能整体放行或拒绝,Gerrit 需要接管命令、替换仓库视图,于是把 JGit 当库嵌进进程,只在仓库对象和 push 钩子两处插手。 - push 到
refs/for/main发生了什么? Gerrit 在钩子里按 Change-Id 认领提交、分配编号,原子地写下 patch set 引用和 meta 引用,再把原命令标成 OK,JGit 就不会创建它。 - 评审数据为什么进 Git? ReviewDb 和 Git 两份存储难以一起备份和复制;NoteDb 把每次评审操作写成 meta 分支上的一个提交,读时从新到旧重放。
- 引用爆炸怎么办? NoteDb 依赖原子批量更新,packed-refs 的代价与引用总数成线性;reftable 用不可变小表的追加、前缀压缩和块索引,把查找和更新都降到与引用总数基本无关。
- 谁能看见什么? 替换引用数据库,让 JGit 只看见过滤后的引用;push 广告去掉
refs/changes/*,用.have弥补协商。 - 合并怎么保证一致? 一个项目内,分支和所有 meta 引用在同一个原子批次里 CAS;冲突就整体重试;跨项目只能依次执行。
- 索引和缓存怎么配合? 索引负责找,用
ref_state发现自己过期;缓存按 meta 哈希寻址,不需要失效;真相永远在 NoteDb。 - 多台机器呢? 复制插件推引用就复制了评审数据;多站点再加全局引用库防脑裂、消息队列同步索引和缓存。
手边有一个 Gerrit 账号的话,可以自己看看这些引用:
# 某个 change 的所有引用(把 34/1234 换成真实的末两位/编号)git ls-remote origin 'refs/changes/34/1234/*'
# 读它的评审记录git fetch origin refs/changes/34/1234/meta && git log --format='%an%n%B' FETCH_HEAD
# 推送时带上 topic 和评审人git push origin HEAD:refs/for/main%topic=demo,r=alice@example.com参考资料
Footnotes
-
Gerrit Code Review - NoteDb Backendgithub.com ↗
Documentation/note-db.txt:NoteDb 的优点、格式和迁移https://github.com/GerritCodeReview/gerrit/blob/master/Documentation/note-db.txt ↩ ↩2 -
CommitMessageOutputTest.javagithub.com ↗meta 提交信息的逐字断言,包括投票和 Submit 两种https://github.com/GerritCodeReview/gerrit/blob/master/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java ↩
-
Gerrit's Historygerritcodereview.com ↗Gerrit 从 Rietveld 的补丁起步;2.x 用 Java 重写,3.x 用 NoteDb 取代 SQL 数据库https://gerritcodereview.com/about.html ↩
-
githooksgit-scm.com ↗pre-receive 只能整体放行或拒绝;proc-receive 可以接管匹配的命令https://git-scm.com/docs/githooks ↩
-
Git 2.29 发布说明github.com ↗receive-pack 把引用更新外包给新的 proc-receive 钩子https://github.com/git/git/blob/master/Documentation/RelNotes/2.29.0.adoc ↩
-
ReceivePack.javagithub.com ↗
service()::2321调 PreReceiveHook;executeCommands()(:1848)只执行NOT_ATTEMPTED的命令https://github.com/eclipse-jgit/jgit/blob/master/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java ↩ -
ReceiveCommits.javagithub.com ↗
processCommandsUnsafe()、selectNewAndReplacedChangesFromMagicBranch()(:2693)、setResult(OK)(:1325)https://github.com/GerritCodeReview/gerrit/blob/master/java/com/google/gerrit/server/git/receive/ReceiveCommits.java ↩ -
commit-msg 钩子github.com ↗Change-Id =
I+ 对提交者身份、HEAD 和提交信息做git hash-objecthttps://github.com/GerritCodeReview/gerrit/blob/master/resources/com/google/gerrit/server/tools/root/hooks/commit-msg ↩ -
RepoSequence.javagithub.com ↗change 编号存在
refs/sequences/changes指向的 blob 里,按批领取https://github.com/GerritCodeReview/gerrit/blob/master/java/com/google/gerrit/server/notedb/RepoSequence.java ↩ -
Gerrit Code Review - Backupgithub.com ↗2.x 时代数据库和 Git 数据要放在同一个卷上做快照https://github.com/GerritCodeReview/gerrit/blob/master/Documentation/backup.txt ↩
-
ChangeUpdate.javagithub.com ↗
applyImpl()拼出 meta 提交的尾注;addLabelFooter()写Label:https://github.com/GerritCodeReview/gerrit/blob/master/java/com/google/gerrit/server/notedb/ChangeUpdate.java ↩ -
ChangeNotesParser.javagithub.com ↗
parseAll()从 meta 的 tip 往旧遍历https://github.com/GerritCodeReview/gerrit/blob/master/java/com/google/gerrit/server/notedb/ChangeNotesParser.java ↩ -
NoteDbUpdateManager.javagithub.com ↗
:319-326:先写项目仓库,再写 All-Users 的草稿,两者不是原子的https://github.com/GerritCodeReview/gerrit/blob/master/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java ↩ -
Gerrit Code Review - System Designgithub.com ↗
Documentation/dev-design.txt:NoteDb、读权限、索引、冗余与可靠性https://github.com/GerritCodeReview/gerrit/blob/master/Documentation/dev-design.txt ↩ ↩2 ↩3 ↩4 -
BatchUpdate.javagithub.com ↗
:698:引用库不支持原子批量更新就不能用 NoteDbhttps://github.com/GerritCodeReview/gerrit/blob/master/java/com/google/gerrit/server/update/BatchUpdate.java ↩ -
NoteDbUpdateExecutor.javagithub.com ↗
bru.setAtomic(true)(:72)https://github.com/GerritCodeReview/gerrit/blob/master/java/com/google/gerrit/server/notedb/NoteDbUpdateExecutor.java ↩ -
PackedBatchRefUpdate.javagithub.com ↗类注释:原子批量更新要重写 packed-refs,耗时与引用总数成线性https://github.com/eclipse-jgit/jgit/blob/master/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java ↩
-
reftablegithub.com ↗
Documentation/technical/reftable.md:格式、事务、压缩和性能数字https://github.com/eclipse-jgit/jgit/blob/master/Documentation/technical/reftable.md ↩ -
Git 2.45 发布说明github.com ↗reftable 作为引用存储后端进入 Git 本身https://github.com/git/git/blob/master/Documentation/RelNotes/2.45.0.adoc ↩
-
gerrit convert-ref-storagegithub.com ↗https://github.com/GerritCodeReview/gerrit/blob/master/Documentation/cmd-convert-ref-storage.txt ↩
-
PermissionAwareReadOnlyRefDatabase.javagithub.com ↗https://github.com/GerritCodeReview/gerrit/blob/master/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java ↩
-
DefaultRefFilter.javagithub.com ↗
filterRefs();change 引用按可见 change 集合判断,tag 做可达性计算https://github.com/GerritCodeReview/gerrit/blob/master/java/com/google/gerrit/server/permissions/DefaultRefFilter.java ↩ -
RefVisibilityControl.javagithub.com ↗
isVisible():每类 Gerrit 引用的可见规则https://github.com/GerritCodeReview/gerrit/blob/master/java/com/google/gerrit/server/permissions/RefVisibilityControl.java ↩ -
ReceiveCommitsAdvertiseRefsHook.javagithub.com ↗push 广告去掉
refs/changes/*,补最近 32 个 open change 作为.havehttps://github.com/GerritCodeReview/gerrit/blob/master/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java ↩ -
Submit Typesgithub.com ↗
Documentation/config-project-config.txt的 submit-type 一节https://github.com/GerritCodeReview/gerrit/blob/master/Documentation/config-project-config.txt ↩ -
MergeOp.javagithub.com ↗
merge():索引算集合、NoteDb 重读、integrateIntoHistory整体重试https://github.com/GerritCodeReview/gerrit/blob/master/java/com/google/gerrit/server/submit/MergeOp.java ↩ -
BatchUpdates.javagithub.com ↗
execute():多个项目的 BatchUpdate 依次执行,之后才建索引、发事件https://github.com/GerritCodeReview/gerrit/blob/master/java/com/google/gerrit/server/update/BatchUpdates.java ↩ -
StalenessChecker.javagithub.com ↗比较索引里的
ref_state与仓库里的引用值;字段定义见ChangeField.javahttps://github.com/GerritCodeReview/gerrit/blob/master/java/com/google/gerrit/server/index/change/StalenessChecker.java ↩ -
gerrit.config: indexgithub.com ↗
index.autoReindexIfStale、index.indexChangesAsync、receive.advertiseOpenChangesRefshttps://github.com/GerritCodeReview/gerrit/blob/master/Documentation/config-gerrit.txt ↩ -
ChangeNotesCache.javagithub.com ↗缓存键 = (project, changeId, meta 的 ObjectId);
change_notes的磁盘存储默认关闭(config-gerrit.txt的cache.<name>.diskLimit)https://github.com/GerritCodeReview/gerrit/blob/master/java/com/google/gerrit/server/notedb/ChangeNotesCache.java ↩ -
Gerrit Code Review - Glossarygithub.com ↗primary、replica、multi-primary、multi-site 的定义https://github.com/GerritCodeReview/gerrit/blob/master/Documentation/glossary.txt ↩
-
replication: Remove the NewProjectCreatedListener implementationgerrit.googlesource.com ↗这次提交的 diff 里可以看到
ReplicationQueue implements ... GitReferenceUpdatedListenerhttps://gerrit.googlesource.com/plugins/replication/+/c16fe9c5a6da6c23dbf45a533f788dcc527e209c%5E%21 ↩ -
Replication plugin configurationgerrit.googlesource.com ↗
replicationDelay默认 15 秒;push默认refs/*:refs/*;replicateNoteDbMetaRefs默认 truehttps://gerrit.googlesource.com/plugins/replication/+/refs/heads/master/src/main/resources/Documentation/config.md ↩ -
multi-site plugin DESIGN.mdgerrit.googlesource.com ↗split brain、global ref-db(比较并设置)、events brokerhttps://gerrit.googlesource.com/plugins/multi-site/+/HEAD/DESIGN.md ↩