跳轉到

2005 - Git 分散式版本控制:十天寫出的版本控制革命#

案件摘要#

2005 年 4 月,BitKeeper 收回 Linux 核心的免費授權,Linus Torvalds 在氣憤之下停工十天,寫出了 Git。 它把版本控制從「向中央伺服器租借」翻轉成「每個人都是完整倉庫」,分支從昂貴的目錄複製變成一次指標寫入。 Git 以內容定址的 commit DAG 取代檔案差異,讓數千人的核心開發重新有了秩序。 此後二十年,Git 成為版本控制的事實標準,GitHub 則把程式設計推向社交化與開源民主化。

前因 -- 為什麼會有這個案子#

  • 集中式版本控制的罩門:RCS(1985)/CVS(1990)/Subversion(2000)都假設一個中央倉庫——單點故障、離線不可用、提交歷史只有一份真相。伺服器掛了,整個團隊就停擺。
  • 分支昂貴:CVS/SVN 的分支是目錄級複製,合併時要逐檔三方比對,長期分支的合併衝突讓工程師視分支為猛獸,寧可大家都擠在 trunk 上。
  • Linux 核心的規模:2005 年 Linux 核心有數千貢獻者、每年數萬次提交,CVS/SVN 的合併複雜度隨貢獻者數量近似 \(O(n^2)\) 地上升,patch bomb 郵件已經撐不住。
  • BitKeeper 授權爭議:2002 年起核心開發改用專有的 BitKeeper,2005 年 4 月 Andrew Tridgell 反向工程其協定,BitMover 的 Larry McVoy 遂收回免費授權——Torvalds 認定「不能依賴別人的恩惠」。
  • 關鍵推理:Torvalds 對版本控制的需求排序是——速度第一、分散式第二、合併第三、分支必須便宜:
\[ \text{分支成本} = O(1)\ \text{(寫一個 41 位元組的指標)},\qquad \text{倉庫} = \bigotimes_{\text{每位開發者}} \text{完整歷史} \]

2005 年 4 月 3 日動工,4 月 7 日 Git 宣佈 self-hosting,6 月 16 日 2.6.12 核心用 Git 發行——十天寫出的原型成為核心的新動脈。

線索與推理 -- 數學式、程式、理論#

1. 案件現場:內容定址的物件資料庫#

\[ \text{blob} \xrightarrow{\text{tree 打包}} \text{目錄快照} \xrightarrow{\text{commit 引用}} \text{歷史節點} \xrightarrow{\text{ref 指向}} \text{branch / tag} \]

Git 只有一種資料結構:內容定址的 key-value 儲存。每個物件以 SHA-1 為鍵:

\[ \text{key} = \text{SHA-1}\big(\text{type} \mathbin{\|} \text{size} \mathbin{\|} \text{content}\big) \]
  • 快照而非差異:CVS/SVN 記錄「這次改了哪幾行」,Git 的 commit 記錄「這一刻整棵樹長什麼樣」——差異只是兩個快照之間推導出來的觀點。
  • 內容定址的好處:內容不變則 hash 不變,物件天然去重;任何篡改都會改變 hash,歷史不可否認地自驗真偽。
  • 四種物件:blob(檔案內容)、tree(目錄)、commit(快照+訊息+父節點)、tag(附簽名的標記)。

2. Commit DAG:分支只是指標#

Git 的歷史是一張有向無環圖(DAG):每個 commit 指向一或多個父節點。

\[ G = (V, E),\quad e = (v_{\text{child}}, v_{\text{parent}}),\quad \text{merge commit 有兩個 parent} \]
  • 分支 = 指標:master、feature-x 都只是 .git/refs/ 下的一個文字檔,內容是一個 commit hash——建立分支的成本是寫 41 位元組, \(O(1)\) ,與專案大小無關。
  • SVN 對比:SVN 建立分支要複製數千個檔案的元資料,成本 \(O(n)\) ;這解釋了為什麼 SVN 時代沒人敢開分支,Git 時代每人同時開十個。
  • 合併即圖運算:git merge 找出兩個分支的最近共同祖先(merge-base),做三方合併:
\[ \text{merge}(A, B, M) \quad\text{其中}\ M = \text{merge-base}(A, B),\quad \text{三方逐行比對 } \{M \to A\},\ \{M \to B\} \]

3. 三方合併的數學#

兩方合併(只比 A、B)無法區分「新增」與「刪除」,三方合併引入共同祖先 \(M\) 作為基準: \(\text{diff}(M, A)\) = 我改的, \(\text{diff}(M, B)\) = 你改的。判決規則:

  • 只有我改 → 取我的;只有你改 → 取你的;都改且相同 → 取共同結果;都改且不同 → 衝突,交給人。

這個演算法源自 diff3(1988),Git 以遞迴merge-base(多祖先時先合併祖先)處理複雜的十字交叉分支。rebase 則是另一條路:把我的 commit 逐一重放到你的分支頂端,讓歷史變成線性——merge 保留真相的拓撲,rebase 換取歷史的整潔。

4. 分散式的拓撲:每人都是完整倉庫#

