package repo import ( "context" "errors" "fmt" "strings" "time" ) const ( ReviewApproved = "approved" ReviewChangesRequested = "changes_requested" ) var ErrInvalidReviewState = errors.New("invalid review state") type Review struct { ID int64 PullID int64 ReviewerID int64 ReviewerUsername string ReviewerAvatar string State string Body string CreatedAt time.Time UpdatedAt time.Time } // UpsertReview records or updates the reviewer's verdict on a pull request. func (s *Service) UpsertReview(ctx context.Context, pullID, reviewerID int64, state, body string) error { state = strings.TrimSpace(state) if state != ReviewApproved && state != ReviewChangesRequested { return ErrInvalidReviewState } body = strings.TrimSpace(body) if len(body) > 10000 { body = body[:10000] } cctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() now := time.Now().UTC() _, err := s.db.ExecContext(cctx, ` INSERT INTO pull_request_reviews (pull_id, reviewer_id, state, body, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(pull_id, reviewer_id) DO UPDATE SET state = excluded.state, body = excluded.body, updated_at = excluded.updated_at `, pullID, reviewerID, state, body, now, now) if err != nil { return fmt.Errorf("upsert review: %w", err) } return nil } // ListReviews returns all reviews left on a pull request, oldest first. func (s *Service) ListReviews(ctx context.Context, pullID int64) ([]Review, error) { cctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() rows, err := s.db.QueryContext(cctx, ` SELECT rv.id, rv.pull_id, rv.reviewer_id, u.username, u.avatar_url, rv.state, rv.body, rv.created_at, rv.updated_at FROM pull_request_reviews rv JOIN users u ON u.id = rv.reviewer_id WHERE rv.pull_id = ? ORDER BY rv.updated_at ASC `, pullID) if err != nil { return nil, fmt.Errorf("list reviews: %w", err) } defer rows.Close() var list []Review for rows.Next() { var rv Review if err := rows.Scan(&rv.ID, &rv.PullID, &rv.ReviewerID, &rv.ReviewerUsername, &rv.ReviewerAvatar, &rv.State, &rv.Body, &rv.CreatedAt, &rv.UpdatedAt); err != nil { return nil, fmt.Errorf("scan review: %w", err) } list = append(list, rv) } return list, rows.Err() } // GetReview returns the review left by a specific user, if any. func (s *Service) GetReview(ctx context.Context, pullID, reviewerID int64) (*Review, error) { cctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() var rv Review err := s.db.QueryRowContext(cctx, ` SELECT rv.id, rv.pull_id, rv.reviewer_id, u.username, u.avatar_url, rv.state, rv.body, rv.created_at, rv.updated_at FROM pull_request_reviews rv JOIN users u ON u.id = rv.reviewer_id WHERE rv.pull_id = ? AND rv.reviewer_id = ? `, pullID, reviewerID).Scan(&rv.ID, &rv.PullID, &rv.ReviewerID, &rv.ReviewerUsername, &rv.ReviewerAvatar, &rv.State, &rv.Body, &rv.CreatedAt, &rv.UpdatedAt) if err != nil { return nil, err } return &rv, nil } // ReviewCounts returns how many reviewers approved and how many requested changes. func (s *Service) ReviewCounts(ctx context.Context, pullID int64) (approved int, changesRequested int) { cctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() _ = s.db.QueryRowContext(cctx, `SELECT COUNT(*) FROM pull_request_reviews WHERE pull_id = ? AND state = ?`, pullID, ReviewApproved).Scan(&approved) _ = s.db.QueryRowContext(cctx, `SELECT COUNT(*) FROM pull_request_reviews WHERE pull_id = ? AND state = ?`, pullID, ReviewChangesRequested).Scan(&changesRequested) return approved, changesRequested }