開源 Git 專案剛剛發布了 Git 2.55,包含來自超過 100 位貢獻者的功能與錯誤修正,其中 33 位是首次貢獻者。我們上次與您分享 Git 的最新動態是在 2.54 發布時

為了慶祝這次最新版本發布,以下是 GitHub 對自上次以來所引入的一些最有趣功能與變更的看法。

使用增量多重包索引進行重新打包

回顧本系列的讀者可能還記得我們對 增量多重包索引增量多重包可達性位圖 的報導。如果您需要快速複習,這裡是簡短版本。

Git 將您的儲存庫內容儲存為個別的 物件:提交、樹與 blob。這些物件通常存在於 包檔 中,這是壓縮的物件集合。每個包檔都有一個對應的 包索引,讓 Git 能快速定位包檔內的任何物件。但大型儲存庫通常不會只有一個包檔:隨著時間推移,擷取、推送、維護任務與重新打包可能會留下許多包檔。

多重包索引(或稱 MIDX)讓 Git 能為多個包檔建立單一索引。Git 不需要開啟並搜尋每個包檔的個別索引,而是可以詢問 MIDX 給定物件位於哪個包檔以及其偏移量。這對大型儲存庫特別有用,也是 GitHub 儲存庫維護策略的基礎之一。

正如我們在 Git 2.47 引入增量 MIDX 格式 時所報導的,儲存庫可以將其 MIDX 儲存為一連串層級,而不是單一涵蓋所有包檔的 MIDX。單一檔案的 MIDX 讀取簡單且有效率,但有重要的維護成本;因為該檔案包含它所涵蓋的每個包檔,即使是小更新也可能需要在已經很大的儲存庫中進行大量寫入。

增量 MIDX 透過儲存 MIDX 層級鏈來解決這個問題。每個層級涵蓋某些包檔集合,而鏈檔則記錄這些層級的順序。將新層級附加到鏈的最前端不會使舊層級失效,因此 Git 可以在不重寫涵蓋整個儲存庫的單一 MIDX 的情況下為新建立的包檔建立索引。

Git 2.55 教導 git repack 如何直接寫入這些增量 MIDX 鏈:

$ git repack --write-midx=incremental 

在沒有其他選項的情況下,該模式是僅附加的:Git 會為重新打包所建立的包檔寫入新層級,並保留現有層級。這在您想要最小化維護執行期間重寫的元資料量時已經很有用。

但僅附加的鏈無法永遠增長。如果每次維護執行都新增一個層級,那麼最終鏈本身就會成為您需要維護的東西。Git 2.55 也支援將 --write-midx=incremental 與幾何重新打包結合:

$ git repack --write-midx=incremental --geometric=2 -d

當這些模式一起使用時,每次重新打包會建立新的頂端層級,然後決定是否應將相鄰層級壓縮在一起。預設規則由 repack.midxSplitFactor 控制:如果較新層級中累積的物件數量相對於下一個較舊層級變得足夠大,Git 會將這些層級合併為單一替換層級。否則,較舊層級會保持不動。

從高層次來看,演算法運作如下。下方的 NN 指的是 repack.midxNewLayerThreshold 值,而 ff 指的是 repack.midxSplitFactor 值:

  1. 選擇未被 MIDX 涵蓋的包檔作為幾何重新打包候選。如果頂端 MIDX 層級至少有 NN 個包檔,也將那些納入候選。
  2. 將一般的幾何重新打包規則套用到該候選集合,並寫入涵蓋結果包檔的新頂端 MIDX 層級。
  3. 壓縮相鄰 MIDX 層級,同時較新層級(或多個)的累積物件數量超過下一個較深層級物件數量的 1/f1/f。

為了了解這些部分如何組合在一起,讓我們從已經有增量 MIDX 鏈的儲存庫開始。較舊層級在左側,頂端層級在右側。同時,正常的儲存庫活動持續產生新包檔。這些包檔尚未被任何 MIDX 層級涵蓋,這表示下一次維護執行有兩項工作:決定要重新打包什麼,以及決定要重寫 MIDX 鏈的多少部分。

Diagram showing a chain of MIDX layers with newly written packs.

通常,那些未被 MIDX 涵蓋的包檔是唯一的幾何重新打包候選:Git 可以在不影響任何現有層級的情況下寫入新包檔與新頂端 MIDX 層級。下圖顯示更有趣的情況,當前頂端層級累積了足夠的包檔以達到設定的 repack.midxNewLayerThreshold。一旦達到該閾值,頂端層級的包檔就可以與新寫入的包檔一起成為幾何重新打包候選。

