MOOSSin
選單

全部文章 · 開源工具

git bundle:不加 remote,也能把整個 repository 搬到另一台機器

Git 內建的 bundle 把歷史打包成一個檔案,可以 verify、可以 fetch、只准 fast-forward。這篇講它怎麼用、為什麼我拿它當正式機唯一的程式碼入口、Windows 到 Linux 之間會碰到的行尾和權限問題,還有一個關於寄包裹的比喻。

大家用 Git 的方式大概都一樣:git push 上去,git pull 下來,中間有一個叫 GitHub 或 GitLab 的地方。push 和 pull 用久了會覺得那就是 Git 的全部,好像程式碼天生就該透過網路在兩台機器之間流動。

但 Git 從 2007 年的 1.5.1 版起就有一個很少人用的指令,git bundle。它把一個 repository 的歷史打包成單一檔案,另一端可以對這個檔案 git fetch,就像對一個 remote 一樣。差別在於它就是個檔案,你可以複製、雜湊、留存、放進隨身碟、用 email 寄、丟進雲端硬碟。我第一次知道這個功能的時候,感覺像發現自己家裡一直有一扇沒開過的門。

寄包裹,不是開水管

push 和 pull 像是在兩台機器之間接一條水管,水一直可以流。bundle 像是寄包裹:你把東西裝箱、封好、寫上重量和內容,寄過去,對方簽收前先確認箱子沒破、重量跟單子上一樣。

水管方便,但方便的代價是任何人只要接上水管就能放水進來。包裹慢,可是每一個包裹都是一次有意識的動作,有單據,有簽收。哪一種比較好,取決於水龍頭另一端是什麼。如果另一端是每天有讀者在看的正式機,我要包裹。

三個指令

git bundle create site.bundle main 打包 main 分支的完整歷史。到另一台機器上,git bundle verify site.bundle 檢查它是否完整、目前的 repository 有沒有它需要的前置提交;git fetch site.bundle main 把提交拉進來,接著 git merge --ff-only FETCH_HEAD。

--ff-only 值得多說一句。它的意思是只接受「對方的歷史是我的延伸」,任何需要合併的情況都拒絕。翻成人話就是:你寄來的包裹裡的東西,必須接在我已經有的東西後面,不能要求我把自己的東西拆掉重排。

為什麼我不加 remote

正式機上的 repository 沒有 remote,這是我刻意的。有 remote 就有 push,就有某個工具、某支腳本、某個看起來很像 main 的分支在我沒注意時把正式機更新掉的可能。這種事不常發生,但只要發生一次就夠你記一輩子。

走 bundle 的流程慢一點:打包、算 SHA-256、複製過去、對方再算一次、比對、verify、fetch、ff-only。每一步都留下痕跡,每次變更都有一個可以留存的檔案。這一點摩擦正是我要的。就像有些人會把信用卡放在很難拿的地方,不是拿不到,是要拿的時候會多想一秒。

bundle 也能倒著走。在遠端 git bundle create - main 把 bundle 寫到標準輸出,經 SSH 串流回本機,遠端一個位元組都不用寫。第一次這樣做的時候我有點得意,像是用吸管把飲料從隔壁桌吸過來,還沒碰到對方的杯子。

跨平台的兩個坑

這兩個坑都跟 Windows 有關,而且都是那種「明明什麼都對」的坑。

第一個是行尾。Windows 上的 Git 常開著 core.autocrlf=true,文字檔在工作樹裡是 CRLF,提交時轉回 LF。這對大多數檔案沒事,但 shell 腳本和 systemd 的 unit 檔一旦在 Windows 的工作樹裡被 CRLF 化又沒有正確正規化,到了 Linux 第一行的 #!/bin/sh 後面就多一個看不見的 \r,執行時噴 bad interpreter。你盯著那一行看,它長得跟教學上一模一樣,因為那個字元根本看不見。解法是在 .gitattributes 裡對 *.sh、*.service、*.timer 這類檔案寫 text eol=lf,並用 git ls-files --eol 檢查工作樹的實際狀態。

第二個是執行權限。Windows 的檔案系統沒有執行位元這個概念,所以在 Windows 新增的腳本進 index 之後是 100644,到 Linux 上不能直接執行。git update-index --chmod=+x script.sh 把 100755 釘進 index,跟內容一起走 bundle 過去。這個指令我抄在便條紙上貼了很久,因為每次需要它的時候都是深夜,深夜的我不記得任何東西。

小劇場:這不就是把資料夾壓縮寄過去嗎

「所以 bundle 就是把整個資料夾 zip 起來?」

