English ▾ 主題 ▾ 最新版本 ▾ bundle-uri 最後更新於 2.50.0

Git bundle 是儲存 pack-file 以及額外中繼資料(包括一組參考 ref 和一組(可能為空)必要的 commit)的檔案。詳細資訊請參閱 git-bundle[1]gitformat-bundle[5]

Bundle URI 是 Git 可以下載一個或多個 bundle 的位置,旨在從遠端提取剩餘物件之前,預先引導 (bootstrap) 物件資料庫。

其目標之一是為與原始伺服器網路連接狀況不佳的使用者加速 clone 與 fetch 操作。另一個好處是允許高負載使用者(如 CI 建置農場)使用本地資源來處理大部分的 Git 資料,從而降低原始伺服器的負載。

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

設計目標

Bundle URI 標準旨在具有足夠的彈性以滿足多種工作負載。Bundle 提供者與 Git 用戶端在建立與取用 bundle URI 時有多種選擇。

  • Bundle 可以由伺服器設定任何名稱。此名稱可以透過 bundle 內容的雜湊值來參照不可變 (immutable) 的資料。然而,這意味著每次內容更新後都需要一個新的 URI。如果伺服器正在公告該 URI(且伺服器知道有新的 bundle 被產生),這或許可以接受,但對於使用命令列選項的使用者來說則不夠直覺。

  • Bundle 可以專門為引導完整 clone 而組織,也可以為了引導增量 fetch 而組織。Bundle 提供者必須決定幾種組織方案中的一種,以最小化用戶端在增量 fetch 期間的下載量,但 Git 用戶端也可以選擇是否將 bundle 用於這些操作。

  • Bundle 提供者可以選擇支援完整 clone、部分 clone (partial clone) 或兩者皆支援。用戶端可以偵測哪些 bundle 適合儲存庫的部分 clone 過濾器(若有的話)。

  • Bundle 提供者可以使用單一 bundle(僅供 clone),或使用 bundle 列表。當使用 bundle 列表時,提供者可以指定用戶端進行完整 clone 時是否需要 所有 bundle URI,還是 任何 一個 bundle URI 即已足夠。這允許 bundle 提供者針對不同地理位置使用不同的 URI。

  • Bundle 提供者可以使用啟發式演算法(例如建立權杖 creation tokens)來組織 bundle,協助用戶端避免下載不需要的 bundle。當 bundle 提供者未提供這些啟發式演算法時,用戶端可以使用最佳化手段來最小化下載資料量。

  • Bundle 提供者不需要與 Git 伺服器關聯。用戶端可以選擇使用不受 Git 伺服器公告的 bundle 提供者。

  • 用戶端可以選擇發現由 Git 伺服器公告的 bundle 提供者。這可以發生在 git clonegit fetch 期間,兩者皆可,或兩者皆不可。使用者可以選擇最適合他們的組合。

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

每個儲存庫皆不相同,每個 Git 伺服器也有不同的需求。希望 bundle URI 功能足夠靈活以滿足所有需求。若不然,該功能亦可透過其版本機制進行擴充。

伺服器需求

若要提供 bundle 伺服器的伺服器端實作,不需要 Git 協定的其他部分。這允許伺服器維護人員使用 CDN 等靜態內容解決方案來提供 bundle 檔案。

在當前 bundle URI 功能的範疇內,所有 URI 預期皆為 HTTP(S) URL,內容透過對該 URL 發送 GET 請求下載至本地檔案。伺服器可以對這些請求加入驗證需求,旨在觸發已設定的憑證輔助程式以進行安全存取。(未來擴充可能會使用 "file://" URI 或 SSH URI。)

假設伺服器回傳 200 OK,則會檢查該 URL 的內容。首先,Git 嘗試將該檔案解析為版本 2 或以上的 bundle 檔案。如果該檔案不是 bundle,則會將其解析為 Git 設定檔格式的純文字檔。該設定檔中的鍵值對預期會描述一個 bundle URI 列表。如果這些解析嘗試皆不成功,Git 將向使用者報告錯誤,指出提供的 bundle URI 包含錯誤資料。

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

Bundle 列表

Git 伺服器可以使用一組 key=value 對來公告 bundle URI。Bundle URI 也可以提供一個 Git 設定格式的純文字檔,其中包含相同的 key=value 對。在這兩種情況下,我們皆視其為 bundle 列表。這些鍵值對指定了 bundle 的相關資訊,用戶端可據此決定要下載哪些 bundle 以及忽略哪些 bundle。

