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
142 lines
4.6 KiB
Go
142 lines
4.6 KiB
Go
package review
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
|
|
gitignore "github.com/sabhiram/go-gitignore"
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"github.com/zzet/gortex/internal/config"
|
|
"github.com/zzet/gortex/internal/platform"
|
|
)
|
|
|
|
// RuleResolver grounds each changed file to the review rule that governs
|
|
// it, across four configuration layers. The rule list is flattened and
|
|
// its globs precompiled once at construction; RuleFor is a read-only hot
|
|
// path with no further compilation.
|
|
//
|
|
// Layer precedence, highest first (first match across the merged list
|
|
// wins): custom (an explicit --rules file or the repo-local
|
|
// .gortex/review.yaml) → project (.gortex.yaml `review:`) → global
|
|
// (platform.ConfigDir()/config.yaml `review:`) → embedded defaults.
|
|
type RuleResolver struct {
|
|
rules []compiledRule
|
|
}
|
|
|
|
// compiledRule pairs a config rule with its precompiled gitignore
|
|
// matcher so RuleFor never recompiles. The matcher is read-only after
|
|
// construction, so RuleFor is safe to call concurrently.
|
|
type compiledRule struct {
|
|
rule config.ReviewRule
|
|
matcher *gitignore.GitIgnore
|
|
}
|
|
|
|
// NewRuleResolver merges the four rule layers in precedence order,
|
|
// drops Disabled rules, precompiles each remaining rule's glob, and
|
|
// returns a resolver ready for RuleFor.
|
|
//
|
|
// customPath, when non-empty, names an explicit review-rule file (the
|
|
// --rules flag). When empty, the repo-local .gortex/review.yaml under
|
|
// repoRoot is used as the custom layer if it exists. repoRoot is the
|
|
// repository root the project layer (.gortex.yaml) is loaded from; it
|
|
// may be empty to skip the project layer.
|
|
func NewRuleResolver(customPath, repoRoot string) (*RuleResolver, error) {
|
|
var merged []config.ReviewRule
|
|
|
|
// Layer 1 (highest precedence) — custom: an explicit --rules file,
|
|
// else the repo-local .gortex/review.yaml.
|
|
custom := customPath
|
|
if custom == "" && repoRoot != "" {
|
|
repoLocal := filepath.Join(repoRoot, ".gortex", "review.yaml")
|
|
if _, err := os.Stat(repoLocal); err == nil {
|
|
custom = repoLocal
|
|
}
|
|
}
|
|
if custom != "" {
|
|
rc, err := loadReviewFile(custom)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
merged = append(merged, rc.Rules...)
|
|
}
|
|
|
|
// Layer 2 — project: .gortex.yaml `review:` under repoRoot.
|
|
if repoRoot != "" {
|
|
projectPath := filepath.Join(repoRoot, ".gortex.yaml")
|
|
if _, err := os.Stat(projectPath); err == nil {
|
|
cfg, err := config.Load(projectPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
merged = append(merged, cfg.Review.Rules...)
|
|
}
|
|
}
|
|
|
|
// Layer 3 — global: platform.ConfigDir()/config.yaml `review:`.
|
|
globalPath := filepath.Join(platform.ConfigDir(), "config.yaml")
|
|
if _, err := os.Stat(globalPath); err == nil {
|
|
cfg, err := config.Load(globalPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
merged = append(merged, cfg.Review.Rules...)
|
|
}
|
|
|
|
// Layer 4 (lowest precedence) — embedded defaults.
|
|
merged = append(merged, defaultReviewRules()...)
|
|
|
|
compiled := make([]compiledRule, 0, len(merged))
|
|
for _, r := range merged {
|
|
if r.Disabled || r.Path == "" {
|
|
continue
|
|
}
|
|
compiled = append(compiled, compiledRule{
|
|
rule: r,
|
|
matcher: gitignore.CompileIgnoreLines(r.Path),
|
|
})
|
|
}
|
|
|
|
return &RuleResolver{rules: compiled}, nil
|
|
}
|
|
|
|
// RuleFor returns the first rule whose glob matches filePath, scanning
|
|
// the merged layers in precedence order (custom → project → global →
|
|
// embedded). filePath should be repo-root-relative with forward-slash
|
|
// separators. The second return is false only when no rule matched —
|
|
// which cannot happen once the embedded `**` catch-all is present, but
|
|
// callers should still check it.
|
|
func (r *RuleResolver) RuleFor(filePath string) (config.ReviewRule, bool) {
|
|
for _, cr := range r.rules {
|
|
if cr.matcher.MatchesPath(filePath) {
|
|
return cr.rule, true
|
|
}
|
|
}
|
|
return config.ReviewRule{}, false
|
|
}
|
|
|
|
// loadReviewFile reads a standalone review-rule file (--rules or
|
|
// .gortex/review.yaml). The file's top-level shape is a ReviewConfig:
|
|
// a bare `rules:` list plus the optional gating knobs.
|
|
func loadReviewFile(path string) (config.ReviewConfig, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return config.ReviewConfig{}, err
|
|
}
|
|
var rc config.ReviewConfig
|
|
if err := yaml.Unmarshal(data, &rc); err != nil {
|
|
return config.ReviewConfig{}, err
|
|
}
|
|
return rc, nil
|
|
}
|
|
|
|
// defaultReviewRules is the embedded lowest-precedence layer: a test
|
|
// rulepack for *_test.go files and a general catch-all for everything
|
|
// else. The catch-all `**` guarantees RuleFor always resolves.
|
|
func defaultReviewRules() []config.ReviewRule {
|
|
return []config.ReviewRule{
|
|
{Name: "test", Path: "**/*_test.go", Rulepack: "test"},
|
|
{Name: "general", Path: "**", Rulepack: "general"},
|
|
}
|
|
}
|