「不是,zip 是把檔案現在的樣子壓起來;bundle 是把整段歷史打包,包括每一次提交、誰改的、什麼時候改的。」

「那我為什麼要歷史?我只要最新的。」

「因為對方拿到之後可以 verify,確認這包東西接得上他手上的那一段。zip 做不到,zip 只能相信你。」

「所以 bundle 是有收據的包裹。」

「而且收據上寫著它是從哪一段歷史長出來的。」

一個關於工作目錄的教訓

幾個 repository 並排放在同一個資料夾時,平行執行的 shell 可能共用同一個工作目錄。我遇過一個 commit 落到隔壁 repository 的情況,還順手打了一個標錯名字的 bundle。發現的時候那種感覺,大概像是把作業交到隔壁班的收件箱,而且封面還寫對了名字。之後我的規矩是跨 repository 的 git 操作一次只做一個,路徑一律寫絕對路徑。

適合誰

適合任何「兩台機器之間要搬程式碼,但不想讓其中一台能被 push」的場合,也適合把某次部署的程式碼原封不動存檔,或是在沒有網路的環境裡搬東西。它不適合多人協作的日常,那是 remote 的工作,水管在那裡是對的。

Git 裡面還有很多這種沒開過的門。bundle 是我打開之後覺得最值得的一扇,因為它讓我對「程式碼怎麼從這裡到那裡」這件事,第一次有了完全的掌握感。

一點關於 Git 的背景

Git 是 Linus Torvalds 在 2005 年花大約兩個星期寫出來的,起因是 Linux 核心原本用的版本控制工具突然不能免費用了。他需要一個東西,而且需要得很急,於是自己寫了一個。這個故事的教訓不是「你也可以兩個星期寫出 Git」,是「有時候最好的工具來自一個非常具體、非常急迫的需求」。

Git 的設計從一開始就是分散式的:每一份 clone 都是完整的 repository,有全部的歷史。remote 只是「另一份 clone 住在哪裡」的地址簿,不是 Git 的必要成分。這件事很多人用了很多年 Git 都沒有意識到,因為 GitHub 太方便了,方便到大家以為沒有它 Git 就不能用。bundle 就是這個「不需要 remote」的哲學最直接的體現。

git bundle 在 2007 年加進來,最初的用途之一就是那些完全沒有網路的環境:軍事、金融、某些研究機構,程式碼要進出只能靠實體媒介。它不是什麼冷門的 hack,它是 Git 一開始就考慮到的情境,只是大部分人的日常不需要。

常見的三個誤會

第一個誤會是「bundle 一定要包整段歷史」。不用,git bundle create fix.bundle main ^origin/main 只打包對方還沒有的那一段,檔案會小很多。完整歷史是我自己的選擇,因為我想要每個 bundle 都能獨立 verify,不依賴對方手上有什麼。

第二個誤會是「verify 過就一定能 fetch」。verify 檢查的是「這個 bundle 完整而且我有它需要的前置提交」,fetch 之後的 --ff-only 才是「這段歷史接不接得上」。兩個都過才算數,只過第一個的情況我遇過,那次是對方的 main 上有一個我沒有的提交。

第三個誤會是「用 bundle 就不用 remote 了」。日常協作還是用 remote,bundle 是給特定場合的:正式機、離線環境、存檔。硬要用 bundle 做多人協作,你會很快變成那個大家都在等的人。

一個週末可以試的小練習

找一個你自己的小專案,git bundle create ~/test.bundle main。把那個檔案複製到另一個資料夾,git clone ~/test.bundle 新專案。對,bundle 可以直接 clone。進去看 git log,全部的歷史都在。然後回到原專案改一個檔案、commit,再打一個 bundle,到新專案裡 git bundle verify 然後 git fetch 加 merge --ff-only。

最後做一件壞事:在新專案裡自己 commit 一個東西,再從原專案送一個 bundle 過來 fetch,然後 --ff-only。它會拒絕你,因為兩邊的歷史分岔了。那個拒絕的訊息,就是我在正式機上最想看到的那一句話。

後記

我對 bundle 的喜愛有一部分是不理性的。它讓我覺得程式碼是一個可以拿在手上的東西,而不是一條看不見的水流。這種感覺在什麼都在雲端的年代有點過時,但過時的東西偶爾會剛好是對的。

官方網站:git-scm.com/docs/git-bundle

閱讀偏好

返回全部文章作者與內容更正

留言(0)

還沒有留言。讀完有想法,歡迎留下第一則。

留言經站長審核後才會顯示。暱稱和留言會公開,請不要寫下電話、地址等個人資料;資料怎麼處理,請見隱私權政策。