重點摘要
- Git 的本質是一個 content-addressed key-value 資料庫,所有的 branch、rebase、reflog 都是建構在這個底層上的抽象
- Jujutsu(jj)不是 Git 的替代品,而是把 Git 的 content-addressed 哲學「再往上推一層」 — 從檔案快照擴展到操作快照
- Git 的核心限制不是指令不爽,而是它要求人類的同步注意力 — merge 衝突會 block 整個世界等你處理
- jj 允許異步注意力 — 衝突變成 commit 的一種狀態,rebase 永遠不會停,你可以明天再一次處理完
- Agent Team 有三種任務類型,jj 的價值差很多:Review(只讀)完全不需要 jj;Debug(試驗假設)是 jj 最被低估的甜蜜點;Dev(平行開發)在合併階段需要 jj
- 最優的使用模式是 git 為主、jj 只在 Agent Team 合併階段出場 — 由 orchestrator agent 承擔 jj 學習成本,人類使用者和 subagent 全程保持 git 心智
- 決策準則:默認 Git,三個訊號任一觸發時考慮 jj — 平行 Agent 撞車、review round 批次修正、Agent 實驗成本太高
最近 GitHub 上 jj-vcs/jj(Jujutsu)越來越熱,社群討論開始出現「Git 要被取代了嗎」的聲音。我花了幾個小時認真研究這個工具,過程中意外地被迫重新理解 Git 的底層設計 — 因為只有懂 Git 真正是什麼,才看得懂 jj 想解決什麼。這篇文章就是這趟理解的完整紀錄,從 Git 底層原理開始,一路推到 AI Agent Team 時代的務實使用模式。
結論先說:Jujutsu 和 Git 不是互斥關係。jj 的儲存後端其實就是 Git,你可以隨時切回純 Git 指令。真正該問的不是「要不要換」,而是「你的工作流是否已經撞到 Git 的設計假設」。而這個答案,必須先從 Git 的底層長什麼樣講起。
先搞懂一件事:Git 其實是一個資料庫
很多人用 Git 很多年,但沒意識到一件事:Git 本身就是一個資料庫,而且是一個設計極其精巧的資料庫。整個 .git/ 目錄就是一個 key-value store,只是它用檔案系統當儲存引擎而已。
你現在去任何 git repo 跑 ls .git/objects/,會看到一堆兩位數的資料夾:00/ 01/ ... ff/,每個資料夾裡面有一堆 hash 開頭的檔案。這就是 Git 的資料庫。它叫 Object Database,key 是 SHA-1 hash,value 是壓縮過的內容。所有其他概念 — commit、branch、tag、stash、reflog — 全部都是建構在這個 key-value store 之上的抽象。
四種物件,解構整個 Git
- Blob:儲存單一檔案的內容,不包含檔名。
SHA1(content) = hash,內容相同 hash 必然相同。同樣內容只存一份(自動去重)。 - Tree:儲存一個資料夾的結構,內容是一張表 — 列出這個資料夾有哪些檔案(指向 blob)和哪些子資料夾(指向另一個 tree)。Tree 可以遞迴指向其他 tree。
- Commit:儲存一個時間點的快照,內容只有幾行 — 指向根目錄的 tree hash、前一個 commit 的 hash、作者時間、commit message。Commit 本身不含任何檔案內容,它只是一個指標鏈的進入點。
- Tag:annotated tag 的容器,對理解本質不重要,跳過。
一個 commit 怎麼「記錄整個資料夾」
你 commit 時,Git 為每個檔案內容計算 hash 建 blob,為每個資料夾建 tree(一張表,列出那個資料夾裡所有 blob 和 sub-tree 的 hash + 檔名),最後建 commit,只包含 root tree hash + parent hash + 作者 + 訊息。結果是一個指標鏈:commit → tree → sub-tree → blob。每個 commit 都完整地、不可變地描述了那一刻整個資料夾的狀態,不是 diff,是完整快照。
但每次存完整快照不會爆嗎?
這就是 Git 設計最美的地方。答案是:不會爆,因為 content-addressed 帶來自動去重。你 commit 了 1000 次,但 99% 的檔案沒動,那 99% 的檔案永遠只有一個 blob,因為內容一樣 hash 一樣,儲存只有一份。改一個檔案的成本 = 1 個新 blob + 2-3 個新 tree + 1 個新 commit,其他全部重用。實際效果:Linux kernel 有 100 萬+ commits,整個 .git/ 也就 5 GB 左右。
Git 從來不儲存 diff。git diff 是每次問的時候走訪兩個 commit 的 tree DAG、現場跑 Myers 演算法算出來的。這個設計的好處是:讀任何一個歷史版本都是 O(1),不用還原一串 diff 才能看到那個版本的樣子。這一切都是「content-addressed immutable store + pointer DAG」這一個設計決策的延伸。Linus 2005 年花兩週寫出 Git 核心,就是這個設計。後來所有東西都是在這個底層上加糖。
Jujutsu 的核心創新:把 Git 的哲學再推一層
現在你懂了 Git 的底層,就能看懂 jj 在做什麼。jj 沒有推翻 Git 的設計,它做的事情是:把 Git 的 content-addressed DAG 從「檔案狀態」擴展到「操作狀態」 — 同樣的哲學,應用在不同的層級。
雙層身份:commit hash + change-id
Git 只有一個 ID — commit hash,由內容決定。這導致 rebase 後 hash 全變,你找不到「原本那個 commit」。jj 對每個 commit 同時維護兩個 ID:commit hash(跟 Git 一樣)+ change-id(隨機產生的 128-bit ID,只在 commit 誕生時生成一次,rebase/amend/split/squash 都不變)。這個設計的意義是:身份認同不綁在 hash 上,綁在 change-id 上。你可以隨便改歷史,change-id 永遠追蹤得到「同一個概念上的改動」。這就是為什麼 jj 可以把「歷史重寫」當日常操作,而 Git 必須當儀式感操作。
Op Log:第二層 content-addressed DAG
jj 的另一個核心是 operation log。每個 jj 操作都產生一個 operation 物件,包含 operation id(hash)、parent operation、執行的指令、操作前後 repo 整體狀態的快照。這些 operation 本身也組成一個 DAG,content-addressed 儲存。注意這裡的模式跟 Git 完全一樣:不可變物件 + 指標 DAG + 內容定址。jj 只是把 Git 用來管檔案狀態的那套機制,再蓋一層管操作狀態。
結果是 jj undo 變成一步 pointer 移動 — 直接把 repo view 指回前一個 operation 的「操作前快照」就好,不用反向計算任何東西。而且 undo 本身也是一個 operation,可以被 undo,可以無限回退。
指令層面的六個差異
| 面向 | Git | Jujutsu (jj) |
|---|---|---|
| Staging Area | working dir → index → commit(三層) | working dir 就是 commit(兩層,無 git add) |
| Branch 概念 | 一等公民,必須 checkout/rebase/merge | 可選的 bookmark,每個 commit 有穩定 change-id |
| Undo 機制 | reflog(補救機制,90 天 TTL) | jj undo(第一等操作,可無限回退) |
| Conflict 處理 | 停止世界,當場解完才能繼續 | 衝突直接存進 commit,可延後解 |
| Stash | 必要指令(stash push/pop/apply) |
不存在,jj new 直接開新 commit |
| 歷史重寫 | rebase -i 是儀式感操作 |
jj split / jj squash 是日常 |
最重要的差異:同步注意力 vs 異步注意力
到這裡為止,都還是「指令差異」和「設計哲學」的層面。但 jj 真正的價值,不在指令爽不爽,而在它如何消耗人類的注意力。這個差異是質變,不是量變。
Git 是同步注意力模型 — merge/rebase 遇到衝突會停止世界,直到人類出現、看懂、解決、繼續。它要求你在「不可預期的時刻」立刻在場。這個模型在單人或 2-3 人團隊下完全沒問題,因為衝突頻率低、你本來就在鍵盤前。
jj 是異步注意力模型 — rebase 遇到衝突不停下來,衝突被存進 commit 變成一種有效狀態,rebase 繼續往下跑。你可以明天處理、或一次批次處理所有衝突、或開另一個 Agent 專門解衝突。jj 允許人類「異步在場」 — 系統自己跑,有問題幫你記下來,你有空再來看。
Git 的設計誕生於 2005 年,當時的假設是「careful human curator」 — 每個 commit 都是人類慎重思考後的決定,每個 merge 都有人類在場。但 Claude Code、Cursor、Copilot Agent 這類 AI 工具改變了「誰在產生 commit」這件事。Agent 產出的是高頻、探索性、非線性、大量並行的中間態。你不可能一邊看 3 個 Agent 平行工作,一邊在它們合併時即時解衝突,一邊決定每個中間態要不要保留。人類同步注意力的頻寬根本跟不上 Agent 的產出速度。
jj 在這裡的價值不是「比 Git 更爽」,而是它讓 Git 做不到的事變可能 — 讓 Agent Team 真的平行跑到底,明天早上來一次處理所有衝突。這不是量變(爽度提升),是質變(能做的事情不同)。
Workspace 隔離:平行的真正解法
要澄清一個常見誤解:jj 本身不能解決「兩個 Agent 同時寫同一個檔案」這種檔案系統 race condition。檔案系統的併發是 OS 管的,任何 VCS 都介入不了。平行 Agent 的正確架構是每個 Agent 給一間房 — Git 用 git worktree add,jj 用 jj workspace add。每個 workspace 是獨立的 working directory,三個 Agent 在不同資料夾各寫各的,完全不會互相污染。
Git 和 jj 在 workspace 隔離這一步的行為相同。差異出現在下一步 — 把三個 Agent 的輸出合併回 main 時。Git 會序列化 blocking(解一個、繼續、解下一個、繼續),你的注意力被三次阻塞切碎;jj 則是三個動作一氣呵成,最後一次性 batch 處理所有衝突。Workspace 隔離是必要條件,jj 的優勢出現在合併階段的併發模型。
Agent Team 的三種任務類型:jj 的價值差很多
這是這篇文章最重要的一個區分,也是大部分「jj 推坑文」忽略的。你叫 Agent Team 做不同類型的事,jj 的價值完全不同 — 甚至有些情境 jj 根本沒有用。決定性的問題是:這些 agent 需要寫檔案才能完成任務嗎?
類型 1:Review(只讀) — 完全不需要 jj
場景:叫 Agent Team 做 code review、security audit、dead code 掃描、架構審閱。這些 agent 只讀檔案,產出報告,不寫任何東西。
這種場景 jj 沒有價值:沒有 commit、沒有 merge、沒有 undo 需求。三個 agent 同時讀同一個檔案完全沒問題,OS 的 file handle 對讀不是互斥資源。純 git 就好,甚至不需要 workspace 隔離。
常見誤區:很多人把「開 Agent Team」跟「需要 jj」畫等號,導致在純 review 場景也導入 jj 的學習成本。這是沒必要的。
類型 2:Debug(試驗假設) — jj 最被低估的甜蜜點
場景:叫 Agent Team debug 複雜 bug。Debug 的本質是「形成假設 → 驗證 → 推翻 → 再假設」的循環,而驗證通常需要動手改 code — 加 log、改條件、插 assert、暫時跳過某段邏輯。
Git 下的 debug 痛點:加了 10 個 print() 驗證完要全部刪掉,漏刪一個就污染 production;改了某個函式測試假設,後來發現方向錯,手動改回去容易漏;想試兩個假設 A 和 B 哪個對,兩個互相覆蓋,不能同時存在。
這是 jj 最強的場景,**比一般開發還強**。原因很微妙:development 的產出是「最終正確的 code」,中間過程 git 可以容忍(最終 squash 成 clean commit 就好)。但 debug 的產出是「對問題的理解」,這個理解存在於中間過程本身,不存在於最終狀態。
Git 丟掉中間過程 = 丟掉 debug 的精華。jj 保留所有中間過程 — 每個假設是一個 commit、jj undo 可以乾淨撤銷、多個假設可以平行存在於 op log,等於保留 debug 的完整價值。debug agent 可以試 5 個假設、留下完整推理紀錄、報告說「我試了 A/B/C/D,D 可行因為…」,你事後可以 jj op restore 跳回任何一個假設狀態看 agent 那時候看到什麼。
結論:如果你常叫 AI debug 複雜問題,jj 的價值比你想的大很多。這是 op log 從「檔案歷史」升級為「推理紀錄」的唯一場景。
類型 3:Dev(平行開發) — jj 在合併階段是必要品
場景:叫 Agent Team 平行開發多個模組。多個 agent 會產出多個改動分支,最後要合併回 main。這是大家熟悉的場景 — 前面講的「同步 vs 異步注意力」、「workspace 隔離」、「conflict as commit」全部都在這個場景發揮。
但注意:jj 在這個場景的價值集中在合併階段,不是 agent 工作期間。agent 自己工作時,Git 和 jj 沒差。差異出現在「把 N 個平行輸出 reconcile 回主線」這一步。沒有 jj 的非阻塞合併,你的 Agent Team 上限會被 git 的序列化 blocking 硬生生壓到小規模 — 即使記憶體還有餘裕。
三種使用模式:該怎麼在日常工作裡擺 jj
理解完上面的分析,最後的問題是:你該怎麼把 jj 放進你的日常工作?有三種模式,各自適合不同情境。
模式 1:純 git(現狀)
適合:單人或「1 RD + 1 Reviewer」工作流、穩定專案、強烈依賴 IDE Git 整合、團隊只認 git。這個模式沒什麼好討論,就是你現在的樣子,對大多數人完全夠用。
模式 2:jj 為主,git 只是後端
安裝 jj colocated mode,日常指令都打 jj,幾乎不打 git。這個模式的問題是:邊界仍然是 git 世界。IDE Git panel 顯示的是 jj 上次匯出的狀態,你在本地 jj rebase 一下,push 後 GitHub 還是看到 commit hash 全變,change-id 根本沒傳出去。你本地很爽,對外還是 git 的世界。
這個模式只適合「單人、沒有協作壓力、不在意 IDE 整合退化」的使用者。對多數人代價太大。
模式 3:git 為主,jj 只在 Agent Team 合併階段出場
這是我研究完之後推薦的模式,也是最被低估的一個。玩法是:
- 你日常全程用 git — 習慣、IDE、CI、PR 流程完全不變
- Subagent 也用 git — 他們在各自 workspace 裡用
git commit,根本不知道 jj 存在 - Orchestrator agent(Claude Code)在合併階段用 jj — 只需要學 5 個指令:
workspace add、git import、rebase、log、workspace forget - 一次性設定:
jj git init --colocate,之後你完全忘記它存在
這個模式最精妙的地方:學習成本完全落在 orchestrator agent 身上,不是你身上。你的認知負擔幾乎為零,只需要記住一個 trigger:「要開 Agent Team 時,叫 Claude」。其他時間 jj 完全不存在。
失敗零成本:任何一個環節出問題,你都可以回退到純 git。subagent 的 commit 是正常 git commit,無論是否用 jj 都能 git cherry-pick;合併失敗就 git merge 手動解;jj 整個壞掉就刪掉 .jj/,git repo 毫髮無傷。沒有 lock-in,沒有 point of no return。
Review Round 批次修正:寫之前切 vs 寫之後切
另一個 jj 爆發的場景是 review round 工作流。很多團隊的 QA/review 會產出幾十個 issue,你拿到一份清單要一次修完。典型的 commit message 長這樣:fix(fee): R5-12/13/03/04/10/16/17 remaining module items — 塞了 7 個獨立的 issue 修正在一個 commit。
為什麼塞一起?因為 Git 下的三個選擇都不理想:每個 issue 一個 commit 要 git add -p 切七次太累;全部一個 commit 未來想單獨 backport 做不到;開 7 個 branch 管理成本爆炸。結果大多數人選「全部一個 commit」,歷史變成粗顆粒。
jj 的解法:打開 editor,一次改完 7 個 issue,不用管 commit 切分。改完後 jj split,互動畫面讓你把每個 hunk 分配到哪個 commit,七次後自動產生 7 個獨立 commit。Git 的 add -p 是「寫之前」切,jj 的 split 是「寫之後」切。這讓你把「commit 切分」這個決策延後到資訊最充足的時候(全部改完後),而不是在你還在思考邏輯的時候被迫處理。
決策準則:你該用哪個模式?
純 git 就夠的訊號
- 單人或「1 RD + 1 Reviewer」工作流 — 沒有平行檔案衝突
- Agent Team 只做 review / audit(agent 不寫檔案) — jj 沒有價值
- 穩定專案、強依賴 IDE Git 整合
- 老 Git 肌肉記憶還在運作,且團隊只認 git
考慮模式 3(git + jj for Agent Team merge)的訊號
- Agent Team 在共享檔案上撞車 — 你為了避免 merge 衝突開始序列化 Agent,生產力被 VCS 綁住
- Review round 產生批次修正 — 你寫過大量
fix: XXX-01/02/03/04/05這種塞一堆 issue 的 commit,事後想拆卻沒工具 - AI 常常需要做 debug 試驗 — 你希望 AI 大膽試很多假設,但現在因為回退成本太高,限制了 agent 的自由度
三個訊號都沒中?純 git 就夠了,別為了新鮮感切工具。
我的具體建議:安全試用路徑
- 選一個個人實驗性專案,執行
jj git init --colocate(一次性,30 秒) - 繼續用 git 做 99% 的事 — 你的肌肉記憶完全不變
- 下次真的要開 Agent Team 時,讓 orchestrator agent 用 jj 的 workspace + 非阻塞 rebase 處理合併階段
- 如果是 debug 場景,給 agent 「大膽試驗」的權限,利用 op log 保留推理紀錄
- 一個月後評估:有沒有真的遇到 jj 幫上忙的場景?沒有就卸載,git repo 完全不受影響
跨專案的架構原則
這次研究讓我提煉出一個跨專案適用的原則,寫進我的領域腦系統:
VCS 的選型要跟「人類注意力的可用模式」匹配,不是跟「指令爽不爽」匹配。Git 假設人類可以同步在場,jj 允許人類異步在場。AI Agent 工作流打破了 Git 的第一個假設,但是否影響到你,取決於你的團隊形狀和工作型態,不取決於你對 AI 的熱情。
更深一層的 insight:Git 的偉大不在於它的指令設計,而在於 content-addressed DAG 這個設計模式。這個模式你會在 IPFS、Nix、Docker layers、區塊鏈到處看到 — 懂 Git 底層等於懂了這一整類系統的通用結構。jj 的聰明,是看到這個模式可以往上再推一層,應用到操作本身,而不只是檔案狀態。
AI 時代最大的認知陷阱是「新工具一定比舊工具好」,但真實世界是新舊工具的取捨點會隨場景變化。把場景描述清楚,比把工具吹捧清楚重要得多。下次你看到任何新的版本控制、儲存系統、分散式工具時,問自己這三個問題就好:它的不可變單位是什麼?怎麼定址?DAG 結構長怎樣?答案對上 content-addressed DAG 模式,你就已經懂 80% 了。
發佈留言