feat(tool): add global git subprocess concurrency limiter and propagate context.Context to diff layer

Introduce gitcmd.Runner with channel-based semaphore to cap concurrent
git subprocesses (default 16, configurable via --max-git-procs). Route
all tool-layer and diff-layer git calls through the shared runner.
Also fix diff.Provider.runGit lacking context.Context — now the full
chain (GetDiff → MergeBase → ParseDiffText → finalizeDiff) propagates
the caller's context for proper cancellation and timeout support.
This commit is contained in:
kite 2026-06-06 20:20:34 +08:00
parent 363d971916
commit ef46dfdac9
11 changed files with 208 additions and 57 deletions

View file

@ -106,6 +106,7 @@ type reviewOptions struct {
concurrency int
perFileTimeout int
maxTools int
maxGitProcs int
preview bool
showHelp bool
}
@ -127,6 +128,7 @@ func parseReviewFlags(args []string) (reviewOptions, error) {
a.StringVar(&opts.audience, "audience", "human", "output audience: human (show progress) or agent (summary only)")
a.StringVarP(&opts.background, "background", "b", "", "optional requirement/business context for the review")
a.IntVar(&opts.maxTools, "max-tools", 0, "max tool call rounds per file; only takes effect when greater than template default")
a.IntVar(&opts.maxGitProcs, "max-git-procs", 16, "max concurrent git subprocesses")
a.BoolVarP(&opts.preview, "preview", "p", false, "preview which files will be reviewed without running the LLM")
if err := a.Parse(args); err != nil {
@ -163,6 +165,10 @@ func parseReviewFlags(args []string) (reviewOptions, error) {
return opts, fmt.Errorf("--max-tools must be a non-negative integer (0 means use template default)")
}
if opts.maxGitProcs < 0 {
return opts, fmt.Errorf("--max-git-procs must be a non-negative integer (0 means use default 16)")
}
return opts, nil
}
@ -201,6 +207,7 @@ Flags:
-c, --commit string single commit hash or tag to review (vs its parent)
-f, --format string output format: text or json (default "text")
--concurrency int max concurrent file reviews (default 8)
--max-git-procs int max concurrent git subprocesses (default 16)
--from string source ref to start diff from (e.g., 'main')
--max-tools int max tool call rounds per file; only takes effect when greater than template default
-p, --preview preview which files will be reviewed without running the LLM

View file

@ -12,6 +12,7 @@ import (
"github.com/open-code-review/open-code-review/internal/config/template"
"github.com/open-code-review/open-code-review/internal/config/toolsconfig"
"github.com/open-code-review/open-code-review/internal/diff"
"github.com/open-code-review/open-code-review/internal/gitcmd"
"github.com/open-code-review/open-code-review/internal/llm"
"github.com/open-code-review/open-code-review/internal/stdout"
"github.com/open-code-review/open-code-review/internal/telemetry"
@ -91,6 +92,8 @@ func runReview(args []string) error {
llmClient := llm.NewLLMClient(ep)
model := ep.Model
gitRunner := gitcmd.New(opts.maxGitProcs)
collector := tool.NewCommentCollector()
mode := tool.ParseReviewMode(opts.from, opts.to, opts.commit)
ref, _ := mode.RefValue(opts.to, opts.commit)
@ -98,6 +101,7 @@ func runReview(args []string) error {
RepoDir: repoDir,
Mode: mode,
Ref: ref,
Runner: gitRunner,
}
tools := buildToolRegistry(collector, fileReader)
@ -119,6 +123,7 @@ func runReview(args []string) error {
ConcurrentTaskTimeout: opts.perFileTimeout,
Model: model,
Background: opts.background,
GitRunner: gitRunner,
})
// Silence progress output during execution; restore before Summary in agent mode.
@ -212,15 +217,17 @@ func requireGitRepo(dir string) error {
}
func runPreview(repoDir string, opts reviewOptions, fileFilter *rules.FileFilter) error {
gitRunner := gitcmd.New(opts.maxGitProcs)
ag := agent.New(agent.Args{
RepoDir: repoDir,
From: opts.from,
To: opts.to,
Commit: opts.commit,
FileFilter: fileFilter,
GitRunner: gitRunner,
})
preview, err := ag.Preview()
preview, err := ag.Preview(context.Background())
if err != nil {
return fmt.Errorf("preview failed: %w", err)
}