本文旨在為中階 Jujutsu 使用者以及對 Jujutsu 感到好奇的 Git 使用者提供指引。
我是一位 Jujutsu 的重度使用者,並發現自己越來越依賴 Jujutsu 社群中俗稱的「巨型合併」(megamerge)工作流程來進行日常開發。這個工作流程在少數進階使用者之外,似乎討論得相當少,因此我想分享它的樣貌以及為何它如此方便,特別是當您處於複雜的開發環境或傾向於提交大量小型 PR 時。
趕時間嗎?直接跳到文末獲取一些快速提示。
如果您是一位普通的 Git 使用者(甚至是尚未深入研究進階工作流程的 Jujutsu 使用者),您可能會驚訝地發現,合併提交(merge commit)本身並沒有任何特別之處。它不是一個有自己規則的特殊案例。它只是一個有多個父提交的普通提交。它甚至不必是空的!¹
您可能會更驚訝地發現,合併提交不僅限於只有兩個父提交。我們非正式地將有三個或更多父提交的合併提交稱為「章魚合併」(octopus merges),雖然您可能會想:「在什麼情況下我會想合併超過兩個分支?」但這其實是一個非常強大的概念。章魚合併是整個巨型合併工作流程的基礎!
基本上,在巨型合併工作流程中,您很少直接在分支的頂端進行工作。取而論之,您會建立一個章魚合併提交(以下簡稱「巨型合併」),作為您關心的每一個工作分支的子提交。這意味著錯誤修復、功能分支、您正在等待 PR 的分支、您需要與之協同工作的他人的分支、本地環境設定分支,甚至是可能不存在於任何分支或不屬於任何分支的私人提交。所有您關心的內容都會被納入巨型合併。重要的是要記住,您不會推送巨型合併本身,只推送它所組成的分支。
如果這聽起來很多,也沒關係。畢竟,您知道當您必須重新審視舊的 PR 以便進行審查時,您需要付出多少精力來切換上下文。然而,這能為您帶來幾個非常有價值的優勢:
開始一個巨型合併非常簡單:只需建立一個新的提交,將您想納入巨型合併的每一個分支都作為其父提交。我喜歡給這個提交一個名稱並讓它保持空白,如下所示:
然後,您會在整個結構的頂端留下一個空白提交。這就是您進行工作的地方!巨型合併提交之上的任何內容都被視為 WIP(進行中)。您可以隨意將其拆分,根據該巨型合併提交建立多個分支,隨心所欲。您所寫的任何內容都將基於巨型合併中所有內容的總和,正如我們所期望的!
當然,總有一天您會對您所擁有的感到滿意,並開始思考:
如何將您的 WIP 變更納入您的巨型合併,取決於它們需要歸屬何處。如果您正在進行的變更應該歸入現有的變更中,您可以使用帶有 `--to` 標誌的 `squash` 命令將它們移動到正確的下游提交。如果您的提交包含多個提交的變更,您可以先將其拆分為多個提交再進行壓縮,或者(我更喜歡)使用 `squash --interactive` 進行互動式壓縮,只挑選要移動的特定部分。
當然,Jujutsu 是一款優秀的軟體,並提供了一些自動化功能!`absorb` 命令會為您完成許多這項工作,它會識別您當前提交中的每一行或每一個程式碼塊屬於哪個下游可變動提交,並自動將它們壓縮下來。每次使用時,我都覺得這簡直是魔法(而且不是那種令人費解的、無法理解的邪惡黑魔法),它是 Jujutsu 功能的核心部分,讓巨型合併工作流程如此無縫。
`absorb` 並非總是能捕捉到您提交中的所有內容,但它通常能捕捉到您至少 90% 的變更。其餘的變更則很容易向下游壓縮,或與任何先前提交無關。
方便的是,如果我有屬於新提交的變更,情況也沒複雜多少。如果該提交屬於我正在工作的某個分支,我可以直接進行 rebase 並相應地移動書籤。
讓我們來分解這個 rebase,以便更好地理解它是如何工作的:
如果我已經開始了一個全新的功能,或者發現了一個不相關的錯誤需要修復,那就更簡單了!使用一些別名,我可以非常輕鬆地將新變更包含在我的巨型合併中:²
這裡對 `closest_merge(to)` 實際在做什麼進行快速解釋:
使用這個 revset 別名,`stack` 讓我們可以鎖定任何我們想要的 revset,並將其插入到 `trunk()`(您的主要開發分支)和我們的巨型合併提交之間:
如果我有需要同時包含的多個變更堆疊,這會更有用;如果只有一個,我還有另一個別名,它只獲取巨型合併之後的整個變更堆疊:
這個別名不需要任何輸入!只需準備好您的提交並暫存它們:
這個巨型合併拼圖中最後缺失的一塊是(不幸的是)處理現實世界中的其他人:
這是個好問題,也是我花了幾個月時間試圖廣泛回答的問題。Jujutsu 提供了一種非常簡單的方式,可以將您的整個工作樹 rebase 到您的主分支上:
然而,這僅在您的整個工作樹都是您的變更時才有效。當您嘗試引用您不擁有的提交(例如未追蹤的書籤或其他人的分支)時,Jujutsu 會提前停止,以保護它們不被重寫。³
讓我們透過只 rebase 我們實際控制的提交來解決這個問題。我為此苦惱了一段時間,但幸運的是,Jujutsu 社群非常棒。感謝 Stephen Jennings 提出了這個很棒的 revset:
這個別名不是試圖 rebase 我們整個工作樹(正如 `jj rebase --onto trunk()` 所嘗試的那樣),而是只針對我們實際允許移動的提交。這樣可以留下我們不控制的分支,以及堆疊在他人分支之上的工作。透過 `--simplify-parents`,我們還可以清理這個過程留下的任何醜陋邊緣。即使是面對龐大的、包含九個貢獻者的混合式巨型合併,它也從未讓我失望過!(試著快速說五次。)
Jujutsu 的巨型合併非常酷,讓您可以同時處理多個工作流。閱讀整篇文章以深入了解它們的工作原理。為了獲得極致的人體工學設定,請將這些添加到您的設定檔中,執行 `jj config edit --user`:
使用 `absorb` 和/或 `squash --interactive` 將新變更納入現有提交,使用 `commit` 和 `rebase` 建立巨型合併下的新提交,並使用 `commit` 搭配 `stack` 或 `stage` 將整個分支移動到您的巨型合併中。⁴
請記住,巨型合併實際上並非設計來推送遠端伺服器;它們只是方便您檢視整體情況的一種方式。您仍然需要像往常一樣單獨發布分支。
巨型合併可能不是每個人的菜——在我展示我的工作樹時,我確實收到了一些驚恐的眼神——但一旦您嘗試了它們,您可能會發現它們能讓您幾乎毫不費力地在任務之間切換。試試看吧!
在 Git 中,包含除了衝突解決之外的新變更的合併提交被稱為「邪惡合併」(evil merge)。在 Jujutsu 中,邪惡合併並非真正「邪惡」,因為它比 Git 擁有更一致的模型。⁵
這絕對是題外話,但我認為有必要提及。↩
別名是 Jujutsu 中一個非常強大的部分。有兩種類型您應該深入研究:revset 別名,它允許您使用 revset 語言建立返回一個或多個提交的自訂函數;以及命令別名,它允許您擴展 Jujutsu 的預設功能並添加您自己的功能。↩
還有模板別名,它允許您使用模板語言更改 Jujutsu 在終端機上的輸出;以及 fileset 別名,它與 revset 別名類似,但使用 fileset 語言作用於檔案而不是修訂版本。↩
Jujutsu 有可變動提交(mutable commits)和不可變動提交(immutable commits)的概念,這基本上決定了您在正常情況下可以修改哪些提交。這很大程度上只是一個檢查,因為您可以使用 `--ignore-immutable` 來覆蓋它,但它有助於避免麻煩。您可以使用 `mutable()` 和 `immutable()` 別名分別選擇可變動和不可變動的提交。↩
如果 `restack` 的行為不符合您的預期,請嘗試整合 Austin Seipp 的這個設定。我的預設設定會重新堆疊儲存庫中的每一個可變動提交,當您有很多過去的、尚未清理的可變動分支時,這種行為表現不佳。↩
感謝 Andrew Hoog 協助我解決 Astro 中的註腳問題。您知道您可以引用其他註腳中的註腳嗎?↩
特別感謝 msmetko、Cole Helbling、Hardy Jones、Alpha Chen、Jeremy Brown、Luke Randall、789.ha 和 Philip Metzger 閱讀了早期草稿並分享了他們的意見回饋!我們都站在巨人的肩膀上看得更遠。↩
我喜歡建構工具、打破工作流程,然後將它們重新組合得更好。如果您喜歡我的作品並想支持我,您可以請我喝杯咖啡 ☕ 或在 Liberapay 💛 上支持我。↩