singularity-forge/.plans/issue-524-git2-migration.md
Flux Labs 343a43f028 feat: move git operations to Rust via git2 crate (#572)
* 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
2026-03-15 20:02:10 -06:00

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_exists
    • git_has_merge_conflicts, git_working_tree_status, git_has_changes
    • git_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:

  1. Native-first implementation
  2. execSync fallback for when native module unavailable
  3. Proper error handling
  4. Type definitions

Phase 4: Consumer Migration

Update each TypeScript file to use native bridge functions:

4.1 — git-service.ts

  • smartStage() → use nativeAddAll() + nativeResetPaths()
  • commit() → use nativeCommit()
  • autoCommit() → use nativeHasStagedChanges()
  • createSnapshot() → use nativeUpdateRef()
  • Runtime file cleanup → use nativeRmCached()
  • runPreMergeCheck() → use nativeReadFile() or keep fs.readFileSync (not git)

4.2 — worktree-manager.ts

  • getMainBranch() → use nativeDetectMainBranch() (already exists!)
  • createWorktree() → use nativeWorktreeAdd(), nativeBranchForceReset()
  • listWorktrees() → use nativeWorktreeList()
  • removeWorktree() → use nativeWorktreeRemove(), nativeWorktreePrune(), nativeBranchDelete()
  • diffWorktreeGSD() → use nativeDiffNameStatus()
  • diffWorktreeAll() → use nativeDiffNameStatus()
  • diffWorktreeNumstat() → use nativeDiffNumstat()
  • getWorktreeGSDDiff() → use nativeDiffContent()
  • getWorktreeCodeDiff() → use nativeDiffContent()
  • getWorktreeLog() → use nativeLogOneline()
  • mergeWorktreeToMain() → use nativeMergeSquash() + nativeCommit()

4.3 — auto-worktree.ts

  • getCurrentBranch() → use nativeGetCurrentBranch() (already exists!)
  • autoCommitDirtyState() → use nativeWorkingTreeStatus() + nativeAddAll() + nativeCommit()
  • mergeMilestoneToMain() → use native merge, checkout, commit, branch delete

4.4 — auto.ts

  • git rev-parse --git-dir → use nativeIsRepo()
  • git init -b → use nativeInit()
  • git add -A .gsd .gitignore && git commit → use nativeAddPaths() + nativeCommit()

4.5 — auto-supervisor.ts

  • detectWorkingTreeActivity() → use nativeHasChanges() (already exists!)

4.6 — git-self-heal.ts

  • abortAndReset() → use nativeMergeAbort() + nativeRebaseAbort() + nativeResetHard()

4.7 — guided-flow.ts

  • Same pattern as auto.ts for init + bootstrap

4.8 — doctor.ts

  • git rev-parse --git-dir → use nativeIsRepo()
  • git worktree remove --force → use nativeWorktreeRemove()
  • git branch --list milestone/* → use nativeBranchList()
  • git branch -D → use nativeBranchDelete()
  • git ls-files → use nativeLsFiles()
  • git rm --cached → use nativeRmCached()
  • git branch --format... → use nativeBranchList()

4.9 — gitignore.ts

  • untrackRuntimeFiles() → use nativeRmCached()

4.10 — commands.ts

  • handleCleanupBranches() → use nativeBranchList(), nativeBranchListMerged(), nativeBranchDelete()
  • handleCleanupSnapshots() → use nativeForEachRef(), nativeUpdateRef()

4.11 — undo.ts

  • git revert --no-commit → use nativeRevertCommit()
  • git revert --abort → use nativeRevertAbort()

4.12 — session-forensics.ts

  • getGitChanges() → use nativeWorkingTreeStatus() + nativeDiffStat()

4.13 — worktree-command.ts

  • git merge --abort → use nativeMergeAbort()

Kept as execSync (out of scope)

  • git push <remote> <branch> — Credential handling too complex for git2
  • cat package.json — Not a git command (already just fs.readFileSync)
  • npm test / custom commands — Not git operations

Implementation Order

  1. Rust functions (git.rs) — all read functions first, then write functions
  2. TypeScript bridge (native-git-bridge.ts) — add all new bridge functions
  3. Consumer migration — update each .ts file to use bridge functions
  4. Remove dead code — delete local runGit() helpers from files that no longer need them
  5. 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