Some checks failed
CI / Check for .beads changes (push) Has been skipped
CI / Check embedded formulas (push) Failing after 20s
CI / Test (push) Failing after 1m28s
CI / Lint (push) Failing after 21s
CI / Integration Tests (push) Successful in 1m12s
CI / Coverage Report (push) Has been skipped
Windows CI / Windows Build and Unit Tests (push) Has been cancelled
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
166 lines
4.6 KiB
Go
166 lines
4.6 KiB
Go
// Package version provides version information and staleness checking for gt.
|
|
package version
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"runtime/debug"
|
|
"strings"
|
|
|
|
"github.com/steveyegge/gastown/internal/git"
|
|
)
|
|
|
|
// These variables are set at build time via ldflags in cmd package.
|
|
// We provide fallback methods to read from build info.
|
|
var (
|
|
// Commit can be set from cmd package or read from build info
|
|
Commit = ""
|
|
)
|
|
|
|
// StaleBinaryInfo contains information about binary staleness.
|
|
type StaleBinaryInfo struct {
|
|
IsStale bool // True if binary commit doesn't match repo HEAD
|
|
BinaryCommit string // Commit hash the binary was built from
|
|
RepoCommit string // Current repo HEAD commit
|
|
CommitsBehind int // Number of commits binary is behind (0 if unknown)
|
|
Error error // Any error encountered during check
|
|
}
|
|
|
|
// resolveCommitHash gets the commit hash from build info or the Commit variable.
|
|
func resolveCommitHash() string {
|
|
if Commit != "" {
|
|
return Commit
|
|
}
|
|
|
|
if info, ok := debug.ReadBuildInfo(); ok {
|
|
for _, setting := range info.Settings {
|
|
if setting.Key == "vcs.revision" && setting.Value != "" {
|
|
return setting.Value
|
|
}
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// ShortCommit returns first 12 characters of a hash.
|
|
func ShortCommit(hash string) string {
|
|
if len(hash) > 12 {
|
|
return hash[:12]
|
|
}
|
|
return hash
|
|
}
|
|
|
|
// commitsMatch compares two commit hashes, handling different lengths.
|
|
// Returns true if one is a prefix of the other (minimum 7 chars to avoid false positives).
|
|
func commitsMatch(a, b string) bool {
|
|
minLen := len(a)
|
|
if len(b) < minLen {
|
|
minLen = len(b)
|
|
}
|
|
// Need at least 7 chars for a reasonable comparison
|
|
if minLen < 7 {
|
|
return false
|
|
}
|
|
return strings.HasPrefix(a, b[:minLen]) || strings.HasPrefix(b, a[:minLen])
|
|
}
|
|
|
|
// CheckStaleBinary compares the binary's embedded commit with the repo HEAD.
|
|
// It returns staleness info including whether the binary needs rebuilding.
|
|
// This check is designed to be fast and non-blocking - errors are captured
|
|
// but don't interrupt normal operation.
|
|
func CheckStaleBinary(repoDir string) *StaleBinaryInfo {
|
|
info := &StaleBinaryInfo{}
|
|
|
|
// Get binary commit
|
|
info.BinaryCommit = resolveCommitHash()
|
|
if info.BinaryCommit == "" {
|
|
info.Error = fmt.Errorf("cannot determine binary commit (dev build?)")
|
|
return info
|
|
}
|
|
|
|
// Get repo HEAD
|
|
cmd := exec.Command("git", "rev-parse", "HEAD")
|
|
cmd.Dir = repoDir
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
info.Error = fmt.Errorf("cannot get repo HEAD: %w", err)
|
|
return info
|
|
}
|
|
info.RepoCommit = strings.TrimSpace(string(output))
|
|
|
|
// Compare commits using prefix matching (handles short vs full hash)
|
|
// Use the shorter of the two commit lengths for comparison
|
|
if !commitsMatch(info.BinaryCommit, info.RepoCommit) {
|
|
info.IsStale = true
|
|
|
|
// Try to count commits between binary and HEAD
|
|
countCmd := exec.Command("git", "rev-list", "--count", info.BinaryCommit+"..HEAD")
|
|
countCmd.Dir = repoDir
|
|
if countOutput, err := countCmd.Output(); err == nil {
|
|
if count, parseErr := fmt.Sscanf(strings.TrimSpace(string(countOutput)), "%d", &info.CommitsBehind); parseErr != nil || count != 1 {
|
|
info.CommitsBehind = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
// GetRepoRoot returns the git repository root for the gt source code.
|
|
// It looks for the gastown repo by checking known paths.
|
|
func GetRepoRoot() (string, error) {
|
|
// First, check if GT_ROOT environment variable is set
|
|
if gtRoot := os.Getenv("GT_ROOT"); gtRoot != "" {
|
|
if isGitRepo(gtRoot) && hasGastownMarker(gtRoot) {
|
|
return gtRoot, nil
|
|
}
|
|
}
|
|
|
|
// Try common development paths relative to home
|
|
home := os.Getenv("HOME")
|
|
if home != "" {
|
|
candidates := []string{
|
|
home + "/gt/gastown",
|
|
home + "/gastown",
|
|
home + "/src/gastown",
|
|
home + "/dev/gastown",
|
|
}
|
|
for _, candidate := range candidates {
|
|
if isGitRepo(candidate) && hasGastownMarker(candidate) {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if current directory is in a gastown repo
|
|
// Uses cached git.RepoRoot() to avoid repeated subprocess calls
|
|
if root, err := git.RepoRoot(); err == nil {
|
|
if hasGastownMarker(root) {
|
|
return root, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("cannot locate gt source repository")
|
|
}
|
|
|
|
// isGitRepo checks if a directory is a git repository.
|
|
func isGitRepo(dir string) bool {
|
|
cmd := exec.Command("git", "rev-parse", "--git-dir")
|
|
cmd.Dir = dir
|
|
return cmd.Run() == nil
|
|
}
|
|
|
|
// hasGastownMarker checks if a directory looks like the gastown repo.
|
|
func hasGastownMarker(dir string) bool {
|
|
// Check for cmd/gt directory which is unique to gastown
|
|
cmd := exec.Command("test", "-d", dir+"/cmd/gt")
|
|
return cmd.Run() == nil
|
|
}
|
|
|
|
// SetCommit allows the cmd package to pass in the build-time commit.
|
|
func SetCommit(commit string) {
|
|
Commit = commit
|
|
}
|