chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user