a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
119 lines
3.5 KiB
Go
119 lines
3.5 KiB
Go
// Package gitcmd is the single chokepoint every git shell-out routes
|
|
// through. A repository scan can fan out dozens of `git` invocations at
|
|
// once (per-file blame, per-tag ls-tree, per-commit log); left
|
|
// unbounded they thrash the disk and starve CPU. A package-global
|
|
// weighted semaphore caps the number of concurrent git subprocesses so
|
|
// the rest of the indexer keeps making progress.
|
|
//
|
|
// The limiter is process-wide on purpose: it bounds the total git
|
|
// concurrency across every caller (churn, blame, releases, the index
|
|
// poller and git watcher), not per-package. Callers acquire a slot
|
|
// before spawning and release it when the subprocess exits.
|
|
//
|
|
// Run captures stdout and stderr separately and, on a non-nil exec
|
|
// error, wraps git's own stderr into the returned error so the failure
|
|
// reason survives. Callers that previously ignored git errors keep
|
|
// doing so by ignoring the returned err.
|
|
package gitcmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"runtime"
|
|
"sync"
|
|
|
|
"golang.org/x/sync/semaphore"
|
|
)
|
|
|
|
var (
|
|
semMu sync.Mutex
|
|
// sem is the package-global limiter, swapped under semMu by
|
|
// SetConcurrency. Its default weight is min(GOMAXPROCS, 8).
|
|
sem *semaphore.Weighted = semaphore.NewWeighted(defaultConcurrency())
|
|
)
|
|
|
|
// defaultConcurrency returns the default semaphore weight:
|
|
// min(runtime.GOMAXPROCS(0), 8).
|
|
func defaultConcurrency() int64 {
|
|
n := runtime.GOMAXPROCS(0)
|
|
if n > 8 {
|
|
n = 8
|
|
}
|
|
if n < 1 {
|
|
n = 1
|
|
}
|
|
return int64(n)
|
|
}
|
|
|
|
// SetConcurrency resizes the global git limiter, called once at
|
|
// daemon/CLI init. A value < 1 is clamped to 1. The swap is done under
|
|
// semMu; in-flight Run calls that already hold a slot are unaffected.
|
|
func SetConcurrency(n int) {
|
|
if n < 1 {
|
|
n = 1
|
|
}
|
|
semMu.Lock()
|
|
sem = semaphore.NewWeighted(int64(n))
|
|
semMu.Unlock()
|
|
}
|
|
|
|
// currentSem returns the live limiter under semMu so a concurrent
|
|
// SetConcurrency can swap the package var without racing the read.
|
|
func currentSem() *semaphore.Weighted {
|
|
semMu.Lock()
|
|
s := sem
|
|
semMu.Unlock()
|
|
return s
|
|
}
|
|
|
|
// Run acquires the global semaphore (ctx-cancellable), runs
|
|
// `git [-C dir] args...`, and on error wraps git's own stderr into the
|
|
// returned error. The acquire aborts before spawning the subprocess
|
|
// when ctx is already cancelled, returning ctx.Err().
|
|
//
|
|
// On a non-nil exec error, Run returns
|
|
// fmt.Errorf("git %s: %w: %s", args[0], err, bytes.TrimSpace(stderr)).
|
|
// The captured stdout is always returned, even on error.
|
|
func Run(ctx context.Context, dir string, args ...string) ([]byte, error) {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
// Abort before spawning if ctx is already cancelled.
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
s := currentSem()
|
|
if err := s.Acquire(ctx, 1); err != nil {
|
|
// ctx cancelled while (or before) waiting for a slot — no
|
|
// subprocess was spawned.
|
|
return nil, err
|
|
}
|
|
defer s.Release(1)
|
|
|
|
full := args
|
|
if dir != "" {
|
|
full = append([]string{"-C", dir}, args...)
|
|
}
|
|
cmd := exec.CommandContext(ctx, "git", full...)
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
name := "git"
|
|
if len(args) > 0 {
|
|
name = args[0]
|
|
}
|
|
return stdout.Bytes(), fmt.Errorf("git %s: %w: %s", name, err, bytes.TrimSpace(stderr.Bytes()))
|
|
}
|
|
return stdout.Bytes(), nil
|
|
}
|
|
|
|
// Output is the one-shot convenience: it runs Run and returns
|
|
// strings.TrimSpace(stdout) for callers that ignore stderr framing.
|
|
func Output(ctx context.Context, dir string, args ...string) (string, error) {
|
|
out, err := Run(ctx, dir, args...)
|
|
return string(bytes.TrimSpace(out)), err
|
|
}
|