有幾個鍵專注於列表本身的屬性。

bundle.version

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

bundle.mode

(必要)此值為 allany 其中之一。當指定 all 時,用戶端應預期需要所有符合儲存庫需求且列出的 bundle URI。當指定 any 時,用戶端應預期任何一個符合儲存庫需求的 bundle URI 即已足夠。通常 any 選項用於列出位於不同地理位置的多個不同 bundle 伺服器。

bundle.heuristic

如果存在此字串類型的鍵,則該 bundle 列表設計為可與增量 git fetch 指令良好配合。該啟發式參數表示每個 bundle 都有額外的鍵可用,有助於決定用戶端應該下載哪些 bundle 子集。目前唯一計畫的啟發式參數為 creationToken

其餘鍵包含一個 <id> 區段,這是伺服器為每個可用 bundle 指定的名稱。<id> 必須僅包含英數字元與 - 字元。

bundle.<id>.uri

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

bundle.<id>.filter

此字串值表示一個物件過濾器,它也應該出現在該 bundle 的標頭中。伺服器使用此值來區分不同種類的 bundle,用戶端可從中選擇符合其物件過濾器的 bundle。

bundle.<id>.creationToken

此值為非負 64 位元整數,用於排序 bundle 列表。當 bundle.heuristic=creationToken 時,這用於在 fetch 期間下載 bundle 的子集。

bundle.<id>.location

此字串值公告了提供該 bundle URI 的實際地理位置。這可用於向使用者提供選擇使用哪個 bundle URI 的選項,或僅作為 Git 所選 bundle URI 的參考指標。此值僅在 bundle.modeany 時有價值。

以下是一個使用 Git 設定格式的 bundle 列表範例

