diff --git a/gh-react b/gh-react old mode 100644 new mode 100755 index 492da35..a989db0 --- a/gh-react +++ b/gh-react @@ -1,261 +1,249 @@ #!/usr/bin/env bash # Color codes for better output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -PURPLE='\033[0;35m' -CYAN='\033[0;36m' -WHITE='\033[1;37m' -NC='\033[0m' # No Color - -# Emojis for better visual feedback -CHECK="✅" -CROSS="❌" -ROCKET="🚀" -EYES="👀" -COMMENT="💬" -REACTION="😊" - -# Function to print colored output -print_status() { - echo -e "${BLUE}$1${NC}" +RED=$'\e[0;31m' +GREEN=$'\e[0;32m' +YELLOW=$'\e[1;33m' +BLUE=$'\e[0;34m' +PURPLE=$'\e[0;35m' +CYAN=$'\e[0;36m' +WHITE=$'\e[1;37m' +NC=$'\e[0m' # No Color + +# Error codes +declare -A err=([BADPARAM]=1 [NOPR]=2 [NOCOMMS]=3 [UNDEFTYPE]=4 [ADDFAIL]=5) + +print_header() { + printf "${PURPLE}%s${NC}\n" "$@" } print_error() { - echo -e "${RED}$1${NC}" + printf "${RED}%s${NC}\n" "$@" } print_info() { - echo -e "${CYAN}$1${NC}" + printf "${NC}%s\n" "$@" } print_success() { - echo -e "${GREEN}$1${NC}" + printf "${GREEN}%s${NC}\n" "$@" } -# Check if PR number is provided -if [ -z "$1" ]; then - echo -e "${YELLOW}Usage:${NC} ${WHITE}gh react ${NC}" - echo "" - echo "Examples:" - echo " gh react 23 # React to comments in PR #23" - echo " gh react 156 # React to comments in PR #156" - exit 1 -fi +die() { + code="$1"; shift + printf "${RED}%s${NC}\n" "$1" + cat + exit "$code" +} -PR_NUMBER=$1 -echo -e "${PURPLE}${COMMENT} GitHub PR Reaction Tool${NC}" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -print_info "Fetching comments for PR #${WHITE}$PR_NUMBER${NC}..." +hl() { + printf "${WHITE}%s${NC}" "$*" +} +hl2() { + printf "${PURPLE}%s${NC}" "$*" +} +tip() { + printf "💡 ${YELLOW}%s${NC}" "${1:-Tip}:" +} -OWNER=$(gh repo view --json owner -q ".owner.login") -REPO=$(gh repo view --json name -q ".name") +# Pretty-printer for response data +render() { + jq \ + --arg nc "$NC" \ + --argjson color \ + "$(jq --null-input '$ARGS.named' \ + --arg PR_BODY "$PURPLE" \ + --arg ISSUE "$GREEN" \ + --arg REVIEW "$YELLOW" \ + --arg REVIEW_SUMMARY "$BLUE" \ + --arg USER "$CYAN" + )" \ + -r ' + def renderType: .type + | "\($color[.])" + { + PR_BODY: "📄 PR Description", + ISSUE: "💬 Comment", + REVIEW: "🔍 Code Review", + REVIEW_SUMMARY: "📝 Review Summary", + }[.] + $nc; + def renderUser: .author + | "\($color["USER"])\(.)\($nc)"; + def renderBody: .body | gsub("\\s"; " ") + | "\t└─\(.[:80])\(if length > 80 then "..." else "" end)"; + '"$1" +} -print_info "Repository: ${WHITE}$OWNER/$REPO${NC}" +usage() { + cat <<-EOF + $(hl2 Usage:) $(hl gh react [-R [HOST/]OWNER/REPO] PR_NUMBER) -# Check if PR exists first -print_info "Validating PR #${WHITE}$PR_NUMBER${NC}..." -PR_EXISTS=$(gh pr view $PR_NUMBER --json number 2>/dev/null) -if [ -z "$PR_EXISTS" ]; then - print_error "PR #$PR_NUMBER does not exist or you don't have access to it." - echo "" - echo "💡 ${YELLOW}Tip:${NC} Make sure you're in the correct repository and the PR number exists." - exit 1 -fi -print_status "PR #$PR_NUMBER found!" + Flags: + -R [HOST/]OWNER/REPO Select another repository using the [HOST/]OWNER/REPO format -# Use gh api to get raw comments for the issue/PR -echo "" -print_info "Gathering all comments and content..." + Examples: + gh react 23 # React to comments in PR #23 + gh react 156 # React to comments in PR #156 + EOF +} -# Get the PR description/body itself -PR_BODY=$(gh api repos/$OWNER/$REPO/pulls/$PR_NUMBER --jq 'select(.body != null and .body != "") | "PR_BODY|\(.number)|\(.user.login): \(.body)"' 2>/dev/null || echo "") +while getopts 'hR:' opt; do + case "$opt" in + h) usage; exit;; + R) + # canonicalize by removing the HOST part of [HOST/]OWNER/REPO + # Note % and # expansions are noops if the string isn't an affix of + # the parameter! + repo="${OPTARG#"${OPTARG%/*/*}"/}";; + *) usage; exit "${err[BADPARAM]}";; + esac +done +shift $((OPTIND - 1)) -# Get issue comments (general PR comments) -ISSUE_COMMENTS=$(gh api repos/$OWNER/$REPO/issues/$PR_NUMBER/comments --jq '.[] | "ISSUE|\(.id)|\(.user.login): \(.body)"' 2>/dev/null || echo "") +# Check if PR number is provided +if [ $# -ne 1 ]; then + usage +fi -# Get review comments (inline code review comments) -REVIEW_COMMENTS=$(gh api repos/$OWNER/$REPO/pulls/$PR_NUMBER/comments --jq '.[] | "REVIEW|\(.id)|\(.user.login): \(.body)"' 2>/dev/null || echo "") +PR_NUMBER=$1 -# Get review summary comments (comments submitted with reviews) -REVIEW_SUMMARY_COMMENTS=$(gh api repos/$OWNER/$REPO/pulls/$PR_NUMBER/reviews --jq '.[] | select(.body != null and .body != "") | "REVIEW_SUMMARY|\(.id)|\(.user.login): \(.body)"' 2>/dev/null || echo "") +if [ -z "$repo" ]; then + OWNER=$(gh repo view --json owner -q ".owner.login") + REPO=$(gh repo view --json name -q ".name") + repo="$OWNER/$REPO" +fi -# Combine all comments -ALL_COMMENTS="" -COMMENT_COUNT=0 +ghGet() { + gh api repos/"$repo"/"$1" --jq "$2" 2>/dev/null || echo "" +} -if [ ! -z "$PR_BODY" ]; then - ALL_COMMENTS="$ALL_COMMENTS$PR_BODY" - COMMENT_COUNT=$((COMMENT_COUNT + 1)) -fi -if [ ! -z "$ISSUE_COMMENTS" ]; then - if [ ! -z "$ALL_COMMENTS" ]; then - ALL_COMMENTS="$ALL_COMMENTS -" - fi - ALL_COMMENTS="$ALL_COMMENTS$ISSUE_COMMENTS" - COMMENT_COUNT=$((COMMENT_COUNT + $(echo "$ISSUE_COMMENTS" | wc -l))) -fi -if [ ! -z "$REVIEW_COMMENTS" ]; then - if [ ! -z "$ALL_COMMENTS" ]; then - ALL_COMMENTS="$ALL_COMMENTS -" - fi - ALL_COMMENTS="$ALL_COMMENTS$REVIEW_COMMENTS" - COMMENT_COUNT=$((COMMENT_COUNT + $(echo "$REVIEW_COMMENTS" | wc -l))) -fi -if [ ! -z "$REVIEW_SUMMARY_COMMENTS" ]; then - if [ ! -z "$ALL_COMMENTS" ]; then - ALL_COMMENTS="$ALL_COMMENTS -" - fi - ALL_COMMENTS="$ALL_COMMENTS$REVIEW_SUMMARY_COMMENTS" - COMMENT_COUNT=$((COMMENT_COUNT + $(echo "$REVIEW_SUMMARY_COMMENTS" | wc -l))) -fi +ghPost() { + gh api --method POST repos/"$repo"/"$1" \ + -f "content=$2" \ + -H "Accept: application/vnd.github+json" 2>&1 +} -if [ -z "$ALL_COMMENTS" ]; then - print_error "No comments found for PR #$PR_NUMBER" - echo "" - echo "💡 ${YELLOW}Tip:${NC} This PR doesn't have any comments yet. Try adding a comment first!" - exit 0 -fi -print_status "Found ${WHITE}$COMMENT_COUNT${NC} items to react to!" -echo "" -echo -e "${WHITE}📋 Available Content:${NC}" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +print_header '💬 GitHub PR Reaction Tool' +print_info '———————————————————————————————————————————————————' +print_info "Fetching comments for PR #$(hl "$PR_NUMBER")..." -# Create arrays to store comment data for easy selection -declare -a COMMENT_IDS -declare -a COMMENT_TYPES -declare -a COMMENT_AUTHORS -declare -a COMMENT_CONTENTS -COUNTER=1 +print_info "Repository: $(hl "$repo")" -# Format and display comments with numbers -echo "$ALL_COMMENTS" | while IFS='|' read -r type id author_and_content; do - author=$(echo "$author_and_content" | cut -d':' -f1) - content=$(echo "$author_and_content" | cut -d':' -f2- | head -c 80) - - case $type in - "PR_BODY") - echo -e "${WHITE}[$COUNTER]${NC} ${PURPLE}📄 PR Description${NC} by ${CYAN}@$author${NC}" - ;; - "ISSUE") - echo -e "${WHITE}[$COUNTER]${NC} ${GREEN}💬 Comment${NC} by ${CYAN}@$author${NC}" - ;; - "REVIEW") - echo -e "${WHITE}[$COUNTER]${NC} ${YELLOW}🔍 Code Review${NC} by ${CYAN}@$author${NC}" - ;; - "REVIEW_SUMMARY") - echo -e "${WHITE}[$COUNTER]${NC} ${BLUE}📝 Review Summary${NC} by ${CYAN}@$author${NC}" - ;; - esac - echo -e " ${WHITE}└─${NC} $(echo "$content" | tr '\n' ' ')..." - echo "" - COUNTER=$((COUNTER + 1)) -done +# Check if PR exists first +print_info "Validating PR #$(hl "$PR_NUMBER")..." +PR_EXISTS=$(gh pr view "$PR_NUMBER" --json number 2>/dev/null) +if [ -z "$PR_EXISTS" ]; then + die "${err[NOPR]}" \ + "PR #$PR_NUMBER does not exist or you don't have access to it." <<-EOF -# Store the comment data in temporary file for later retrieval -echo "$ALL_COMMENTS" > /tmp/gh_react_comments_$$ + $(tip) Make sure you're in the correct repository and the PR number exists. + EOF +fi +print_success "PR #$PR_NUMBER found!" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +# Use gh api to get raw comments for the issue/PR echo "" -echo -e "${WHITE}${REACTION} Choose what to react to:${NC}" -read -p "� Enter number (1-$COMMENT_COUNT): " SELECTION +print_info "Gathering all comments and content..." -# Validate selection is a number and in range -if ! [[ "$SELECTION" =~ ^[0-9]+$ ]] || [ "$SELECTION" -lt 1 ] || [ "$SELECTION" -gt "$COMMENT_COUNT" ]; then - print_error "Invalid selection: $SELECTION" - echo "" - echo "💡 ${YELLOW}Tip:${NC} Please enter a number between 1 and $COMMENT_COUNT" - rm -f /tmp/gh_react_comments_$$ - exit 1 +commentsFile="$(mktemp)" +trap 'rm -- "$commentsFile"' EXIT + +>"$commentsFile" jq -s . \ + <( # Get the PR description/body itself + ghGet pulls/"$PR_NUMBER" ' + select(.body != null and .body != "") + | {type: "PR_BODY", id: .number, author: .user.login, body}') \ + <( # Get issue comments (general PR comments) + ghGet issues/"$PR_NUMBER"/comments ' + .[] + | {type: "ISSUE", id, author: .user.login, body}') \ + <( # Get review comments (inline code review comments) + ghGet pulls/"$PR_NUMBER"/comments ' + .[] | {type: "REVIEW", id, author: .user.login, body}') \ + <( # Get review summary comments (comments submitted with reviews) + ghGet pulls/"$PR_NUMBER"/reviews ' + .[] | select(.body != null and .body != "") + | {type: "REVIEW_SUMMARY", id, author: .user.login, body}') + +COMMENT_COUNT="$(jq length "$commentsFile")" + +if [ "$COMMENT_COUNT" -eq 0 ]; then + die "${err[NOCOMMS]}" "No comments found for PR #$PR_NUMBER" <<-EOF + + $(tip) This PR doesn't have any comments yet. Try adding a comment first! + EOF fi -# Get the selected comment data -SELECTED_LINE=$(sed -n "${SELECTION}p" /tmp/gh_react_comments_$$) -COMMENT_TYPE=$(echo "$SELECTED_LINE" | cut -d'|' -f1) -COMMENT_ID=$(echo "$SELECTED_LINE" | cut -d'|' -f2) -SELECTED_AUTHOR=$(echo "$SELECTED_LINE" | cut -d'|' -f3 | cut -d':' -f1) +print_success "Found $(hl "$COMMENT_COUNT") items to react to!" +echo "" +print_info "$(hl 📋 Available content:)" +echo '———————————————————————————————————————————————————' -# Clean up temp file -rm -f /tmp/gh_react_comments_$$ +# Format and display comments with numbers +<"$commentsFile" render ' + to_entries[] | {ix: .key + 1} + .value + | "[\(.ix)] \(renderType) by \(renderUser)\n\(renderBody)" + ' +echo '———————————————————————————————————————————————————' echo "" -print_info "Selected: ${WHITE}$COMMENT_TYPE${NC} by ${CYAN}@$SELECTED_AUTHOR${NC}" +print_info "$(hl 😊 Choose what to react to:)" +while read -rp "Enter number (1-$COMMENT_COUNT): " SELECTION; do + # Validate selection is a number and in range + if [[ "$SELECTION" =~ ^[0-9]+$ ]] \ + && [ "$SELECTION" -ge 1 ] \ + && [ "$SELECTION" -le "$COMMENT_COUNT" ]; then + break + fi + print_error "Invalid selection: $SELECTION" +done + +COMMENT_TYPE=$(jq -r ".[$SELECTION-1].type" "$commentsFile") +COMMENT_ID=$(jq ".[$SELECTION-1].id" "$commentsFile") +SELECTED_AUTHOR=$(jq -r ".[$SELECTION-1].author" "$commentsFile") echo "" -echo -e "${WHITE}😊 Available reactions:${NC}" -echo " 👍 +1 👎 -1 😄 laugh" -echo " ❤️ heart 🎉 hooray 🚀 rocket 👀 eyes" -echo "" -read -p "🎯 Pick a reaction: " REACTION - -# Validate reaction -case $REACTION in - "+1"|"-1"|"laugh"|"heart"|"hooray"|"rocket"|"eyes") - ;; - *) - print_error "Invalid reaction: $REACTION" - echo "" - echo "💡 ${YELLOW}Valid options:${NC} +1, -1, laugh, heart, hooray, rocket, eyes" - exit 1 - ;; -esac +<"$commentsFile" render ".[$SELECTION-1]"' + | "Selected \(renderType) by \(renderUser)"' + +declare -A reactions=( + [👍]=+1 [👎]=-1 [😄]=laugh [❤️]=heart [🎉]=hooray [🚀]=rocket [👀]=eyes +) +PS3='🎯 Pick a reaction: ' +select REACTION in "${!reactions[@]}"; do + if [ -n "$REACTION" ]; then + break + fi + print_info 'Invalid reaction' +done +unset PS3 echo "" -print_info "Sending ${WHITE}$REACTION${NC} reaction to comment ${WHITE}$COMMENT_ID${NC}..." -if [ "$COMMENT_TYPE" = "PR_BODY" ]; then - # React to PR description/body - RESPONSE=$(gh api --method POST repos/$OWNER/$REPO/issues/$PR_NUMBER/reactions \ - -f "content=$REACTION" \ - -H "Accept: application/vnd.github+json" 2>&1) -elif [ "$COMMENT_TYPE" = "ISSUE" ]; then - # React to issue comment - RESPONSE=$(gh api --method POST repos/$OWNER/$REPO/issues/comments/$COMMENT_ID/reactions \ - -f "content=$REACTION" \ - -H "Accept: application/vnd.github+json" 2>&1) -elif [ "$COMMENT_TYPE" = "REVIEW" ]; then - # React to review comment (inline code comment) - RESPONSE=$(gh api --method POST repos/$OWNER/$REPO/pulls/comments/$COMMENT_ID/reactions \ - -f "content=$REACTION" \ - -H "Accept: application/vnd.github+json" 2>&1) -elif [ "$COMMENT_TYPE" = "REVIEW_SUMMARY" ]; then - # React to review summary comment - RESPONSE=$(gh api --method POST repos/$OWNER/$REPO/pulls/reviews/$COMMENT_ID/reactions \ - -f "content=$REACTION" \ - -H "Accept: application/vnd.github+json" 2>&1) -else - print_error "Could not determine comment type for ID $COMMENT_ID" - exit 1 +print_info "Sending $(hl "$REACTION") reaction to comment $(hl "$COMMENT_ID")..." +declare -A endpoints=( + [PR_BODY]=issues + [ISSUE]=issues/comments + [REVIEW]=pulls/comments + [REVIEW_SUMMARY]=pulls/reviews +) +if [ -z "${endpoints[$COMMENT_TYPE]+a}" ]; then + die "${err[UNDEFTYPE]}" \ + "Could not determine comment type for ID $COMMENT_ID" <<-EOF + + This shouldn't be possible -- *we* choose the comment types. + Please open a ticket for this. + EOF fi -# Check if the reaction was successful -if [ $? -eq 0 ]; then - echo "" - print_success "Reaction added successfully! 🎉" - echo "" - echo -e "${WHITE}🔗 View PR:${NC} https://github.com/$OWNER/$REPO/pull/$PR_NUMBER" - - # Show reaction emoji based on type - case $REACTION in - "+1") echo -e "${GREEN}👍 Added thumbs up!${NC}" ;; - "-1") echo -e "${RED}👎 Added thumbs down!${NC}" ;; - "laugh") echo -e "${YELLOW}😄 Added laugh!${NC}" ;; - "heart") echo -e "${RED}❤️ Added heart!${NC}" ;; - "hooray") echo -e "${PURPLE}🎉 Added hooray!${NC}" ;; - "rocket") echo -e "${BLUE}🚀 Added rocket!${NC}" ;; - "eyes") echo -e "${CYAN}👀 Added eyes!${NC}" ;; - esac -else - print_error "Failed to add reaction" - echo "" - echo "Error details:" - echo "$RESPONSE" - echo "" - echo "💡 ${YELLOW}Tip:${NC} Make sure you have permission to react to this content." - exit 1 +if ! RESPONSE=$(ghPost "${endpoints[$COMMENT_TYPE]}"/"$COMMENT_ID"/reactions \ + "${reactions[$REACTION]}"); then + die "${err[ADDFAIL]}" 'Failed to add reaction' <<-EOF + + Error details: $RESPONSE + + $(tip) Make sure you have permission to react to this content. + EOF fi + +print_success "Reaction added successfully! 🎉" +echo -e "$(hl 🔗 View PR:) https://github.com/$repo/pull/$PR_NUMBER"