幾何重新打包接著會對最新候選包檔提出本地問題。幾何重新打包接著會對最新候選包檔提出本地問題:緊鄰某個包檔後綴(𝒫\mathcal{P})左側的包檔是否足夠大,以在 Git 將 𝒫\mathcal{P} 合併時保留幾何級數?在下方的第一次嘗試中,𝒫\mathcal{P} 包含當前頂端層級的最小包檔以及新的未被 MIDX 涵蓋的包檔。但分割點左側的包檔只有 30,000 個物件,小於 𝒫\mathcal{P} 大小的兩倍,因此這個分割點太靠右了。

Diagram indicating the first geometric split is too small.

因此 Git 將分割點向前移動一個包檔並再次提出相同的問題。現在 𝒫\mathcal{P} 包含頂端層級的另一個包檔。分割點左側的包檔有 100,000 個物件,至少是所選後綴的兩倍大。這是幾何不變性成立的點,因此 Git 可以將恰好這些包檔合併為新包檔。

Diagram showing how moving the split left finds a geometric roll-up.

寫入該新包檔後,Git 會在先前頂端層級的存活包檔與新寫入的合併包檔之上寫入新的頂端 MIDX 層級。此時,包檔本身已處於良好狀態,但 MIDX 鏈可能仍累積了太多小型相鄰層級。Git 對 MIDX 層級本身套用相同的「較新相對於較舊」直覺:如果較新層級相對於其鄰近層級足夠大,則將其元資料壓縮為替換層級。

Diagram showing the new tip layer cannot compact with its neighbor.

該壓縮步驟刻意僅限於元資料。Git 不會再次重新打包這些層級中的物件;它會寫入涵蓋相同包檔的新 MIDX 層級。然後它會考慮下一個較舊層級。在這裡,壓縮後的層級仍然小於較深層級的一半,因此 Git 停止。較舊層級保持不動,這是讓此維護保持增量的關鍵特性。

Diagram showing compaction stops before rewriting the deeper layer.

結果是兩種極端之間的折衷。單一檔案 MIDX 能最小化查詢複雜度,但可能需要在維護期間進行大量重寫。純粹僅附加的增量 MIDX 能最小化每次寫入,但允許鏈無限制增長。幾何增量重新打包能保持層級數量與總物件數呈對數關係,同時確保最新的、最小的層級比舊的、較大的層級更常被重寫。

這也與 Git 現有的重新打包機制整合。尚未被 MIDX 鏈涵蓋的新寫入包檔永遠是幾何重新打包的候選;較深 MIDX 層級中的包檔則保持不動。頂端 MIDX 層級中的包檔只有在頂端層級至少有 repack.midxNewLayerThreshold 個包檔後才會加入候選集合。如果頂端層級仍小於該閾值,Git 會完全跳過干擾它,僅為新寫入的包檔附加新層級。

對於接收穩定新物件流的儲存庫而言,這表示例行維護可以增量更新儲存庫的包檔元資料,而不需要強迫每次維護執行重寫涵蓋整個物件存放區的單一 MIDX。

[source]

使用 git history 修復較早的提交

任何在送出審查前潤飾提交系列的人可能都有過這種經驗:您注意到工作樹中的變更其實應該屬於較早的提交,而不是分支的最前端。

今天,處理這件事的一種常見方式是建立修正提交,然後自動壓縮:

$ git commit --fixup=<commit> 
$ git rebase --autosquash <commit>^

這可行,但它要求您明確說明機制而非意圖。Git 2.55 建構在實驗性的 git history 命令之上,該命令由 Git 2.54 引入,新增了新的 fixup 子命令。它將索引中目前暫存的變更套用到較早的提交:

$ git history fixup <commit>

這裡是一個小範例。第一個提交引入了鬆餅食譜,接著在其上進行了幾次提交。後來,我們發現食譜缺少楓糖漿。在暫存那一行變更後,git history fixup <commit> 將其合併到原始食譜提交中,並在其上重播後續提交。

An animated gif showing Here is a small example. The first commit introduced a pancake recipe, followed by a few more commits on top. Later, we realize that the recipe was missing maple syrup. After staging that one-line change, git history fixup <commit> folds it into the original recipe commit and replays the descendant commits on top.

在此,暫存的變更成為目標提交本身的一部分。目標提交預設保留其訊息與作者身份,除非您傳遞 --reedit-message,而 Git 會重寫後續的提交,使分支以修正位於正確位置的等效歷史結束。

git history 的其餘部分一樣,此命令仍處於實驗階段。它也刻意保持保守。因為 fixup 從索引讀取,所以需要工作樹且無法在裸儲存庫中操作;如果套用暫存變更會產生衝突,命令會中止而不是讓您處於有狀態重寫的中間。

