* feat: move git operations to Rust via git2 crate (#524) Eliminates ~70 execSync/execFileSync git CLI calls across 15 TypeScript files by implementing native libgit2 operations in Rust and routing all consumers through the native-git-bridge. Rust (native/crates/engine/src/git.rs): - Added 28 new NAPI functions covering both read and write operations - Read: git_is_repo, git_has_staged_changes, git_diff_stat, git_diff_name_status, git_diff_numstat, git_diff_content, git_log_oneline, git_worktree_list, git_branch_list, git_branch_list_merged, git_ls_files, git_for_each_ref, git_conflict_files, git_batch_info - Write: git_init, git_add_all, git_add_paths, git_reset_paths, git_commit, git_checkout_branch, git_checkout_theirs, git_merge_squash, git_merge_abort, git_rebase_abort, git_reset_hard, git_branch_delete, git_branch_force_reset, git_rm_cached, git_rm_force, git_worktree_add, git_worktree_remove, git_worktree_prune, git_revert_commit, git_revert_abort, git_update_ref TypeScript (native-git-bridge.ts): - Added 35 bridge functions with native-first + execSync fallback - New types: GitDiffStat, GitNameStatus, GitNumstat, GitLogEntry, GitWorktreeEntry, GitBatchInfo, GitMergeResult Consumer migrations (15 files): - worktree-manager.ts: removed local runGit/getMainBranch, all ops native - auto-worktree.ts: merge, checkout, conflict resolution all native - git-service.ts: smart staging, commits, snapshots all native - auto.ts, guided-flow.ts: repo init/bootstrap native - auto-supervisor.ts: working tree detection native - git-self-heal.ts: merge/rebase abort, reset all native - doctor.ts: health checks, branch listing, worktree cleanup native - commands.ts: branch/snapshot cleanup native - session-forensics.ts: diff stat queries native - auto-recovery.ts: merge state reconciliation native - gitignore.ts, undo.ts, worktree-command.ts: remaining ops native Kept as execSync (by design): - git push (credential handling too complex for libgit2) - native-git-bridge.ts fallbacks (graceful degradation) - runPreMergeCheck (runs arbitrary user commands) Closes #524 * fix: restore getMainBranch export from worktree-manager The agent migration removed getMainBranch from worktree-manager.ts but worktree-command.ts still imports it. Re-add as a thin wrapper around nativeDetectMainBranch. * fix: address PR #572 review feedback — security, correctness, error handling CRITICAL: - Path traversal protection via validate_path_within_repo() for git_rm_force and git_checkout_theirs - git_branch_delete defaults to safe delete (force=false) HIGH: - Replace silent .ok() with proper error propagation in git_commit, git_merge_abort, git_rebase_abort, git_rm_force, git_checkout_theirs - nativeDiffStat fallback parses numeric stats from git output - nativeBatchInfo fallback counts staged/unstaged from porcelain status MEDIUM: - Wire up dead force param in removeWorktree() - Read MERGE_MSG/SQUASH_MSG when commit message empty - nativeLsFiles uses gitFileExec without fragile quote wrapping - Fix operator precedence in git_ls_files
12 KiB
Issue #524: Move Git Operations to Rust via git2 Crate
Current State
- git2 crate (v0.20) already a dependency with vendored libgit2
- 7 read-only functions already native in
git.rs+native-git-bridge.ts:git_current_branch,git_main_branch,git_branch_existsgit_has_merge_conflicts,git_working_tree_status,git_has_changesgit_commit_count_between
- ~73 execSync/execFileSync git calls remain across 14 TypeScript files
- All native functions follow the same pattern: native-first with execSync fallback
Scope
This plan covers Phase 1: migrate all remaining read operations and high-value write operations to native git2. Push operations stay as execSync (credential handling too complex for git2). The "Additional Rust Opportunities" (state derivation, JSONL parser) are out of scope for this PR.
Phase 1: New Native Read Functions (git.rs)
1.1 — git_is_repo(path: String) -> bool
Replaces: git rev-parse --git-dir (3 calls in auto.ts, guided-flow.ts, doctor.ts)
Implementation: Repository::open(path).is_ok()
1.2 — git_has_staged_changes(repo_path: String) -> bool
Replaces: git diff --cached --stat (2 calls in git-service.ts)
Implementation: Diff index vs HEAD tree, check if delta count > 0
1.3 — git_diff_stat(repo_path, from_ref?, to_ref?) -> GitDiffStat
Replaces: git diff --stat HEAD, git diff --stat --cached HEAD (session-forensics.ts)
Returns: { files_changed: u32, insertions: u32, deletions: u32, summary: String }
Implementation: Diff between two trees/index/workdir, count deltas
1.4 — git_diff_name_status(repo_path, from_ref, to_ref, pathspec?) -> Vec<GitNameStatus>
Replaces: git diff --name-status main...branch -- .gsd/ (worktree-manager.ts, 3 calls)
Returns: Vec<{ status: String, path: String }>
Implementation: Tree-to-tree diff with pathspec filter
1.5 — git_diff_numstat(repo_path, from_ref, to_ref) -> Vec<GitNumstat>
Replaces: git diff --numstat main branch (worktree-manager.ts, 1 call)
Returns: Vec<{ added: u32, removed: u32, path: String }>
1.6 — git_diff_content(repo_path, from_ref, to_ref, pathspec?, exclude?) -> String
Replaces: git diff main...branch -- .gsd/ and -- . :(exclude).gsd/ (worktree-manager.ts, 2 calls)
Returns: Unified diff string
1.7 — git_log_oneline(repo_path, from_ref, to_ref) -> Vec<GitLogEntry>
Replaces: git log --oneline main..branch (worktree-manager.ts, 1 call)
Returns: Vec<{ sha: String, message: String }>
1.8 — git_worktree_list(repo_path) -> Vec<GitWorktreeEntry>
Replaces: git worktree list --porcelain (worktree-manager.ts, 2 calls)
Returns: Vec<{ path: String, branch: String, is_bare: bool }>
Implementation: Repository::worktrees() + individual worktree info
1.9 — git_branch_list(repo_path, pattern?) -> Vec<String>
Replaces: git branch --list milestone/*, git branch --list gsd/* (doctor.ts, commands.ts, 3 calls)
Returns: Branch names matching pattern
1.10 — git_branch_list_merged(repo_path, target, pattern?) -> Vec<String>
Replaces: git branch --merged main --list gsd/* (commands.ts, 1 call)
Returns: Branch names merged into target
1.11 — git_ls_files(repo_path, pathspec) -> Vec<String>
Replaces: git ls-files "<exclusion>" (doctor.ts, 1 call)
Implementation: Read index, filter by pathspec
1.12 — git_for_each_ref(repo_path, prefix) -> Vec<String>
Replaces: git for-each-ref refs/gsd/snapshots/ --format=%(refname) (commands.ts, 1 call)
Implementation: repo.references_glob(prefix/*)
1.13 — git_conflict_files(repo_path) -> Vec<String>
Replaces: git diff --name-only --diff-filter=U (auto-worktree.ts, 1 call)
Implementation: Read index conflicts
1.14 — git_batch_info(repo_path) -> GitBatchInfo
NEW batch function: status + branch + diff summary in ONE call
Returns: { branch: String, has_changes: bool, status: String, staged_count: u32, unstaged_count: u32 }
Phase 2: New Native Write Functions (git.rs)
2.1 — git_init(path, branch?) -> void
Replaces: git init -b <branch> (auto.ts, guided-flow.ts, 2 calls)
Implementation: Repository::init() + set initial branch
2.2 — git_add_all(repo_path) -> void
Replaces: git add -A (auto-worktree.ts, git-service.ts, 4 calls)
Implementation: Add all to index via repo.index().add_all()
2.3 — git_add_paths(repo_path, paths: Vec<String>) -> void
Replaces: git add -- <file> (auto-worktree.ts, git-service.ts, 3 calls)
Implementation: Add specific paths to index
2.4 — git_reset_paths(repo_path, paths: Vec<String>) -> void
Replaces: git reset HEAD -- <path> (git-service.ts, in loop)
Implementation: Reset index entries to HEAD for specific paths
2.5 — git_commit(repo_path, message, options?) -> String
Replaces: git commit -m <msg>, git commit --no-verify -F - (11+ calls across files)
Returns: Commit SHA
Implementation: Write index as tree → create commit → update HEAD
Options: { allow_empty: bool }
2.6 — git_checkout_branch(repo_path, branch) -> void
Replaces: git checkout <branch> (auto-worktree.ts, 1 call)
Implementation: Set HEAD + checkout tree
2.7 — git_checkout_theirs(repo_path, paths: Vec<String>) -> void
Replaces: git checkout --theirs -- <file> (auto-worktree.ts, in loop)
Implementation: Resolve index conflict with "theirs" strategy
2.8 — git_merge_squash(repo_path, branch) -> GitMergeResult
Replaces: git merge --squash <branch> (auto-worktree.ts, worktree-manager.ts, 3 calls)
Returns: { success: bool, conflicts: Vec<String> }
Implementation: Find merge base → merge trees → apply to index
2.9 — git_merge_abort(repo_path) -> void
Replaces: git merge --abort (git-self-heal.ts, worktree-command.ts, 2 calls)
Implementation: Reset to ORIG_HEAD, clean merge state
2.10 — git_rebase_abort(repo_path) -> void
Replaces: git rebase --abort (git-self-heal.ts, 1 call)
2.11 — git_reset_hard(repo_path) -> void
Replaces: git reset --hard HEAD (git-self-heal.ts, 1 call)
Implementation: repo.reset(HEAD, Hard)
2.12 — git_branch_delete(repo_path, branch, force: bool) -> void
Replaces: git branch -D/-d <branch> (5 calls across files)
Implementation: repo.find_branch().delete()
2.13 — git_branch_force_reset(repo_path, branch, target) -> void
Replaces: git branch -f <branch> <target> (worktree-manager.ts, 1 call)
2.14 — git_rm_cached(repo_path, paths: Vec<String>, recursive: bool) -> Vec<String>
Replaces: git rm --cached -r --ignore-unmatch (git-service.ts, doctor.ts, gitignore.ts, 6 calls)
Returns: List of removed paths
2.15 — git_rm_force(repo_path, paths: Vec<String>) -> void
Replaces: git rm --force -- <file> (auto-worktree.ts, 1 call)
2.16 — git_worktree_add(repo_path, path, branch, create_from?) -> void
Replaces: git worktree add commands (worktree-manager.ts, 2 calls)
Implementation: repo.worktree() API
2.17 — git_worktree_remove(repo_path, path, force: bool) -> void
Replaces: git worktree remove --force (worktree-manager.ts, doctor.ts, 3 calls)
2.18 — git_worktree_prune(repo_path) -> void
Replaces: git worktree prune (worktree-manager.ts, 3 calls)
2.19 — git_revert_commit(repo_path, sha, no_commit: bool) -> void
Replaces: git revert --no-commit <sha> (undo.ts, 1 call)
2.20 — git_revert_abort(repo_path) -> void
Replaces: git revert --abort (undo.ts, 1 call)
2.21 — git_update_ref(repo_path, refname, target?) -> void
Replaces: git update-ref <ref> HEAD and git update-ref -d <ref> (git-service.ts, commands.ts, 2 calls)
When target is null/empty, deletes the ref.
Phase 3: TypeScript Bridge Updates (native-git-bridge.ts)
Add bridge functions for ALL new native functions, each with:
- Native-first implementation
- execSync fallback for when native module unavailable
- Proper error handling
- Type definitions
Phase 4: Consumer Migration
Update each TypeScript file to use native bridge functions:
4.1 — git-service.ts
smartStage()→ usenativeAddAll()+nativeResetPaths()commit()→ usenativeCommit()autoCommit()→ usenativeHasStagedChanges()createSnapshot()→ usenativeUpdateRef()- Runtime file cleanup → use
nativeRmCached() runPreMergeCheck()→ usenativeReadFile()or keep fs.readFileSync (not git)
4.2 — worktree-manager.ts
getMainBranch()→ usenativeDetectMainBranch()(already exists!)createWorktree()→ usenativeWorktreeAdd(),nativeBranchForceReset()listWorktrees()→ usenativeWorktreeList()removeWorktree()→ usenativeWorktreeRemove(),nativeWorktreePrune(),nativeBranchDelete()diffWorktreeGSD()→ usenativeDiffNameStatus()diffWorktreeAll()→ usenativeDiffNameStatus()diffWorktreeNumstat()→ usenativeDiffNumstat()getWorktreeGSDDiff()→ usenativeDiffContent()getWorktreeCodeDiff()→ usenativeDiffContent()getWorktreeLog()→ usenativeLogOneline()mergeWorktreeToMain()→ usenativeMergeSquash()+nativeCommit()
4.3 — auto-worktree.ts
getCurrentBranch()→ usenativeGetCurrentBranch()(already exists!)autoCommitDirtyState()→ usenativeWorkingTreeStatus()+nativeAddAll()+nativeCommit()mergeMilestoneToMain()→ use native merge, checkout, commit, branch delete
4.4 — auto.ts
git rev-parse --git-dir→ usenativeIsRepo()git init -b→ usenativeInit()git add -A .gsd .gitignore && git commit→ usenativeAddPaths()+nativeCommit()
4.5 — auto-supervisor.ts
detectWorkingTreeActivity()→ usenativeHasChanges()(already exists!)
4.6 — git-self-heal.ts
abortAndReset()→ usenativeMergeAbort()+nativeRebaseAbort()+nativeResetHard()
4.7 — guided-flow.ts
- Same pattern as auto.ts for init + bootstrap
4.8 — doctor.ts
git rev-parse --git-dir→ usenativeIsRepo()git worktree remove --force→ usenativeWorktreeRemove()git branch --list milestone/*→ usenativeBranchList()git branch -D→ usenativeBranchDelete()git ls-files→ usenativeLsFiles()git rm --cached→ usenativeRmCached()git branch --format...→ usenativeBranchList()
4.9 — gitignore.ts
untrackRuntimeFiles()→ usenativeRmCached()
4.10 — commands.ts
handleCleanupBranches()→ usenativeBranchList(),nativeBranchListMerged(),nativeBranchDelete()handleCleanupSnapshots()→ usenativeForEachRef(),nativeUpdateRef()
4.11 — undo.ts
git revert --no-commit→ usenativeRevertCommit()git revert --abort→ usenativeRevertAbort()
4.12 — session-forensics.ts
getGitChanges()→ usenativeWorkingTreeStatus()+nativeDiffStat()
4.13 — worktree-command.ts
git merge --abort→ usenativeMergeAbort()
Kept as execSync (out of scope)
git push <remote> <branch>— Credential handling too complex for git2cat package.json— Not a git command (already just fs.readFileSync)npm test/ custom commands — Not git operations
Implementation Order
- Rust functions (git.rs) — all read functions first, then write functions
- TypeScript bridge (native-git-bridge.ts) — add all new bridge functions
- Consumer migration — update each .ts file to use bridge functions
- Remove dead code — delete local
runGit()helpers from files that no longer need them - Testing — build native module, run CI, verify all operations work
Risk Mitigation
- Every native function has an execSync fallback in the bridge
- Write operations are tested by existing integration tests
- git2's vendored libgit2 matches git CLI behavior for standard operations
- The
loadNative()pattern means if ANY native function crashes, ALL functions fall back to CLI
Expected Impact
- ~70 execSync calls eliminated when native module is available
- Zero process spawns for git operations in the common path
- Batch operations (git_batch_info) reduce 3-4 calls to 1
- Type-safe errors instead of parsing stderr strings
- Consistent cross-platform behavior via libgit2