// Package rules evaluates user-defined threshold rules against a scan // result, producing labels for repositories that match (e.g. flag a branch // as "stale" when it is 10+ commits behind). package rules import ( "fmt" "gitea.oblak.solutions/dimitar/gitFlow/pkg/status" ) // Rule flags a repository when a numeric field crosses a threshold. type Rule struct { Name string `yaml:"name"` // descriptive name, for config output Field string `yaml:"field"` // ahead, behind, stash, or changes Op string `yaml:"op"` // ==, !=, <, <=, >, >= Value int `yaml:"value"` // threshold to compare against Label string `yaml:"label"` // flag label, e.g. "critical" } // Validate checks that the rule can be evaluated. func (r Rule) Validate() error { switch r.Field { case "ahead", "behind", "stash", "changes": default: return fmt.Errorf("rules: %s: unknown field %q (want ahead, behind, stash, or changes)", r.Name, r.Field) } switch r.Op { case "==", "!=", "<", "<=", ">", ">=": default: return fmt.Errorf("rules: %s: unknown operator %q (want ==, !=, <, <=, >, >=)", r.Name, r.Op) } if r.Label == "" { return fmt.Errorf("rules: %s: label must not be empty", r.Name) } return nil } // Flag is a rule match on a repository. type Flag struct { RepoPath string Label string } // Matches reports whether the rule matches the repository. func (r Rule) Matches(repo status.RepoInfo) (bool, error) { v, err := fieldValue(r.Field, repo) if err != nil { return false, err } switch r.Op { case "==": return v == r.Value, nil case "!=": return v != r.Value, nil case "<": return v < r.Value, nil case "<=": return v <= r.Value, nil case ">": return v > r.Value, nil case ">=": return v >= r.Value, nil } return false, fmt.Errorf("rules: %s: unknown operator %q", r.Name, r.Op) } // Eval applies the rules to the result and returns every match, in scan // order. Rules are validated up front; an invalid rule is an error. func Eval(rules []Rule, result status.ScanResult) ([]Flag, error) { for _, rule := range rules { if err := rule.Validate(); err != nil { return nil, err } } var out []Flag for _, rule := range rules { for _, repo := range result.Repos { ok, err := rule.Matches(repo) if err != nil { return nil, err } if ok { out = append(out, Flag{RepoPath: repo.Path, Label: rule.Label}) } } } return out, nil } // fieldValue extracts the numeric field a rule compares against. func fieldValue(field string, repo status.RepoInfo) (int, error) { switch field { case "ahead": return repo.AheadBy, nil case "behind": return repo.BehindBy, nil case "stash": return repo.StashCount, nil case "changes": return repo.FileCount(), nil } return 0, fmt.Errorf("rules: unknown field %q", field) }