Skip to content

Instantly share code, notes, and snippets.

@hannesoid
Last active September 9, 2025 08:35
Show Gist options
  • Save hannesoid/5e1a887efbf3a6c29b4755a6793853aa to your computer and use it in GitHub Desktop.
Save hannesoid/5e1a887efbf3a6c29b4755a6793853aa to your computer and use it in GitHub Desktop.
GitHub PR Tree Visualizer - Shows pull request dependencies in a hierarchical tree view with activity indicators
#!/bin/bash
# GitHub PR Tree Visualizer
#
# This script creates a hierarchical tree view of open GitHub pull requests,
# showing their dependency relationships based on base branches. It provides
# visual indicators for PR activity and highlights your current working branch.
#
# Features:
# - Tree structure showing PR dependencies (which PRs are based on which branches)
# - Current branch highlighting with bold green colors
# - Activity indicators: πŸ”¨ for PRs updated in last 12 hours
# - Dimmed styling for stale PRs (not updated in 2+ weeks)
# - Clean format: PR# "Title" branch-name URL
# - Color-coded for easy scanning
#
# Requirements:
# - GitHub CLI (gh) installed and authenticated
# - jq for JSON parsing
# - Git repository with open pull requests
#
# Example output:
# main
# β”œβ”€β”€ #123 "Add user authentication" feature/auth https://github.com/user/repo/pull/123
# β”œβ”€β”€ #124 "Fix login bug" πŸ”¨ bugfix/login-issue https://github.com/user/repo/pull/124
# β”‚ └── #125 "Update tests" test/login-fixes https://github.com/user/repo/pull/125
# β”œβ”€β”€ #126 "Database migration" feature/db-migration https://github.com/user/repo/pull/126
# β”‚ └── #127 "Performance improvements" enhancement/db-perf https://github.com/user/repo/pull/127
#
# develop
# β”œβ”€β”€ #128 "Feature branch sync" feature/sync https://github.com/user/repo/pull/128
# Get current branch
current_branch=$(git branch --show-current 2>/dev/null || echo "")
# Fetch PR data with update times
pr_data=$(gh pr list --state open --json number,title,baseRefName,headRefName,url,updatedAt --limit 100)
# Colors
CYAN='\033[96m'
YELLOW='\033[33m'
DARK_GRAY='\033[90m'
GREEN='\033[92m'
RED='\033[91m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
# Function to create clickable URL
make_clickable() {
local url="$1"
printf '\e]8;;%s\e\\%s\e]8;;\e\\' "$url" "$url"
}
# Function to get PR age styling based on last update
get_pr_styling() {
local pr_num="$1"
local updated_at
# Extract updatedAt for this PR from the JSON data
updated_at=$(echo "$pr_data" | jq -r ".[] | select(.number == $pr_num) | .updatedAt")
if [[ -z "$updated_at" || "$updated_at" == "null" ]]; then
# Default styling if we can't get the date
echo "normal"
return
fi
# Convert ISO date to epoch seconds for comparison
if command -v gdate >/dev/null 2>&1; then
# Use GNU date if available (brew install coreutils)
updated_epoch=$(gdate -d "$updated_at" +%s 2>/dev/null || echo "0")
current_epoch=$(gdate +%s)
else
# Use BSD date (macOS default)
updated_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "${updated_at%.*}Z" +%s 2>/dev/null || echo "0")
current_epoch=$(date +%s)
fi
if [[ "$updated_epoch" -eq 0 ]]; then
echo "normal"
return
fi
# Calculate age in hours and days
age_seconds=$((current_epoch - updated_epoch))
age_hours=$((age_seconds / 3600))
age_days=$((age_seconds / 86400))
if [[ $age_hours -le 12 ]]; then
echo "very_recent" # Updated in last 12 hours
elif [[ $age_days -ge 14 ]]; then
echo "old" # Not updated for 2+ weeks
else
echo "normal" # Normal styling
fi
}
# Function to print PR line with current branch highlighting
print_pr_line() {
local prefix="$1"
local pr_num="$2"
local branch="$3"
local title="$4"
local url="$5"
# Get styling based on PR age
local pr_styling=$(get_pr_styling "$pr_num")
# Set colors based on age and current branch
local pr_color="${CYAN}"
local title_color="${YELLOW}"
local branch_color=""
local age_indicator=""
if [[ "$current_branch" == "$branch" ]]; then
# Current branch always gets bold green highlighting
pr_color="${BOLD}${GREEN}"
branch_color="${BOLD}"
elif [[ "$pr_styling" == "very_recent" ]]; then
# Very recent PRs (last 12 hours) get hammer indicator
pr_color="${CYAN}"
title_color="${YELLOW}"
age_indicator=" πŸ”¨"
elif [[ "$pr_styling" == "old" ]]; then
# Old PRs get dimmed styling
pr_color="${DIM}${CYAN}"
title_color="${DIM}${YELLOW}"
branch_color="${DIM}"
fi
echo -e "${prefix} ${pr_color}#${pr_num}${RESET} ${title_color}\"${title}\"${RESET}${age_indicator} ${branch_color}${branch}${RESET} ${DARK_GRAY}${url}${RESET}"
}
# Function to find root branches (branches that don't have PRs targeting them)
get_root_branches() {
local all_bases=($(echo "$pr_data" | jq -r '.[].baseRefName' | sort -u))
local all_heads=($(echo "$pr_data" | jq -r '.[].headRefName' | sort -u))
# Root branches are bases that are not also head branches
local root_branches=()
for base in "${all_bases[@]}"; do
local is_head=false
for head in "${all_heads[@]}"; do
if [[ "$base" == "$head" ]]; then
is_head=true
break
fi
done
if [[ "$is_head" == "false" ]]; then
root_branches+=("$base")
fi
done
printf '%s\n' "${root_branches[@]}"
}
# Function to build tree structure from PR data
build_pr_tree() {
local root_branches=($(get_root_branches))
for root in "${root_branches[@]}"; do
echo "$root"
print_branch_prs "$root" ""
echo ""
done
}
# Function to print PRs for a branch and their dependencies
print_branch_prs() {
local branch="$1"
local indent="$2"
local branch_prs=($(echo "$pr_data" | jq -r ".[] | select(.baseRefName == \"$branch\") | .number"))
for i in "${!branch_prs[@]}"; do
local pr_num="${branch_prs[$i]}"
local pr_info=$(echo "$pr_data" | jq -r ".[] | select(.number == $pr_num)")
local title=$(echo "$pr_info" | jq -r '.title')
local head_branch=$(echo "$pr_info" | jq -r '.headRefName')
local url=$(echo "$pr_info" | jq -r '.url')
# Determine prefix based on position
local prefix="${indent}β”œβ”€β”€"
if [[ $i -eq $((${#branch_prs[@]} - 1)) ]]; then
prefix="${indent}└──"
fi
print_pr_line "$prefix" "$pr_num" "$head_branch" "$title" "$url"
# Recursively show PRs that are based on this PR's head branch
print_branch_prs "$head_branch" "${indent}β”‚ "
done
}
build_pr_tree
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment