mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-07-09 17:28:58 +00:00
219 lines
6.3 KiB
Go
219 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/open-code-review/open-code-review/internal/agent"
|
|
"github.com/open-code-review/open-code-review/internal/config/rules"
|
|
"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/llm"
|
|
"github.com/open-code-review/open-code-review/internal/stdout"
|
|
"github.com/open-code-review/open-code-review/internal/telemetry"
|
|
"github.com/open-code-review/open-code-review/internal/tool"
|
|
)
|
|
|
|
func runReview(args []string) error {
|
|
opts, err := parseReviewFlags(args)
|
|
if err != nil {
|
|
return fmt.Errorf("parse flags: %w", err)
|
|
}
|
|
if opts.showHelp {
|
|
printReviewUsage()
|
|
return nil
|
|
}
|
|
|
|
if err := requireGitRepo(opts.repoDir); err != nil {
|
|
return err
|
|
}
|
|
|
|
tpl, err := template.LoadDefault()
|
|
if err != nil {
|
|
return fmt.Errorf("load default template: %w", err)
|
|
}
|
|
if err := tpl.Validate(); err != nil {
|
|
return fmt.Errorf("invalid config: %w", err)
|
|
}
|
|
|
|
sysRule, err := rules.LoadDefault()
|
|
if err != nil {
|
|
return fmt.Errorf("load default system rule: %w", err)
|
|
}
|
|
if opts.rulePath != "" {
|
|
sysRule, err = loadSystemRule(opts.rulePath)
|
|
if err != nil {
|
|
return fmt.Errorf("load system rule: %w", err)
|
|
}
|
|
}
|
|
|
|
toolEntries, err := toolsconfig.Load(opts.toolConfigPath)
|
|
if err != nil {
|
|
return fmt.Errorf("load tools: %w", err)
|
|
}
|
|
planToolDefs := agent.BuildToolDefs(toolEntries, true)
|
|
mainToolDefs := agent.BuildToolDefs(toolEntries, false)
|
|
|
|
repoDir, err := resolveRepoDir(opts.repoDir)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve repo: %w", err)
|
|
}
|
|
|
|
appCfg, err := LoadAppConfig(defaultConfigPath())
|
|
if err != nil {
|
|
return fmt.Errorf("load app config: %w", err)
|
|
}
|
|
if appCfg != nil {
|
|
tpl.ApplyLanguage(appCfg.Language)
|
|
}
|
|
|
|
ep, err := llm.ResolveEndpoint(defaultConfigPath())
|
|
if err != nil {
|
|
return fmt.Errorf("resolve LLM endpoint: %w", err)
|
|
}
|
|
|
|
llmClient := llm.NewLLMClient(ep)
|
|
model := ep.Model
|
|
|
|
collector := tool.NewCommentCollector()
|
|
mode := tool.ParseReviewMode(opts.from, opts.to, opts.commit)
|
|
ref, _ := mode.RefValue(opts.to, opts.commit)
|
|
diffMap := make(map[string]string)
|
|
fileReader := &tool.FileReader{
|
|
RepoDir: repoDir,
|
|
Mode: mode,
|
|
Ref: ref,
|
|
}
|
|
tools := buildToolRegistry(collector, fileReader, diffMap)
|
|
|
|
ag := agent.New(agent.Args{
|
|
RepoDir: repoDir,
|
|
From: opts.from,
|
|
To: opts.to,
|
|
Commit: opts.commit,
|
|
DiffMap: diffMap,
|
|
Template: *tpl,
|
|
SystemRule: sysRule,
|
|
LLMClient: llmClient,
|
|
Tools: tools,
|
|
PlanToolDefs: planToolDefs,
|
|
MainToolDefs: mainToolDefs,
|
|
CommentCollector: collector,
|
|
CommentWorkerPool: agent.NewCommentWorkerPool(opts.concurrency),
|
|
MaxConcurrency: opts.concurrency,
|
|
ConcurrentTaskTimeout: opts.perFileTimeout,
|
|
Model: model,
|
|
Background: opts.background,
|
|
})
|
|
|
|
// Silence progress output during execution; restore before Summary in agent mode.
|
|
var unsilence func()
|
|
if opts.outputFormat == "json" || opts.audience == "agent" {
|
|
unsilence = stdout.Quiet()
|
|
defer func() {
|
|
if unsilence != nil {
|
|
unsilence()
|
|
}
|
|
}()
|
|
}
|
|
|
|
ctx, span := telemetry.StartSpan(context.Background(), "review.run")
|
|
defer span.End()
|
|
startTime := time.Now()
|
|
|
|
comments, err := ag.Run(ctx)
|
|
if err != nil {
|
|
telemetry.SetAttr(span, "error", err.Error())
|
|
return fmt.Errorf("review failed: %w", err)
|
|
}
|
|
|
|
// Resolve line numbers by matching existing_code against diff hunks.
|
|
comments = diff.ResolveLineNumbers(comments, ag.Diffs())
|
|
|
|
// Record summary metrics (files_reviewed is refined by agent.Run).
|
|
duration := time.Since(startTime)
|
|
telemetry.RecordReviewDuration(ctx, duration)
|
|
if len(comments) > 0 {
|
|
telemetry.RecordCommentsGenerated(ctx, int64(len(comments)))
|
|
}
|
|
|
|
// If no files were reviewed (e.g. workspace has no changes), inform the caller in JSON mode.
|
|
if opts.outputFormat == "json" && len(comments) == 0 && ag.FilesReviewed() == 0 {
|
|
return outputJSONNoFiles()
|
|
}
|
|
|
|
// In agent mode, restore stdout so Summary reaches the terminal.
|
|
if opts.audience == "agent" && unsilence != nil {
|
|
unsilence()
|
|
unsilence = nil
|
|
}
|
|
|
|
telemetry.PrintTraceSummary(ag.FilesReviewed(), int64(len(comments)), ag.TotalInputTokens(), ag.TotalOutputTokens(), ag.TotalTokensUsed(), duration)
|
|
|
|
if opts.outputFormat == "json" {
|
|
return outputJSONWithWarnings(comments, ag.Warnings())
|
|
}
|
|
if opts.audience == "agent" {
|
|
outputTextWithWarnings(comments, ag.Warnings())
|
|
return nil
|
|
}
|
|
outputTextWithWarnings(comments, ag.Warnings())
|
|
|
|
return nil
|
|
}
|
|
|
|
func loadSystemRule(path string) (*rules.SystemRule, error) {
|
|
return rules.LoadFile(path)
|
|
}
|
|
|
|
func resolveRepoDir(input string) (string, error) {
|
|
if input == "" {
|
|
var err error
|
|
input, err = os.Getwd()
|
|
if err != nil {
|
|
return "", fmt.Errorf("get working directory: %w", err)
|
|
}
|
|
}
|
|
absPath, err := filepath.Abs(input)
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolve absolute path: %w", err)
|
|
}
|
|
out, err := runGitCmd(absPath, "rev-parse", "--git-dir")
|
|
if err != nil || len(out) == 0 {
|
|
return "", fmt.Errorf("%s is not a git repository", absPath)
|
|
}
|
|
return absPath, nil
|
|
}
|
|
|
|
// requireGitRepo validates that the given directory is part of a git repository.
|
|
func requireGitRepo(dir string) error {
|
|
repoDir, err := filepath.Abs(dir)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve path: %w", err)
|
|
}
|
|
out, err := runGitCmd(repoDir, "rev-parse", "--git-dir")
|
|
if err != nil || len(out) == 0 {
|
|
return fmt.Errorf("%s is not a git repository, code review requires a valid git repository", repoDir)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func buildToolRegistry(collector *tool.CommentCollector, fr *tool.FileReader, diffMap map[string]string) tool.Registry {
|
|
reg := tool.NewRegistry()
|
|
reg.Register(tool.NewFileRead(fr))
|
|
reg.Register(tool.NewFileFind(fr))
|
|
reg.Register(tool.NewFileReadDiff())
|
|
reg.Register(tool.NewCodeSearch(fr))
|
|
reg.Register(&tool.CodeCommentProvider{Collector: collector})
|
|
|
|
// Wire up FileReadDiffProvider with shared diffMap pointer so Agent's loadDiffs populates it.
|
|
if p, ok := reg[tool.FileReadDiff.Name()].(*tool.FileReadDiffProvider); ok {
|
|
p.DiffMap = diffMap
|
|
}
|
|
|
|
return reg
|
|
}
|