Commit 6c998ed56e5b
Changed files (5)
pkgs
systems
kyushu
tools
gh-resolve-conflicts
pkgs/default.nix
@@ -22,6 +22,7 @@ in
batzconverter = pkgs.callPackage ./batzconverter { };
manifest-tool = pkgs.callPackage ./manifest-tool { };
gh-restart-failed = pkgs.callPackage ../tools/gh-restart-failed { };
+ gh-resolve-conflicts = pkgs.callPackage ../tools/gh-resolve-conflicts { };
arr = pkgs.callPackage ../tools/arr { };
download-kiwix-zim = pkgs.callPackage ../tools/download-kiwix-zim { };
toggle-color-scheme = pkgs.callPackage ./toggle-color-scheme { };
systems/kyushu/home.nix
@@ -54,6 +54,7 @@
go-org-readwise
gh-restart-failed
+ gh-resolve-conflicts
arr
claude-hooks
toggle-color-scheme
tools/gh-resolve-conflicts/default.nix
@@ -0,0 +1,42 @@
+{
+ stdenv,
+ lib,
+ makeWrapper,
+ gh,
+ fzf,
+ jq,
+ git,
+ emacs,
+}:
+
+stdenv.mkDerivation {
+ name = "gh-resolve-conflicts";
+ pname = "gh-resolve-conflicts";
+ version = "0.1.0";
+
+ src = ./.;
+
+ nativeBuildInputs = [ makeWrapper ];
+
+ installPhase = ''
+ mkdir -p $out/bin
+ cp gh-resolve-conflicts.sh $out/bin/gh-resolve-conflicts
+ chmod +x $out/bin/gh-resolve-conflicts
+
+ wrapProgram $out/bin/gh-resolve-conflicts \
+ --prefix PATH : ${
+ lib.makeBinPath [
+ gh
+ fzf
+ jq
+ git
+ emacs
+ ]
+ }
+ '';
+
+ meta = {
+ description = "List and resolve merge conflicts in GitHub pull requests interactively";
+ platforms = lib.platforms.unix;
+ };
+}
tools/gh-resolve-conflicts/gh-resolve-conflicts.sh
@@ -0,0 +1,623 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Help message
+usage() {
+ cat <<EOF
+Usage: gh-resolve-conflicts [OPTIONS] [REPOSITORY[#PR_NUMBER]]
+
+List pull requests with merge conflicts and resolve them interactively.
+
+Options:
+ -w, --worktree DIR Create worktrees in DIR (default: /tmp/gh-resolve-conflicts-worktrees)
+ -n, --no-worktree Use existing repo (cd into it) instead of creating worktrees
+ -N, --no-push Do NOT automatically force-push after resolution (default: auto-push)
+ -o, --org ORG Filter PRs by organization
+ -a, --author AUTHOR Filter PRs by author (default: @me)
+ -h, --help Show this help message
+
+Arguments:
+ REPOSITORY Optional repository in OWNER/REPO format.
+ If not provided, searches across all repos (when -o is used).
+ Can include #PR_NUMBER to directly resolve a specific PR.
+
+Dependencies:
+ - gh (GitHub CLI)
+ - fzf (fuzzy finder, for interactive mode)
+ - jq (JSON processor)
+ - git
+ - emacs (for ediff conflict resolution)
+
+Examples:
+ gh-resolve-conflicts # List all your conflicting PRs (interactive)
+ gh-resolve-conflicts -o tektoncd # List conflicting PRs in tektoncd org
+ gh-resolve-conflicts owner/repo#123 # Directly resolve PR #123
+ gh-resolve-conflicts -N # Don't auto-push after resolution
+ gh-resolve-conflicts -n # Use existing repo, no worktree
+
+Workflow:
+ 1. Scans for PRs with merge conflicts
+ 2. Interactive selection with fzf (multi-select supported)
+ 3. For each PR:
+ - Creates worktree or uses existing repo
+ - Checks out PR branch
+ - Attempts rebase against base branch
+ - Launches emacs ediff for conflicts
+ - Continues rebase after resolution
+ - Optionally force-pushes
+
+EOF
+ exit 0
+}
+
+# Check dependencies
+check_dependencies() {
+ local missing=()
+
+ for cmd in gh jq git; do
+ if ! command -v "$cmd" &> /dev/null; then
+ missing+=("$cmd")
+ fi
+ done
+
+ if [ ${#missing[@]} -gt 0 ]; then
+ echo -e "${RED}Error: Missing required dependencies: ${missing[*]}${NC}" >&2
+ echo "Please install them and try again." >&2
+ exit 1
+ fi
+}
+
+# Check if emacs is available
+check_emacs() {
+ if ! command -v emacs &> /dev/null; then
+ echo -e "${YELLOW}Warning: emacs not found. Conflict resolution will use default git merge tool.${NC}" >&2
+ return 1
+ fi
+ return 0
+}
+
+# Default settings
+WORKTREE_DIR="/tmp/gh-resolve-conflicts-worktrees"
+USE_WORKTREE=true
+AUTO_PUSH=true
+ORG_FILTER=""
+AUTHOR_FILTER="@me"
+REPO_ARG=""
+PR_NUMBER=""
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -h|--help)
+ usage
+ ;;
+ -w|--worktree)
+ if [ -n "${2:-}" ]; then
+ WORKTREE_DIR="$2"
+ shift 2
+ else
+ echo -e "${RED}Error: --worktree requires a directory argument${NC}" >&2
+ exit 1
+ fi
+ ;;
+ -n|--no-worktree)
+ USE_WORKTREE=false
+ shift
+ ;;
+ -N|--no-push)
+ AUTO_PUSH=false
+ shift
+ ;;
+ -o|--org)
+ if [ -n "${2:-}" ]; then
+ ORG_FILTER="$2"
+ shift 2
+ else
+ echo -e "${RED}Error: --org requires an organization argument${NC}" >&2
+ exit 1
+ fi
+ ;;
+ -a|--author)
+ if [ -n "${2:-}" ]; then
+ AUTHOR_FILTER="$2"
+ shift 2
+ else
+ echo -e "${RED}Error: --author requires an author argument${NC}" >&2
+ exit 1
+ fi
+ ;;
+ -*)
+ echo -e "${RED}Error: Unknown option: $1${NC}" >&2
+ usage
+ ;;
+ *)
+ REPO_ARG="$1"
+ # Check if it contains #PR_NUMBER
+ if [[ "$REPO_ARG" =~ ^(.+)#([0-9]+)$ ]]; then
+ REPO_ARG="${BASH_REMATCH[1]}"
+ PR_NUMBER="${BASH_REMATCH[2]}"
+ fi
+ shift
+ ;;
+ esac
+done
+
+check_dependencies
+
+# Check fzf only if in interactive mode (no PR_NUMBER specified)
+if [ -z "$PR_NUMBER" ] && ! command -v fzf &> /dev/null; then
+ echo -e "${RED}Error: fzf is required for interactive mode${NC}" >&2
+ echo "Please install fzf or specify a PR number directly (e.g., owner/repo#123)" >&2
+ exit 1
+fi
+
+HAS_EMACS=false
+if check_emacs; then
+ HAS_EMACS=true
+fi
+
+# Fetch conflicting PRs
+echo -e "${BLUE}Fetching pull requests with merge conflicts...${NC}" >&2
+
+# Build search query
+SEARCH_ARGS=(--author "$AUTHOR_FILTER" --state open)
+if [ -n "$ORG_FILTER" ]; then
+ SEARCH_ARGS+=(--owner "$ORG_FILTER")
+fi
+
+if [ -n "$PR_NUMBER" ]; then
+ # Direct PR mode
+ if [ -z "$REPO_ARG" ]; then
+ echo -e "${RED}Error: Repository must be specified when using #PR_NUMBER${NC}" >&2
+ exit 1
+ fi
+
+ echo -e "${BLUE}Fetching PR #$PR_NUMBER from $REPO_ARG...${NC}" >&2
+
+ pr_info=$(gh pr view "$PR_NUMBER" -R "$REPO_ARG" \
+ --json number,title,headRefName,baseRefName,author,mergeable,url \
+ 2>/dev/null)
+
+ if [ -z "$pr_info" ]; then
+ echo -e "${RED}Error: PR #$PR_NUMBER not found${NC}" >&2
+ exit 1
+ fi
+
+ mergeable=$(echo "$pr_info" | jq -r '.mergeable')
+ if [ "$mergeable" != "CONFLICTING" ]; then
+ echo -e "${YELLOW}PR #$PR_NUMBER does not have merge conflicts (status: $mergeable)${NC}"
+ exit 0
+ fi
+
+ pr_title=$(echo "$pr_info" | jq -r '.title')
+ pr_branch=$(echo "$pr_info" | jq -r '.headRefName')
+ base_branch=$(echo "$pr_info" | jq -r '.baseRefName')
+ pr_author=$(echo "$pr_info" | jq -r '.author.login')
+ pr_url=$(echo "$pr_info" | jq -r '.url')
+
+ selected_prs="$REPO_ARG|#$PR_NUMBER|$pr_title|@$pr_author|$pr_branch|$base_branch|$pr_url"
+else
+ # Interactive mode: Search for conflicting PRs
+ prs_json=$(gh search prs "${SEARCH_ARGS[@]}" \
+ --json number,title,repository,url \
+ --limit 100)
+
+ if [ -z "$prs_json" ] || [ "$prs_json" = "[]" ]; then
+ echo -e "${YELLOW}No open pull requests found.${NC}"
+ exit 0
+ fi
+
+ # Check each PR for conflicts
+ conflicting_prs=""
+ total=$(echo "$prs_json" | jq 'length')
+ current=0
+
+ echo -e "${BLUE}Checking $total PRs for merge conflicts...${NC}" >&2
+
+ while IFS= read -r pr; do
+ ((current++)) || true
+ repo=$(echo "$pr" | jq -r '.repository.nameWithOwner')
+ number=$(echo "$pr" | jq -r '.number')
+ title=$(echo "$pr" | jq -r '.title')
+ url=$(echo "$pr" | jq -r '.url')
+
+ echo -ne "${YELLOW}\rChecking PR $current/$total...${NC}" >&2
+
+ # Fetch detailed PR info including mergeable status
+ pr_details=$(gh pr view "$number" -R "$repo" \
+ --json mergeable,headRefName,baseRefName,author 2>/dev/null || echo "{}")
+
+ mergeable=$(echo "$pr_details" | jq -r '.mergeable // "UNKNOWN"')
+
+ if [ "$mergeable" = "CONFLICTING" ]; then
+ branch=$(echo "$pr_details" | jq -r '.headRefName')
+ base_branch=$(echo "$pr_details" | jq -r '.baseRefName')
+ author=$(echo "$pr_details" | jq -r '.author.login')
+ conflicting_prs+="$repo|#$number|$title|@$author|$branch|$base_branch|$url"$'\n'
+ fi
+ done < <(echo "$prs_json" | jq -c '.[]')
+
+ echo -e "\r${GREEN}Done checking PRs.${NC} " >&2
+
+ if [ -z "$conflicting_prs" ]; then
+ echo -e "${GREEN}No pull requests with merge conflicts found!${NC}"
+ exit 0
+ fi
+
+ echo -e "${YELLOW}Found pull requests with merge conflicts:${NC}" >&2
+ echo ""
+
+ # Use fzf to select PRs
+ selected_prs=$(echo "$conflicting_prs" | fzf \
+ --multi \
+ --ansi \
+ --delimiter='|' \
+ --with-nth=1,2,3,4 \
+ --header="Select PRs to resolve conflicts (TAB for multi-select, ENTER to confirm)" \
+ --preview="echo {} | cut -d'|' -f7 | xargs -I % echo 'URL: %'; echo ''; repo=\$(echo {} | cut -d'|' -f1); pr=\$(echo {} | cut -d'|' -f2 | tr -d '#'); gh pr diff \"\$pr\" -R \"\$repo\" 2>/dev/null | head -50 || echo 'Loading...'" \
+ --preview-window=right:60%:wrap \
+ --bind='ctrl-/:toggle-preview' \
+ --height=80%)
+
+ if [ -z "$selected_prs" ]; then
+ echo -e "${YELLOW}No pull requests selected.${NC}"
+ exit 0
+ fi
+fi
+
+echo ""
+echo -e "${BLUE}Processing selected pull requests...${NC}"
+echo ""
+
+# Function to resolve conflicts with emacs ediff
+resolve_with_ediff() {
+ local file="$1"
+
+ if [ "$HAS_EMACS" = false ]; then
+ echo -e "${YELLOW}Emacs not available, using git mergetool...${NC}"
+ git mergetool "$file"
+ return $?
+ fi
+
+ echo -e "${GREEN}Launching emacs ediff for: $file${NC}"
+
+ # Create temporary elisp script for ediff
+ local ediff_script
+ ediff_script=$(mktemp)
+ cat > "$ediff_script" <<'ELISP'
+(defun resolve-conflict-and-quit ()
+ "Resolve git conflict with ediff and quit when done."
+ (let* ((file (car command-line-args-left))
+ (buffer (find-file-noselect file)))
+ (with-current-buffer buffer
+ ;; Check if file has conflict markers
+ (goto-char (point-min))
+ (if (search-forward "<<<<<<< " nil t)
+ (progn
+ ;; Use ediff-merge for 3-way merge
+ (let* ((base-file (concat file ".base"))
+ (local-file (concat file ".LOCAL"))
+ (remote-file (concat file ".REMOTE")))
+ (if (and (file-exists-p local-file)
+ (file-exists-p remote-file))
+ ;; If git created the temp files, use them
+ (ediff-merge-files-with-ancestor local-file remote-file base-file nil file)
+ ;; Otherwise, try to extract from conflict markers
+ (vc-resolve-conflicts))))
+ ;; No conflict markers found
+ (message "No conflict markers found in %s" file)))
+ (setq command-line-args-left nil)))
+
+(add-hook 'ediff-quit-hook
+ (lambda ()
+ (save-buffers-kill-terminal t)))
+
+(resolve-conflict-and-quit)
+ELISP
+
+ # Launch emacs with ediff (using user's config)
+ emacs --load "$ediff_script" "$file" 2>/dev/null
+
+ rm -f "$ediff_script"
+
+ # Check if conflict markers still exist
+ if grep -q "<<<<<<< " "$file"; then
+ echo -e "${RED}Conflict markers still present in $file${NC}"
+ return 1
+ fi
+
+ # Mark as resolved
+ git add "$file"
+ return 0
+}
+
+# Function to resolve a single PR
+resolve_pr() {
+ local repo="$1"
+ local pr_number="$2"
+ local pr_title="$3"
+ local pr_branch="$4"
+ local base_branch="$5"
+ local pr_url="$6"
+
+ echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+ echo -e "${BLUE}Repository: $repo${NC}"
+ echo -e "${BLUE}PR #$pr_number: $pr_title${NC}"
+ echo -e "${BLUE}Branch: $pr_branch -> $base_branch${NC}"
+ echo -e "${BLUE}URL: $pr_url${NC}"
+ echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+ echo ""
+
+ # Get PR details to find the head repository (fork)
+ echo -e "${YELLOW}Finding fork repository...${NC}"
+ pr_details=$(gh pr view "$pr_number" -R "$repo" \
+ --json headRepository,headRepositoryOwner,isCrossRepository 2>/dev/null)
+
+ if [ -z "$pr_details" ]; then
+ echo -e "${RED}Failed to get PR details${NC}"
+ return 1
+ fi
+
+ local is_cross_repo
+ is_cross_repo=$(echo "$pr_details" | jq -r '.isCrossRepository')
+ local fork_repo
+
+ if [ "$is_cross_repo" = "true" ]; then
+ # PR is from a fork - construct fork repo name
+ local fork_owner
+ fork_owner=$(echo "$pr_details" | jq -r '.headRepositoryOwner.login')
+ local fork_name
+ fork_name=$(echo "$pr_details" | jq -r '.headRepository.name')
+
+ # Try to get full nameWithOwner, fall back to constructing it
+ fork_repo=$(echo "$pr_details" | jq -r '.headRepository.nameWithOwner')
+ if [ -z "$fork_repo" ] || [ "$fork_repo" = "null" ] || [ "$fork_repo" = "" ]; then
+ fork_repo="$fork_owner/$fork_name"
+ fi
+
+ echo -e "${BLUE}PR is from fork: $fork_repo${NC}"
+ else
+ # PR is from same repo (branch)
+ fork_repo="$repo"
+ echo -e "${BLUE}PR is from branch in same repo${NC}"
+ fi
+
+ local work_dir
+
+ if [ "$USE_WORKTREE" = true ]; then
+ # Use worktree
+ local repo_name
+ repo_name=$(echo "$repo" | tr '/' '-')
+ work_dir="$WORKTREE_DIR/$repo_name/pr-$pr_number"
+
+ echo -e "${YELLOW}Creating worktree at: $work_dir${NC}"
+
+ # Create parent directory
+ mkdir -p "$WORKTREE_DIR/$repo_name"
+
+ # Clone fork if not exists
+ local repo_dir="$WORKTREE_DIR/$repo_name/main"
+ if [ ! -d "$repo_dir" ]; then
+ echo -e "${YELLOW}Cloning fork: $fork_repo...${NC}"
+ gh repo clone "$fork_repo" "$repo_dir" -- --bare
+
+ # Add upstream remote if this is a fork
+ if [ "$is_cross_repo" = "true" ]; then
+ echo -e "${YELLOW}Adding upstream remote: $repo...${NC}"
+ git -C "$repo_dir" remote add upstream "https://github.com/$repo.git" 2>/dev/null || true
+ fi
+ else
+ echo -e "${YELLOW}Fetching latest changes from fork...${NC}"
+ git -C "$repo_dir" fetch origin
+
+ # Ensure upstream remote exists if this is a fork
+ if [ "$is_cross_repo" = "true" ]; then
+ if ! git -C "$repo_dir" remote | grep -q "^upstream$"; then
+ echo -e "${YELLOW}Adding upstream remote: $repo...${NC}"
+ git -C "$repo_dir" remote add upstream "https://github.com/$repo.git"
+ fi
+ fi
+ fi
+
+ # Fetch from upstream if this is a fork
+ if [ "$is_cross_repo" = "true" ]; then
+ echo -e "${YELLOW}Fetching from upstream: $repo...${NC}"
+ git -C "$repo_dir" fetch upstream
+ fi
+
+ # Remove existing worktree if present
+ if [ -d "$work_dir" ]; then
+ echo -e "${YELLOW}Removing existing worktree...${NC}"
+ git -C "$repo_dir" worktree remove "$work_dir" --force 2>/dev/null || rm -rf "$work_dir"
+ fi
+
+ # Fetch PR branch from fork
+ echo -e "${YELLOW}Fetching PR branch: $pr_branch...${NC}"
+ git -C "$repo_dir" fetch origin "$pr_branch:pr-$pr_number" || {
+ echo -e "${RED}Failed to fetch PR branch from fork${NC}"
+ return 1
+ }
+
+ # Create new worktree
+ echo -e "${YELLOW}Creating worktree for branch $pr_branch...${NC}"
+ git -C "$repo_dir" worktree add "$work_dir" "pr-$pr_number" || {
+ echo -e "${RED}Failed to create worktree${NC}"
+ return 1
+ }
+
+ cd "$work_dir"
+ else
+ # Use existing repo
+ echo -e "${YELLOW}Using existing repository (no worktree)${NC}"
+
+ # Determine which repo we should be in
+ current_repo=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || echo "")
+
+ # We should be in the fork, not upstream
+ if [ "$current_repo" != "$fork_repo" ]; then
+ echo -e "${RED}Error: Current directory is $current_repo, expected $fork_repo${NC}"
+ echo -e "${YELLOW}Please cd to your fork or use --worktree mode${NC}"
+ return 1
+ fi
+
+ work_dir=$(pwd)
+
+ # Ensure upstream remote exists if this is a fork
+ if [ "$is_cross_repo" = "true" ]; then
+ if ! git remote | grep -q "^upstream$"; then
+ echo -e "${YELLOW}Adding upstream remote: $repo...${NC}"
+ git remote add upstream "https://github.com/$repo.git"
+ fi
+ echo -e "${YELLOW}Fetching from upstream...${NC}"
+ git fetch upstream
+ fi
+
+ # Fetch PR
+ echo -e "${YELLOW}Fetching PR #$pr_number...${NC}"
+ gh pr checkout "$pr_number" -R "$repo" || {
+ echo -e "${RED}Failed to checkout PR${NC}"
+ return 1
+ }
+ fi
+
+ # Determine the correct remote for base branch
+ local base_remote
+ if [ "$is_cross_repo" = "true" ]; then
+ base_remote="upstream"
+ else
+ base_remote="origin"
+ fi
+
+ # Fetch base branch
+ echo -e "${YELLOW}Fetching base branch from $base_remote: $base_branch${NC}"
+ git fetch "$base_remote" "$base_branch" || {
+ echo -e "${RED}Failed to fetch base branch${NC}"
+ return 1
+ }
+
+ # Start rebase
+ echo -e "${YELLOW}Starting rebase onto $base_remote/$base_branch...${NC}"
+ echo ""
+
+ if git rebase "$base_remote/$base_branch"; then
+ echo -e "${GREEN}✓ Rebase completed successfully with no conflicts!${NC}"
+ else
+ echo -e "${YELLOW}Conflicts detected. Starting conflict resolution...${NC}"
+ echo ""
+
+ # Get list of conflicted files
+ conflicted_files=$(git diff --name-only --diff-filter=U)
+
+ if [ -z "$conflicted_files" ]; then
+ echo -e "${RED}Error: Rebase failed but no conflicted files found${NC}"
+ git rebase --abort
+ return 1
+ fi
+
+ echo -e "${BLUE}Conflicted files:${NC}"
+ echo "$conflicted_files" | while read -r file; do
+ echo -e " ${RED}✗${NC} $file"
+ done
+ echo ""
+
+ # Resolve each conflict
+ while read -r file; do
+ echo -e "${BLUE}Resolving: $file${NC}"
+
+ if ! resolve_with_ediff "$file"; then
+ echo -e "${RED}Failed to resolve conflict in $file${NC}"
+ echo -e "${YELLOW}Options:${NC}"
+ echo -e " ${YELLOW}1)${NC} Skip this file and continue"
+ echo -e " ${YELLOW}2)${NC} Abort rebase"
+ echo -e " ${YELLOW}3)${NC} Open file manually"
+ read -rp "Choice [1-3]: " choice
+
+ case $choice in
+ 1)
+ echo -e "${YELLOW}Skipping $file${NC}"
+ continue
+ ;;
+ 2)
+ echo -e "${YELLOW}Aborting rebase${NC}"
+ git rebase --abort
+ return 1
+ ;;
+ 3)
+ ${EDITOR:-vim} "$file"
+ git add "$file"
+ ;;
+ *)
+ echo -e "${RED}Invalid choice, aborting${NC}"
+ git rebase --abort
+ return 1
+ ;;
+ esac
+ fi
+ done <<< "$conflicted_files"
+
+ # Continue rebase
+ echo -e "${YELLOW}Continuing rebase...${NC}"
+ if git rebase --continue; then
+ echo -e "${GREEN}✓ Rebase completed successfully!${NC}"
+ else
+ echo -e "${RED}Failed to continue rebase${NC}"
+ echo -e "${YELLOW}You may need to resolve remaining conflicts manually${NC}"
+ echo -e "${YELLOW}Working directory: $work_dir${NC}"
+ return 1
+ fi
+ fi
+
+ echo ""
+ echo -e "${GREEN}✓ Conflicts resolved successfully!${NC}"
+ echo ""
+
+ # Push changes
+ if [ "$AUTO_PUSH" = true ]; then
+ echo -e "${YELLOW}Force-pushing changes...${NC}"
+ if git push --force-with-lease; then
+ echo -e "${GREEN}✓ Changes pushed successfully!${NC}"
+ else
+ echo -e "${RED}Failed to push changes${NC}"
+ echo -e "${YELLOW}You may need to push manually from: $work_dir${NC}"
+ return 1
+ fi
+ else
+ echo -e "${YELLOW}Changes not pushed. To push manually:${NC}"
+ echo -e " cd $work_dir"
+ echo -e " git push --force-with-lease"
+ fi
+
+ echo ""
+
+ # Cleanup worktree
+ if [ "$USE_WORKTREE" = true ]; then
+ echo -e "${YELLOW}Note: Worktree kept at: $work_dir${NC}"
+ echo -e "${YELLOW}To remove: git worktree remove $work_dir${NC}"
+ fi
+
+ return 0
+}
+
+# Process each selected PR
+while IFS='|' read -r repo pr_number pr_title pr_author pr_branch base_branch pr_url; do
+ pr_number=$(echo "$pr_number" | tr -d '#' | xargs)
+
+ if ! resolve_pr "$repo" "$pr_number" "$pr_title" "$pr_branch" "$base_branch" "$pr_url"; then
+ echo -e "${RED}Failed to resolve PR #$pr_number${NC}"
+ echo ""
+ continue
+ fi
+
+ echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+ echo ""
+done <<< "$selected_prs"
+
+echo -e "${GREEN}Done!${NC}"
tools/gh-resolve-conflicts/README.md
@@ -0,0 +1,236 @@
+# gh-resolve-conflicts
+
+Interactive tool to scan, identify, and resolve merge conflicts in GitHub pull requests using emacs ediff.
+
+## Features
+
+- **Automatic Scanning**: Searches for all open PRs with merge conflicts across organizations
+- **Interactive Selection**: Uses fzf for multi-select PR picking with preview
+- **Fork-Aware**: Automatically detects and clones your fork, sets up upstream remote
+- **Git Worktrees**: Isolates conflict resolution in separate worktrees (or uses existing repo)
+- **Emacs Ediff Integration**: Launches emacs with ediff-merge for 3-way conflict resolution
+- **Automated Rebase**: Handles git rebase workflow automatically against upstream
+- **Auto-Push by Default**: Automatically force-pushes resolved changes to your fork (disable with `--no-push`)
+- **Uses Your Emacs Config**: Launches ediff with your full emacs configuration
+
+## Installation
+
+### With Nix
+
+Add to your Nix configuration:
+
+```nix
+let
+ gh-resolve-conflicts = pkgs.callPackage ./tools/gh-resolve-conflicts { };
+in {
+ home.packages = [ gh-resolve-conflicts ];
+}
+```
+
+### Manual
+
+```bash
+# Make executable
+chmod +x gh-resolve-conflicts.sh
+
+# Optionally link to PATH
+ln -s "$(pwd)/gh-resolve-conflicts.sh" ~/.local/bin/gh-resolve-conflicts
+```
+
+## Dependencies
+
+- `gh` (GitHub CLI)
+- `fzf` (fuzzy finder, for interactive mode)
+- `jq` (JSON processor)
+- `git`
+- `emacs` (for ediff conflict resolution, falls back to git mergetool if not available)
+
+## Usage
+
+### Interactive Mode
+
+Scan all your PRs in the tektoncd organization:
+
+```bash
+gh-resolve-conflicts -o tektoncd
+```
+
+Scan all your PRs across all organizations:
+
+```bash
+gh-resolve-conflicts
+```
+
+### Direct PR Mode
+
+Resolve a specific PR directly:
+
+```bash
+gh-resolve-conflicts tektoncd/mcp-server#94
+```
+
+### Options
+
+```
+-w, --worktree DIR Create worktrees in DIR (default: /tmp/gh-resolve-conflicts-worktrees)
+-n, --no-worktree Use existing repo instead of creating worktrees
+-N, --no-push Do NOT automatically force-push after resolution (default: auto-push)
+-o, --org ORG Filter PRs by organization
+-a, --author AUTHOR Filter PRs by author (default: @me)
+-h, --help Show help message
+```
+
+### Examples
+
+**Interactive selection (auto-pushes by default):**
+```bash
+gh-resolve-conflicts -o tektoncd
+```
+
+**Interactive selection without auto-push:**
+```bash
+gh-resolve-conflicts -o tektoncd -N
+```
+
+**Use existing repo (no worktree):**
+```bash
+cd ~/src/tektoncd/pipeline
+gh-resolve-conflicts -n tektoncd/pipeline#9197
+```
+
+**Custom worktree directory:**
+```bash
+gh-resolve-conflicts -w ~/tmp/worktrees -o tektoncd
+```
+
+## Workflow
+
+1. **Scan**: Tool searches for open PRs with merge conflicts
+2. **Select**: Interactive fzf interface shows conflicting PRs with preview
+3. **Fork Detection**: Automatically identifies your fork of the upstream repository
+4. **Setup**:
+ - Clones your fork (not upstream)
+ - Adds upstream as a remote
+ - Fetches from both fork and upstream
+5. **Checkout**: Creates worktree and checks out PR branch from your fork
+6. **Rebase**: Attempts rebase against `upstream/base-branch`
+7. **Resolve**: When conflicts occur:
+ - Lists all conflicted files
+ - Launches emacs ediff for each file
+ - Ediff provides 3-way merge interface
+ - Automatically marks files as resolved after ediff
+8. **Complete**: Continues rebase after all conflicts resolved
+9. **Push**: Automatically force-pushes changes to your fork (with `--force-with-lease`, unless `--no-push`)
+
+## Emacs Ediff
+
+When conflicts are detected, the tool launches emacs with ediff-merge using your full emacs configuration:
+
+- **Uses Your Config**: Loads your complete emacs setup (themes, packages, keybindings)
+- **3-way merge interface**: See your changes, their changes, and the common ancestor
+- **Visual conflict resolution**: Navigate conflicts with keyboard shortcuts
+- **Automatic staging**: Resolved files are automatically git-added
+
+### Ediff Controls
+
+- `n` / `p`: Next/previous conflict
+- `a`: Choose variant A (yours)
+- `b`: Choose variant B (theirs)
+- `ab` / `ba`: Combine both variants
+- `q`: Quit ediff (saves and marks resolved)
+
+## Worktree Isolation
+
+By default, the tool creates git worktrees for each PR resolution:
+
+**Benefits:**
+- Isolates work from your main repository
+- Multiple PRs can be resolved in parallel
+- Original repo remains untouched
+- Easy cleanup
+- Automatically handles fork setup (origin = your fork, upstream = original repo)
+
+**Location:**
+```
+/tmp/gh-resolve-conflicts-worktrees/
+├── tektoncd-pipeline/
+│ ├── main/ (bare clone of YOUR fork)
+│ │ ├── origin -> vdemeester/tektoncd-pipeline (your fork)
+│ │ └── upstream -> tektoncd/pipeline (upstream)
+│ └── pr-9197/ (worktree for PR #9197)
+└── tektoncd-mcp-server/
+ ├── main/ (bare clone of YOUR fork)
+ │ ├── origin -> vdemeester/tektoncd-mcp-server
+ │ └── upstream -> tektoncd/mcp-server
+ └── pr-94/ (worktree for PR #94)
+```
+
+**Cleanup:**
+```bash
+# Remove specific worktree
+git worktree remove /tmp/gh-resolve-conflicts-worktrees/tektoncd-pipeline/pr-9197
+
+# Or just delete the directory
+rm -rf /tmp/gh-resolve-conflicts-worktrees/
+```
+
+## Use Cases
+
+**Resolve conflicts across multiple repos:**
+```bash
+# Finds all conflicting PRs in tektoncd org and lets you pick which to fix
+gh-resolve-conflicts -o tektoncd
+```
+
+**Quick fix for a single PR:**
+```bash
+# Directly resolve PR #94, auto-pushes when done
+gh-resolve-conflicts tektoncd/mcp-server#94
+```
+
+**Resolve in existing checkout:**
+```bash
+# Use your current repo checkout instead of worktree
+cd ~/src/tektoncd/chains
+gh-resolve-conflicts -n tektoncd/chains#1487
+```
+
+## Troubleshooting
+
+**"emacs not found" warning:**
+- Tool falls back to `git mergetool`
+- Install emacs for better conflict resolution experience
+
+**Ediff doesn't show conflicts:**
+- Some conflicts may need manual resolution
+- Option to open file in $EDITOR is provided
+
+**Rebase fails:**
+- Tool offers options to skip file, abort, or open manually
+- Worktree is preserved for manual intervention
+
+**Can't push after resolution:**
+- Check if you have write access to the repository
+- May need to configure git credentials
+- Use `--force-with-lease` manually if needed
+
+## Related Tools
+
+- `gh-restart-failed`: Restart failed GitHub workflow checks
+- `gh pr checkout`: GitHub CLI built-in PR checkout
+- `gh-pr-worktree`: GitHub CLI extension for PR worktrees
+
+## Contributing
+
+This tool follows the same structure as other tools in `~/src/home/tools/`:
+
+```
+gh-resolve-conflicts/
+├── gh-resolve-conflicts.sh Main script
+├── default.nix Nix package definition
+└── README.md Documentation
+```
+
+## License
+
+MIT