From 6202d783c2549a90ae2b612385b19a17aa272002 Mon Sep 17 00:00:00 2001 From: onyx Date: Sat, 24 Jan 2026 17:22:17 -0800 Subject: [PATCH] perf(git): cache git rev-parse results within sessions Multiple gt commands call git rev-parse --show-toplevel, adding ~50ms each invocation. Results rarely change within a session, and multiple agents calling git concurrently contend on .git/index.lock. Add cached RepoRoot() and RepoRootFrom() functions to the git package and update all callers to use them. This ensures a single git subprocess call per process for the common case of checking the current directory's repo root. Files updated: - internal/git/git.go: Add RepoRoot() and RepoRootFrom() - internal/cmd/prime.go: Use cached git.RepoRoot() - internal/cmd/molecule_status.go: Use cached git.RepoRoot() - internal/cmd/sling_helpers.go: Use cached git.RepoRoot() - internal/cmd/rig_quick_add.go: Use git.RepoRootFrom() for path arg - internal/version/stale.go: Use cached git.RepoRoot() Closes: bd-2zd.5 --- internal/cmd/molecule_status.go | 10 +++----- internal/cmd/prime.go | 9 +++----- internal/cmd/rig_quick_add.go | 9 ++------ internal/cmd/sling_helpers.go | 7 +++--- internal/git/git.go | 41 +++++++++++++++++++++++++++++++++ internal/version/stale.go | 7 +++--- 6 files changed, 57 insertions(+), 26 deletions(-) diff --git a/internal/cmd/molecule_status.go b/internal/cmd/molecule_status.go index abf4500f..9863048e 100644 --- a/internal/cmd/molecule_status.go +++ b/internal/cmd/molecule_status.go @@ -4,13 +4,13 @@ import ( "encoding/json" "fmt" "os" - "os/exec" "path/filepath" "strings" "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/config" + "github.com/steveyegge/gastown/internal/git" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/workspace" ) @@ -963,13 +963,9 @@ func outputMoleculeCurrent(info MoleculeCurrentInfo) error { } // getGitRootForMolStatus returns the git root for hook file lookup. +// Uses cached value to avoid repeated git subprocess calls. func getGitRootForMolStatus() (string, error) { - cmd := exec.Command("git", "rev-parse", "--show-toplevel") - out, err := cmd.Output() - if err != nil { - return "", err - } - return strings.TrimSpace(string(out)), nil + return git.RepoRoot() } // isTownLevelRole returns true if the agent ID is a town-level role. diff --git a/internal/cmd/prime.go b/internal/cmd/prime.go index 400e9a21..0f3e0bc6 100644 --- a/internal/cmd/prime.go +++ b/internal/cmd/prime.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/beads" + "github.com/steveyegge/gastown/internal/git" "github.com/steveyegge/gastown/internal/lock" "github.com/steveyegge/gastown/internal/state" "github.com/steveyegge/gastown/internal/style" @@ -544,13 +545,9 @@ func buildRoleAnnouncement(ctx RoleContext) string { } // getGitRoot returns the root of the current git repository. +// Uses cached value to avoid repeated git subprocess calls. func getGitRoot() (string, error) { - cmd := exec.Command("git", "rev-parse", "--show-toplevel") - out, err := cmd.Output() - if err != nil { - return "", err - } - return strings.TrimSpace(string(out)), nil + return git.RepoRoot() } // getAgentIdentity returns the agent identity string for hook lookup. diff --git a/internal/cmd/rig_quick_add.go b/internal/cmd/rig_quick_add.go index 52337483..066bda31 100644 --- a/internal/cmd/rig_quick_add.go +++ b/internal/cmd/rig_quick_add.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/spf13/cobra" + "github.com/steveyegge/gastown/internal/git" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/workspace" ) @@ -138,13 +139,7 @@ func runRigQuickAdd(cmd *cobra.Command, args []string) error { } func findGitRoot(path string) (string, error) { - cmd := exec.Command("git", "rev-parse", "--show-toplevel") - cmd.Dir = path - out, err := cmd.Output() - if err != nil { - return "", err - } - return strings.TrimSpace(string(out)), nil + return git.RepoRootFrom(path) } func findGitRemoteURL(gitRoot string) (string, error) { diff --git a/internal/cmd/sling_helpers.go b/internal/cmd/sling_helpers.go index f19ebd0e..e22a9c7d 100644 --- a/internal/cmd/sling_helpers.go +++ b/internal/cmd/sling_helpers.go @@ -10,6 +10,7 @@ import ( "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/constants" + "github.com/steveyegge/gastown/internal/git" "github.com/steveyegge/gastown/internal/tmux" "github.com/steveyegge/gastown/internal/workspace" ) @@ -378,13 +379,13 @@ func ensureAgentReady(sessionName string) error { } // detectCloneRoot finds the root of the current git clone. +// Uses cached value to avoid repeated git subprocess calls. func detectCloneRoot() (string, error) { - cmd := exec.Command("git", "rev-parse", "--show-toplevel") - out, err := cmd.Output() + root, err := git.RepoRoot() if err != nil { return "", fmt.Errorf("not in a git repository") } - return strings.TrimSpace(string(out)), nil + return root, nil } // detectActor returns the current agent's actor string for event logging. diff --git a/internal/git/git.go b/internal/git/git.go index 7778626f..226664b9 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -9,8 +9,49 @@ import ( "os/exec" "path/filepath" "strings" + "sync" ) +// Cached repo root for the current process. +// Since CLI commands are short-lived and the working directory doesn't change +// during a single invocation, caching this avoids repeated git subprocess calls +// that add ~50ms each and contend on .git/index.lock. +var ( + cachedRepoRoot string + cachedRepoRootOnce sync.Once + cachedRepoRootErr error +) + +// RepoRoot returns the root directory of the git repository containing the +// current working directory. The result is cached for the lifetime of the process. +// This avoids repeated git rev-parse calls that are expensive (~50ms each) and +// can cause lock contention when multiple agents are running. +func RepoRoot() (string, error) { + cachedRepoRootOnce.Do(func() { + cmd := exec.Command("git", "rev-parse", "--show-toplevel") + out, err := cmd.Output() + if err != nil { + cachedRepoRootErr = err + return + } + cachedRepoRoot = strings.TrimSpace(string(out)) + }) + return cachedRepoRoot, cachedRepoRootErr +} + +// RepoRootFrom returns the root directory of the git repository containing the +// specified path. Unlike RepoRoot(), this is not cached because it depends on +// the input path. Use RepoRoot() when checking the current working directory. +func RepoRootFrom(path string) (string, error) { + cmd := exec.Command("git", "rev-parse", "--show-toplevel") + cmd.Dir = path + out, err := cmd.Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(out)), nil +} + // GitError contains raw output from a git command for agent observation. // ZFC: Callers observe the raw output and decide what to do. // The error interface methods provide human-readable messages, but agents diff --git a/internal/version/stale.go b/internal/version/stale.go index d5e13c03..2c62f188 100644 --- a/internal/version/stale.go +++ b/internal/version/stale.go @@ -7,6 +7,8 @@ import ( "os/exec" "runtime/debug" "strings" + + "github.com/steveyegge/gastown/internal/git" ) // These variables are set at build time via ldflags in cmd package. @@ -133,9 +135,8 @@ func GetRepoRoot() (string, error) { } // Check if current directory is in the gt source repo - cmd := exec.Command("git", "rev-parse", "--show-toplevel") - if output, err := cmd.Output(); err == nil { - root := strings.TrimSpace(string(output)) + // Uses cached git.RepoRoot() to avoid repeated subprocess calls + if root, err := git.RepoRoot(); err == nil { if hasGtSource(root) { return root, nil }