問題
src-tauri/src/modules/git/mod.rs 其實早就跑在系統的 git binary 上:非測試碼有約 96 處呼叫點透過 run_git / git_command(mod.rs:1061-1107)。只剩 12 處 還在用 git2。它已經是一個殘留的第二套 git 後端,而我們為它付兩種代價:
一個原生 C 相依。 git2 0.19 會帶入 libgit2-sys 0.17.0+1.8.1,每次 clean build 都要編譯內附的 libgit2。它在 windows-build.yml:6 與 .claude/skills/windows-tauri/SKILL.md:99 都被點名為讓 Windows bundle 無法跨編譯的原因之一,而且已經為它加過 default-features = false 的 workaround(版本0.3.0無法在MacOS上開啟 #262 :動態連結到建置機的 Homebrew OpenSSL,導致啟動即崩潰)。
同一個功能有兩套行為模型。 這個分歧在程式碼裡已經留下記錄:mod.rs:851-857 把 graph log 從 libgit2 revwalk 改回 CLI,因為 libgit2 必須 eager 走完整份可達歷史(O(repo size))才能吐出第一筆。而 run_git 自己的註解(mod.rs:1079-1080)就寫著走 CLI 是為了「沿用使用者設定的 credentials/helpers」。
關於效能
這個改動的訴求不是效能,請不要當成效能改善來評估。 誠實的預期是:那 6 處 O(1) 的 ref 讀取會變慢 (從 in-process 讀幾個檔案變成一次 process spawn,Windows 上約十幾到數十 ms —— mod.rs:1064-1069 記錄過沒有 CREATE_NO_WINDOW 時每次 spawn 約 +100ms,有旗標後小很多但不是零)。這些全都在使用者點擊觸發的路徑上,實際上感受不到。唯一可能變快的是 status 與 worktree_dirty_count 這兩處走檔案系統的操作,因為真 git 會使用 libgit2 不用的 index extensions(untracked cache、fsmonitor)—— 但這只在大型 repo 成立,小 repo 上 spawn 成本會蓋過收益,必須實測、不要假設 。
提議的變更
把這 12 處轉成 CLI,然後刪掉相依。不動任何 IPC 介面、也不動前端:GitStatus、WorktreeInfo、BranchInfo、FileStatus 的結構完全不變。
函式
目前的 git2 用法
建議的 CLI
resolve_repo(:177)
Repository::discover + workdir
rev-parse --show-toplevel
status(:183)
statuses() + index_status/worktree_status(:143、:160)
status --porcelain=v2 -z -uall --branch —— 一次 spawn 拿完 ,# branch.head 欄位一併給出分支名(未出生分支回 (detached) 以外的正確值),不必另外叫 rev-parse
head_shorthand(:242)
head(),fallback find_reference("HEAD")
不需要獨立命令 —— 它是被 status 與 worktree_info 呼叫的內部函式,兩邊都改成在自己那一次呼叫裡順便取得分支名(見上下兩列)
worktree_info(:259)
discover、is_worktree、往上找 git-dir 祖先(:279-290)
rev-parse --show-toplevel --git-common-dir --git-dir --abbrev-ref HEAD —— 一次 spawn 拿完 (rev-parse 會依序輸出各項)。git-dir ≠ common-dir 即為 linked worktree,common-dir 的 parent 就是主工作樹
worktree_add 的分支預檢(:501)
find_branch(_, Local)
show-ref --verify --quiet refs/heads/<branch>
worktree_dirty_count(:710)
statuses().len()
status --porcelain -z -uall,計算筆數
stage(:766)
index.add_path 失敗再 remove_path
add -- :(literal)<path>(同時涵蓋刪除的情況)
unstage(:778)
reset_default,未出生時改動 index
reset -q HEAD -- <path>;HEAD 未出生時用 rm --cached -- <path>
commit(:797)
write_tree + repo.commit + Signature
commit -m <message>
branches(:1345)
branches(Local) / branches(Remote)
for-each-ref --format=%(refname:short)%00%(HEAD) refs/heads refs/remotes(保留 :1377 現有的 */HEAD 跳過邏輯)
pr::remote_origin_url(pr/mod.rs:104)
discover + find_remote("origin").url()
remote get-url origin
測試(:2680、:2748)
Repository::init + config.set_str
其他測試已在用的 run_git(["init","-b","main"]) + run_git(["config", ...]) fixture
實作要求:一個原本 in-process 的操作,轉換後不得變成多次 spawn。 天真的逐一對應會讓 status 從 1 次變 2 次(因為 head_shorthand 是它呼叫的內部函式)。上表已經把每個操作都收斂成單一命令,請維持。
請重用既有機制、不要新增 helper:NUL 分隔的 status 解析在 copy_local_files(:645,status --porcelain -z --ignored)已經做過;:(literal) pathspec 在 restore_file(:1168)已經在用;ensure_not_flag(:1114)負責擋 argv flag smuggling;git_command 已經設好 CREATE_NO_WINDOW,所以新增的 spawn 在 Windows 上不會閃視窗。
建議拆成兩個 PR :先做唯讀的那幾處(resolve_repo、head_shorthand、worktree_info、branches、worktree_dirty_count、status、remote_origin_url、測試),再做寫入的三處(stage / unstage / commit)—— 下面那些行為決策都落在後者。
動工前需要先決定的行為變更
以下是真正的語意差異,不是機械替換:
Commit 身分的 fallback。 目前 commit 在 user.name/user.email 未設定時,會 fallback 到硬編碼的 TempoTerm <tempoterm@localhost> 簽名(:806-809);git commit 則是直接報錯。選項:偵測到設定缺失時注入 -c user.name=… -c user.email=… 以維持現狀,或是讓 git 的錯誤浮到使用者面前(後者可能更正確 —— 現在的 fallback 會默默用一個假身分寫下 commit)。
hooks 與簽章會開始生效。 git commit 會跑 pre-commit / commit-msg,也會遵守 commit.gpgsign;libgit2 兩者都不做。對開發者工具來說這幾乎肯定是使用者預期的行為,但它是 user-visible 的變更,而且把使用者的任意程式放進了 commit 路徑。建議考慮設上限 —— wait-timeout 已經是相依,當初就是為了 gh spawn 的同樣理由加的。
git add 會走 .gitattributes 的 clean filter 與 Git LFS ,index.add_path 不會。同樣是改善,同樣是變更。
未追蹤檔的遞迴行為必須保持。 recurse_untracked_dirs(true)(:191、:715)對應的是 -uall,不是 git 預設的 normal。漏掉 -uall 會讓未追蹤目錄塌成單一筆,同時改掉檔案清單與 dirty count。
狀態碼映射。 index_status/worktree_status(:143-174)回傳固定優先序的單一字元,且 WT_NEW 用 ?。從 porcelain 的 XY 碼映回來時必須完全一致,包含 rename(R)與 ?? → ? 的轉換,否則版本控制面板的圖示會跑掉。
不在範圍內
不是改用 gix。 已評估並排除:push、完整的 merge 流程、rebase、cherry-pick/revert,以及 linked worktree 的 add/remove/prune 在上游都還沒實作(crate-status.md ),因此 gix 無法取代 worktree 管理、push、merge/rebase 這些功能所依賴的 CLI 層。而若只把這 12 處 git2 換成 gix,跨編譯的阻礙依然存在(font-kit、portable-pty、rusqlite 都還在),卻要多帶約 40 個 crate 與一組 0.x 的 API 要跟版。
不動任何 #[tauri::command] 介面,也不動前端。
驗收標準
src-tauri/Cargo.toml 與 Cargo.lock 裡不再有 git2;lockfile 裡不再有 libgit2-sys。
grep -r git2 src-tauri/src 沒有結果;README.md:78(以及 zh-Hans / zh-Hant 兩份)、windows-build.yml:6、.claude/skills/windows-tauri/SKILL.md:99 裡的 git2 描述都已更新。
PR 內記錄前後的 clean build 時間與 release binary 大小。
PR 內記錄 status 與 worktree_dirty_count 的前後耗時,各在兩種 repo 上量:一個小 repo,以及一個裝好 node_modules 的大 repo。這是唯一可能出現效能回歸的地方,不量就不知道。
每個轉換後的操作都只 spawn 一次 git(見上面的實作要求)。
測試計畫
cargo test —— 擴充 git/mod.rs 的測試以涵蓋:未出生分支(status 與 head_shorthand)、含空白與非 ASCII 字元的路徑、HEAD 未出生時的 unstage、user.name/user.email 未設定時的 commit、被 rename 的檔案、含巢狀檔案的未追蹤目錄(驗證 -uall)。
cargo clippy -- -D warnings;PR 上跑 windows-check.yml,並手動 dispatch 一次 windows-build.yml 確認綠燈。
npx pnpm exec tsc --noEmit、npx pnpm exec vitest run(App.test.tsx / App.windows.test.tsx 的 mock 有提到 git2,要一起檢查)。
手動驗證:版本控制面板的 stage / unstage / commit / diff;worktree 管理器每列的 dirty 圓點;分支清單的目前分支標記;在有 origin 的 repo 上做 PR 查詢。
CHANGELOG-NEXT.md:若行為維持完全一致則不需要條目;若 hooks / 簽章 / LFS 開始生效(第 2、3 點),那是 user-visible 的變更 → 在正體中文與 English 兩區的 feat 下各補一條。
互相獨立,可任意順序進行。 #309 (事件驅動的 git 狀態刷新)決定的是「何時 」跑 git,本 issue 決定的是「用什麼 」跑。唯一交集是上表的 worktree_info:改成 rev-parse --show-toplevel --git-common-dir --git-dir --abbrev-ref HEAD 之後,取到的 git-dir / common-dir 區分正是 #309 的 watcher 監看集合所需要的資訊 —— 抽成共用 helper 兩邊都能用。
以上由 ai 整理提供
問題
src-tauri/src/modules/git/mod.rs其實早就跑在系統的gitbinary 上:非測試碼有約 96 處呼叫點透過run_git/git_command(mod.rs:1061-1107)。只剩 12 處還在用git2。它已經是一個殘留的第二套 git 後端,而我們為它付兩種代價:git2 0.19會帶入libgit2-sys 0.17.0+1.8.1,每次 clean build 都要編譯內附的 libgit2。它在windows-build.yml:6與.claude/skills/windows-tauri/SKILL.md:99都被點名為讓 Windows bundle 無法跨編譯的原因之一,而且已經為它加過default-features = false的 workaround(版本0.3.0無法在MacOS上開啟 #262:動態連結到建置機的 Homebrew OpenSSL,導致啟動即崩潰)。mod.rs:851-857把 graph log 從 libgit2 revwalk 改回 CLI,因為 libgit2 必須 eager 走完整份可達歷史(O(repo size))才能吐出第一筆。而run_git自己的註解(mod.rs:1079-1080)就寫著走 CLI 是為了「沿用使用者設定的 credentials/helpers」。關於效能
這個改動的訴求不是效能,請不要當成效能改善來評估。 誠實的預期是:那 6 處 O(1) 的 ref 讀取會變慢(從 in-process 讀幾個檔案變成一次 process spawn,Windows 上約十幾到數十 ms ——
mod.rs:1064-1069記錄過沒有CREATE_NO_WINDOW時每次 spawn 約 +100ms,有旗標後小很多但不是零)。這些全都在使用者點擊觸發的路徑上,實際上感受不到。唯一可能變快的是status與worktree_dirty_count這兩處走檔案系統的操作,因為真 git 會使用 libgit2 不用的 index extensions(untracked cache、fsmonitor)—— 但這只在大型 repo 成立,小 repo 上 spawn 成本會蓋過收益,必須實測、不要假設。提議的變更
把這 12 處轉成 CLI,然後刪掉相依。不動任何 IPC 介面、也不動前端:
GitStatus、WorktreeInfo、BranchInfo、FileStatus的結構完全不變。git2用法resolve_repo(:177)Repository::discover+workdirrev-parse --show-toplevelstatus(:183)statuses()+index_status/worktree_status(:143、:160)status --porcelain=v2 -z -uall --branch—— 一次 spawn 拿完,# branch.head欄位一併給出分支名(未出生分支回(detached)以外的正確值),不必另外叫rev-parsehead_shorthand(:242)head(),fallbackfind_reference("HEAD")status與worktree_info呼叫的內部函式,兩邊都改成在自己那一次呼叫裡順便取得分支名(見上下兩列)worktree_info(:259)discover、is_worktree、往上找 git-dir 祖先(:279-290)rev-parse --show-toplevel --git-common-dir --git-dir --abbrev-ref HEAD—— 一次 spawn 拿完(rev-parse會依序輸出各項)。git-dir ≠ common-dir 即為 linked worktree,common-dir 的 parent 就是主工作樹worktree_add的分支預檢(:501)find_branch(_, Local)show-ref --verify --quiet refs/heads/<branch>worktree_dirty_count(:710)statuses().len()status --porcelain -z -uall,計算筆數stage(:766)index.add_path失敗再remove_pathadd -- :(literal)<path>(同時涵蓋刪除的情況)unstage(:778)reset_default,未出生時改動 indexreset -q HEAD -- <path>;HEAD 未出生時用rm --cached -- <path>commit(:797)write_tree+repo.commit+Signaturecommit -m <message>branches(:1345)branches(Local)/branches(Remote)for-each-ref --format=%(refname:short)%00%(HEAD) refs/heads refs/remotes(保留:1377現有的*/HEAD跳過邏輯)pr::remote_origin_url(pr/mod.rs:104)discover+find_remote("origin").url()remote get-url origin:2680、:2748)Repository::init+config.set_strrun_git(["init","-b","main"])+run_git(["config", ...])fixture實作要求:一個原本 in-process 的操作,轉換後不得變成多次 spawn。 天真的逐一對應會讓
status從 1 次變 2 次(因為head_shorthand是它呼叫的內部函式)。上表已經把每個操作都收斂成單一命令,請維持。請重用既有機制、不要新增 helper:NUL 分隔的 status 解析在
copy_local_files(:645,status --porcelain -z --ignored)已經做過;:(literal)pathspec 在restore_file(:1168)已經在用;ensure_not_flag(:1114)負責擋 argv flag smuggling;git_command已經設好CREATE_NO_WINDOW,所以新增的 spawn 在 Windows 上不會閃視窗。建議拆成兩個 PR:先做唯讀的那幾處(
resolve_repo、head_shorthand、worktree_info、branches、worktree_dirty_count、status、remote_origin_url、測試),再做寫入的三處(stage/unstage/commit)—— 下面那些行為決策都落在後者。動工前需要先決定的行為變更
以下是真正的語意差異,不是機械替換:
commit在user.name/user.email未設定時,會 fallback 到硬編碼的TempoTerm <tempoterm@localhost>簽名(:806-809);git commit則是直接報錯。選項:偵測到設定缺失時注入-c user.name=… -c user.email=…以維持現狀,或是讓 git 的錯誤浮到使用者面前(後者可能更正確 —— 現在的 fallback 會默默用一個假身分寫下 commit)。git commit會跑pre-commit/commit-msg,也會遵守commit.gpgsign;libgit2 兩者都不做。對開發者工具來說這幾乎肯定是使用者預期的行為,但它是 user-visible 的變更,而且把使用者的任意程式放進了 commit 路徑。建議考慮設上限 ——wait-timeout已經是相依,當初就是為了ghspawn 的同樣理由加的。git add會走.gitattributes的 clean filter 與 Git LFS,index.add_path不會。同樣是改善,同樣是變更。recurse_untracked_dirs(true)(:191、:715)對應的是-uall,不是 git 預設的normal。漏掉-uall會讓未追蹤目錄塌成單一筆,同時改掉檔案清單與 dirty count。index_status/worktree_status(:143-174)回傳固定優先序的單一字元,且WT_NEW用?。從 porcelain 的 XY 碼映回來時必須完全一致,包含 rename(R)與??→?的轉換,否則版本控制面板的圖示會跑掉。不在範圍內
gix。 已評估並排除:push、完整的merge流程、rebase、cherry-pick/revert,以及 linked worktree 的add/remove/prune在上游都還沒實作(crate-status.md),因此 gix 無法取代 worktree 管理、push、merge/rebase 這些功能所依賴的 CLI 層。而若只把這 12 處git2換成 gix,跨編譯的阻礙依然存在(font-kit、portable-pty、rusqlite都還在),卻要多帶約 40 個 crate 與一組 0.x 的 API 要跟版。#[tauri::command]介面,也不動前端。驗收標準
src-tauri/Cargo.toml與Cargo.lock裡不再有git2;lockfile 裡不再有libgit2-sys。grep -r git2 src-tauri/src沒有結果;README.md:78(以及 zh-Hans / zh-Hant 兩份)、windows-build.yml:6、.claude/skills/windows-tauri/SKILL.md:99裡的 git2 描述都已更新。status與worktree_dirty_count的前後耗時,各在兩種 repo 上量:一個小 repo,以及一個裝好node_modules的大 repo。這是唯一可能出現效能回歸的地方,不量就不知道。git(見上面的實作要求)。測試計畫
cargo test—— 擴充git/mod.rs的測試以涵蓋:未出生分支(status 與head_shorthand)、含空白與非 ASCII 字元的路徑、HEAD 未出生時的unstage、user.name/user.email未設定時的commit、被 rename 的檔案、含巢狀檔案的未追蹤目錄(驗證-uall)。cargo clippy -- -D warnings;PR 上跑windows-check.yml,並手動 dispatch 一次windows-build.yml確認綠燈。npx pnpm exec tsc --noEmit、npx pnpm exec vitest run(App.test.tsx/App.windows.test.tsx的 mock 有提到 git2,要一起檢查)。origin的 repo 上做 PR 查詢。CHANGELOG-NEXT.md:若行為維持完全一致則不需要條目;若 hooks / 簽章 / LFS 開始生效(第 2、3 點),那是 user-visible 的變更 → 在正體中文與 English 兩區的feat下各補一條。與 #309 的關係
互相獨立,可任意順序進行。 #309(事件驅動的 git 狀態刷新)決定的是「何時」跑 git,本 issue 決定的是「用什麼」跑。唯一交集是上表的
worktree_info:改成rev-parse --show-toplevel --git-common-dir --git-dir --abbrev-ref HEAD之後,取到的 git-dir / common-dir 區分正是 #309 的 watcher 監看集合所需要的資訊 —— 抽成共用 helper 兩邊都能用。