From 922dd8c70b70a488930ebcfebfa67aabd7df06a9 Mon Sep 17 00:00:00 2001 From: gesh Date: Wed, 22 Oct 2025 15:45:45 +0300 Subject: [PATCH 01/12] Make gh-react executable Otherwise gh complains of a permissions error --- gh-react | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 gh-react diff --git a/gh-react b/gh-react old mode 100644 new mode 100755 From 9df08296f94b26eed32f8f4c5f65a98e588487b0 Mon Sep 17 00:00:00 2001 From: gesh Date: Wed, 22 Oct 2025 16:48:15 +0300 Subject: [PATCH 02/12] Add -R option as in other gh commands Enables reacting from outside the repository --- gh-react | 62 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/gh-react b/gh-react index 492da35..f018e29 100755 --- a/gh-react +++ b/gh-react @@ -35,25 +35,49 @@ print_success() { echo -e "${GREEN}$1${NC}" } -# Check if PR number is provided -if [ -z "$1" ]; then - echo -e "${YELLOW}Usage:${NC} ${WHITE}gh react ${NC}" +usage() { + echo -e "${YELLOW}Usage:${NC} ${WHITE}gh react [-R [HOST/]OWNER/REPO] ${NC}" + echo "" + echo "Flags:" + echo "-R [HOST/]OWNER/REPO Select another repository using the [HOST/]OWNER/REPO format" 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 +} + +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 1;; + esac +done +shift $((OPTIND - 1)) + +# Check if PR number is provided +if [ $# -ne 1 ]; then + usage fi PR_NUMBER=$1 + +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 + + echo -e "${PURPLE}${COMMENT} GitHub PR Reaction Tool${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" print_info "Fetching comments for PR #${WHITE}$PR_NUMBER${NC}..." -OWNER=$(gh repo view --json owner -q ".owner.login") -REPO=$(gh repo view --json name -q ".name") - -print_info "Repository: ${WHITE}$OWNER/$REPO${NC}" +print_info "Repository: ${WHITE}$repo${NC}" # Check if PR exists first print_info "Validating PR #${WHITE}$PR_NUMBER${NC}..." @@ -71,16 +95,16 @@ echo "" print_info "Gathering all comments and content..." # 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 "") +PR_BODY=$(gh api repos/$repo/pulls/$PR_NUMBER --jq 'select(.body != null and .body != "") | "PR_BODY|\(.number)|\(.user.login): \(.body)"' 2>/dev/null || echo "") # 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 "") +ISSUE_COMMENTS=$(gh api repos/$repo/issues/$PR_NUMBER/comments --jq '.[] | "ISSUE|\(.id)|\(.user.login): \(.body)"' 2>/dev/null || echo "") # 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 "") +REVIEW_COMMENTS=$(gh api repos/$repo/pulls/$PR_NUMBER/comments --jq '.[] | "REVIEW|\(.id)|\(.user.login): \(.body)"' 2>/dev/null || echo "") # 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 "") +REVIEW_SUMMARY_COMMENTS=$(gh api repos/$repo/pulls/$PR_NUMBER/reviews --jq '.[] | select(.body != null and .body != "") | "REVIEW_SUMMARY|\(.id)|\(.user.login): \(.body)"' 2>/dev/null || echo "") # Combine all comments ALL_COMMENTS="" @@ -138,7 +162,7 @@ COUNTER=1 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}" @@ -210,22 +234,22 @@ 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 \ + RESPONSE=$(gh api --method POST repos/$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 \ + RESPONSE=$(gh api --method POST repos/$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 \ + RESPONSE=$(gh api --method POST repos/$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 \ + RESPONSE=$(gh api --method POST repos/$repo/pulls/reviews/$COMMENT_ID/reactions \ -f "content=$REACTION" \ -H "Accept: application/vnd.github+json" 2>&1) else @@ -238,8 +262,8 @@ 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" - + echo -e "${WHITE}🔗 View PR:${NC} https://github.com/$repo/pull/$PR_NUMBER" + # Show reaction emoji based on type case $REACTION in "+1") echo -e "${GREEN}👍 Added thumbs up!${NC}" ;; From 1acc433e6eb0e9d612a6421bd493207ab955df73 Mon Sep 17 00:00:00 2001 From: gesh Date: Wed, 22 Oct 2025 21:21:03 +0300 Subject: [PATCH 03/12] Basic shellcheck improvements Dead code elimination, quoting --- gh-react | 41 ++++++++++++++--------------------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/gh-react b/gh-react index f018e29..8088bda 100755 --- a/gh-react +++ b/gh-react @@ -10,14 +10,6 @@ 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}" @@ -73,7 +65,7 @@ if [ -z "$repo" ]; then fi -echo -e "${PURPLE}${COMMENT} GitHub PR Reaction Tool${NC}" +echo -e "${PURPLE}💬 GitHub PR Reaction Tool${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" print_info "Fetching comments for PR #${WHITE}$PR_NUMBER${NC}..." @@ -81,7 +73,7 @@ print_info "Repository: ${WHITE}$repo${NC}" # 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) +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 "" @@ -95,16 +87,16 @@ echo "" print_info "Gathering all comments and content..." # Get the PR description/body itself -PR_BODY=$(gh api repos/$repo/pulls/$PR_NUMBER --jq 'select(.body != null and .body != "") | "PR_BODY|\(.number)|\(.user.login): \(.body)"' 2>/dev/null || echo "") +PR_BODY=$(gh api repos/"$repo"/pulls/"$PR_NUMBER" --jq 'select(.body != null and .body != "") | "PR_BODY|\(.number)|\(.user.login): \(.body)"' 2>/dev/null || echo "") # Get issue comments (general PR comments) -ISSUE_COMMENTS=$(gh api repos/$repo/issues/$PR_NUMBER/comments --jq '.[] | "ISSUE|\(.id)|\(.user.login): \(.body)"' 2>/dev/null || echo "") +ISSUE_COMMENTS=$(gh api repos/"$repo"/issues/"$PR_NUMBER"/comments --jq '.[] | "ISSUE|\(.id)|\(.user.login): \(.body)"' 2>/dev/null || echo "") # Get review comments (inline code review comments) -REVIEW_COMMENTS=$(gh api repos/$repo/pulls/$PR_NUMBER/comments --jq '.[] | "REVIEW|\(.id)|\(.user.login): \(.body)"' 2>/dev/null || echo "") +REVIEW_COMMENTS=$(gh api repos/"$repo"/pulls/"$PR_NUMBER"/comments --jq '.[] | "REVIEW|\(.id)|\(.user.login): \(.body)"' 2>/dev/null || echo "") # Get review summary comments (comments submitted with reviews) -REVIEW_SUMMARY_COMMENTS=$(gh api repos/$repo/pulls/$PR_NUMBER/reviews --jq '.[] | select(.body != null and .body != "") | "REVIEW_SUMMARY|\(.id)|\(.user.login): \(.body)"' 2>/dev/null || echo "") +REVIEW_SUMMARY_COMMENTS=$(gh api repos/"$repo"/pulls/"$PR_NUMBER"/reviews --jq '.[] | select(.body != null and .body != "") | "REVIEW_SUMMARY|\(.id)|\(.user.login): \(.body)"' 2>/dev/null || echo "") # Combine all comments ALL_COMMENTS="" @@ -151,15 +143,10 @@ echo "" echo -e "${WHITE}📋 Available Content:${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -# 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 # Format and display comments with numbers -echo "$ALL_COMMENTS" | while IFS='|' read -r type id author_and_content; do +echo "$ALL_COMMENTS" | while IFS='|' read -r type _ author_and_content; do author=$(echo "$author_and_content" | cut -d':' -f1) content=$(echo "$author_and_content" | cut -d':' -f2- | head -c 80) @@ -187,8 +174,8 @@ echo "$ALL_COMMENTS" > /tmp/gh_react_comments_$$ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" -echo -e "${WHITE}${REACTION} Choose what to react to:${NC}" -read -p "� Enter number (1-$COMMENT_COUNT): " SELECTION +echo -e "${WHITE}😊 Choose what to react to:${NC}" +read -rp "� Enter number (1-$COMMENT_COUNT): " SELECTION # Validate selection is a number and in range if ! [[ "$SELECTION" =~ ^[0-9]+$ ]] || [ "$SELECTION" -lt 1 ] || [ "$SELECTION" -gt "$COMMENT_COUNT" ]; then @@ -216,7 +203,7 @@ echo -e "${WHITE}😊 Available reactions:${NC}" echo " 👍 +1 👎 -1 😄 laugh" echo " ❤️ heart 🎉 hooray 🚀 rocket 👀 eyes" echo "" -read -p "🎯 Pick a reaction: " REACTION +read -rp "🎯 Pick a reaction: " REACTION # Validate reaction case $REACTION in @@ -234,22 +221,22 @@ 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/$repo/issues/$PR_NUMBER/reactions \ + RESPONSE=$(gh api --method POST repos/"$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/$repo/issues/comments/$COMMENT_ID/reactions \ + RESPONSE=$(gh api --method POST repos/"$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/$repo/pulls/comments/$COMMENT_ID/reactions \ + RESPONSE=$(gh api --method POST repos/"$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/$repo/pulls/reviews/$COMMENT_ID/reactions \ + RESPONSE=$(gh api --method POST repos/"$repo"/pulls/reviews/"$COMMENT_ID"/reactions \ -f "content=$REACTION" \ -H "Accept: application/vnd.github+json" 2>&1) else From 12870d6b2ff7adc9027dcc4c235859bb1f7534f6 Mon Sep 17 00:00:00 2001 From: gesh Date: Wed, 22 Oct 2025 21:27:56 +0300 Subject: [PATCH 04/12] Finish shellcheck corrections - Pull out common request-crafting code - Handle unhappy paths so the happy path is straightline code - Test exit codes directly to avoid them being masked --- gh-react | 87 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/gh-react b/gh-react index 8088bda..d59755f 100755 --- a/gh-react +++ b/gh-react @@ -64,6 +64,15 @@ if [ -z "$repo" ]; then repo="$OWNER/$REPO" fi +ghGet() { + gh api repos/"$repo"/"$1" --jq "$2" 2>/dev/null || echo "" +} + +ghPost() { + gh api --method POST repos/"$repo"/"$1" \ + -f "content=$2" \ + -H "Accept: application/vnd.github+json" 2>&1 +} echo -e "${PURPLE}💬 GitHub PR Reaction Tool${NC}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @@ -87,16 +96,22 @@ echo "" print_info "Gathering all comments and content..." # Get the PR description/body itself -PR_BODY=$(gh api repos/"$repo"/pulls/"$PR_NUMBER" --jq 'select(.body != null and .body != "") | "PR_BODY|\(.number)|\(.user.login): \(.body)"' 2>/dev/null || echo "") +PR_BODY=$(ghGet pulls/"$PR_NUMBER" ' + select(.body != null and .body != "") + | "PR_BODY|\(.number)|\(.user.login): \(.body)"') # Get issue comments (general PR comments) -ISSUE_COMMENTS=$(gh api repos/"$repo"/issues/"$PR_NUMBER"/comments --jq '.[] | "ISSUE|\(.id)|\(.user.login): \(.body)"' 2>/dev/null || echo "") +ISSUE_COMMENTS=$(ghGet issues/"$PR_NUMBER"/comments ' + .[] | "ISSUE|\(.id)|\(.user.login): \(.body)"') # Get review comments (inline code review comments) -REVIEW_COMMENTS=$(gh api repos/"$repo"/pulls/"$PR_NUMBER"/comments --jq '.[] | "REVIEW|\(.id)|\(.user.login): \(.body)"' 2>/dev/null || echo "") +REVIEW_COMMENTS=$(ghGet pulls/"$PR_NUMBER"/comments ' + .[] | "REVIEW|\(.id)|\(.user.login): \(.body)") # Get review summary comments (comments submitted with reviews) -REVIEW_SUMMARY_COMMENTS=$(gh api repos/"$repo"/pulls/"$PR_NUMBER"/reviews --jq '.[] | select(.body != null and .body != "") | "REVIEW_SUMMARY|\(.id)|\(.user.login): \(.body)"' 2>/dev/null || echo "") +REVIEW_SUMMARY_COMMENTS=$(ghGet pulls/"$PR_NUMBER"/reviews ' + .[] | select(.body != null and .body != "") + | "REVIEW_SUMMARY|\(.id)|\(.user.login): \(.body)"') # Combine all comments ALL_COMMENTS="" @@ -219,49 +234,19 @@ esac 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/"$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/"$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/"$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/"$repo"/pulls/reviews/"$COMMENT_ID"/reactions \ - -f "content=$REACTION" \ - -H "Accept: application/vnd.github+json" 2>&1) -else +declare -A endpoints=( + [PR_BODY]=issues + [ISSUE]=issues/comments + [REVIEW]=pulls/comments + [REVIEW_SUMMARY]=pulls/reviews +) +if [ -z "${endpoints[$COMMENT_TYPE]+a}" ]; then print_error "Could not determine comment type for ID $COMMENT_ID" exit 1 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/$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 +if ! RESPONSE=$(ghPost "${endpoints[$COMMENT_TYPE]}"/"$COMMENT_ID"/reactions \ + "$REACTION"); then print_error "Failed to add reaction" echo "" echo "Error details:" @@ -270,3 +255,19 @@ else echo "💡 ${YELLOW}Tip:${NC} Make sure you have permission to react to this content." exit 1 fi + +echo "" +print_success "Reaction added successfully! 🎉" +echo "" +echo -e "${WHITE}🔗 View PR:${NC} https://github.com/$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 From 57119ad65e1c7da9d11b181a41a3459c6afb6814 Mon Sep 17 00:00:00 2001 From: gesh Date: Wed, 22 Oct 2025 22:47:27 +0300 Subject: [PATCH 05/12] Use traps to clean up temp files, use mktemp This is less error-prone than trying to remember to do so on all exit paths. Use mktemp to avoid collisions and enable the sysadmins to configure where to put temp files. --- gh-react | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/gh-react b/gh-react index d59755f..bddf197 100755 --- a/gh-react +++ b/gh-react @@ -185,7 +185,9 @@ echo "$ALL_COMMENTS" | while IFS='|' read -r type _ author_and_content; do done # Store the comment data in temporary file for later retrieval -echo "$ALL_COMMENTS" > /tmp/gh_react_comments_$$ +commentsFile="$(mktemp)" +trap 'rm -- "$commentsFile"' EXIT +echo "$ALL_COMMENTS" > "$commentsFile" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" @@ -197,19 +199,15 @@ if ! [[ "$SELECTION" =~ ^[0-9]+$ ]] || [ "$SELECTION" -lt 1 ] || [ "$SELECTION" 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 fi # Get the selected comment data -SELECTED_LINE=$(sed -n "${SELECTION}p" /tmp/gh_react_comments_$$) +SELECTED_LINE=$(sed -n "${SELECTION}p" "$commentsFile") 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) -# Clean up temp file -rm -f /tmp/gh_react_comments_$$ - echo "" print_info "Selected: ${WHITE}$COMMENT_TYPE${NC} by ${CYAN}@$SELECTED_AUTHOR${NC}" From b72d752c8b703b6fcc0e595942490fc4d878c5ab Mon Sep 17 00:00:00 2001 From: gesh Date: Wed, 22 Oct 2025 21:31:12 +0300 Subject: [PATCH 06/12] Use symbolic escapes instead of codepoints This is clearer to the reader --- gh-react | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gh-react b/gh-react index bddf197..d348a97 100755 --- a/gh-react +++ b/gh-react @@ -1,14 +1,14 @@ #!/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 +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 # Function to print colored output print_status() { From d2537c3f763c034079151f1459d8b9ace8c73b33 Mon Sep 17 00:00:00 2001 From: gesh Date: Wed, 22 Oct 2025 21:55:00 +0300 Subject: [PATCH 07/12] Use semantic markup, avoid echo echo's escape expansion is non-portable, using printf directly is preferable here. Or heredocs, as can be seen in some of the lengthier cases. See too: https://www.shellcheck.net/wiki/SC2028 --- gh-react | 108 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 63 insertions(+), 45 deletions(-) diff --git a/gh-react b/gh-react index d348a97..06f9115 100755 --- a/gh-react +++ b/gh-react @@ -12,30 +12,49 @@ NC=$'\e[0m' # No Color # Function to print colored output print_status() { - echo -e "${BLUE}$1${NC}" + printf "${BLUE}%s${NC}\n" "$@" +} + +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 "${CYAN}%s${NC}\n" "$@" } print_success() { - echo -e "${GREEN}$1${NC}" + printf "${GREEN}%s${NC}\n" "$@" +} + +hl() { + printf "${WHITE}%s${NC}" "$*" +} +hl2() { + printf "${PURPLE}%s${NC}" "$*" +} +tip() { + printf "💡 ${YELLOW}%s${NC}" "${1:-Tip}:" +} +user() { + printf "${CYAN}@%s${NC}" "$1" } usage() { - echo -e "${YELLOW}Usage:${NC} ${WHITE}gh react [-R [HOST/]OWNER/REPO] ${NC}" - echo "" - echo "Flags:" - echo "-R [HOST/]OWNER/REPO Select another repository using the [HOST/]OWNER/REPO format" - echo "" - echo "Examples:" - echo " gh react 23 # React to comments in PR #23" - echo " gh react 156 # React to comments in PR #156" + cat <<-EOF + $(hl2 Usage:) $(hl gh react [-R [HOST/]OWNER/REPO] PR_NUMBER) + + Flags: + -R [HOST/]OWNER/REPO Select another repository using the [HOST/]OWNER/REPO format + + Examples: + gh react 23 # React to comments in PR #23 + gh react 156 # React to comments in PR #156 + EOF } while getopts 'hR:' opt; do @@ -74,19 +93,22 @@ ghPost() { -H "Accept: application/vnd.github+json" 2>&1 } -echo -e "${PURPLE}💬 GitHub PR Reaction Tool${NC}" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -print_info "Fetching comments for PR #${WHITE}$PR_NUMBER${NC}..." -print_info "Repository: ${WHITE}$repo${NC}" +print_header '💬 GitHub PR Reaction Tool' +echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━' +print_info "Fetching comments for PR #$(hl "$PR_NUMBER")..." + +print_info "Repository: $(hl "$repo")" # Check if PR exists first -print_info "Validating PR #${WHITE}$PR_NUMBER${NC}..." +print_info "Validating PR #$(hl "$PR_NUMBER")..." 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." + cat <<-EOF + + $(tip) Make sure you're in the correct repository and the PR number exists. + EOF exit 1 fi print_status "PR #$PR_NUMBER found!" @@ -148,15 +170,17 @@ fi 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!" + cat <<-EOF + + $(tip) This PR doesn't have any comments yet. Try adding a comment first! + EOF exit 0 fi -print_status "Found ${WHITE}$COMMENT_COUNT${NC} items to react to!" +print_status "Found $(hl "$COMMENT_COUNT") items to react to!" echo "" -echo -e "${WHITE}📋 Available Content:${NC}" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +print_info "$(hl 📋 Available content:)" +echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━' COUNTER=1 @@ -166,20 +190,13 @@ echo "$ALL_COMMENTS" | while IFS='|' read -r type _ author_and_content; do 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}" - ;; + PR_BODY) label="${PURPLE}📄 PR Description${NC}" ;; + ISSUE) label="${GREEN}💬 Comment${NC}" ;; + REVIEW) label="${YELLOW}🔍 Code Review${NC}" ;; + REVIEW_SUMMARY) label="${BLUE}📝 Review Summary${NC}" ;; esac - echo -e " ${WHITE}└─${NC} $(echo "$content" | tr '\n' ' ')..." + printf "$(hl [$COUNTER]) $label by $(user $author)" + printf " $(hl └─) $(echo "$content" | tr '\n' ' ')..." echo "" COUNTER=$((COUNTER + 1)) done @@ -209,7 +226,7 @@ COMMENT_ID=$(echo "$SELECTED_LINE" | cut -d'|' -f2) SELECTED_AUTHOR=$(echo "$SELECTED_LINE" | cut -d'|' -f3 | cut -d':' -f1) echo "" -print_info "Selected: ${WHITE}$COMMENT_TYPE${NC} by ${CYAN}@$SELECTED_AUTHOR${NC}" +print_info "Selected: $(hl "$COMMENT_TYPE") by $(user "$SELECTED_AUTHOR")" echo "" echo -e "${WHITE}😊 Available reactions:${NC}" @@ -231,7 +248,7 @@ case $REACTION in esac echo "" -print_info "Sending ${WHITE}$REACTION${NC} reaction to comment ${WHITE}$COMMENT_ID${NC}..." +print_info "Sending $(hl "$REACTION") reaction to comment $(hl "$COMMENT_ID")..." declare -A endpoints=( [PR_BODY]=issues [ISSUE]=issues/comments @@ -246,18 +263,19 @@ fi if ! RESPONSE=$(ghPost "${endpoints[$COMMENT_TYPE]}"/"$COMMENT_ID"/reactions \ "$REACTION"); then 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." + cat <<-EOF + + Error details: $RESPONSE + + $(tip) Make sure you have permission to react to this content. + EOF exit 1 fi echo "" print_success "Reaction added successfully! 🎉" echo "" -echo -e "${WHITE}🔗 View PR:${NC} https://github.com/$repo/pull/$PR_NUMBER" +echo -e "$(hl 🔗 View PR:) https://github.com/$repo/pull/$PR_NUMBER" # Show reaction emoji based on type case $REACTION in From 8dfa5b01beb1ddd29c76fb7fa7782736153d8fd9 Mon Sep 17 00:00:00 2001 From: gesh Date: Wed, 22 Oct 2025 22:01:41 +0300 Subject: [PATCH 08/12] Use explicit error codes to distinguish fail modes --- gh-react | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/gh-react b/gh-react index 06f9115..4e3a1e1 100755 --- a/gh-react +++ b/gh-react @@ -10,6 +10,9 @@ 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) + # Function to print colored output print_status() { printf "${BLUE}%s${NC}\n" "$@" @@ -31,6 +34,13 @@ print_success() { printf "${GREEN}%s${NC}\n" "$@" } +die() { + code="$1"; shift + printf "${RED}%s${NC}\n" "$1" + cat + exit "$code" +} + hl() { printf "${WHITE}%s${NC}" "$*" } @@ -65,7 +75,7 @@ while getopts 'hR:' opt; do # Note % and # expansions are noops if the string isn't an affix of # the parameter! repo="${OPTARG#"${OPTARG%/*/*}"/}";; - *) usage; exit 1;; + *) usage; exit "${err[BADPARAM]}";; esac done shift $((OPTIND - 1)) @@ -104,12 +114,11 @@ print_info "Repository: $(hl "$repo")" print_info "Validating PR #$(hl "$PR_NUMBER")..." 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." - cat <<-EOF + die "${err[NOPR]}" \ + "PR #$PR_NUMBER does not exist or you don't have access to it." <<-EOF $(tip) Make sure you're in the correct repository and the PR number exists. EOF - exit 1 fi print_status "PR #$PR_NUMBER found!" @@ -169,12 +178,10 @@ if [ ! -z "$REVIEW_SUMMARY_COMMENTS" ]; then fi if [ -z "$ALL_COMMENTS" ]; then - print_error "No comments found for PR #$PR_NUMBER" - cat <<-EOF + 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 - exit 0 fi print_status "Found $(hl "$COMMENT_COUNT") items to react to!" @@ -256,20 +263,22 @@ declare -A endpoints=( [REVIEW_SUMMARY]=pulls/reviews ) if [ -z "${endpoints[$COMMENT_TYPE]+a}" ]; then - print_error "Could not determine comment type for ID $COMMENT_ID" - exit 1 + 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 if ! RESPONSE=$(ghPost "${endpoints[$COMMENT_TYPE]}"/"$COMMENT_ID"/reactions \ "$REACTION"); then - print_error "Failed to add reaction" - cat <<-EOF + die "${err[ADDFAIL]}" 'Failed to add reaction' <<-EOF Error details: $RESPONSE $(tip) Make sure you have permission to react to this content. EOF - exit 1 fi echo "" From e8d4de32e7f8ce2b09a418d21a33e1f458b40cbf Mon Sep 17 00:00:00 2001 From: gesh Date: Wed, 22 Oct 2025 21:53:39 +0300 Subject: [PATCH 09/12] Make the prompts retry until correct input Also solidify them by using select loops where possible (which restricts the valid inputs) --- gh-react | 48 +++++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/gh-react b/gh-react index 4e3a1e1..82fa659 100755 --- a/gh-react +++ b/gh-react @@ -215,16 +215,16 @@ echo "$ALL_COMMENTS" > "$commentsFile" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" -echo -e "${WHITE}😊 Choose what to react to:${NC}" -read -rp "� Enter number (1-$COMMENT_COUNT): " SELECTION - -# Validate selection is a number and in range -if ! [[ "$SELECTION" =~ ^[0-9]+$ ]] || [ "$SELECTION" -lt 1 ] || [ "$SELECTION" -gt "$COMMENT_COUNT" ]; then +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" - echo "" - echo "💡 ${YELLOW}Tip:${NC} Please enter a number between 1 and $COMMENT_COUNT" - exit 1 -fi +done # Get the selected comment data SELECTED_LINE=$(sed -n "${SELECTION}p" "$commentsFile") @@ -235,24 +235,18 @@ SELECTED_AUTHOR=$(echo "$SELECTED_LINE" | cut -d'|' -f3 | cut -d':' -f1) echo "" print_info "Selected: $(hl "$COMMENT_TYPE") by $(user "$SELECTED_AUTHOR")" -echo "" -echo -e "${WHITE}😊 Available reactions:${NC}" -echo " 👍 +1 👎 -1 😄 laugh" -echo " ❤️ heart 🎉 hooray 🚀 rocket 👀 eyes" -echo "" -read -rp "🎯 Pick a reaction: " REACTION +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 -# 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 echo "" print_info "Sending $(hl "$REACTION") reaction to comment $(hl "$COMMENT_ID")..." @@ -272,7 +266,7 @@ if [ -z "${endpoints[$COMMENT_TYPE]+a}" ]; then fi if ! RESPONSE=$(ghPost "${endpoints[$COMMENT_TYPE]}"/"$COMMENT_ID"/reactions \ - "$REACTION"); then + "${reactions[$REACTION]}"); then die "${err[ADDFAIL]}" 'Failed to add reaction' <<-EOF Error details: $RESPONSE From 4abf69996b01276c1ca68ec3f592c0b521460736 Mon Sep 17 00:00:00 2001 From: gesh Date: Wed, 22 Oct 2025 22:08:43 +0300 Subject: [PATCH 10/12] Use jq more extensively for output massaging --- gh-react | 122 +++++++++++++++++++++---------------------------------- 1 file changed, 47 insertions(+), 75 deletions(-) diff --git a/gh-react b/gh-react index 82fa659..1818a10 100755 --- a/gh-react +++ b/gh-react @@ -126,58 +126,29 @@ print_status "PR #$PR_NUMBER found!" echo "" print_info "Gathering all comments and content..." -# Get the PR description/body itself -PR_BODY=$(ghGet pulls/"$PR_NUMBER" ' - select(.body != null and .body != "") - | "PR_BODY|\(.number)|\(.user.login): \(.body)"') - -# Get issue comments (general PR comments) -ISSUE_COMMENTS=$(ghGet issues/"$PR_NUMBER"/comments ' - .[] | "ISSUE|\(.id)|\(.user.login): \(.body)"') - -# Get review comments (inline code review comments) -REVIEW_COMMENTS=$(ghGet pulls/"$PR_NUMBER"/comments ' - .[] | "REVIEW|\(.id)|\(.user.login): \(.body)") +commentsFile="$(mktemp)" +trap 'rm -- "$commentsFile"' EXIT -# Get review summary comments (comments submitted with reviews) -REVIEW_SUMMARY_COMMENTS=$(ghGet pulls/"$PR_NUMBER"/reviews ' +>"$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 != "") - | "REVIEW_SUMMARY|\(.id)|\(.user.login): \(.body)"') - -# Combine all comments -ALL_COMMENTS="" -COMMENT_COUNT=0 + | {type: "REVIEW_SUMMARY", id, author: .user.login, body}') -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 +COMMENT_COUNT="$(jq length "$commentsFile")" -if [ -z "$ALL_COMMENTS" ]; then +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! @@ -189,29 +160,32 @@ echo "" print_info "$(hl 📋 Available content:)" echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━' -COUNTER=1 - # Format and display comments with numbers -echo "$ALL_COMMENTS" | while IFS='|' read -r type _ 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) label="${PURPLE}📄 PR Description${NC}" ;; - ISSUE) label="${GREEN}💬 Comment${NC}" ;; - REVIEW) label="${YELLOW}🔍 Code Review${NC}" ;; - REVIEW_SUMMARY) label="${BLUE}📝 Review Summary${NC}" ;; - esac - printf "$(hl [$COUNTER]) $label by $(user $author)" - printf " $(hl └─) $(echo "$content" | tr '\n' ' ')..." - echo "" - COUNTER=$((COUNTER + 1)) -done - -# Store the comment data in temporary file for later retrieval -commentsFile="$(mktemp)" -trap 'rm -- "$commentsFile"' EXIT -echo "$ALL_COMMENTS" > "$commentsFile" +<"$commentsFile" 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)"; + to_entries[] | {ix: .key + 1} + .value + | "[\(.ix)] \(renderType) by \(renderUser)\n\(renderBody)" + ' echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" @@ -226,11 +200,9 @@ while read -rp "� Enter number (1-$COMMENT_COUNT): " SELECTION; do print_error "Invalid selection: $SELECTION" done -# Get the selected comment data -SELECTED_LINE=$(sed -n "${SELECTION}p" "$commentsFile") -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) +COMMENT_TYPE=$(jq -r ".[$SELECTION-1].type" "$commentsFile") +COMMENT_ID=$(jq ".[$SELECTION-1].id" "$commentsFile") +SELECTED_AUTHOR=$(jq -r ".[$SELECTION-1].author" "$commentsFile") echo "" print_info "Selected: $(hl "$COMMENT_TYPE") by $(user "$SELECTED_AUTHOR")" From c25c04056e7a09d846b86642f42cfb5c8ec321c7 Mon Sep 17 00:00:00 2001 From: gesh Date: Wed, 22 Oct 2025 22:24:35 +0300 Subject: [PATCH 11/12] Extract jq rendering code to use throughout --- gh-react | 54 +++++++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/gh-react b/gh-react index 1818a10..04a7016 100755 --- a/gh-react +++ b/gh-react @@ -50,8 +50,32 @@ hl2() { tip() { printf "💡 ${YELLOW}%s${NC}" "${1:-Tip}:" } -user() { - printf "${CYAN}@%s${NC}" "$1" + +# 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" } usage() { @@ -161,28 +185,7 @@ print_info "$(hl 📋 Available content:)" echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━' # Format and display comments with numbers -<"$commentsFile" 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)"; +<"$commentsFile" render ' to_entries[] | {ix: .key + 1} + .value | "[\(.ix)] \(renderType) by \(renderUser)\n\(renderBody)" ' @@ -205,7 +208,8 @@ COMMENT_ID=$(jq ".[$SELECTION-1].id" "$commentsFile") SELECTED_AUTHOR=$(jq -r ".[$SELECTION-1].author" "$commentsFile") echo "" -print_info "Selected: $(hl "$COMMENT_TYPE") by $(user "$SELECTED_AUTHOR")" +<"$commentsFile" render ".[$SELECTION-1]"' + | "Selected \(renderType) by \(renderUser)"' declare -A reactions=( [👍]=+1 [👎]=-1 [😄]=laugh [❤️]=heart [🎉]=hooray [🚀]=rocket [👀]=eyes From 8cabc22abc898e2ba94f9d1f1f6c847aa338bc14 Mon Sep 17 00:00:00 2001 From: gesh Date: Wed, 22 Oct 2025 22:27:44 +0300 Subject: [PATCH 12/12] Matters of taste - "Status" updates are more accurately success reports - Reduce color, line weight -- current choice is very noisy - No need to remind us at the end which reaction was picked -- it should still be on screen - Remove broken char in comment selection prompt --- gh-react | 33 +++++++-------------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/gh-react b/gh-react index 04a7016..a989db0 100755 --- a/gh-react +++ b/gh-react @@ -13,11 +13,6 @@ NC=$'\e[0m' # No Color # Error codes declare -A err=([BADPARAM]=1 [NOPR]=2 [NOCOMMS]=3 [UNDEFTYPE]=4 [ADDFAIL]=5) -# Function to print colored output -print_status() { - printf "${BLUE}%s${NC}\n" "$@" -} - print_header() { printf "${PURPLE}%s${NC}\n" "$@" } @@ -27,7 +22,7 @@ print_error() { } print_info() { - printf "${CYAN}%s${NC}\n" "$@" + printf "${NC}%s\n" "$@" } print_success() { @@ -129,7 +124,7 @@ ghPost() { print_header '💬 GitHub PR Reaction Tool' -echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━' +print_info '———————————————————————————————————————————————————' print_info "Fetching comments for PR #$(hl "$PR_NUMBER")..." print_info "Repository: $(hl "$repo")" @@ -144,7 +139,7 @@ if [ -z "$PR_EXISTS" ]; then $(tip) Make sure you're in the correct repository and the PR number exists. EOF fi -print_status "PR #$PR_NUMBER found!" +print_success "PR #$PR_NUMBER found!" # Use gh api to get raw comments for the issue/PR echo "" @@ -179,10 +174,10 @@ if [ "$COMMENT_COUNT" -eq 0 ]; then EOF fi -print_status "Found $(hl "$COMMENT_COUNT") items to react to!" +print_success "Found $(hl "$COMMENT_COUNT") items to react to!" echo "" print_info "$(hl 📋 Available content:)" -echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━' +echo '———————————————————————————————————————————————————' # Format and display comments with numbers <"$commentsFile" render ' @@ -190,10 +185,10 @@ echo '━━━━━━━━━━━━━━━━━━━━━━━━ | "[\(.ix)] \(renderType) by \(renderUser)\n\(renderBody)" ' -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo '———————————————————————————————————————————————————' echo "" print_info "$(hl 😊 Choose what to react to:)" -while read -rp "� Enter number (1-$COMMENT_COUNT): " SELECTION; do +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 ] \ @@ -223,7 +218,6 @@ select REACTION in "${!reactions[@]}"; do done unset PS3 - echo "" print_info "Sending $(hl "$REACTION") reaction to comment $(hl "$COMMENT_ID")..." declare -A endpoints=( @@ -251,18 +245,5 @@ if ! RESPONSE=$(ghPost "${endpoints[$COMMENT_TYPE]}"/"$COMMENT_ID"/reactions \ EOF fi -echo "" print_success "Reaction added successfully! 🎉" -echo "" echo -e "$(hl 🔗 View PR:) https://github.com/$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