// Package git is a thin wrapper around the git binary. It runs porcelain // commands via os/exec with a per-command timeout so a scan can never hang // on a broken or unresponsive repository. package git import ( "bytes" "context" "fmt" "os" "os/exec" "strings" "time" ) // DefaultTimeout bounds every git invocation. A single hung git call would // otherwise stall the whole scan. const DefaultTimeout = 10 * time.Second // Executor runs git commands against a repository working directory. type Executor struct { bin string timeout time.Duration env []string } // Option configures an Executor (functional options pattern). type Option func(*Executor) // WithBinary overrides the git executable path (default "git"). func WithBinary(bin string) Option { return func(e *Executor) { e.bin = bin } } // WithTimeout overrides the per-command timeout (default 10s). func WithTimeout(d time.Duration) Option { return func(e *Executor) { e.timeout = d } } // WithEnv appends extra environment variables to every command. func WithEnv(env ...string) Option { return func(e *Executor) { e.env = append(e.env, env...) } } // NewExecutor builds an Executor with sensible defaults. func NewExecutor(opts ...Option) *Executor { e := &Executor{bin: "git", timeout: DefaultTimeout} for _, opt := range opts { opt(e) } return e } // Run executes git in dir with a context timeout and returns the // trimmed stdout. Context cancellation is honoured; the configured timeout // guards against a hanging repository. func (e *Executor) Run(ctx context.Context, dir string, args ...string) (string, error) { ctx, cancel := context.WithTimeout(ctx, e.timeout) defer cancel() cmd := exec.CommandContext(ctx, e.bin, args...) cmd.Dir = dir cmd.Env = append(os.Environ(), e.env...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { if ctx.Err() != nil { return "", fmt.Errorf("git %s: timed out after %s", strings.Join(args, " "), e.timeout) } msg := strings.TrimSpace(stderr.String()) if msg == "" { msg = err.Error() } return "", fmt.Errorf("git %s: %s", strings.Join(args, " "), msg) } return strings.TrimSpace(stdout.String()), nil }