|
#!/bin/bash |
|
set -euo pipefail |
|
|
|
# PR Migration Script for Zora Monorepo |
|
# Interactively migrate PRs from old repos to the monorepo |
|
|
|
# Color codes for 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' |
|
NC='\033[0m' # No Color |
|
|
|
# Repository configurations |
|
BACKEND_REPO="ourzora/zora-backend-v2" |
|
FRONTEND_REPO="ourzora/zora-co" |
|
PROTOCOL_REPO="ourzora/zora-protocol-private" |
|
|
|
BACKEND_WORKSPACE="backend" |
|
FRONTEND_WORKSPACE="frontend" |
|
PROTOCOL_WORKSPACE="protocol" |
|
|
|
# Temporary files |
|
TEMP_DIR=$(mktemp -d) |
|
PR_LIST_FILE="$TEMP_DIR/pr_list.txt" |
|
trap "rm -rf $TEMP_DIR" EXIT |
|
|
|
############################################################################# |
|
# Dependency Checks |
|
############################################################################# |
|
|
|
check_dependencies() { |
|
echo -e "${BLUE}Checking dependencies...${NC}" |
|
|
|
if ! command -v gh &> /dev/null; then |
|
echo -e "${RED}β GitHub CLI (gh) not found${NC}" |
|
echo "Install with: brew install gh" |
|
echo "Then authenticate: gh auth login" |
|
exit 1 |
|
fi |
|
|
|
if ! gh auth status &> /dev/null; then |
|
echo -e "${RED}β Not authenticated with GitHub CLI${NC}" |
|
echo "Run: gh auth login" |
|
exit 1 |
|
fi |
|
|
|
if ! command -v fzf &> /dev/null; then |
|
echo -e "${YELLOW}β fzf not found - using simple selection${NC}" |
|
echo "For better experience, install fzf: brew install fzf" |
|
USE_FZF=false |
|
else |
|
USE_FZF=true |
|
fi |
|
|
|
if ! command -v jq &> /dev/null; then |
|
echo -e "${RED}β jq not found${NC}" |
|
echo "Install with: brew install jq" |
|
exit 1 |
|
fi |
|
|
|
if [ ! -d ".git" ]; then |
|
echo -e "${RED}β Not in a git repository${NC}" |
|
echo "Run this script from the monorepo root directory" |
|
exit 1 |
|
fi |
|
|
|
echo -e "${GREEN}β All dependencies satisfied${NC}\n" |
|
} |
|
|
|
############################################################################# |
|
# Fetch PRs from all repos |
|
############################################################################# |
|
|
|
fetch_prs() { |
|
local repo=$1 |
|
local workspace=$2 |
|
local color=$3 |
|
|
|
echo -e "${CYAN}Fetching open PRs from ${repo}...${NC}" |
|
|
|
gh pr list \ |
|
--repo "$repo" \ |
|
--state open \ |
|
--json number,title,author,createdAt,headRefName,body,reviewDecision,labels \ |
|
--limit 100 | \ |
|
jq -r --arg workspace "$workspace" '.[] | |
|
"[\($workspace)] #\(.number) β @\(.author.login) β β° \(.createdAt | fromdateiso8601 | (now - .) / 86400 | floor)d ago β \(if .reviewDecision == "APPROVED" then "β" elif .reviewDecision == "CHANGES_REQUESTED" then "β" else "β" end) β \(.labels | length) labels β \(.title)β\(.body // "" | gsub("\n"; " "))"' | \ |
|
while IFS= read -r line; do |
|
printf "${color}%s${NC}\n" "$line" |
|
done |
|
} |
|
|
|
build_pr_list() { |
|
echo -e "${BLUE}βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}" |
|
echo -e "${BLUE} Fetching Open PRs from All Repositories${NC}" |
|
echo -e "${BLUE}βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}\n" |
|
|
|
> "$PR_LIST_FILE" |
|
|
|
# Fetch PRs from each repo |
|
fetch_prs "$BACKEND_REPO" "$BACKEND_WORKSPACE" "$RED" >> "$PR_LIST_FILE" || true |
|
fetch_prs "$FRONTEND_REPO" "$FRONTEND_WORKSPACE" "$BLUE" >> "$PR_LIST_FILE" || true |
|
fetch_prs "$PROTOCOL_REPO" "$PROTOCOL_WORKSPACE" "$YELLOW" >> "$PR_LIST_FILE" || true |
|
|
|
if [ ! -s "$PR_LIST_FILE" ]; then |
|
echo -e "${YELLOW}No open PRs found in any repository${NC}" |
|
exit 0 |
|
fi |
|
|
|
echo -e "\n${GREEN}β Found $(wc -l < "$PR_LIST_FILE") open PRs${NC}\n" |
|
} |
|
|
|
############################################################################# |
|
# Interactive Selection |
|
############################################################################# |
|
|
|
select_pr() { |
|
if [ "$USE_FZF" = true ]; then |
|
select_pr_with_fzf |
|
else |
|
select_pr_simple |
|
fi |
|
} |
|
|
|
select_pr_with_fzf() { |
|
echo -e "${BLUE}Use arrow keys to navigate, type to filter, Enter to select${NC}" |
|
echo -e "${BLUE}Tip: Prefix search with ' for exact match (e.g., 'frontend)${NC}\n" |
|
|
|
local selected=$(cat "$PR_LIST_FILE" | \ |
|
fzf --ansi \ |
|
--height=80% \ |
|
--border \ |
|
--prompt="Select PR to migrate > " \ |
|
--header="Search: workspace, author, title, PR# | Use 'term for exact match" \ |
|
--delimiter='β' \ |
|
--nth=1 \ |
|
--scheme=path \ |
|
--tiebreak=begin,length,index \ |
|
--preview=' |
|
line={} |
|
workspace=$(echo "$line" | grep -o "\[.*\]" | head -1) |
|
pr_num=$(echo "$line" | grep -o "#[0-9]\+" | head -1) |
|
author=$(echo "$line" | grep -o "@[^ β]*" | head -1) |
|
age=$(echo "$line" | grep -o "[0-9]\+d ago" | head -1) |
|
pr_status=$(echo "$line" | grep -o "[βββ]" | head -1) |
|
labels=$(echo "$line" | grep -o "[0-9]\+ labels" | head -1) |
|
title=$(echo "$line" | cut -d"β" -f1 | sed "s/.*labels β //" | sed "s/[[:space:]]*$//") |
|
desc=$(echo "$line" | cut -d"β" -f2) |
|
review_text="Pending" |
|
[ "$pr_status" = "β" ] && review_text="Approved" |
|
[ "$pr_status" = "β" ] && review_text="Changes requested" |
|
echo -e "\033[1;36mβββββββββββββββββββββββββββββββββββββββββββββββββββ\033[0m" |
|
echo -e "\033[1m$workspace $pr_num\033[0m" |
|
echo "" |
|
echo -e " Author: $author" |
|
echo -e " Created: $age" |
|
echo -e " Review: $pr_status $review_text" |
|
echo -e " Labels: $labels" |
|
echo "" |
|
echo -e "\033[1mTitle:\033[0m" |
|
echo -e " $title" |
|
echo "" |
|
echo -e "\033[1mDescription:\033[0m" |
|
if [ -n "$desc" ]; then |
|
echo "$desc" | fold -w 68 -s | sed "s/^/ /" |
|
else |
|
echo " (No description)" |
|
fi |
|
echo "" |
|
echo -e "\033[1;36mβββββββββββββββββββββββββββββββββββββββββββββββββββ\033[0m" |
|
' \ |
|
--preview-window=up:20:wrap) |
|
|
|
if [ -z "$selected" ]; then |
|
echo -e "${YELLOW}No PR selected, exiting${NC}" |
|
exit 0 |
|
fi |
|
|
|
parse_selected_pr "$selected" |
|
} |
|
|
|
select_pr_simple() { |
|
cat "$PR_LIST_FILE" | nl |
|
echo "" |
|
read -p "Enter PR number to select (1-$(wc -l < "$PR_LIST_FILE")): " selection |
|
|
|
if ! [[ "$selection" =~ ^[0-9]+$ ]] || [ "$selection" -lt 1 ] || [ "$selection" -gt $(wc -l < "$PR_LIST_FILE") ]; then |
|
echo -e "${RED}Invalid selection${NC}" |
|
exit 1 |
|
fi |
|
|
|
local selected=$(sed -n "${selection}p" "$PR_LIST_FILE") |
|
parse_selected_pr "$selected" |
|
} |
|
|
|
parse_selected_pr() { |
|
local line="$1" |
|
|
|
# Extract workspace and PR number from the formatted line (only from start, before β) |
|
local metadata=$(echo "$line" | cut -d'β' -f1) |
|
SELECTED_WORKSPACE=$(echo "$metadata" | grep -o '^\[.*\]' | tr -d '[]') |
|
SELECTED_PR=$(echo "$metadata" | grep -o '#[0-9]\+' | tr -d '#' | head -1) |
|
|
|
case "$SELECTED_WORKSPACE" in |
|
"$BACKEND_WORKSPACE") |
|
SELECTED_REPO="$BACKEND_REPO" |
|
;; |
|
"$FRONTEND_WORKSPACE") |
|
SELECTED_REPO="$FRONTEND_REPO" |
|
;; |
|
"$PROTOCOL_WORKSPACE") |
|
SELECTED_REPO="$PROTOCOL_REPO" |
|
;; |
|
*) |
|
echo -e "${RED}Unknown workspace: $SELECTED_WORKSPACE${NC}" |
|
exit 1 |
|
;; |
|
esac |
|
|
|
echo -e "\n${GREEN}Selected: ${SELECTED_REPO}#${SELECTED_PR} β monorepo/${SELECTED_WORKSPACE}/${NC}\n" |
|
} |
|
|
|
############################################################################# |
|
# PR Migration Logic |
|
############################################################################# |
|
|
|
migrate_pr() { |
|
echo -e "${BLUE}βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}" |
|
echo -e "${BLUE} Starting Automated PR Migration${NC}" |
|
echo -e "${BLUE}βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}\n" |
|
|
|
# Fetch PR details |
|
echo -e "${CYAN}Fetching PR details...${NC}" |
|
PR_DATA=$(gh pr view "$SELECTED_PR" --repo "$SELECTED_REPO" --json title,body,author,number,headRefName,baseRefName) |
|
|
|
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') |
|
PR_BODY=$(echo "$PR_DATA" | jq -r '.body // ""') |
|
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login') |
|
PR_BRANCH=$(echo "$PR_DATA" | jq -r '.headRefName') |
|
PR_BASE=$(echo "$PR_DATA" | jq -r '.baseRefName') |
|
|
|
NEW_BRANCH="migrate-${SELECTED_WORKSPACE}-pr-${SELECTED_PR}" |
|
|
|
echo -e "${GREEN}β PR Details:${NC}" |
|
echo -e " Title: ${PR_TITLE}" |
|
echo -e " Author: @${PR_AUTHOR}" |
|
echo -e " Branch: ${PR_BRANCH}" |
|
echo -e " Base: ${PR_BASE}\n" |
|
|
|
# Confirm migration |
|
read -p "Continue with migration? (Y/n): " confirm |
|
if [[ "$confirm" =~ ^[Nn]$ ]]; then |
|
echo -e "${YELLOW}Migration cancelled${NC}" |
|
exit 0 |
|
fi |
|
|
|
# Create new branch |
|
echo -e "\n${CYAN}Creating migration branch: ${NEW_BRANCH}${NC}" |
|
git checkout -b "$NEW_BRANCH" main 2>/dev/null || { |
|
echo -e "${YELLOW}Branch already exists, checking out${NC}" |
|
git checkout "$NEW_BRANCH" |
|
} |
|
|
|
# Fetch PR commits |
|
echo -e "${CYAN}Fetching PR commits from ${SELECTED_REPO}...${NC}" |
|
|
|
# Add remote if not exists |
|
if ! git remote | grep -q "^migrate-temp$"; then |
|
git remote add migrate-temp "https://github.com/${SELECTED_REPO}.git" |
|
fi |
|
|
|
# Fetch both the PR head and base branches |
|
echo -e "${PURPLE}β Fetching PR head branch...${NC}" |
|
git fetch migrate-temp "pull/${SELECTED_PR}/head:temp-pr-${SELECTED_PR}" || { |
|
echo -e "${RED}Failed to fetch PR branch${NC}" |
|
cleanup_migration |
|
exit 1 |
|
} |
|
|
|
echo -e "${PURPLE}β Fetching base branch (${PR_BASE})...${NC}" |
|
git fetch migrate-temp "${PR_BASE}:temp-base-${SELECTED_PR}" || { |
|
echo -e "${RED}Failed to fetch base branch${NC}" |
|
cleanup_migration |
|
exit 1 |
|
} |
|
|
|
# Get commits in PR (compare PR head against PR base, not monorepo main) |
|
COMMITS=$(git log --reverse --format=%H temp-base-${SELECTED_PR}..temp-pr-${SELECTED_PR} 2>/dev/null || echo "") |
|
|
|
if [ -z "$COMMITS" ]; then |
|
echo -e "${RED}No commits found in PR${NC}" |
|
cleanup_migration |
|
exit 1 |
|
fi |
|
|
|
COMMIT_COUNT=$(echo "$COMMITS" | wc -l | tr -d ' ') |
|
echo -e "${GREEN}β Found ${COMMIT_COUNT} commits to migrate${NC}\n" |
|
|
|
# Export commits as patches and apply with path prefix |
|
echo -e "${CYAN}Applying commits with path prefix (${SELECTED_WORKSPACE}/)...${NC}" |
|
|
|
PATCH_DIR="$TEMP_DIR/patches" |
|
mkdir -p "$PATCH_DIR" |
|
|
|
# Generate patches |
|
echo -e "${PURPLE}Generating patches...${NC}" |
|
git format-patch temp-base-${SELECTED_PR}..temp-pr-${SELECTED_PR} -o "$PATCH_DIR" --quiet |
|
|
|
PATCH_COUNT=$(ls -1 "$PATCH_DIR"/*.patch 2>/dev/null | wc -l | tr -d ' ') |
|
|
|
if [ "$PATCH_COUNT" -eq 0 ]; then |
|
echo -e "${RED}No patches generated${NC}" |
|
cleanup_migration |
|
exit 1 |
|
fi |
|
|
|
echo -e "${GREEN}β Generated ${PATCH_COUNT} patches${NC}\n" |
|
|
|
# Apply patches with directory prefix |
|
local patch_num=0 |
|
for patch in "$PATCH_DIR"/*.patch; do |
|
patch_num=$((patch_num + 1)) |
|
patch_name=$(basename "$patch") |
|
subject=$(grep '^Subject:' "$patch" | sed 's/Subject: \[PATCH[^]]*\] //' | head -c 60) |
|
|
|
echo -e "${PURPLE}[$patch_num/$PATCH_COUNT]${NC} Applying: $subject..." |
|
|
|
# Apply patch with directory prefix and three-way merge |
|
if ! git am --directory="${SELECTED_WORKSPACE}/" --3way "$patch" 2>&1 | tee /tmp/am_output.txt; then |
|
# Check if it's a conflict that needs resolution |
|
if grep -q "Patch failed" /tmp/am_output.txt; then |
|
echo -e "${YELLOW} β Patch has conflicts, resolving...${NC}" |
|
|
|
# Check for conflict markers |
|
conflicted=$(git diff --name-only --diff-filter=U 2>/dev/null) |
|
|
|
if [ -n "$conflicted" ]; then |
|
echo -e "${YELLOW} Conflicted files: ${NC}" |
|
echo "$conflicted" | sed 's/^/ /' |
|
echo "" |
|
echo -e "${RED} β Cannot auto-resolve conflicts${NC}" |
|
echo -e "${YELLOW} Please resolve manually:${NC}" |
|
echo -e " 1. Fix conflicts in the files above" |
|
echo -e " 2. Run: git add <files>" |
|
echo -e " 3. Run: git am --continue" |
|
echo -e " 4. Re-run this script to continue migration" |
|
exit 1 |
|
fi |
|
|
|
# Try to continue if conflicts were auto-resolved |
|
git am --continue 2>/dev/null || { |
|
echo -e "${RED} β Failed to apply patch${NC}" |
|
git am --abort 2>/dev/null |
|
cleanup_migration |
|
exit 1 |
|
} |
|
else |
|
echo -e "${RED} β Failed to apply patch${NC}" |
|
echo -e "${YELLOW} Patch: $patch_name${NC}" |
|
git am --abort 2>/dev/null |
|
cleanup_migration |
|
exit 1 |
|
fi |
|
fi |
|
done |
|
|
|
echo -e "\n${GREEN}β Successfully applied all ${PATCH_COUNT} patches${NC}" |
|
|
|
# Push branch (force in case it exists from previous migration attempt) |
|
echo -e "\n${CYAN}Pushing migration branch...${NC}" |
|
git push origin "$NEW_BRANCH" --force |
|
|
|
# Create new PR |
|
echo -e "${CYAN}Creating new PR in monorepo...${NC}" |
|
|
|
NEW_PR_TITLE="[${SELECTED_WORKSPACE}] ${PR_TITLE}" |
|
NEW_PR_BODY="Migrated from ${SELECTED_REPO}#${SELECTED_PR} |
|
|
|
**Original PR:** https://github.com/${SELECTED_REPO}/pull/${SELECTED_PR} |
|
**Original Author:** @${PR_AUTHOR} |
|
|
|
--- |
|
|
|
${PR_BODY} |
|
|
|
--- |
|
|
|
π€ *This PR was automatically migrated using the monorepo migration script*" |
|
|
|
gh pr create \ |
|
--repo ourzora/zora-monorepo-poc \ |
|
--title "$NEW_PR_TITLE" \ |
|
--body "$NEW_PR_BODY" \ |
|
--base main \ |
|
--head "$NEW_BRANCH" || { |
|
echo -e "${RED}Failed to create PR${NC}" |
|
echo -e "${YELLOW}Branch pushed successfully, but PR creation failed.${NC}" |
|
echo -e "${YELLOW}Create PR manually at: https://github.com/ourzora/zora-monorepo-poc/compare/${NEW_BRANCH}${NC}" |
|
exit 1 |
|
} |
|
|
|
# Cleanup |
|
cleanup_migration |
|
|
|
echo -e "\n${GREEN}βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}" |
|
echo -e "${GREEN} Migration Complete! π${NC}" |
|
echo -e "${GREEN}βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}\n" |
|
echo -e "${GREEN}β Migrated ${COMMIT_COUNT} commits${NC}" |
|
echo -e "${GREEN}β Created PR: ${NEW_PR_TITLE}${NC}" |
|
echo -e "${GREEN}β Branch: ${NEW_BRANCH}${NC}\n" |
|
|
|
if [ "$PR_AUTHOR" != "$(git config user.name)" ]; then |
|
echo -e "${YELLOW}Note: Don't forget to mention @${PR_AUTHOR} in the PR!${NC}" |
|
fi |
|
} |
|
|
|
cleanup_migration() { |
|
# Cleanup temp remote and branches |
|
git remote remove migrate-temp 2>/dev/null || true |
|
git branch -D "temp-pr-${SELECTED_PR}" 2>/dev/null || true |
|
git branch -D "temp-base-${SELECTED_PR}" 2>/dev/null || true |
|
} |
|
|
|
############################################################################# |
|
# Main Script |
|
############################################################################# |
|
|
|
main() { |
|
echo -e "${PURPLE}" |
|
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ" |
|
echo "β β" |
|
echo "β Zora Monorepo PR Migration Tool β" |
|
echo "β β" |
|
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ" |
|
echo -e "${NC}\n" |
|
|
|
check_dependencies |
|
build_pr_list |
|
select_pr |
|
migrate_pr |
|
} |
|
|
|
main |