Git2.29让Git成功“牵 手”Gerrit
GitHub 和 Gerrit 都是誕生于 2008 年的代碼平臺,兩個平臺各自形成了相互獨立的生態。GitHub 及其模仿者們成為行業主流,托管著大多數開源項目和商業項目的源代碼,而 Gerrit 也有一眾“粉絲”,像大名鼎鼎的安卓(Android)、OpenStack、Golang等。雖然 GitHub 和 Gerrit 都是 Git 倉庫的托管和研發協同平臺,但是二者背后的技術大相徑庭。采用 GitHub 模式的代碼平臺的后端使用原生 Git(cgit)實現,而 Gerrit 則采用 JGit(用 java 重新實現 Git 接口)實現。再有兩者理念不同,一種采用分布式協同,一種是集中式協同。二者的詳細對比參考下面的表格:
我們可以看出 GitHub 模式和 Gerrit 模式各有優劣。那么能否有兩全其美的代碼平臺呢?Git 2.29 讓這成為可能。
1.Git2.29的新功能,讓Git“牽手”Gerrit
Git 2.29.0 于 2020年10月發布,其中包含了兩個阿里巴巴貢獻的新特性。阿里巴巴貢獻的新特性讓 Git 牽手 Gerrit,讓 GitHub 模式的代碼平臺可以像 Gerrit 一樣工作。
1.1服務端新鉤子proc-receive
Git 2.29 在服務端增加了 proc-receive 鉤子。對Git原理熟悉的用戶可能知道 Git 服務端經常使用的兩個鉤子:pre-receive 和 post-receive,在 git 推送時服務端運行的這兩個鉤子會進行前置檢查(授權檢查等)和后處理(發送通知、觸發構建等)。而新引入的 proc-receive 鉤子的執行順序,是介于這兩個鉤子之間,用于替代 Git 內置功能完成分支(引用)的更新操作。這個新鉤子提供給 Git 代碼平臺更為強大的服務端定制能力,存在豐富的想象空間。
例如:一個用戶使用如下命令向服務端推送:
git push origin HEAD:refs/for/master
服務端如果是 Git 2.29 之前的版本,會直接在服務端倉庫中創建名為 refs/for/master 的引用。而 Git 2.29 版本引入的 proc-receive 鉤子,會接管 Git 更新引用的操作。proc-receive 鉤子能做什么,完全取決于開發者的想象:
- 創建一個代碼評審,并在倉庫中產生名為 refs/pull/123/head 的引用,便于用戶下載相關代碼。
- 或者,推送包含的每一個提交都產生一個獨立的代碼評審,就像 Gerrit 那樣。每個評審都產生類似 refs/changes/<num> 的引用。
- 或者,不在服務端產生任何引用,而是將用戶新增提交以郵件方式發到郵件列表,類似 GitGitGadget [1] 那樣。
那么如何能開發一個 proc-receive 鉤子呢?相比 pre-receive 和 post-receive 鉤子,proc-receive 鉤子實現難度稍微大一些,因為它和 Git 服務端程序 git-receive-pack 有著復雜的雙向通訊:服務端程序調用鉤子,將 git push 的命令以及 push-options(如果有的話)發送給鉤子,然后鉤子調用 API 替代 Git 完成引用的更新。如下圖所示:
1.2客戶端新能力report-status-v2
阿里巴巴在給 Git 社區貢獻的第一個版本中,只在服務端引入新的鉤子,并未修改客戶端相關代碼。為了能讓社區接受修改,我以實現 Gerrit 的類似功能作為賣點向社區進行“推銷”。Junio(Git 維護者,Google)第一時間承認這個貢獻的價值:
And I think it is reasonable to add a new hook that takes over the whole flow in "git receive-pack" to do so.
同時指出疑問:向 Gerrit 推送一個引用A (refs/for/master),Gerrit 創建了另外的引用B (refs/changes/1/123),那么 Gerrit 是如何告訴客戶端正確地更新本地跟蹤分支的?
How do Gerrit folks deal with the "we pushed to the server, so let's pretend to have turned around and fetched from the same server immediately after doing so" client-side hack, by the way?
只有屈指可數的人才能像 Junio 這樣發出靈魂的拷問!
于是在后續的代碼評審中,與 Junio 以及 GitHub 的 Jeff King (Peff) 之間進行了多次交流,代碼迭代了19個版本[2],為 Git 服務端和客戶端新增了一個能力:report-status-v2。
簡單的說,在老版本的 report-status 能力下,如果客戶端發送推送命令要求服務器更新 A 分支(如: refs/for/master),而服務端轉而創建了分支 B(refs/changes/1/123)。這種情況下,服務端也只能通知客戶端分支A被創建,而非分支B,否則客戶端會報錯:“服務端沒有按照我的要求去執行”。
擴展后的 report-status-v2,服務端可以報告給客戶端實際修改的分支,可以報告不同的分支初始指向和最新指向,甚至客戶端的一條命令可以對應多條分支的更新。支持該能力的客戶端也能正確地將服務端實際更新的分支顯示出來。
2.云效Codeup是業界第一個支持git 2.29新功能的代碼平臺
阿里云云效Codeup(https://codeup.aliyun.com)是業界首個支持 Git 2.29 新功能的代碼平臺。當用戶執行 git push 命令時,特殊的目標分支會觸發服務端 proc-receive 鉤子,完成特定功能。
2.1命令行創建代碼評審
在云效Codeup的“新建合并請求”按鈕的下方,有一條低調的提示,如圖:
參照提示信息的說明,用戶會看到用標準的 Git 命令行就可以直接在倉庫中創建代碼評審。例如用戶執行下面命令將當前(HEAD)的更改推送到服務端,向服務端的 master 分支創建代碼評審: git push origin HEAD:refs/for/master/local/branch
說明:
1.引用表達式的目標分支包含特殊的前綴“refs/for/",用于向遠程倉庫特定分支“master”發起代碼評審。其中的“local/branch”通常寫做客戶端的本地分支名。多次git push請求,如果是相同用戶、相同的目標分支、相同的“local/branch",則對應用同一個pull request。
2.此外Codeup還支持“refs/drafts/"、“refs/for-review/”等特殊前綴。前綴“refs/drafts/”的格式和“refs/for/”類似,也是針對目標分支創建或者更新pull request,區別在于創建的pull request處于草稿狀態,只能發表評審意見,不能合入。前綴“refs/for-review/”后面跟指定的pull request ID,用于更新指定的pull request。
2.2AGit-Flow工作流
使用上面介紹的命令行創建代碼評審,可以實現無需倉庫派生、無需特性分支、無需特殊授權設置,完成代碼評審的創建和合入。阿里巴巴代碼平臺上支持的這種代碼協同模式,我們稱之為AGit-Flow。
圖中的兩個角色,一個是開發者,另外一個是評審者。
開發者通過如下操作,創建和更新pullrequest:
1.開發者克隆倉庫。
2.本地倉庫內開發,創建提交。
3.工作區中執行命令,推送本地提交到服務器。
4.服務器自動創建新的代碼評審(例如:pullrequest#123)。
5.開發者根據評審意見,在本地工作區繼續開發,新增或修改提交。
6.工作區中再次執行gitpr命令,推送本地提交到服務器。
7.服務器發現目標分支上已經存在來自同一用戶、同一本地分支的pullrequest,因此用戶此次推送沒有創建新的pullrequest,而是更新已經存在的pullrequest。
代碼評審者,不但可以給出評審意見,也可以直接發起對評審代碼的修改,更新pullrequest:
8.代碼評審者執行gitdownload123下載編號為123的pullrequest到本地倉庫。
9.代碼評審者本地修改代碼后,執行gitpr--change123命令,將本地修改推送到服務端。
10.服務端接收到代碼評審者的特殊gitpush命令,更新之前由開發者創建的pullrequest。
11.項目管理者通過點擊pullrequest評審界面的合并按鈕,將pullrequest合入master分支。master分支被更新,同時關閉pullrequest。
2.3GitHub是否會引入Git2.29的新功能?
GitHub 引入 Git 2.29 新功能沒有那么快,原因是 GitHub 的架構是分布式三副本架構,使用的是定制版本的 Git,不能通過升級到 2.29 來支持 proc-receive 鉤子,需要另行開發。
當 proc-receive 特性在 Git 社區評審過程中,我邀請了 GitHub 的 Jeff King 參與代碼評審。我在郵件中提到了如何在分布式多副本架構中引入 proc-receive 鉤子的建議,因為我知道 GitHub 的分布式三副本和阿里巴巴的代碼平臺的分布式架構都面臨 proc-receive 鉤子可能被多次執行的問題。我們采用的路徑是對 Git 協議進行擴展以實現 proc-receive 鉤子執行的冪等性。Jeff King 在回復中介紹了 GitHub 的后端實現:
We do run receive-pack on each replica backend. We have a hacky patch for a config option that tells receive-pack to just skip the actual ref-transaction, leaving it up to the proxy layer to do. I've been pushing for us to actually abandon receive-pack entirely, since most of its heavy lifting can be done by sub-programs (for-each-ref for the advertisement, index-pack to receive the pack, and update-ref to update refs). But it's a non-trivial change, and the benefits are only moderate, so it hasn't quite been worth the effort yet.
就是說 GitHub 的分布式多副本服務器上的 git-receive-pack 是修改版本,并不執行引用更新的操作,而是由代理層執行,主要目的是為了避免 pre-receive、post-receive 等鉤子的多次執行。阿里巴巴的多副本方案和 GitHub 多副本實現不同,我們的實現可以復用大部分 git-receive-pack 的功能。相關討論如下:
Thanks to Peff for providing technical details of the architecture. I understand that "receive-pack" of GitHub backend is not involved in ?references update (executing the commands), so the "proc-receive" hook won't be turned on for GitHub's architecture. While in our ?architecture (inspired by "spokes" of GitHub), the proxy will deliver > not only packfile, but also commands to all three replicas. The proxy will execute "receive-pack" on the replica with a special argument, so the proxy can talk with "receive-pack" with an extended protocol. After running pre-receive hook and release the packfile from quarantine, the replica will stop and wait for the proxy to coordinate. After creating a distributed lock, the proxy will tell all? the replicas continue to update the references. One problem we met is the proc-receive and the post-receive hook must be executed once. We > can make the execution of the hooks idempotent, or let only one of the > replica run the hook. We choose the latter. OK, that makes more sense. We solve that by not updating the refs at all via receive-pack (which gives us flexibility to run our own hooks separately on just one replica, etc).
OK, that makes more sense. We solve that by not updating the refs at all via receive-pack (which gives us flexibility to run our own hooks separately on just one replica, etc).
2.4對于Gerrit會有什么影響么?
Gerrit 擁有兩個核心特性,一個是集中式的工作流,一個是逐提交評審。集中式工作流可以通過 Git 2.29 的新功能在 GitHub 生態中推廣,而 Gerrit 獨特的逐提交評審界面依舊具有強大的生命力。
Git 2.29 版本包含的 report-status-v2 特性,可以為 Gerrit 用戶帶來新的體驗。可以預見 Gerrit 會在服務端增加 report-status-v2 相關實現以便更好地適配 Git 新客戶端。
3.用git-repo擴展Git命令集
Gerrit 生態包含多款客戶端工具,例如:Google 為安卓項目開發了名為 repo 的客戶端工具實現多倉庫管理;OpenStack 社區開發了名為 git-review 的工具,以便簡化 Gerrit 工作流的命令行操作。
我們也為阿里巴巴的 AGit-Flow 工作流設計了一款名為 git-repo 的客戶端工具,這款工具既能像 OpenStack 社區的相關工具那樣對單倉庫執行,也能像 Android 社區的 repo 那樣實現多倉庫項目的協同。
我們將git-repo開源,倉庫地址:https://github.com/alibaba/git-repo-go
關于git-repo的安裝和使用,訪問網址:https://git-repo.info
git-repo 除了可以適配阿里巴巴的代碼平臺(如:云效Codeup)、Gerrit 之外,還可以通過擴展支持其他實現了 Git 2.29 新特性的代碼平臺,詳見 git-repo 相關文檔。
未來已來,全新的Git體驗,訪問云效Codeup。
[1]:https://github.com/gitgitgadget/gitgitgadget
[2]:https://public-inbox.org/git/20200827154551.5966-1-worldhello.net@gmail.com/
總結
以上是生活随笔為你收集整理的Git2.29让Git成功“牵 手”Gerrit的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 闲鱼把各种玩法做成了一个平台:哆啦A梦
- 下一篇: 简单易用高性能!一文了解开源迁移学习框架