[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:一組用於完整 clone,另一組用於不含 blob 的部分 clone。

假設此 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

如果使用者知道他們要 clone 的儲存庫的 bundle URI,他們可以透過命令列選項手動指定該 URI。然而,Git 主機可能希望在 clone 操作期間公告 bundle URI,以幫助尚未察覺此功能的使用者。

此功能唯一的要求是伺服器可以公告一個或多個 bundle URI。此公告採取專用於發現 bundle URI 的新 protocol v2 功能的形式。

用戶端可以選擇任意 bundle URI 作為選項,或者透過某些探索性檢查選擇效能最好的 URI。由 bundle 提供者決定是否提供多個 URI 優於由伺服器端基礎設施進行地理分散的單一 URI。

使用 Bundle URI 進行 Clone

Bundle URI 的主要需求是加速 clone。Git 用戶端將依照以下流程與 bundle URI 互動

  1. 使用者透過 --bundle-uri 命令列選項指定 bundle URI,或者用戶端發現由 Git 伺服器公告的 bundle 列表。

  2. 如果從 bundle URI 下載的資料是一個 bundle,則用戶端會檢查 bundle 標頭以確認用戶端儲存庫中是否存在必要的 commit OID。如果缺少部分 OID,則用戶端會延遲解開 bundle (unbundling),直到其他 bundle 已解開,使得這些 OID 存在為止。當所有必要的 OID 存在時,用戶端使用 refspec 解開該資料。所使用的 refspec 為 +refs/*:refs/bundles/*。這些 ref 會被儲存,以便稍後的 git fetch 交涉可以將每個 bundle 中的 ref 作為 have 進行溝通,減少 Git 協定下的 fetch 大小。為了允許刪除此 ref 命名空間中的 ref,Git 可能會引入一個帶編號的命名空間(例如 refs/bundles/<i>/*),以便可以刪除過期的 bundle ref。

  3. 如果檔案是一個 bundle 列表,則用戶端會檢查 bundle.mode 以查看該列表是 all 還是 any 形式。

    1. 如果 bundle.mode=all,則用戶端會考慮所有 bundle URI。列表會根據符合用戶端儲存庫部分 clone 過濾器的 bundle.<id>.filter 選項進行縮減。然後,請求所有 bundle URI。如果提供了 bundle.<id>.creationToken 啟發式參數,則 bundle 會按照建立權杖的降序下載,直到某個 bundle 擁有所有必要的 OID 為止。然後可以按照建立權杖的升序解開 bundle。用戶端會儲存最新的建立權杖作為啟發式資料,若 bundle 列表未公告具有更大建立權杖的 bundle,則可用於避免未來的下載。

    2. 如果 bundle.mode=any,則用戶端可以選擇檢查任何一個 bundle URI。用戶端可以使用多種方式在這些 URI 之間進行選擇。如果初始選擇無法回傳結果,用戶端也可以切換至另一個 URI。

請注意,在 clone 期間,我們預期所有 bundle 都是必要的,且像 bundle.<uri>.creationToken 這樣的啟發式參數可用於按時間順序或平行下載 bundle。

如果給定的 bundle URI 是一個帶有 bundle.heuristic 值的 bundle 列表,則用戶端可以選擇將該 URI 儲存為其選定的 bundle URI。用戶端之後在呼叫 git fetch 時可以直接導向該 URI。

下載 bundle URI 時,用戶端可以選擇在承諾下載全部內容之前檢查初始內容。這可能提供足夠的資訊來判斷 URI 是 bundle 列表還是 bundle。若是 bundle,用戶端可以檢查 bundle 標頭,判斷所有公告的尖端 (tips) 是否已在用戶端儲存庫中,並取消剩餘的下載。

使用 Bundle URI 進行 Fetch

當用戶端 fetch 新資料時,它可以決定在從原始遠端 fetch 之前先從 bundle 伺服器 fetch。這可以透過命令列選項完成,但使用像 clone 時指定的設定值可能更有用。

Fetch 操作遵循相同的程序從 bundle 列表下載 bundle(儘管我們在這裡 希望使用平行下載)。我們預期當 thin bundle 中的所有必要 commit OID 都已存在於物件資料庫中時,該程序就會結束。

當使用 creationToken 啟發式參數時,若 bundle 的建立權杖不大於已儲存的建立權杖,用戶端可以避免下載任何 bundle。在 fetch 新 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 包含的資訊超過 clone 或 fetch 請求所要求的。主要範例是,如果使用者使用 --single-branch 請求 clone,但卻下載了儲存所有 refs/heads/* 參考可達 commit 的 bundle。這最初可能是浪費的,但也許這些物件在稍後用戶端關心的 ref 更新中會變為可達。

  • git fetch 期間下載的 bundle 包含物件資料庫中已有的物件。如果我們將 bundle 用於 fetch,這是不可避免的,因為在執行完「追趕」(catch-up) fetch 到遠端伺服器後,用戶端幾乎總是會比 bundle 伺服器稍微領先。這種額外的工作在用戶端 fetch 頻率遠高於伺服器計算 bundle 頻率時最為浪費,例如若用戶端使用後台維護進行每小時預提取,但伺服器每週才計算一次 bundle。因此,除非伺服器已透過 bundle.heuristic 值明確建議,否則用戶端不應將 bundle URI 用於 fetch。

Bundle 提供者組織範例

Bundle URI 功能被刻意設計為對 bundle 提供者想要組織物件資料的不同方式具有彈性。然而,在此描述一個完整的組織模型可能對提供者有所幫助,使其能以此為基礎開始。

此範例組織是 GVFS 快取伺服器(參見本文末尾附近章節)所使用模型的一種簡化,儘管它使用了 Git 之外的額外軟體,但對於加速大型儲存庫的 clone 與 fetch 非常有益。

Bundle 提供者跨多個地理區域佈署伺服器。每個伺服器管理自己的 bundle 集。伺服器可以追蹤多個 Git 儲存庫,但會根據模式為每個儲存庫提供 bundle 列表。例如,在鏡像 https://<domain>/<org>/<repo> 的儲存庫時,bundle 伺服器可以在 https://<server-url>/<domain>/<org>/<repo> 提供其 bundle 列表。原始 Git 伺服器可以在 "any" 模式下列出所有這些伺服器

[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,其中包含從原始伺服器 clone 儲存庫所收到的所有物件。該列表使用 creationToken 啟發式參數,並根據伺服器的時間戳記為 bundle 建立 creationToken

Bundle 伺服器執行定期排程的 bundle 列表更新,例如每天一次。在此任務期間,伺服器從原始伺服器 fetch 最新內容,並產生一個包含可從最新原始 ref 到達、但先前計算的 bundle 中未包含的物件之 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

為了避免儘管物件資料在原始伺服器中已不可達,卻仍永久儲存與提供,此 bundle 合併可以更謹慎。與其取舊 bundle 的絕對聯集,不如透過檢查較新的 bundle 並確保其必要 commit 皆存在於此合併 bundle(或另一個較新的 bundle 中)來建立 bundle。這允許「過期」在此時間窗口內未被新 commit 使用的物件資料。該資料可透過稍後的 push 重新引入。

此資料組織的意圖有兩個主要目標。首先,透過從更近的來源下載預先計算的物件資料,使儲存庫的初始 clone 變快。其次,git fetch 指令可以變快,特別是如果用戶端幾天沒有 fetch。然而,如果用戶端 30 天沒有 fetch,則 bundle 列表組織將導致重新下載大量物件資料。

使此組織對於頻繁 fetch 的使用者更有用的一種方法是更頻繁地建立 bundle。例如,可以每小時建立 bundle,然後每天將這些「每小時」bundle 合併為「每日」bundle。每日 bundle 會在 30 天後合併到最舊的 bundle 中。

建議此 bundle 策略針對 blob:none 過濾器重複執行,如果該儲存庫的用戶端預期使用不含 blob 的部分 clone。此不含 blob 的 bundle 列表與完整 bundle 位於同一個列表中,但使用 bundle.<id>.filter 鍵來區分兩組。對於非常大的儲存庫,bundle 提供者可能希望 提供不含 blob 的 bundle。

實作計畫

本設計文件作為願景文件獨立提交,目標是在幾個修補程式系列中實作所有提及的用戶端功能。以下是提交這些功能的潛在大綱

  1. 將 bundle URI 整合至具有 --bundle-uri 選項的 git clone。這將包括一種新的 git fetch --bundle-uri 模式,用於作為 git clone 底層的實作。此處的初始版本將預期在給定 URI 處有一個單一 bundle。

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

  3. 建立 bundle-uri protocol v2 指令,以便 Git 伺服器可以使用鍵值對公告 bundle URI。接入現有對 bundle 列表邏輯的鍵值對輸入。允許 git clone 發現這些 bundle URI 並從 bundle 資料引導用戶端儲存庫。(此選擇是透過設定選項與命令列選項進行的可選操作。)

  4. 允許用戶端理解 bundle.heuristic 設定鍵與 bundle.<id>.creationToken 啟發式參數。當 git clone 發現帶有 bundle.heuristic 的 bundle URI 時,它會設定用戶端儲存庫以在稍後的 git fetch <remote> 指令期間檢查該 bundle URI。

  5. 允許用戶端在 git fetch 期間發現 bundle URI,並在設定 bundle.heuristic 時為稍後的 fetch 設定 bundle URI。

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

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

Git 協定已經有一種功能,即 Git 伺服器在服務用戶端請求時,可以列出一組 URL 以及 packfile 回應。用戶端隨後預期會下載位於這些位置的 packfile,以便對回應有完整的理解。

此機制由 Gerrit 伺服器(使用 JGit 實作)使用,在減少 CPU 負載與提升 clone 的使用者效能方面效果顯著。

此機制的一個主要缺點是原始伺服器需要 完全 知道這些 packfile 中有什麼,且 packfile 需要在伺服器回應後的一段時間內對使用者可用。這種原始伺服器與 packfile 資料之間的耦合很難管理。

此外,此實作要與 fetch 一起工作極為困難。

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

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

然而,一個更微妙的需求是 GET /gvfs/prefetch?lastPackTimestamp=<t> 端點。給定一個可選的時間戳記,快取伺服器會以包含在這些時間間隔內引入的 commit 與 tree 的預先計算 packfile 列表進行回應。

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

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

  2. 每晚,前 24 個每小時 pack 會被捲入一個「每日」pack。

  3. 每晚,所有超過 30 天的預提取 pack 會被捲入一個 pack。

當使用者對擁有快取伺服器的儲存庫執行 gvfs clonescalar clone 時,用戶端會請求所有預提取 packfile,最多為 24 + 30 + 1 個 packfile,僅下載 commit 與 tree。用戶端隨後向原始伺服器發送對參考的請求,並嘗試檢出該尖端參考。(有一個額外的端點有助於從給定 commit 取得所有可達的 tree,以防該 commit 還不在預提取 packfile 中。)

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

值得注意的是,用戶端總是聯繫原始伺服器進行參考公告,因此參考通常會比預提取的 pack 資料「超前」。缺少的物件會在需要時使用 GET gvfs/objects/{oid} 請求按需下載(例如由 git checkoutgit log 指令需要時)。一些 Git 最佳化會停用會導致這些按需下載過於頻繁的檢查。