\[ \text{central: } \{ \text{server} \} \leftarrow \text{client(歷史子集)},\qquad \text{distributed: } \forall i,\ \text{repo}_i \cong \text{repo}_j\ \text{(完整歷史)} \]
  • 離線可用:commit、branch、log、diff 全部本地完成,只有 push/pull 需要網路。
  • 多中心協作:核心開發變成「pull request 的星系」——每個維護者 pull 貢獻者的倉庫,review 後 merge,再 push 給上游。信任沿著 DAG 傳遞。
  • 備份即部署:每個 clone 都是一次完整備份, \(n\) 位開發者就有 \(n\) 份歷史,單點故障從物理上被消滅。

5. GitHub(2008):社交化程式設計#

2008 年上線的 GitHub 把「pull request」從技術名詞變成社交儀式:fork(一鍵 clone 到自己帳號)、branch、commit、PR、code review、merge。開源貢獻從「寄 patch 給維護者」變成「在平台上發起討論」——貢獻的門檻從認識維護者降到一個 GitHub 帳號,開源生態自此指數成長。

6. Python 實作:簡易 commit DAG 模擬#

import hashlib, itertools

class MiniGit:
    def __init__(self):
        self.objects = {}                     # hash -> (parents, message)
        self.refs = {"master": None}          # branch -> commit hash

    def _h(self, parents, msg):
        """內容定址:SHA-1(parents || message)。"""
        payload = ("".join(parents) + msg).encode()
        return hashlib.sha1(payload).hexdigest()[:8]

    def commit(self, branch, msg):
        parent = self.refs[branch]
        h = self._h([parent] if parent else [], msg)
        self.objects[h] = ([parent] if parent else [], msg)
        self.refs[branch] = h                 # 分支只是指標,寫入 O(1)
        return h

    def branch(self, name, frm):
        self.refs[name] = self.refs[frm]      # 開分支 = 複製指標

    def merge_base(self, a, b):
        """最近共同祖先:收集 a 的所有祖先,找 b 的祖先中最近者。"""
        def ancestors(h):
            seen, stack = set(), [h]
            while stack:
                cur = stack.pop()
                if cur in seen: continue
                seen.add(cur)
                stack.extend(self.objects[cur][0])
            return seen
        a_anc = ancestors(a)
        for node in self._bfs_layers(b):      # 由近到遠找
            if node in a_anc and node != a and node != b:
                return node
        return None

    def _bfs_layers(self, h):
        seen, q = set(), [h]
        while q:
            cur = q.pop(0)
            if cur in seen: continue
            seen.add(cur); yield cur
            q.extend(self.objects[cur][0])

    def merge(self, a, b, msg):
        """三方合併:建立有兩個 parent 的 merge commit。"""
        h = self._h([a, b], msg)
        self.objects[h] = ([a, b], msg)
        self.refs["master"] = h
        return h

g = MiniGit()
c1 = g.commit("master", "init")
c2 = g.commit("master", "feature A part 1")
g.branch("dev", "master")
c3 = g.commit("master", "feature A part 2")
c4 = g.commit("dev", "hotfix on dev")
c5 = g.commit("dev", "feature B")
mb = g.merge_base(c3, c5)
print(f"merge-base(master, dev) = {mb}, 即 commit '{g.objects[mb][1]}'")
# merge-base(master, dev) = c2 的 hash,即 commit 'feature A part 1'
m = g.merge(c3, c5, "merge dev into master")
print(f"merge commit {m} 的 parents = {g.objects[m][0]}(兩個父節點,DAG 分叉點)")
# merge commit <hash> 的 parents = [c3 的 hash, c5 的 hash](兩個父節點,DAG 分叉點)

結案 -- 後果與影響#

  • Git 成為事實標準:到 2010 年代中,SVN/Mercurial 幾乎退出主流;Git 成為每位工程師的必備技能。
  • GitHub 與開源生態:2008 年 GitHub、2011 年 GitLab;fork/PR 文化讓開源貢獻民主化,npm、Docker、Kubernetes 等生態都建立在 Git 之上。
  • PR 與 code review 文化:Pull request 把審查變成開發流程的正式關卡,工程實踐(如 trunk-based development 與短命分支)也因分支便宜而成為可能。
  • DevOps 基礎設施:CI/CD、Infrastructure as Code、GitOps——一切「以版本控制為真相來源」的實踐都以 Git 為地基。
  • SHA-1 的餘波:2017 年 SHA-1 碰撞被證明(SHAttered),Git 隨即推出撞擊防護,並逐步遷移至 SHA-256——內容定址的設計讓演算法可替換。

關鍵人物與文獻#

  • Linus Torvalds:Git 原作者;2005 年 4 月在 linux-kernel 郵件列表宣告 "I'm doing the kernel source control again"。
  • Junio Hamano:2005 年 7 月起接手 Git 維護至今,把十天原型修煉成穩定工具。
  • Andrew Tridgell:BitKeeper 反向工程者,引爆授權爭議;Larry McVoy:BitMover 創辦人。
  • Tom Preston-Werner、PJ Hyett、Scott Chacon:GitHub(2008)共同創辦人,把 Git 推向大眾。
  • Scott Chacon & Ben Straub, Pro Git, 2nd ed., Apress, 2014(git-scm.com/book 免費全文)。