Git

Git 套件是儲存一個 pack-file 以及一些額外元資料(包含一組 ref 和一組(可能為空的)必要 commit)的檔案。如需更多資訊,請參閱 git-bundle[1]gitformat-bundle[5]

套件 URI 是 Git 可以從中下載一個或多個套件,以便在從遠端提取剩餘物件之前啟動物件資料庫的位置。

其中一個目標是為與原始伺服器的網路連線不良的使用者加速複製和提取。另一個好處是讓重度使用者(例如 CI 建置農場)可以使用本機資源處理大部分 Git 資料,從而減輕原始伺服器的負載。

若要啟用套件 URI 功能,使用者可以使用命令列選項指定套件 URI,或者原始伺服器可以透過協定 v2 功能宣告一個或多個 URI。

設計目標

套件 URI 標準旨在具有足夠的彈性,以滿足多種工作負載。套件提供者和 Git 用戶端在如何建立和使用套件 URI 方面有幾種選擇。

  • 套件可以使用伺服器想要的任何名稱。此名稱可以使用套件內容的雜湊來參照不可變的資料。然而,這表示每次更新內容後都需要新的 URI。如果伺服器正在宣告 URI(且伺服器知道正在產生新的套件),這可能是可以接受的,但對於使用命令列選項的使用者來說,這並不符合人體工學。

  • 套件可以特別為啟動完整複製而組織,但也可能為了啟動增量提取而組織。套件提供者必須決定幾種組織方案之一,以在增量提取期間盡量減少用戶端下載,但 Git 用戶端也可以選擇是否將套件用於其中任何一種操作。

  • 套件提供者可以選擇支援完整複製、部分複製或兩者皆支援。用戶端可以偵測哪些套件適用於儲存庫的部分複製篩選器(如果有的話)。

  • 套件提供者可以使用單一套件(僅用於複製)或套件清單。當使用套件清單時,提供者可以指定用戶端是否需要所有套件 URI 才能進行完整複製,或者任何一個套件 URI 是否足夠。這允許套件提供者為不同的地理位置使用不同的 URI。

  • 套件提供者可以使用啟發式方法(例如建立權杖)來組織套件,以協助用戶端防止下載不需要的套件。當套件提供者沒有提供這些啟發式方法時,用戶端可以使用最佳化來盡量減少下載的資料量。

  • 套件提供者不需要與 Git 伺服器關聯。用戶端可以選擇使用套件提供者,而無需由 Git 伺服器宣告。

  • 用戶端可以選擇探索由 Git 伺服器宣告的套件提供者。這可能會在 git clone 期間、在 git fetch 期間、兩者皆可或兩者皆不可發生。使用者可以選擇最適合他們的組合。

  • 用戶端可以隨時選擇手動設定套件提供者。用戶端也可以選擇在 git clone 時手動指定套件提供者作為命令列選項。

每個儲存庫都不同,每個 Git 伺服器都有不同的需求。希望套件 URI 功能具有足夠的彈性來滿足所有需求。如果沒有,則可以透過其版本控制機制來擴充該功能。

伺服器需求

若要提供套件伺服器的伺服器端實作,不需要 Git 協定的其他部分。這允許伺服器維護者使用靜態內容解決方案(例如 CDN)來提供套件檔案。

