26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
265 lines
6.4 KiB
Go
265 lines
6.4 KiB
Go
package db
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"slices"
|
|
|
|
"github.com/kunchenguid/no-mistakes/internal/types"
|
|
)
|
|
|
|
// Stats summarizes historical no-mistakes usage across all repositories.
|
|
type Stats struct {
|
|
TotalRepos int
|
|
TotalRuns int
|
|
PullRequests int
|
|
RescueRuns int
|
|
ReportedFindings int
|
|
FixedFindings int
|
|
StepStats []StepStats
|
|
RepoStats []RepoStats
|
|
}
|
|
|
|
// StepStats summarizes reported and fixed findings for one pipeline step.
|
|
type StepStats struct {
|
|
StepName types.StepName
|
|
ReportedFindings int
|
|
FixedFindings int
|
|
}
|
|
|
|
// RepoStats summarizes historical usage for one repository.
|
|
type RepoStats struct {
|
|
RepoID string
|
|
WorkingPath string
|
|
Runs int
|
|
RescueRuns int
|
|
ReportedFindings int
|
|
FixedFindings int
|
|
}
|
|
|
|
// DisplayName returns a compact repository name for terminal reports.
|
|
func (r RepoStats) DisplayName() string {
|
|
name := filepath.Base(r.WorkingPath)
|
|
if name == "." || name == string(filepath.Separator) || name == "" {
|
|
return r.WorkingPath
|
|
}
|
|
return name
|
|
}
|
|
|
|
// GetStats aggregates historical usage across all repositories.
|
|
func (d *DB) GetStats() (*Stats, error) {
|
|
repos, err := d.getRepos()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stats := &Stats{TotalRepos: len(repos)}
|
|
stepStats := map[types.StepName]*StepStats{}
|
|
|
|
for _, repo := range repos {
|
|
repoStats := RepoStats{RepoID: repo.ID, WorkingPath: repo.WorkingPath}
|
|
runs, err := d.GetRunsByRepo(repo.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
repoStats.Runs = len(runs)
|
|
stats.TotalRuns += len(runs)
|
|
|
|
for _, run := range runs {
|
|
if run.PRURL != nil && *run.PRURL != "" {
|
|
stats.PullRequests++
|
|
}
|
|
|
|
runReported, runFixed, err := d.aggregateRunStats(run.ID, stepStats)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stats.ReportedFindings += runReported
|
|
stats.FixedFindings += runFixed
|
|
repoStats.ReportedFindings += runReported
|
|
repoStats.FixedFindings += runFixed
|
|
if runReported > 0 && runFixed > 0 {
|
|
stats.RescueRuns++
|
|
repoStats.RescueRuns++
|
|
}
|
|
}
|
|
|
|
stats.RepoStats = append(stats.RepoStats, repoStats)
|
|
}
|
|
|
|
for _, step := range stepStats {
|
|
if step.ReportedFindings == 0 && step.FixedFindings == 0 {
|
|
continue
|
|
}
|
|
stats.StepStats = append(stats.StepStats, *step)
|
|
}
|
|
sortStepStats(stats.StepStats)
|
|
sortRepoStats(stats.RepoStats)
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
func (d *DB) aggregateRunStats(runID string, stepStats map[types.StepName]*StepStats) (int, int, error) {
|
|
steps, err := d.GetStepsByRun(runID)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
runReported := 0
|
|
runFixed := 0
|
|
for _, step := range steps {
|
|
rounds, err := d.GetRoundsByStep(step.ID)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
findingStats := stepFindingStats(step, rounds)
|
|
reported, fixed := findingStats.ReportedFindings, findingStats.FixedFindings
|
|
|
|
runReported += reported
|
|
runFixed += fixed
|
|
stat := stepStats[step.StepName]
|
|
if stat == nil {
|
|
stat = &StepStats{StepName: step.StepName}
|
|
stepStats[step.StepName] = stat
|
|
}
|
|
stat.ReportedFindings += reported
|
|
stat.FixedFindings += fixed
|
|
}
|
|
|
|
return runReported, runFixed, nil
|
|
}
|
|
|
|
func stepFindingCounts(step *StepResult, rounds []*StepRound) (reported int, final int) {
|
|
stats := stepFindingStats(step, rounds)
|
|
return stats.ReportedFindings, stats.ReportedFindings - stats.FixedFindings
|
|
}
|
|
|
|
func stepFindingStats(step *StepResult, rounds []*StepRound) StepStats {
|
|
stats := StepStats{StepName: step.StepName}
|
|
if len(rounds) == 0 {
|
|
count := findingsCount(step.FindingsJSON)
|
|
stats.ReportedFindings = count
|
|
return stats
|
|
}
|
|
|
|
reported := make(map[types.Finding]bool)
|
|
var current []types.Finding
|
|
for _, round := range rounds {
|
|
items := findingItems(round.FindingsJSON)
|
|
for _, item := range items {
|
|
reported[findingStatsKey(item)] = true
|
|
}
|
|
current = items
|
|
}
|
|
|
|
stats.ReportedFindings = len(reported)
|
|
currentCount := len(current)
|
|
stats.FixedFindings = stats.ReportedFindings - currentCount
|
|
if stats.FixedFindings < 0 {
|
|
stats.FixedFindings = 0
|
|
}
|
|
if stats.FixedFindings > stats.ReportedFindings {
|
|
stats.FixedFindings = stats.ReportedFindings
|
|
}
|
|
return stats
|
|
}
|
|
|
|
// FixedFindingsByStep returns how many findings were resolved for a single step.
|
|
func (d *DB) FixedFindingsByStep(step *StepResult) (int, error) {
|
|
stats, err := d.StepFindingStats(step)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return stats.FixedFindings, nil
|
|
}
|
|
|
|
// StepFindingStats returns reported and fixed finding counts for a single step.
|
|
func (d *DB) StepFindingStats(step *StepResult) (StepStats, error) {
|
|
rounds, err := d.GetRoundsByStep(step.ID)
|
|
if err != nil {
|
|
return StepStats{}, err
|
|
}
|
|
return stepFindingStats(step, rounds), nil
|
|
}
|
|
|
|
func findingsCount(raw *string) int {
|
|
if raw == nil || *raw == "" {
|
|
return 0
|
|
}
|
|
findings, err := types.ParseFindingsJSON(*raw)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return len(findings.Items)
|
|
}
|
|
|
|
func findingItems(raw *string) []types.Finding {
|
|
if raw == nil || *raw == "" {
|
|
return nil
|
|
}
|
|
findings, err := types.ParseFindingsJSON(*raw)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return findings.Items
|
|
}
|
|
|
|
func findingStatsKey(item types.Finding) types.Finding {
|
|
item.ID = ""
|
|
item.Action = ""
|
|
item.Source = ""
|
|
item.UserInstructions = ""
|
|
return item
|
|
}
|
|
|
|
func sortStepStats(stats []StepStats) {
|
|
slices.SortFunc(stats, func(a, b StepStats) int {
|
|
if a.FixedFindings != b.FixedFindings {
|
|
return b.FixedFindings - a.FixedFindings
|
|
}
|
|
if a.ReportedFindings != b.ReportedFindings {
|
|
return b.ReportedFindings - a.ReportedFindings
|
|
}
|
|
return a.StepName.Order() - b.StepName.Order()
|
|
})
|
|
}
|
|
|
|
func sortRepoStats(stats []RepoStats) {
|
|
slices.SortFunc(stats, func(a, b RepoStats) int {
|
|
if a.RescueRuns != b.RescueRuns {
|
|
return b.RescueRuns - a.RescueRuns
|
|
}
|
|
if a.FixedFindings != b.FixedFindings {
|
|
return b.FixedFindings - a.FixedFindings
|
|
}
|
|
if a.Runs != b.Runs {
|
|
return b.Runs - a.Runs
|
|
}
|
|
if a.WorkingPath < b.WorkingPath {
|
|
return -1
|
|
}
|
|
if a.WorkingPath > b.WorkingPath {
|
|
return 1
|
|
}
|
|
return 0
|
|
})
|
|
}
|
|
|
|
func (d *DB) getRepos() ([]*Repo, error) {
|
|
rows, err := d.sql.Query(`SELECT id, working_path, upstream_url, COALESCE(fork_url, ''), default_branch, created_at FROM repos ORDER BY working_path`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get repos: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var repos []*Repo
|
|
for rows.Next() {
|
|
repo := &Repo{}
|
|
if err := rows.Scan(&repo.ID, &repo.WorkingPath, &repo.UpstreamURL, &repo.ForkURL, &repo.DefaultBranch, &repo.CreatedAt); err != nil {
|
|
return nil, fmt.Errorf("scan repo: %w", err)
|
|
}
|
|
repos = append(repos, repo)
|
|
}
|
|
return repos, rows.Err()
|
|
}
|