#!/usr/bin/env bash # x-pr-comments - Fetch GitHub Pull Request comments and review suggestions # Copyright (c) 2025 - Licensed under MIT # # Usage: # x-pr-comments # When run inside a git repository # x-pr-comments # Direct GitHub PR URL # x-pr-comments -h|--help # Show this help # # Examples: # x-pr-comments 1 # PR #1 in current repo # x-pr-comments https://github.com/user/repo/pull/1 # # Requirements: # - gh CLI tool installed and authenticated # - Internet connection for GitHub API access # # Output: # Structured list of PR comments and file change requests with LLM processing directions set -euo pipefail # Colors for output readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly BLUE='\033[0;34m' readonly NC='\033[0m' # No Color # Show usage information show_usage() { sed -n '3,20p' "$0" | sed 's/^# //' | sed 's/^#//' } # Log functions log_error() { echo -e "${RED}ERROR:${NC} $1" >&2 } log_warn() { echo -e "${YELLOW}WARN:${NC} $1" >&2 } log_info() { if [[ "${INFO:-0}" == "1" ]]; then echo -e "${GREEN}INFO:${NC} $1" >&2 fi } log_debug() { if [[ "${DEBUG:-0}" == "1" ]]; then echo -e "${BLUE}DEBUG:${NC} $1" >&2 fi } # Filter out CodeRabbit comments containing "Addressed in commit" filter_coderabbit_addressed_comments() { local input_data="$1" local is_wrapped="$2" # true for {comments: [...]}, false for [...] local jq_filter='select( (.user.login | contains("coderabbit") | not) or (.body | contains("Addressed in commit") | not) )' if [[ "$is_wrapped" == "true" ]]; then echo "$input_data" | jq "{comments: [.comments[] | $jq_filter]}" 2> /dev/null || echo "$input_data" else echo "$input_data" | jq "[.[] | $jq_filter]" 2> /dev/null || echo "$input_data" fi } # Fetch and filter API data with consistent logging fetch_and_filter_data() { local endpoint="$1" local data_name="$2" local is_wrapped="$3" # true/false local data data=$(gh api "$endpoint" 2> /dev/null || echo "[]") if [[ "$is_wrapped" == "true" ]]; then data=$(echo "$data" | jq '{comments: .}' 2> /dev/null || echo '{"comments":[]}') fi data=$(filter_coderabbit_addressed_comments "$data" "$is_wrapped") local count_field="length" [[ "$is_wrapped" == "true" ]] && count_field=".comments | length" local count count=$(echo "$data" | jq -r "$count_field" 2> /dev/null || echo "0") log_debug "$data_name count: $count" echo "$data" } # Format file-specific comments grouped by review format_grouped_review_comments() { local review_comments="$1" local reviews="$2" local repo="$3" local count count=$(echo "$review_comments" | jq -r 'length' 2> /dev/null || echo "0") if [[ "$count" -eq 0 ]]; then echo "No file-specific comments found." return fi # Group comments by review ID and format them echo "$review_comments" | jq -r --argjson reviews "$reviews" ' group_by(.pull_request_review_id) | .[] | . as $comments | ($reviews[] | select(.id == $comments[0].pull_request_review_id)) as $review | "### Review by \($review.user.login) (\($review.submitted_at)) [\($review.state)] Review ID: \($review.id) - API: gh api /repos/'"$repo"'/pulls/1/reviews/\($review.id) " + ([.[] | " #### Comment ID: \(.id) - **File:** \(.path) - **Commit:** \(.commit_id) - **Author:** \(.user.login) (\(.user.type)) - **Lines:** \(if .start_line then "\(.start_line)-\(.line // "N/A")" else "\(.line // "N/A")" end) (original: \(.original_line // "N/A")) - **Position:** \(.position // "N/A") (original: \(.original_position // "N/A")) - **Subject Type:** \(.subject_type // "N/A") - **API:** gh api /repos/'"$repo"'/pulls/comments/\(.id) **Body:** \(.body) "] | join("")) + " --- " ' 2> /dev/null || { log_debug "Error grouping review comments by review ID" echo "Error parsing grouped review comments." } } # Check if gh CLI is available check_dependencies() { if ! command -v gh &> /dev/null; then log_error "GitHub CLI (gh) is not installed. Please install it first:" log_error " https://cli.github.com/" exit 1 fi if ! gh auth status &> /dev/null; then log_error "GitHub CLI is not authenticated. Run: gh auth login" exit 1 fi } # Get repository info from git remote get_repo_info() { if ! git rev-parse --is-inside-work-tree &> /dev/null; then log_error "Not inside a git repository" return 1 fi local remote_url remote_url=$(git remote get-url origin 2> /dev/null || echo "") if [[ -z "$remote_url" ]]; then log_error "No origin remote found" return 1 fi # Parse GitHub URL (both HTTPS and SSH formats) if [[ "$remote_url" =~ github\.com[:/]([^/]+)/([^/]+)(\.git)?$ ]]; then local repo_name="${BASH_REMATCH[2]}" # Remove .git suffix if present repo_name="${repo_name%.git}" echo "${BASH_REMATCH[1]}/${repo_name}" else log_error "Remote URL is not a GitHub repository: $remote_url" return 1 fi } # Parse GitHub PR URL to extract owner/repo/pr-number parse_github_url() { local url="$1" if [[ "$url" =~ github\.com/([^/]+)/([^/]+)/pull/([0-9]+) ]]; then echo "${BASH_REMATCH[1]}/${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" else log_error "Invalid GitHub PR URL format: $url" return 1 fi } # Format output with LLM directions format_output() { local repo="$1" local pr_number="$2" local pr_info="$3" local review_comments="$4" local reviews="$5" # Header and instructions cat << EOF # GitHub PR Comments Analysis Report ## LLM Processing Instructions You are analyzing review comments and change requests from GitHub Pull Request #$pr_number in repository $repo. **Your tasks:** 1. **Review Analysis**: For each review, understand the reviewer's overall feedback and concerns 2. **Change Request Validation**: Check if each file-specific comment is still relevant to the current codebase 3. **Priority Assessment**: Rank change requests by importance and impact on code quality 4. **Implementation Planning**: Create actionable tasks for addressing valid change requests 5. **Pattern Recognition**: Identify recurring issues across different reviews **Tools to use:** - \`find\`, \`cat\`, \`rg\` commands and available tools to examine current codebase - File system tools to verify mentioned files exist and check current state - \`gh pr diff $pr_number\` to see what changes are being reviewed ## Pull Request Information EOF # PR information echo "$pr_info" | jq -r '"**Title:** \(.title) **State:** \(.state) **URL:** \(.url) **Number:** '"$pr_number"' **Repository:** '"$repo"' "' 2> /dev/null || { echo "**Error:** Could not parse PR information" return 1 } # Review Comments Section echo -e "\n## Review Comments and Change Requests\n" format_grouped_review_comments "$review_comments" "$reviews" "$repo" # Footer cat << EOF ## Next Steps for LLM Analysis 1. **Analyze and verify all comments, including nitpick** - Consider all comments from code reviewers as change requests - Analyze all "Outside diff range comments" as change requests - All Copilot and CodeRabbit comments and change requests MUST BE analyzed - Do not trust blindly the comments, validate them against current codebase 2. **Validate change requests:** - Check if mentioned files exist and match current state - Verify if suggestions are still applicable - Identify any already-addressed issues 3. **Generate actionable implementation plan:** - Prioritized list of valid change requests - Grouped by file/area for efficient implementation - Clear next steps for addressing reviewer feedback - All valid change requests MUST BE handled, even if low priority 4. **Use todo lists and memory tools to track progress* - All valid change requests MUST BE handled - Keep handling them until each of them are handled 5. **Identify patterns and recurring issues:** - Highlight common themes across reviews - Suggest broader improvements to code quality and practices - Suggest LLM instructions to avoid similar issues in future EOF } # Fetch and display PR data directly fetch_and_display_pr_data() { local repo="$1" local pr_number="$2" log_info "Fetching PR #$pr_number from $repo..." # Get PR basic info local pr_info pr_info=$(gh pr view "$pr_number" --repo "$repo" --json title,state,url 2> /dev/null) || { log_error "Failed to fetch PR #$pr_number from $repo" return 1 } # Fetch review data using helper function local review_comments reviews review_comments=$(fetch_and_filter_data "/repos/$repo/pulls/$pr_number/comments" "Review comments" "false") reviews=$(fetch_and_filter_data "/repos/$repo/pulls/$pr_number/reviews" "Reviews" "false") # Display formatted output format_output "$repo" "$pr_number" "$pr_info" "$review_comments" "$reviews" } # Main function main() { local repo="" local pr_number="" # Parse arguments case "${1:-}" in -h | --help) show_usage exit 0 ;; "") log_error "Missing argument. Provide PR number or GitHub URL." show_usage exit 1 ;; https://github.com/*) local parsed parsed=$(parse_github_url "$1") || exit 1 read -r repo pr_number <<< "$parsed" ;; [0-9]*) repo=$(get_repo_info) || exit 1 pr_number="$1" ;; *) log_error "Invalid argument: $1" show_usage exit 1 ;; esac check_dependencies log_debug "Repository: $repo" log_debug "PR Number: $pr_number" # Fetch and display data fetch_and_display_pr_data "$repo" "$pr_number" } # Run main function with all arguments main "$@"