在目前套件 URI 功能的範圍內,所有 URI 都應該是 HTTP(S) URL,其中內容會使用對該 URL 的 GET 請求下載到本機檔案。伺服器可以在這些請求中包含驗證需求,目的是觸發已設定的憑證助手以進行安全存取。(未來的擴充功能可以使用「file://」URI 或 SSH URI。)

假設伺服器傳回 200 OK 回應,則會檢查 URL 中的內容。首先,Git 會嘗試將檔案剖析為版本 2 或更高的套件檔案。如果檔案不是套件,則會使用 Git 的設定剖析器將檔案剖析為純文字檔案。該設定檔中的鍵值組應該描述套件 URI 的清單。如果這些剖析嘗試皆未成功,則 Git 會向使用者報告提供的套件 URI 提供了錯誤的資料。

伺服器提供的任何其他資料都被視為錯誤。

套件清單

Git 伺服器可以使用一組 key=value 配對來宣告套件 URI。套件 URI 也可以提供 Git 設定格式的純文字檔案,其中包含這些相同的 key=value 配對。在這兩種情況下,我們都認為這是一個套件清單。這些配對指定了有關套件的資訊,用戶端可以使用這些資訊來決定要下載哪些套件以及要忽略哪些套件。

一些鍵側重於清單本身的屬性。

bundle.version

(必要)此值提供套件清單的版本號碼。如果未來 Git 變更啟用了需要 Git 用戶端對套件清單檔案中的新鍵做出反應的功能,則此版本將會遞增。目前唯一的版本號碼是 1,如果指定任何其他值,則 Git 將無法使用此檔案。

bundle.mode

(必要)此值具有兩個值之一:allany。當指定 all 時,用戶端應該期望需要與其儲存庫需求相符的所有列出套件 URI。當指定 any 時,用戶端應該期望任何一個與其儲存庫需求相符的套件 URI 都足以滿足。通常,any 選項用於列出位於不同地理位置的多個不同套件伺服器。

bundle.heuristic

如果存在此字串值鍵,則套件清單旨在與增量 git fetch 命令良好運作。此啟發式訊號表示每個套件都有其他鍵可用,有助於判斷用戶端應該下載的套件子集。目前計畫的唯一啟發式方法是 creationToken

其餘鍵包含 <id> 片段,它是每個可用套件的伺服器指定名稱。<id> 必須只包含英數字元和 - 字元。

bundle.<id>.uri

(必要)此字串值是用於下載套件 <id> 的 URI。如果 URI 以協定(http://https://)開頭,則該 URI 是絕對的。否則,該 URI 會被解譯為相對於用於套件清單的 URI。如果 URI 以 / 開頭,則該相對路徑是相對於用於套件清單的網域名稱。(這種使用相對路徑的方式旨在更容易在具有不同網域名稱的大量伺服器或 CDN 上分配一組套件。)

bundle.<id>.filter

此字串值代表也應出現在此套件標頭中的物件篩選器。伺服器使用此值來區分不同類型的套件,用戶端可以從中選擇與其物件篩選器相符的套件。

bundle.<id>.creationToken

此值是一個非負 64 位元整數,用於對套件清單進行排序。這用於在 bundle.heuristic=creationToken 時,在提取期間下載套件的子集。

bundle.<id>.location

此字串值會宣告提供套件 URI 的真實世界位置。這可以用來向使用者呈現要使用哪個套件 URI 的選項,或者僅作為 Git 選取哪個套件 URI 的資訊指示器。這僅在 bundle.modeany 時有價值。

以下是使用 Git 設定格式的套件清單範例

[bundle]
	version = 1
	mode = all
	heuristic = creationToken
[bundle "2022-02-09-1644442601-daily"]
	uri = https://bundles.example.com/git/git/2022-02-09-1644442601-daily.bundle
	creationToken = 1644442601
[bundle "2022-02-02-1643842562"]
	uri = https://bundles.example.com/git/git/2022-02-02-1643842562.bundle
	creationToken = 1643842562
[bundle "2022-02-09-1644442631-daily-blobless"]
	uri = 2022-02-09-1644442631-daily-blobless.bundle
	creationToken = 1644442631
	filter = blob:none
[bundle "2022-02-02-1643842568-blobless"]
	uri = /git/git/2022-02-02-1643842568-blobless.bundle
	creationToken = 1643842568
	filter = blob:none

這個範例使用 bundle.mode=all 以及 bundle.<id>.creationToken 啟發式方法。它也使用 bundle.<id>.filter 選項來呈現兩組平行的 bundle:一組用於完整複製,另一組用於無 blob 的部分複製。

假設這個 bundle 清單位於 URI https://bundles.example.com/git/git/,因此兩個無 blob 的 bundle 具有以下完整展開的 URI

  • https://bundles.example.com/git/git/2022-02-09-1644442631-daily-blobless.bundle

  • https://bundles.example.com/git/git/2022-02-02-1643842568-blobless.bundle

宣傳 Bundle URI

如果使用者知道他們正在複製的儲存庫的 bundle URI,那麼他們可以透過命令列選項手動指定該 URI。然而,Git 主機可能希望在複製操作期間宣傳 bundle URI,以幫助不了解此功能的使用者。

此功能唯一需要的是伺服器可以宣傳一個或多個 bundle URI。此宣傳採用新的協定 v2 功能形式,專門用於發現 bundle URI。

客戶端可以選擇任意 bundle URI 作為選項,透過一些探索性檢查選擇具有最佳效能的 URI。是否有多個 URI 優於通過伺服器端基礎架構進行地理分散的單個 URI,取決於 bundle 提供者來決定。

使用 Bundle URI 複製

bundle URI 的主要需求是加快複製速度。Git 客戶端將按照以下流程與 bundle URI 互動

  1. 使用者使用 --bundle-uri 命令列選項指定 bundle URI,客戶端發現 Git 伺服器宣傳的 bundle 清單。

  2. 如果從 bundle URI 下載的資料是 bundle,則客戶端會檢查 bundle 標頭,以確認客戶端儲存庫中是否存在先決條件 commit OID。如果缺少某些 OID,則客戶端會延遲解壓縮 bundle,直到其他 bundle 被解壓縮,使這些 OID 存在。當所有需要的 OID 都存在時,客戶端會使用 refspec 解壓縮該資料。預設的 refspec 是 +refs/heads/*:refs/bundles/*,但可以配置。這些 refs 會被儲存,以便稍後的 git fetch 協商可以將每個 bundle 的 ref 作為 have 來溝通,從而減少透過 Git 協定進行 fetch 的大小。為了允許從此 ref 命名空間中修剪 refs,Git 可以引入編號的命名空間(例如 refs/bundles/<i>/*),以便可以刪除過時的 bundle refs。

  3. 如果該檔案改為 bundle 清單,則客戶端會檢查 bundle.mode 以查看清單是 all 還是 any 形式。

    1. 如果 bundle.mode=all,則客戶端會考慮所有 bundle URI。該清單會根據與客戶端儲存庫的部分複製篩選器匹配的 bundle.<id>.filter 選項進行縮減。然後,會請求所有 bundle URI。如果提供了 bundle.<id>.creationToken 啟發式方法,則會按建立權杖的遞減順序下載 bundle,當 bundle 具有所有需要的 OID 時停止。然後可以按建立權杖的遞增順序解壓縮 bundle。客戶端會儲存最新的建立權杖作為啟發式方法,以避免如果 bundle 清單沒有宣傳具有更大建立權杖的 bundle 時的未來下載。

    2. 如果 bundle.mode=any,則客戶端可以選擇檢查任何一個 bundle URI。客戶端可以使用多種方式在這些 URI 之間進行選擇。如果初始選擇未能返回結果,客戶端也可以回退到另一個 URI。

請注意,在複製期間,我們預期會需要所有 bundle,並且可以使用諸如 bundle.<uri>.creationToken 之類的啟發式方法按時間順序或並行下載 bundle。

如果給定的 bundle URI 是具有 bundle.heuristic 值的 bundle 清單,則客戶端可以選擇將該 URI 儲存為其選擇的 bundle URI。然後,客戶端可以在稍後的 git fetch 呼叫期間直接導航到該 URI。

下載 bundle URI 時,客戶端可以選擇在提交下載整個內容之前檢查初始內容。這可能提供足夠的資訊來確定 URI 是 bundle 清單還是 bundle。在 bundle 的情況下,客戶端可能會檢查 bundle 標頭以確定所有宣傳的提示已在客戶端儲存庫中,並取消剩餘的下載。

使用 Bundle URI 擷取

當客戶端擷取新資料時,它可以決定在從原始遠端擷取之前先從 bundle 伺服器擷取。這可以透過命令列選項來完成,但更有用的是使用複製期間指定的設定值。

擷取操作遵循相同的程序,從 bundle 清單下載 bundle(儘管我們這裡希望使用並行下載)。我們預期當瘦 bundle 中所有先決條件 commit OID 都已在物件資料庫中時,該過程將結束。

使用 creationToken 啟發式方法時,如果 bundle 的建立權杖不比儲存的建立權杖大,則客戶端可以避免下載任何 bundle。擷取新 bundle 後,Git 會更新此本地建立權杖。

如果 bundle 提供者未提供啟發式方法,則客戶端應嘗試在下載完整 bundle 資料之前檢查 bundle 標頭,以防 bundle 提示已存在於客戶端儲存庫中。

錯誤條件

如果 Git 客戶端在根據 bundle URI 或該位置找到的 bundle 清單下載資訊時發現某些意外情況,則 Git 可以忽略該資料,並繼續如同沒有給定 bundle URI 一樣。遠端 Git 伺服器是最終的真實來源,而不是 bundle URI。

以下是一些錯誤條件範例

  • 客戶端無法連線到給定 URI 的伺服器,或連線遺失且沒有任何機會恢復。

  • 客戶端收到 400 等級的回應(例如 404 Not Found401 Not Authorized)。客戶端應使用憑證輔助程式來尋找並提供 URI 的憑證,但在處理特定的 400 等級錯誤時,應與 Git 的其他 HTTP 協定語意匹配。

  • 伺服器回報任何其他失敗回應。

  • 客戶端收到無法解析為 bundle 或 bundle 清單的資料。

  • bundle 包含不符合預期的篩選器。

  • 客戶端無法解壓縮 bundle,因為先決條件 commit OID 不在物件資料庫中,並且沒有更多 bundle 可供下載。

還有一些情況可能會被視為浪費,但不是錯誤條件

  • 下載的 bundle 包含比複製或擷取請求所要求的更多資訊。一個主要的範例是,如果使用者請求使用 --single-branch 進行複製,但下載儲存來自所有 refs/heads/* 參考的所有可到達 commit 的 bundle。這最初可能是浪費的,但也許這些物件將通過客戶端關心的稍後 ref 更新變成可到達的。

  • git fetch 期間下載的 bundle 包含已在物件資料庫中的物件。如果我們使用 bundle 進行擷取,這可能是不可避免的,因為客戶端在對遠端伺服器執行其「追趕」擷取後幾乎總會略微領先 bundle 伺服器。當客戶端擷取的頻率遠高於伺服器計算 bundle 的頻率時,這種額外的工作最為浪費,例如,如果客戶端使用具有背景維護的每小時預先擷取,但伺服器每週計算 bundle。因此,除非伺服器已透過 bundle.heuristic 值明確建議,否則客戶端不應使用 bundle URI 進行擷取。

範例 Bundle 提供者組織

bundle URI 功能經過刻意設計,可以靈活適應 bundle 提供者想要組織物件資料的不同方式。但是,在這裡描述一個完整的組織模型可能會有所幫助,以便提供者可以從該基礎開始。

這個範例組織是 GVFS 快取伺服器所使用模型的簡化版本(請參閱本文檔末尾附近的部分),這些伺服器在加快非常大的儲存庫的複製和擷取速度方面非常有用,儘管使用了 Git 之外的額外軟體。

bundle 提供者在多個地理位置部署伺服器。每個伺服器管理自己的 bundle 集。伺服器可以追蹤多個 Git 儲存庫,但根據模式為每個儲存庫提供 bundle 清單。例如,當鏡像位於 https://<domain>/<org>/<repo> 的儲存庫時,bundle 伺服器可能會在 https://<server-url>/<domain>/<org>/<repo> 提供其 bundle 清單。原始 Git 伺服器可以列出「任何」模式下的所有這些伺服器

[bundle]
	version = 1
	mode = any
[bundle "eastus"]
	uri = https://eastus.example.com/<domain>/<org>/<repo>
[bundle "europe"]
	uri = https://europe.example.com/<domain>/<org>/<repo>
[bundle "apac"]
	uri = https://apac.example.com/<domain>/<org>/<repo>

這個「清單的清單」是靜態的,僅在新增或移除 bundle 伺服器時才會變更。

每個 bundle 伺服器管理自己的 bundle 集。初始 bundle 清單僅包含一個 bundle,其中包含從原始伺服器複製儲存庫所接收的所有物件。該清單使用 creationToken 啟發式方法,並根據伺服器的時間戳為 bundle 建立 creationToken

bundle 伺服器會定期排程更新 bundle 清單,例如每天一次。在此任務期間,伺服器會從原始伺服器擷取最新內容,並產生一個 bundle,其中包含從最新的原始 refs 可到達的物件,但不包含在先前計算的 bundle 中。此 bundle 會新增至清單,並注意 creationToken 必須嚴格大於先前的最大 creationToken

當 bundle 清單變得太大時,例如超過 30 個 bundle,則會將最舊的「N 減 30」個 bundle 合併為單個 bundle。此 bundle 的 creationToken 等於合併 bundle 中最大的 creationToken

此處提供了一個範例 bundle 清單,儘管它只有兩個每日 bundle,而不是完整的 30 個清單

[bundle]
	version = 1
	mode = all
	heuristic = creationToken
[bundle "2022-02-13-1644770820-daily"]
	uri = https://eastus.example.com/<domain>/<org>/<repo>/2022-02-09-1644770820-daily.bundle
	creationToken = 1644770820
[bundle "2022-02-09-1644442601-daily"]
	uri = https://eastus.example.com/<domain>/<org>/<repo>/2022-02-09-1644442601-daily.bundle
	creationToken = 1644442601
[bundle "2022-02-02-1643842562"]
	uri = https://eastus.example.com/<domain>/<org>/<repo>/2022-02-02-1643842562.bundle
	creationToken = 1643842562

為了避免即使物件資料在原始伺服器上變得無法存取,仍永久儲存和提供這些資料,這個捆綁包合併可以更加謹慎。它不採用舊捆綁包的絕對聯集,而是透過查看較新的捆綁包,並確保它們必要的提交都存在於這個合併的捆綁包中(或在另一個較新的捆綁包中)來建立捆綁包。這樣可以「過期」在時間範圍內未被新提交使用的物件資料。這些資料可能會在稍後的推送中重新引入。

這種資料組織方式有兩個主要目標。首先,透過從較近的來源下載預先計算的物件資料,可以加快儲存庫的初始複製速度。其次,git fetch 命令可以更快,特別是如果用戶端已經幾天沒有提取。但是,如果用戶端 30 天沒有提取,捆綁包列表的組織方式會導致重新下載大量物件資料。

讓這種組織方式對頻繁提取的使用者更有用的一種方法是更頻繁地建立捆綁包。例如,可以每小時建立捆綁包,然後每天將這些「每小時」捆綁包合併為一個「每日」捆綁包。每日捆綁包在 30 天後合併到最舊的捆綁包中。

建議如果此儲存庫的用戶端預期使用無 blob 的部分複製,則使用 blob:none 過濾器重複此捆綁包策略。這個無 blob 捆綁包列表與完整捆綁包在同一個列表中,但使用 bundle.<id>.filter 金鑰來分隔這兩組。對於非常大的儲存庫,捆綁包提供者可能只想提供無 blob 捆綁包。

實施計畫

這份設計文件是作為一份期望文件單獨提交的,目標是在幾個修補程式系列中實施所有提到的用戶端功能。以下是提交這些功能的潛在概要

  1. 將捆綁包 URI 整合到 git clone 中,並提供 --bundle-uri 選項。這將包括一個新的 git fetch --bundle-uri 模式,用作 git clone 底層的實作。此處的初始版本將期望在給定的 URI 處有一個捆綁包。

  2. 實作從捆綁包 URI 解析捆綁包列表的能力,並更新 git fetch --bundle-uri 邏輯以正確區分 bundle.mode 選項。特別設計該功能,使組態格式解析將鍵值對列表饋送到捆綁包列表邏輯中。

  3. 建立 bundle-uri 協定 v2 命令,以便 Git 伺服器可以使用鍵值對宣傳捆綁包 URI。插入到現有的鍵值輸入中,以進行捆綁包列表邏輯。允許 git clone 發現這些捆綁包 URI 並從捆綁包資料引導用戶端儲存庫。(此選擇是透過組態選項和命令行選項選擇加入。)

  4. 允許用戶端理解 bundle.heuristic 組態金鑰和 bundle.<id>.creationToken 啟發式方法。當 git clone 發現具有 bundle.heuristic 的捆綁包 URI 時,它會設定用戶端儲存庫,以便在稍後的 git fetch <remote> 命令期間檢查該捆綁包 URI。

  5. 允許用戶端在 git fetch 期間發現捆綁包 URI,如果設定了 bundle.heuristic,則為稍後的提取設定捆綁包 URI。

  6. 實作「檢查標頭」啟發式方法,以在 bundle.<id>.creationToken 啟發式方法不可用時減少資料下載。

在審查這些功能時,此計畫可能會更新。我們還預期,隨著此功能成熟並在現實場景中使用,將發現並實施新的設計。

當伺服器處理用戶端請求時,Git 協定已經有一種功能,其中 Git 伺服器可以列出 URL 集以及 packfile 回應。然後預期用戶端下載這些位置的 packfile,以便完全理解回應。

此機制由 Gerrit 伺服器(使用 JGit 實作)使用,並且在減少 CPU 負載和提高用戶複製效能方面非常有效。

此機制的主要缺點是原始伺服器需要確切地知道這些 packfile 中的內容,並且這些 packfile 需要在伺服器回應後一段時間內對用戶可用。原始伺服器和 packfile 資料之間的這種耦合很難管理。

此外,這種實作很難與提取一起使用。

GVFS 協定 [2] 是一組 HTTP 端點,在建立 Git 的部分複製之前,獨立於 Git 專案設計。此協定的一個功能是「快取伺服器」的概念,它可以與建置機器或開發人員辦公室共置,以傳輸 Git 資料,而不會使中央伺服器過載。

VFS for Git 最著名的端點是 GET /gvfs/objects/{oid} 端點,它允許按需下載物件。這是該產品檔案系統虛擬化的關鍵部分。

但是,更細微的需求是 GET /gvfs/prefetch?lastPackTimestamp=<t> 端點。給定一個可選的時間戳記,快取伺服器會回應一個預先計算的 packfile 列表,其中包含在這些時間間隔內引入的提交和樹。

快取伺服器使用以下策略計算這些「預取」packfile

  1. 每小時,都會產生一個帶有給定時間戳記的「每小時」pack。

  2. 每晚,前 24 個每小時 pack 會合併成一個「每日」pack。

  3. 每晚,所有超過 30 天的預取 pack 都會合併成一個 pack。

當使用者針對具有快取伺服器的儲存庫執行 gvfs clonescalar clone 時,用戶端會請求所有預取 packfile,最多為 24 + 30 + 1 個 packfile,僅下載提交和樹。然後,用戶端會向原始伺服器請求參考,並嘗試檢出該提示參考。(還有一個額外的端點可以協助從給定的提交取得所有可存取的樹,以防該提交尚未在預取 packfile 中。)

git fetch 期間,hook 使用先前下載的預取 packfile 中最新的時間戳記來請求預取端點。只會下載具有較晚時間戳記的 packfile 列表。大多數使用者每小時提取一次,因此他們最多會取得一個每小時預取 pack。機器已關閉或超過 30 天未提取的使用者可能會重新下載所有預取 packfile。這種情況很少見。

重要的是要注意,用戶端始終會聯絡原始伺服器以取得參考廣告,因此參考通常會「超前」於預取 pack 資料。當 git checkoutgit log 等命令需要時,會使用 GET gvfs/objects/{oid} 請求按需下載缺少的物件。某些 Git 優化會停用會導致這些按需下載過於激進的檢查。

scroll-to-top