diff options
| author | Franck Cuny <franck@fcuny.net> | 2025-09-27 15:10:13 -0700 |
|---|---|---|
| committer | Franck Cuny <franck@fcuny.net> | 2025-09-27 15:16:40 -0700 |
| commit | 7095a53bf173ca6680c7888c8d1df4afb1ad29ae (patch) | |
| tree | 63ce5037b38ed8704fffc362c7541ebef311b74c /cmd/pr-analyzer/analyzer.go | |
| parent | fix sha for goget (diff) | |
| download | x-7095a53bf173ca6680c7888c8d1df4afb1ad29ae.tar.gz | |
add pr-analyzer
A tool to analyze pull request review patterns in a GitHub repository, focusing on team members' review behaviors and habits. This tool helps teams understand and improve their code review processes by providing insights into review frequency, speed, thoroughness, and approval patterns.
Diffstat (limited to 'cmd/pr-analyzer/analyzer.go')
| -rw-r--r-- | cmd/pr-analyzer/analyzer.go | 122 |
1 files changed, 122 insertions, 0 deletions
diff --git a/cmd/pr-analyzer/analyzer.go b/cmd/pr-analyzer/analyzer.go new file mode 100644 index 0000000..a793509 --- /dev/null +++ b/cmd/pr-analyzer/analyzer.go @@ -0,0 +1,122 @@ +package main + +import ( + "fmt" + "sort" + "time" +) + +// AnalyzePRs processes a list of PRs and generates metrics for each reviewer. +func (a *Analyzer) AnalyzePRs(prs []PRData) ([]ReviewerMetrics, error) { + reviewerPRs := make(map[string][]PRMetrics) + + for _, pr := range prs { + metrics, err := a.analyzePR(pr) + if err != nil { + return nil, fmt.Errorf("analyzing PR %d: %w", pr.Number, err) + } + + for _, reviewer := range metrics.Reviewers { + reviewerPRs[reviewer] = append(reviewerPRs[reviewer], metrics) + } + } + + var results []ReviewerMetrics + for reviewer, prs := range reviewerPRs { + metrics := a.calculateReviewerMetrics(reviewer, prs) + results = append(results, metrics) + } + + sort.Slice(results, func(i, j int) bool { + return results[i].PRsReviewed > results[j].PRsReviewed + }) + + return results, nil +} + +func (a *Analyzer) analyzePR(pr PRData) (PRMetrics, error) { + metrics := PRMetrics{ + Number: pr.Number, + CreatedAt: pr.CreatedAt, + CommentsPerFile: make(map[string]int), + Reviewers: make([]string, 0), + } + + sort.Slice(pr.Reviews, func(i, j int) bool { + return pr.Reviews[i].SubmittedAt.Before(pr.Reviews[j].SubmittedAt) + }) + + for _, review := range pr.Reviews { + metrics.Reviewers = append(metrics.Reviewers, review.Reviewer) + + pattern := ReviewPattern{ + FirstReviewState: review.State, + TimeToFirstReview: review.SubmittedAt.Sub(pr.CreatedAt), + } + + for _, comment := range review.Comments { + metrics.CommentsPerFile[comment.Path]++ + } + + metrics.ReviewPatterns = append(metrics.ReviewPatterns, pattern) + } + + if len(pr.Reviews) > 0 { + metrics.TimeToFirstReview = pr.Reviews[0].SubmittedAt.Sub(pr.CreatedAt) + } + + if len(metrics.ReviewPatterns) > 0 { + lastReview := pr.Reviews[len(pr.Reviews)-1] + metrics.ReviewPatterns[len(metrics.ReviewPatterns)-1].FinalReviewState = lastReview.State + metrics.ReviewPatterns[len(metrics.ReviewPatterns)-1].TimeToFinalReview = lastReview.SubmittedAt.Sub( + pr.CreatedAt, + ) + } + + return metrics, nil +} + +func (a *Analyzer) calculateReviewerMetrics(reviewer string, prs []PRMetrics) ReviewerMetrics { + metrics := ReviewerMetrics{ + Username: reviewer, + PRsReviewed: len(prs), + } + + var totalReviewSpeed time.Duration + var totalComments int + var immediateApprovals int + + for _, pr := range prs { + var pattern ReviewPattern + for _, p := range pr.ReviewPatterns { + if p.FirstReviewState != "" { + pattern = p + break + } + } + + if pattern.TimeToFirstReview > 0 { + totalReviewSpeed += pattern.TimeToFirstReview + } + + for _, count := range pr.CommentsPerFile { + totalComments += count + } + + if pattern.FirstReviewState == "APPROVED" { + immediateApprovals++ + } + } + + if metrics.PRsReviewed > 0 { + metrics.AverageReviewSpeed = totalReviewSpeed.Hours() / float64(metrics.PRsReviewed) + metrics.AverageComments = float64(totalComments) / float64(metrics.PRsReviewed) + metrics.ImmediateApprovals = float64( + immediateApprovals, + ) / float64( + metrics.PRsReviewed, + ) * 100 + } + + return metrics +} |
