以變更集為核心重新設計 GitHub Pull Request
我曾以超大型開源專案(超過 1000 位貢獻者)的維護者、超大型封閉原始碼企業專案的工程師,以及各種規模更小的專案等身分使用 GitHub。直到今日,在 GitHub 上我幾乎所有時間都花在 GitHub Pull Request 上,而對我來說,那不幸地也是 GitHub 上最令人挫折的部分。
我很希望看到 Pull Request 有許多改進,但只要透過一項主要功能,就能解決我大部分的問題:changesets(變更集)。這篇部落格文章將說明這項建議以及我期望看到的樣貌。
聲明:這裡的想法並非原創!我並不聲稱這些想法是由我提出的。我在此提出的建議是基於已被充分探索的 Git 工作流程,並且已在其他產品如 Gerrit、Phabricator 或傳統的以電子郵件為基礎的 patch 審查中部分或完整地實作。
現今的問題
如今 GitHub Pull Request 的生命週期,實際上就是一個巨大的、可變的 changeset。這是一團混亂!
以下是一個典型的 PR 現況:貢獻者將一組 commit 推送到分支,開啟一個 PR,而該 PR 就代表了這個分支。人們透過留言來討論這個 PR。當貢獻者推送新的變更時,這些變更會直接顯示在同一個 PR 上並立即更新它。審查者可以留言,貢獻者也可以同時推送變更,而所有內容都會更新到同一個 PR 上。
這會帶來許多問題:
審查者可能會針對 PR 先前的狀態留下審查意見,而在審查進行期間貢獻者推送了變更,導致該審查意見立刻過時。
更糟的是,審查意見可能會部分過時,而其他回饋在貢獻者推送的變更脈絡下可能變得無法理解。舉例來說,一則行內留言可能會寫「與前一則留言相同的回饋」,但前一則留言現在已經消失/被隱藏,因為貢獻者推送的變更移動了那些程式碼行。1
審查意見並未包含關於其所依附的 commit 的任何中繼資料,只有提交時的時間戳記。使用者可以大致將時間戳記與 commit 對應,但這並不完全準確,因為如果在審查期間有新的 commit 進來,時間戳記看起來會像是審查在最新 commit 之後提交的,但你的審查實際上可能是針對前一個 commit。😕
用來處理審查回饋、仍在進行中的 commit,一旦推送到分支就會立刻變得可見。這迫使貢獻者必須在單一 commit 中處理所有回饋,或讓審查者去處理僅部分解決的回饋。
你無法輕易地回溯到 PR 先前的狀態。如果你想在忽略後續 commit 的情況下審查一組較早的 commit,你必須手動建立「compare」檢視,或使用本地端 checkout(我採用後者)。但無論哪種方式,你都只能看到程式碼變更,無法同時看到當時的審查者回饋!
與上述類似,如果貢獻者推送了多個新的 commit,你無法輕易地將新的這組 commit與舊的進行比較。你實際上一次只能逐一檢視單一 commit。這時,你又得退回使用本地端的
git來手動建立 diff。還有更多……我稍後會再談一些,但我想已經足以說明我的觀點。
我確定上述某些觀點的細節可能有誤。很可能會有人說「他只要做這個就能解決問題 5(a)」。這很有幫助!但是,我想表達的重點是,如果你退一步看,造成這些問題的根本原因才是真正的問題所在。也就是說,以逐一 commit 為單位追蹤分支的單一、可變 changeset。
Changesets
解決方案就是 changesets:一個 Pull Request 可以透過遞增的單調數字(v1、v2……)來進行版本化。這些版本通常被稱為「changesets」。
每個 changeset 都指向分支在固定時間點的狀態。這些版本是不可變的:當新的 commit 被推送時,它們會成為新的 changeset 的一部分。如果貢獻者強制推送分支,那也會成為新的 changeset 的一部分。前一個 changeset 會被永久保存。
新的 changeset 可以立即發布(逐一 commit),也可以延後到貢獻者決定提出新版本以供審查時再發布。後者讓貢獻者可以透過多個 commit 來處理先前的回饋,並在準備好時才發布這些變更。
在 changesets 的世界裡,回饋是依附在某個 changeset 上的。如果審查者開始審查某個 changeset,而此時發布了新的 changeset,這也沒關係,因為審查作為一個原子單位是依附在先前的 changeset 上的。
在後續的 changesets 中,通常會標示某個檔案或程式碼行在先前的 changesets 中仍有未解決的留言。這可確保較早的 changesets 上的回饋不會遺失,且在任何 changeset 被接受之前都必須被處理。
通常,每個 changeset 都由不同的 Git ref 來表示。舉例來說,如今的 GitHub Pull Request 通常是 refs/pr/1234,你可以在本地端使用 git 來 checkout 任何 Pull Request。changeset 則會像是 refs/pr/1234/v2(假設性的例子),讓你也可以 checkout 個別的 changesets。
審查者不再是「核准」整個 PR 並進行合併,而是核准某個changeset。這意味著貢獻者也可以在單一 PR 中發布多個採用不同解法的 changesets,而維護者則有可能選擇非最新的 changeset 作為他們想要合併的變更集合。
GitHub,拜託了!
Changesets 是在許多開源專案和公司中已確立的模式。在 Gerrit 和 Phabricator 等現有產品中,使用者體驗問題也已經被充分探索。我也相信 changesets 可以以非破壞性的方式引入(因為現有的 PR 就像是單一可變 changeset 模式)。
Changesets 將使 Pull Request 對於更大型的專案和組織而言更具可擴展性。除了可擴展性之外,它們還能讓 Pull Request 中雙方的審查流程更清晰、更安全。
當然,我只能就我個人及我的經驗發言,但這項單一的重大功能將大幅改善我在使用 GitHub 時的生活品質與工作能力2。
註腳
隨機一篇部落格