You are viewing a plain text version of this content. The canonical link for it is here.
Posted to cvs@httpd.apache.org by jo...@apache.org on 2026/08/28 13:40:33 UTC

svn commit: r1937574 - httpd/dev-tools/trunk/github

Author: jorton
Date: Fri Aug 28 13:40:33 2026
New Revision: 1937574

Log:
Evolved version of apply_trunk_pr.sh which can ingest a
GitHub PR into a series of commits (rather than just one),
reviewing/rewriting commit messages on the fly. Includes
options for using Claude to help with the above.

Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>

Added:
   httpd/dev-tools/trunk/github/gh-pr-apply.sh   (contents, props changed)

Added: httpd/dev-tools/trunk/github/gh-pr-apply.sh
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ httpd/dev-tools/trunk/github/gh-pr-apply.sh	Fri Aug 28 13:40:33 2026	(r1937574)
@@ -0,0 +1,252 @@
+#!/bin/bash
+set -euo pipefail
+
+REPO="apache/httpd"
+GITHUB_URL="https://github.com/$REPO"
+CLOG="clog"
+current_user=$(git config user.name 2>/dev/null || true)
+
+# Colours (disabled if stdout is not a terminal)
+if [ -t 1 ]; then
+    BOLD=$'\033[1m'
+    DIM=$'\033[2m'
+    RED=$'\033[31m'
+    GREEN=$'\033[32m'
+    YELLOW=$'\033[33m'
+    CYAN=$'\033[36m'
+    RESET=$'\033[0m'
+else
+    BOLD="" DIM="" RED="" GREEN="" YELLOW="" CYAN="" RESET=""
+fi
+
+PROMPT="${BOLD}${CYAN}[c]${RESET}ommit ${BOLD}${CYAN}[e]${RESET}dit ${BOLD}${CYAN}[g]${RESET}enerate ${BOLD}${CYAN}[p]${RESET}rompt ${BOLD}${CYAN}[v]${RESET}alidate ${BOLD}${CYAN}[t]${RESET}est ${BOLD}${CYAN}[s]${RESET}kip ${BOLD}${CYAN}[r]${RESET}evert ${BOLD}${RED}[a]${RESET}bort${BOLD}${CYAN} > ${RESET}"
+
+usage() {
+    echo "Usage: $0 <PR-number> [commit-URL]" >&2
+    exit 1
+}
+
+write_clog() {
+    local msg_text="$1"
+    local trimmed last
+    trimmed=$(printf '%s' "$msg_text" | sed -e :a -e '/^\n*$/{$d;N;ba;}')
+    last=$(printf '%s' "$trimmed" | tail -1)
+    {
+        echo "$trimmed"
+        if ! echo "$last" | grep -qE '^[A-Za-z-]+:'; then
+            echo
+        fi
+        if [ -z "$current_user" ] || [ "$author_name" != "$current_user" ]; then
+            echo "Submitted by: $author"
+        fi
+        if [ -z "$ONLY_SHA" ] && [ "$i" -eq "$total" ]; then
+            echo "GitHub: closes #$PR"
+        else
+            echo "GitHub: PR #$PR"
+        fi
+    } > "$CLOG"
+}
+
+generate_msg() {
+    local extra="${1:-}"
+    local prompt="Write a GNU ChangeLog-style commit message for this diff. \
+The first line should be a short summary. Then list changed files \
+with (function): description. Use two-space indentation for \
+follow-on lines. Output ONLY the commit message, nothing else."
+    if [ -n "$extra" ]; then
+        prompt="$prompt Additional instructions: $extra"
+    fi
+    echo "${DIM}Generating commit message with Claude...${RESET}"
+    local claude_msg
+    claude_msg=$(svn diff | claude --print "$prompt")
+    if [ -n "$claude_msg" ]; then
+        write_clog "$claude_msg"
+    fi
+    echo
+    echo "${DIM}--- generated commit message ---${RESET}"
+    cat "$CLOG"
+    echo "${DIM}--- end ---${RESET}"
+    echo
+}
+
+[ $# -ge 1 ] && [ $# -le 2 ] || usage
+
+PR="$1"
+ONLY_SHA=""
+if [ $# -eq 2 ]; then
+    if [[ "$2" =~ /([0-9a-f]+)$ ]]; then
+        ONLY_SHA="${BASH_REMATCH[1]}"
+    else
+        echo "${RED}Error: cannot extract commit hash from URL: $2${RESET}" >&2
+        exit 1
+    fi
+fi
+
+if ! [[ "$PR" =~ ^[0-9]+$ ]]; then
+    echo "${RED}Error: PR number must be numeric.${RESET}" >&2
+    exit 1
+fi
+
+if svn_st=$(svn st | grep -v '^?') && [ -n "$svn_st" ]; then
+    echo "${RED}Error: working copy has uncommitted changes:${RESET}" >&2
+    echo "$svn_st" >&2
+    exit 1
+fi
+
+echo "${BOLD}Fetching commits for ${CYAN}$GITHUB_URL/pull/$PR${RESET}${BOLD} ...${RESET}"
+
+commits=$(gh api "repos/$REPO/pulls/$PR/commits" --jq '.[].sha')
+if [ -z "$commits" ]; then
+    echo "${RED}Error: no commits found for PR #$PR${RESET}" >&2
+    exit 1
+fi
+
+total=$(echo "$commits" | wc -l)
+echo "Found ${BOLD}$total${RESET} commit(s)."
+echo
+
+i=0
+for sha in $commits; do
+    i=$((i + 1))
+    short="${sha:0:12}"
+
+    if [ -n "$ONLY_SHA" ] && [[ "$sha" != "$ONLY_SHA"* ]]; then
+        echo "${DIM}Skipping commit $i/$total: $short (not selected)${RESET}"
+        continue
+    fi
+
+    msg=$(gh api "repos/$REPO/commits/$sha" --jq '.commit.message')
+    author_name=$(gh api "repos/$REPO/commits/$sha" --jq '.commit.author.name')
+    author_email=$(gh api "repos/$REPO/commits/$sha" --jq '.commit.author.email')
+    author="$author_name <${author_email/@/ }>"
+
+    echo "${BOLD}${GREEN}=== Commit $i/$total: $short ===${RESET}"
+    echo "$msg"
+    echo "${DIM}Submitted by: $author${RESET}"
+    echo
+
+    # Fetch the patch
+    patch_file=$(mktemp /tmp/gh-pr-patch.XXXXXX)
+    trap "rm -f '$patch_file'" EXIT
+    curl -sL "$GITHUB_URL/commit/$sha.patch" -o "$patch_file"
+
+    # Apply the patch
+    apply_args=""
+    if git apply --check -R "$patch_file" 2>/dev/null; then
+        echo "${YELLOW}Patch appears to be already applied.${RESET}"
+        read -rp "${BOLD}${CYAN}Skip / Abort? [s/a] > ${RESET}" ans
+        case "$ans" in
+            s|S) echo "Skipping."; rm -f "$patch_file"; echo; continue ;;
+            *)   echo "Aborting."; rm -f "$patch_file"; exit 1 ;;
+        esac
+    elif ! git apply --check "$patch_file" 2>/dev/null; then
+        echo "${YELLOW}Warning: patch does not apply cleanly.${RESET}"
+        read -rp "${BOLD}${CYAN}Try with fuzz / Skip / Abort? [f/s/a] > ${RESET}" ans
+        case "$ans" in
+            f|F) apply_args="--recount -C1" ;;
+            s|S) echo "Skipping."; rm -f "$patch_file"; echo; continue ;;
+            *)   echo "Aborting."; rm -f "$patch_file"; exit 1 ;;
+        esac
+    fi
+
+    # Find new files in the patch (--- /dev/null means a new file)
+    new_files=$(awk '/^--- \/dev\/null/{getline; if (/^\+\+\+ b\//) print substr($0,7)}' "$patch_file")
+
+    git apply $apply_args "$patch_file"
+    rm -f "$patch_file"
+
+    # svn add only files newly created by this patch
+    if [ -n "$new_files" ]; then
+        echo "$new_files" | while IFS= read -r f; do
+            echo "${GREEN}Adding new file: $f${RESET}"
+            svn add --parents "$f"
+        done
+    fi
+
+    # Check for compile errors
+    if ! make -sj8 >/dev/null; then
+        echo "${RED}Warning: build failed after applying this patch.${RESET}"
+        read -rp "${BOLD}${CYAN}Continue anyway? [y/N] > ${RESET}" ans
+        case "$ans" in
+            y|Y) ;;
+            *)   echo "Aborting."; exit 1 ;;
+        esac
+    fi
+
+    write_clog "$msg"
+
+    echo
+    echo "${DIM}--- svn diff ---${RESET}"
+    svn diff
+    echo "${DIM}--- end svn diff ---${RESET}"
+    echo
+    echo "${DIM}--- proposed commit message ---${RESET}"
+    cat "$CLOG"
+    echo "${DIM}--- end ---${RESET}"
+    echo
+
+    read -rp "$PROMPT" ans
+    while true; do
+        case "$ans" in
+            c|C)
+                svn commit -F "$CLOG"
+                echo "${GREEN}Committed.${RESET}"
+                break
+                ;;
+            e|E)
+                ${EDITOR:-vi} "$CLOG"
+                echo
+                echo "${DIM}--- updated commit message ---${RESET}"
+                cat "$CLOG"
+                echo "${DIM}--- end ---${RESET}"
+                echo
+                read -rp "$PROMPT" ans
+                ;;
+            g|G)
+                generate_msg
+                read -rp "$PROMPT" ans
+                ;;
+            p|P)
+                read -rp "${BOLD}Additional instructions for Claude:${RESET} " extra_prompt
+                generate_msg "$extra_prompt"
+                read -rp "$PROMPT" ans
+                ;;
+            v|V)
+                echo "${DIM}Validating diff with Claude...${RESET}"
+                svn diff | claude --print \
+                    "Review this diff for correctness, security, and style issues. \
+Flag any bugs, potential crashes, memory safety problems, or logic errors. \
+Be concise. If the change looks correct, say so briefly."
+                echo
+                read -rp "$PROMPT" ans
+                ;;
+            t|T)
+                echo "${DIM}Running tests...${RESET}"
+                make check-test-framework || echo "${YELLOW}Warning: tests failed.${RESET}"
+                echo
+                read -rp "$PROMPT" ans
+                ;;
+            s|S)
+                echo "Skipping commit (changes remain in working copy)."
+                break
+                ;;
+            r|R)
+                echo "${YELLOW}Reverting working copy...${RESET}"
+                svn revert -R .
+                break
+                ;;
+            a|A)
+                echo "${RED}Aborting, reverting working copy...${RESET}"
+                svn revert -R .
+                exit 1
+                ;;
+            *)
+                read -rp "Invalid choice. $PROMPT" ans
+                ;;
+        esac
+    done
+
+    echo
+done
+
+echo "${BOLD}${GREEN}Done.${RESET} Processed $total commit(s) from PR #$PR."