aboutsummaryrefslogblamecommitdiff
path: root/cmd/pr-analyzer/main.go
blob: b12be31bd34bf550034011d11918345bf60ac32a (plain) (tree)























































































































































































                                                                                                   
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)
	}
}