[source]

冰山一角…

現在我們已經詳細涵蓋了最大的變更,讓我們來看看此版本中的一些其他新功能與更新。

  • 回顧本系列的讀者可能還記得我們在 Git 2.54 中對 基於設定的掛鉤 的報導,這些掛鉤讓您可以在 Git 設定中定義掛鉤,而不僅限於 $GIT_DIR/hooks 中的可執行檔案。掛鉤是 Git 在工作流程中眾所周知點執行的腳本,例如在建立提交前或接收推送後。將它們移到設定中使得這些掛鉤更容易分享、組合與選擇性停用,而不需要將腳本複製到每個儲存庫的掛鉤目錄中。

    Git 2.55 擴展了這項工作,允許相容的已設定掛鉤平行執行。例如,專案可能有獨立的預提交掛鉤用於程式碼檢查與單元測試;如果兩者都宣告 hook.<name>.parallel = true,Git 可以同時執行它們。並行作業的數量可以透過 hook.jobs 全域控制、透過 hook.<event>.jobs 針對事件控制,或透過命令列 git hook run -j 控制。需要共享狀態的掛鉤,例如提交訊息掛鉤或其他檢查索引或工作樹的掛鉤,會繼續循序執行。

    [source]

  • 如果您曾執行 git status 卻在終端機遇到長時間暫停,您可能已經使用 Git 的 內建檔案系統監視器 來加快速度。當 core.fsmonitor 啟用時,git status 等命令可以詢問長時間執行的守護程序哪些路徑已變更,而不是掃描整個工作樹。

    到目前為止,該內建守護程序僅在 macOS 與 Windows 上可用。Git 2.55 新增對 Linux 的支援,其實作使用 inotify。這不需要提升權限,但每個目錄需要一個監視,因此非常大的儲存庫可能需要提高 fs.inotify.max_user_watches 限制。如同其他平台,守護程序在網路掛載的儲存庫周圍保持保守,這些儲存庫仍需選擇加入。

    [source]

  • 可達性位圖 是 Git 用來回答「哪些物件可從此提交到達?」等問題而不需從頭走訪整個物件圖的技巧之一。它們讓物件走訪更快,但 Git 仍必須在 git repack --write-midx-bitmaps 等維護任務期間建立與更新這些位圖。

    Git 2.55 透過避免不必要的樹遞迴、重用已計算的選定位圖、快取物件位置以及在 XOR 它們之前先排序位圖,讓該生成路徑更快。在補丁系列的基準測試中,這些一般性改進將一個大型儲存庫的位圖生成時間從約 612 秒 減少到約 294 秒

    同一系列也改進了 偽合併位圖,它將相關參照分組在一起,讓 Git 可以在走訪期間合併預先計算的 位元陣列,而不是重複發現相同的物件。在 一項基準測試 中,偽合併讓完整的 git rev-list --objects --use-bitmap-index 走訪快了近 20 倍,但先前幾乎使位圖生成時間加倍。在這些變更之後,偽合併保留了大部分走訪加速,同時為位圖生成路徑增加了更少的工作。

    [source, source]

  • 如果您使用部分複製、過濾包檔或其他 Git 刻意省略某些物件的工作流程,包檔大小仍然很重要。git pack-objects --path-walk 模式,在 Git 2.51 中引入,在執行第二次壓縮傳遞之前會依路徑分組物件,當路徑局部性很重要時可以產生更好的差異。

    在 Git 2.55 中,--path-walk 可以與包含 blob:noneblob:limit=<n>tree:0object:type=<type>sparse:<oid> 以及相容的 combine: 過濾器的過濾器結合使用。這使得在更多部分複製與過濾包檔工作流程中使用 --path-walk 進行打包成為可能。在 Git 自身儲存庫的一項基準測試中,無 blob 的路徑走訪重新打包產生的包檔小約 16%,但代價是較慢的新鮮差異計算。

    [source]

  • Git 學會了一個新的實驗性命令 git format-rev,用於從標準輸入美化格式化修訂。與走訪歷史範圍的 git log 不同,git format-rev 專為您一次遇到一個提交或嵌入在其他文字中的情況而設計。

    例如,假設您正在使用 git last-modified 列印某個目錄中每個路徑最後修改的提交。如果您想知道每個路徑最後是誰修改的,而不僅是哪個提交修改的呢?您可以透過將其輸出管道傳遞給類似這樣的東西來將那些提交替換為作者名稱:

    $ git last-modified | perl -F'\t' -lane ' 
      chomp($F[0] = qx(git show -s --format=%an $F[0])); 
      print join "\t", @F 
    ' 
    Junio C Hamano	builtin/commit.c 
    [...]

    這可行,但它必須為每一列啟動新的 Git 程序只是為了格式化提交。在 Git 2.55 中,git format-rev 可以將該部分作為正常管道處理:

    $ git last-modified | 
      git format-rev --stdin-mode=text --format=%an 
    Junio C Hamano	builtin/commit.c 
    [...]

    命令的文字模式也可以重寫在自由格式文字中找到的完整提交物件名稱,這使其對提交訊息掛鉤或其他腳本工作流程很有用。

    [source]

  • 當您將儲存庫推送到某處時,您可能注意到以 remote: 開頭的輸出:

    $ git push origin main 
    Enumerating objects: 5, done. 
    [...] 
    remote: Resolving deltas: 100% (2/2), completed with 1 local object.

    當用戶端擷取或推送時,Git 可以在同一連線上多工處理不同的資料流:一個側頻帶傳遞包檔資料(正在傳輸的實際物件),另一個傳遞用戶端通常列印到 stderr 的進度訊息,第三個傳遞來自遠端的錯誤。

    這些進度訊息很有用,但它們也來自連線的另一端,可能會直接列印到您的終端機。在 Git 2.55 之前,這表示伺服器可以在側頻帶輸出中包含任意終端機控制序列,包括移動游標或清除文字的序列。Git 現在預設會遮罩大部分這些控制字元,同時仍允許 ANSI 顏色序列,因此彩色進度輸出繼續運作。

    [source]

  • 假設您在編輯檔案到一半時發現您從錯誤的分支開始。如果您想要切換到的分支變更了相同的路徑,普通的 git checkout <branch> 將拒絕移動並有覆寫您工作的風險。git checkout -m <branch> 是該操作的「嘗試將我的本機編輯帶著走」版本。

    但是當另一端對相同路徑有修改時會發生什麼?以前,git checkout -m 給您一次機會立即解決產生的衝突。Git 2.55 透過在內部使用自動儲存使其更安全,因此衝突的本機變更會儲存為儲存項目,您可以立即解決或稍後重新套用。

    [source]

  • 有些專案需要將同一個分支發布到多個地方,例如主要主機與一個或多個鏡像。遠端群組早已可用於 git fetch,其中群組是使用 remotes.<name> 設定為以空白分隔的遠端清單。Git 2.55 讓 git push 使用相同的簡寫:

    $ git config remotes.publish "github gitlab mirror" 
    $ git push publish main

    這等同於依序推送到群組中的每個遠端。由於原子性只能保證單一傳輸連線,因此在推送到群組時不支援 --atomic

    [source]

  • git log --graph 很適合視覺化分支結構,直到圖形本身變得太寬而無法閱讀。在有許多平行分支的儲存庫中,圖形通道可能會在您看到提交主旨之前就消耗掉大部分終端機。

    Git 2.55 為 git log --graph 與相關命令新增了 --graph-lane-limit=<n>。超過限制的通道會被替換為 `~`,使圖形輸出在具有非常寬歷史的儲存庫中更易於管理。

    [source]

  • 假設您想要列出分支上最近的 10 個提交。這很簡單:git log -n 10 恰好可以做到。但如果您想要最舊的 10 個提交呢?如果您在想,「肯定不是 git log --reverse --10」,那麼恭喜您:您是資深的 Git 使用者!Git 不是反轉歷史然後列印 10 個提交,而是取得最近的 10 個提交並反轉它們的順序。

    您可以透過後處理整個範圍來達到這個目的(例如使用 git log --reverse <range> | tail -10),但這樣做仍然要求 Git 列印並格式化 shell 將丟棄的所有提交。Git 2.55 為 git rev-listgit log 系列命令新增了新的 --max-count-oldest=<n> 選項,它會選擇範圍內最舊的 n 個提交。

    [source]

  • 在擷取期間,用戶端與伺服器透過讓用戶端廣告它已有的提交作為 have 行來進行協商。這讓伺服器可以避免傳送用戶端已經可以到達的物件。但在有許多參照的儲存庫中,協商演算法可能會跳過對尋找共同歷史特別重要的參照。

    Git 2.55 新增了對哪些參照參與協商的控制。新的 include 與 restrict 選項,以及對應的 remote.* 設定,允許使用者要求某些參照作為 have 行傳送,或將協商限制在特定參照集合。

    [source]

…冰山的其餘部分

這只是最新版本變更的樣本。如需更多資訊,請查看 2.55 的發行說明,或 任何先前版本Git 儲存庫 中。