Reorient GitHub Pull Requests Around Changesets

Mitchell Hashimoto

以 Changeset 為核心重塑 GitHub Pull Request

原文由 Mitchell Hashimoto 發布,訂閱此部落格

我曾以大型開源專案(超過 1000 位貢獻者)的維護者、也曾以大型封閉原始碼企業專案的工程師身分使用 GitHub,各種規模更小的專案也都經歷過。直到今天,在 GitHub 上我幾乎所有時間都花在 pull request 上,而對我來說,那也是 GitHub 上最讓人感到挫折的部分。

雖然我希望 pull request 能有許多改進,但其中很大一部分問題,只要靠一個主要功能就能解決:changeset。這篇文章就是要說明這個提議,以及我期待看到的樣貌。

聲明:這裡的想法並非原創!我並不是要宣稱這些想法是我發明的。這裡的建議是基於已經被充分探索的 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)」。那當然有幫助!但我想表達的重點是,如果退一步看,造成這些問題的根本原因才是真正的癥結。也就是說,用單一、可變的 changeset 以逐一 commit 的方式去追蹤一個分支。


Changeset

解決辦法就是 changeset:讓 pull request 可以透過單調遞增的編號來進行版本控管(v1、v2……)。這些版本通常就被稱為「changeset」。

每個 changeset 都指向一個分支在某個固定時間點的狀態。這些版本是不可變的:當有新的 commit 被推送時,它們會成為新的 changeset 的一部分。如果貢獻者強制推送分支,那同樣也會形成一個新的 changeset。而先前的 changeset 會永遠被保留下來。

新的 changeset 可以在每次 commit 後立刻發布,也可以先暫緩,直到貢獻者決定要提出新版本供審查時再發布。後者讓貢獻者可以先用多個 commit 來處理先前的回饋,等到覺得準備好了再一次發布這些變更。

在 changeset 的世界裡,回饋是附加在某個 changeset 上的。如果審查者開始審查某個 changeset,而此時又有新的 changeset 發布,那也沒關係,因為整份審查作為一個原子單位,本來就是附著在先前的 changeset 上。

在後續的 changeset 中,通常會標示出哪些檔案或程式碼行在先前的 changeset 中還有未解決的留言。這能確保較早 changeset 的回饋不會遺失,並且在任何一個 changeset 被接受之前都必須被處理。

通常,每個 changeset 會由不同的 Git ref 來代表。舉例來說,現在 GitHub 的 pull request 通常是 refs/pr/1234,你可以在本機用 git 這樣來 checkout 任何一個 pull request。而 changeset 則會像是 refs/pr/1234/v2(假設性的例子),讓你也可以 checkout 個別的 changeset。

與其「核准」整個 PR 然後合併,不如讓審查者核准的是某個 changeset。這也意味著,貢獻者可以在同一個 PR 中提出採用不同做法的多個 changeset,而維護者可以選擇非最新的某個 changeset 作為最終要合併的變更。


GitHub,拜託了!

Changeset 已經是許多開源專案和公司行之有年的模式。在 Gerrit 和 Phabricator 這類現有產品中,與之相關的使用者體驗問題也已經被充分探索過了。我也相信 changeset 可以用非破壞性的方式引入(因為現在的 PR 就像是單一可變 changeset 的模式)。

Changeset 能讓 pull request 在面對更大型的專案和組織時更具可擴展性。除了可擴展性之外,它也能讓審查流程對參與 pull request 的雙方來說都更清晰、更安全。

當然,我只能就我個人和我的經驗來說,但這個單一的重大功能,將會大幅改善我在 GitHub 上的生活品質與工作能力2

註腳

  1. 這是一個微小但帶來不便的問題,但這個問題擴大後會變成嚴重的問題。

  2. 「那就別用 GitHub 就好了啊!」我以前就聽過這種回饋。我現在使用 GitHub 還有很多其他原因,所以對我個人而言,這目前不是一個可行的選項。如果你可以不用 GitHub,那麼確實可以在其他產品中找到對 changeset 的支援。

本文章由 muse-spark-1.2-contributor 進行翻譯

留言