GitHub 和 Gerrit 都是誕生于 2008 年的代碼平台,兩個平台各自形成了互相獨立的生态。GitHub 及其模仿者們成為行業主流,托管着大多數開源項目和商業項目的源代碼,而 Gerrit 也有一衆“粉絲”,像大名鼎鼎的安卓(Android)、OpenStack、Golang等。雖然 GitHub 和 Gerrit 都是 Git 倉庫的托管和研發協同平台,但是二者背後的技術大相徑庭。采用 GitHub 模式的代碼平台的後端使用原生 Git(cgit)實作,而 Gerrit 則采用 JGit(用 java 重新實作 Git 接口)實作。再有兩者理念不同,一種采用分布式協同,一種是集中式協同。二者的詳細對比參考下面的表格:
https://intranetproxy.alipay.com/skylark/lark/0/2020/png/158866/1602673172841-5a109a1c-1c7f-4173-b4f9-988c57270ead.png
我們可以看出 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/ 的引用。
或者,不在服務端産生任何引用,而是将使用者新增送出以郵件方式發到郵件清單,類似 GitGitGadget [1] 那樣。
那麼如何能開發一個 proc-receive 鈎子呢?相比 pre-receive 和 post-receive 鈎子,proc-receive 鈎子實作難度稍微大一些,因為它和 Git 服務端程式 git-receive-pack 有着複雜的雙向通訊:服務端程式調用鈎子,将 git push 的指令以及 push-options(如果有的話)發送給鈎子,然後鈎子調用 API 替代 Git 完成引用的更新。如下圖所示:
https://intranetproxy.alipay.com/skylark/lark/0/2020/png/158866/1602674624880-e7c314a7-b3c3-4d34-8e58-a9c0211356ea.png1.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的“建立合并請求”按鈕的下方,有一條低調的提示,如圖:
https://intranetproxy.alipay.com/skylark/lark/0/2020/png/158866/1602687193823-9bfe6e3a-c302-4cf4-aebe-2f0c8e4c73ac.png參照提示資訊的說明,使用者會看到用标準的 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。
https://intranetproxy.alipay.com/skylark/lark/0/2020/png/158866/1602689027191-ce75d650-f50f-43fb-bb56-a8471599c3e4.png圖中的兩個角色,一個是開發者,另外一個是評審者。
開發者通過如下操作,建立和更新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.infogit-repo 除了可以适配阿裡巴巴的代碼平台(如:雲效Codeup)、Gerrit 之外,還可以通過擴充支援其他實作了 Git 2.29 新特性的代碼平台,詳見 git-repo 相關文檔。
未來已來,全新的Git體驗,通路雲效Codeup。
本文作者:蔣鑫