aboutsummaryrefslogtreecommitdiff
path: root/cmd/pr-analyzer/main.go
diff options
context:
space:
mode:
authorFranck Cuny <franck@fcuny.net>2025-09-27 15:10:13 -0700
committerFranck Cuny <franck@fcuny.net>2025-09-27 15:16:40 -0700
commit7095a53bf173ca6680c7888c8d1df4afb1ad29ae (patch)
tree63ce5037b38ed8704fffc362c7541ebef311b74c /cmd/pr-analyzer/main.go
parentfix sha for goget (diff)
downloadx-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/main.go')
-rw-r--r--cmd/pr-analyzer/main.go184
1 files changed, 184 insertions, 0 deletions
diff --git a/cmd/pr-analyzer/main.go b/cmd/pr-analyzer/main.go
new file mode 100644
index 0000000..b12be31
--- /dev/null
+++ b/cmd/pr-analyzer/main.go
@@ -0,0 +1,184 @@
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "strings"
+ "time"
+
+ "go.fcuny.net/x/internal/github"
+)
+
+type Config struct {
+ Token string
+ Repo string
+ StartDate time.Time
+ EndDate time.Time
+ APIURL string
+}
+
+func parseFlags() (*Config, error) {
+ config := &Config{}
+
+ flag.StringVar(&config.Token, "token", "", "GitHub personal access token (required)")
+ flag.StringVar(&config.Repo, "repo", "", "Repository in owner/repo format (required)")
+
+ startDateStr := flag.String(
+ "start",
+ time.Now().AddDate(0, -1, 0).Format("2006-01-02"),
+ "Start date (YYYY-MM-DD)",
+ )
+ endDateStr := flag.String("end", time.Now().Format("2006-01-02"), "End date (YYYY-MM-DD)")
+ flag.StringVar(
+ &config.APIURL,
+ "api-url",
+ "https://api.github.com",
+ "GitHub API URL (for GitHub Enterprise)",
+ )
+
+ flag.Parse()
+
+ if config.Token == "" {
+ return nil, fmt.Errorf("--token is required")
+ }
+
+ if config.Repo == "" {
+ return nil, fmt.Errorf("--repo is required")
+ }
+
+ startDate, err := time.Parse("2006-01-02", *startDateStr)
+ if err != nil {
+ return nil, fmt.Errorf("invalid start date format: %v", err)
+ }
+ config.StartDate = startDate
+
+ endDate, err := time.Parse("2006-01-02", *endDateStr)
+ if err != nil {
+ return nil, fmt.Errorf("invalid end date format: %v", err)
+ }
+ config.EndDate = endDate
+
+ return config, nil
+}
+
+func convertToAnalyzerTypes(
+ prs []github.PullRequest,
+ reviews map[int][]github.Review,
+ comments map[int][]github.ReviewComment,
+) []PRData {
+ var prData []PRData
+
+ for _, pr := range prs {
+ prReviews := reviews[pr.Number]
+ if prReviews == nil {
+ prReviews = []github.Review{}
+ }
+
+ prComments := comments[pr.Number]
+ if prComments == nil {
+ prComments = []github.ReviewComment{}
+ }
+
+ commentsByReview := make(map[int64][]CommentData)
+ for _, comment := range prComments {
+ commentsByReview[comment.ReviewID] = append(
+ commentsByReview[comment.ReviewID],
+ CommentData{
+ Reviewer: comment.User.Login,
+ CreatedAt: comment.CreatedAt,
+ Path: comment.Path,
+ Line: comment.Line,
+ },
+ )
+ }
+
+ var reviewData []ReviewData
+ for _, review := range prReviews {
+ reviewData = append(reviewData, ReviewData{
+ Reviewer: review.User.Login,
+ State: review.State,
+ SubmittedAt: review.SubmittedAt,
+ Comments: commentsByReview[review.ID],
+ })
+ }
+
+ prData = append(prData, PRData{
+ Number: pr.Number,
+ CreatedAt: pr.CreatedAt,
+ Reviews: reviewData,
+ })
+ }
+
+ return prData
+}
+
+func runAnalysis(ctx context.Context, config *Config) error {
+ client := github.NewClient(config.Token, config.APIURL)
+ analyzer := NewAnalyzer(config.StartDate, config.EndDate)
+ formatter := NewFormatter(config.Repo, config.StartDate, config.EndDate)
+
+ parts := strings.Split(config.Repo, "/")
+ if len(parts) != 2 {
+ return fmt.Errorf("invalid repo format: %s (expected owner/repo)", config.Repo)
+ }
+ owner, repo := parts[0], parts[1]
+
+ fmt.Print(formatter.FormatProgress("Fetching pull requests..."))
+ prs, err := client.GetPullRequests(ctx, owner, repo, config.StartDate, config.EndDate)
+ if err != nil {
+ return fmt.Errorf("fetching pull requests: %w", err)
+ }
+
+ reviews := make(map[int][]github.Review)
+ comments := make(map[int][]github.ReviewComment)
+
+ for i, pr := range prs {
+ fmt.Print(
+ formatter.FormatProgress(
+ fmt.Sprintf("Processing PR %d/%d: #%d", i+1, len(prs), pr.Number),
+ ),
+ )
+
+ prReviews, err := client.GetPullRequestReviews(ctx, owner, repo, pr.Number)
+ if err != nil {
+ return fmt.Errorf("fetching reviews for PR #%d: %w", pr.Number, err)
+ }
+ reviews[pr.Number] = prReviews
+
+ prComments, err := client.GetPullRequestReviewComments(ctx, owner, repo, pr.Number)
+ if err != nil {
+ return fmt.Errorf("fetching comments for PR #%d: %w", pr.Number, err)
+ }
+ comments[pr.Number] = prComments
+ }
+
+ prData := convertToAnalyzerTypes(prs, reviews, comments)
+
+ fmt.Print(formatter.FormatProgress("Analyzing PR data..."))
+ metrics, err := analyzer.AnalyzePRs(prData)
+ if err != nil {
+ return fmt.Errorf("analyzing PRs: %w", err)
+ }
+
+ fmt.Print(formatter.FormatTable(metrics))
+ return nil
+}
+
+func main() {
+ config, err := parseFlags()
+ if err != nil {
+ log.Printf("Error: %v\n", err)
+ flag.Usage()
+ os.Exit(1)
+ }
+
+ ctx := context.Background()
+
+ if err := runAnalysis(ctx, config); err != nil {
+ log.Printf("Error: %v\n", err)
+ os.Exit(1)
+ }
+}