Add gpr GitHub PR helper and include in git nix module
This commit is contained in:
parent
ec2e093a24
commit
8b54a94c54
5 changed files with 465 additions and 19 deletions
445
flakes/common/nix_modules/git/gpr.func.sh
Normal file
445
flakes/common/nix_modules/git/gpr.func.sh
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
# gpr - GitHub PR helper commands
|
||||
# Usage: gpr <command> [args]
|
||||
# gpr create - Create a new PR from current branch
|
||||
# gpr update - Update existing PR description
|
||||
|
||||
gpr() {
|
||||
local cmd="${1:-}"
|
||||
shift 2>/dev/null || true
|
||||
|
||||
case "$cmd" in
|
||||
create) gpr_create "$@" ;;
|
||||
update) gpr_update "$@" ;;
|
||||
-h|--help|help|"")
|
||||
cat <<EOF
|
||||
Usage: gpr <command>
|
||||
|
||||
Commands:
|
||||
create Create a new PR from current branch
|
||||
update Update existing PR description
|
||||
|
||||
Both commands generate a PR description using LLM based on:
|
||||
- Full diff against base branch
|
||||
- All commit messages on the branch
|
||||
- Opens in \$EDITOR for review before submitting
|
||||
EOF
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $cmd" >&2
|
||||
echo "Run 'gpr help' for usage." >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_gpr_check_deps() {
|
||||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
echo "Not inside a git repository." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "Missing dependency: gh (GitHub CLI)" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "Missing dependency: curl" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "Missing dependency: jq" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_gpr_get_base_branch() {
|
||||
# Try to get the base branch from git config or default to origin default
|
||||
local base
|
||||
base=$(git config --get init.defaultBranch 2>/dev/null || true)
|
||||
if [ -z "$base" ]; then
|
||||
base=$(git remote show origin 2>/dev/null | grep "HEAD branch" | sed 's/.*: //' || true)
|
||||
fi
|
||||
if [ -z "$base" ]; then
|
||||
base="main"
|
||||
fi
|
||||
echo "$base"
|
||||
}
|
||||
|
||||
_gpr_get_current_branch() {
|
||||
git rev-parse --abbrev-ref HEAD
|
||||
}
|
||||
|
||||
_gpr_generate_description() {
|
||||
local base_branch="$1"
|
||||
local existing_description="${2:-}"
|
||||
|
||||
local LITELLM_BASE_URL="http://h001.net.joshuabell.xyz:8094"
|
||||
local LITELLM_MODEL="azure-gpt-5-mini-2025-08-07"
|
||||
|
||||
local current_branch
|
||||
current_branch=$(_gpr_get_current_branch)
|
||||
|
||||
# Get diff against base branch
|
||||
local diff
|
||||
diff=$(git diff --no-color --no-ext-diff "${base_branch}...HEAD" 2>/dev/null || git diff --no-color --no-ext-diff "${base_branch}..HEAD" 2>/dev/null || true)
|
||||
|
||||
# Truncate diff if too large
|
||||
local max_diff_chars=15000
|
||||
diff=$(printf "%s" "$diff" | head -c "$max_diff_chars")
|
||||
|
||||
# Get commit messages
|
||||
local commits
|
||||
commits=$(git log --oneline "${base_branch}..HEAD" 2>/dev/null || true)
|
||||
|
||||
# Get detailed commit messages
|
||||
local commit_details
|
||||
commit_details=$(git log --pretty=format:"%h %s%n%b" "${base_branch}..HEAD" 2>/dev/null || true)
|
||||
|
||||
# Get file changes summary
|
||||
local files_changed
|
||||
files_changed=$(git diff --stat "${base_branch}...HEAD" 2>/dev/null || git diff --stat "${base_branch}..HEAD" 2>/dev/null || true)
|
||||
|
||||
local prompt
|
||||
if [ -n "$existing_description" ]; then
|
||||
prompt=$(cat <<EOF
|
||||
Update the following GitHub Pull Request description based on the current state of the branch.
|
||||
The existing description may be stale - update it to reflect the actual changes.
|
||||
|
||||
Current PR description:
|
||||
${existing_description}
|
||||
|
||||
---
|
||||
|
||||
Branch: ${current_branch}
|
||||
Base: ${base_branch}
|
||||
|
||||
Commits on this branch:
|
||||
${commits}
|
||||
|
||||
Detailed commit messages:
|
||||
${commit_details}
|
||||
|
||||
Files changed:
|
||||
${files_changed}
|
||||
|
||||
Diff (truncated):
|
||||
${diff}
|
||||
|
||||
---
|
||||
|
||||
Generate an updated PR description in Markdown format. Include:
|
||||
1. A brief summary (2-3 sentences) of what this PR does
|
||||
2. A bullet list of key changes
|
||||
3. Any breaking changes or migration notes if applicable
|
||||
|
||||
Output ONLY the PR description body (no title, no markdown code fences around the whole thing).
|
||||
EOF
|
||||
)
|
||||
else
|
||||
prompt=$(cat <<EOF
|
||||
Generate a GitHub Pull Request description for the following changes.
|
||||
|
||||
Branch: ${current_branch}
|
||||
Base: ${base_branch}
|
||||
|
||||
Commits on this branch:
|
||||
${commits}
|
||||
|
||||
Detailed commit messages:
|
||||
${commit_details}
|
||||
|
||||
Files changed:
|
||||
${files_changed}
|
||||
|
||||
Diff (truncated):
|
||||
${diff}
|
||||
|
||||
---
|
||||
|
||||
Generate a PR description in Markdown format. Include:
|
||||
1. A brief summary (2-3 sentences) of what this PR does
|
||||
2. A bullet list of key changes
|
||||
3. Any breaking changes or migration notes if applicable
|
||||
|
||||
Output ONLY the PR description body (no title, no markdown code fences around the whole thing).
|
||||
EOF
|
||||
)
|
||||
fi
|
||||
|
||||
local payload
|
||||
payload=$(jq -n \
|
||||
--arg model "$LITELLM_MODEL" \
|
||||
--arg content "$prompt" \
|
||||
'{
|
||||
model: $model,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: "You write clear, concise GitHub Pull Request descriptions. Focus on the what and why of changes."
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: $content
|
||||
}
|
||||
],
|
||||
temperature: 0.3,
|
||||
max_tokens: 1024
|
||||
}')
|
||||
|
||||
local curl_out http_code body
|
||||
curl_out=$(curl -sS -w "\n%{http_code}" \
|
||||
-X POST "${LITELLM_BASE_URL}/v1/chat/completions" \
|
||||
-H "Content-Type: application/json" \
|
||||
${LITELLM_API_KEY:+-H "Authorization: Bearer ${LITELLM_API_KEY}"} \
|
||||
-d "$payload") || return 1
|
||||
|
||||
http_code=$(printf "%s" "$curl_out" | tail -n 1)
|
||||
body=$(printf "%s" "$curl_out" | sed '$d')
|
||||
|
||||
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
|
||||
echo "LiteLLM request failed (HTTP $http_code)." >&2
|
||||
printf "%s\n" "$body" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local message
|
||||
message=$(printf "%s" "$body" | jq -r '
|
||||
.choices[0].message.content
|
||||
| if type == "string" then .
|
||||
elif type == "array" then (map(select(.type=="text") | .text) | join(""))
|
||||
else ""
|
||||
end
|
||||
' 2>/dev/null || true)
|
||||
|
||||
if [ -z "$message" ] || [ "$message" = "null" ]; then
|
||||
echo "Failed to parse model response." >&2
|
||||
printf "%s\n" "$body" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf "%s" "$message"
|
||||
}
|
||||
|
||||
_gpr_generate_title() {
|
||||
local base_branch="$1"
|
||||
|
||||
local LITELLM_BASE_URL="http://h001.net.joshuabell.xyz:8094"
|
||||
local LITELLM_MODEL="azure-gpt-5-mini-2025-08-07"
|
||||
|
||||
local current_branch
|
||||
current_branch=$(_gpr_get_current_branch)
|
||||
|
||||
# Get commit messages
|
||||
local commits
|
||||
commits=$(git log --oneline "${base_branch}..HEAD" 2>/dev/null || true)
|
||||
|
||||
local prompt
|
||||
prompt=$(cat <<EOF
|
||||
Generate a concise GitHub Pull Request title for the following changes.
|
||||
|
||||
Branch: ${current_branch}
|
||||
Commits:
|
||||
${commits}
|
||||
|
||||
Rules:
|
||||
- Output ONLY the PR title (one line)
|
||||
- Max 72 characters
|
||||
- Imperative mood (e.g., "Add feature" not "Added feature")
|
||||
- No quotes, no trailing period
|
||||
EOF
|
||||
)
|
||||
|
||||
local payload
|
||||
payload=$(jq -n \
|
||||
--arg model "$LITELLM_MODEL" \
|
||||
--arg content "$prompt" \
|
||||
'{
|
||||
model: $model,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: "You write concise PR titles."
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: $content
|
||||
}
|
||||
],
|
||||
temperature: 0.2,
|
||||
max_tokens: 64
|
||||
}')
|
||||
|
||||
local curl_out http_code body
|
||||
curl_out=$(curl -sS -w "\n%{http_code}" \
|
||||
-X POST "${LITELLM_BASE_URL}/v1/chat/completions" \
|
||||
-H "Content-Type: application/json" \
|
||||
${LITELLM_API_KEY:+-H "Authorization: Bearer ${LITELLM_API_KEY}"} \
|
||||
-d "$payload") || return 1
|
||||
|
||||
http_code=$(printf "%s" "$curl_out" | tail -n 1)
|
||||
body=$(printf "%s" "$curl_out" | sed '$d')
|
||||
|
||||
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
|
||||
echo "LiteLLM request failed (HTTP $http_code)." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local message
|
||||
message=$(printf "%s" "$body" | jq -r '.choices[0].message.content' 2>/dev/null || true)
|
||||
message=$(printf "%s" "$message" | sed -n '1p' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
|
||||
if [ -z "$message" ] || [ "$message" = "null" ]; then
|
||||
# Fallback to branch name
|
||||
echo "$current_branch"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf "%s" "$message"
|
||||
}
|
||||
|
||||
gpr_create() {
|
||||
_gpr_check_deps || return 1
|
||||
|
||||
local current_branch
|
||||
current_branch=$(_gpr_get_current_branch)
|
||||
|
||||
local base_branch
|
||||
base_branch=$(_gpr_get_base_branch)
|
||||
|
||||
# Check we're not on the default branch
|
||||
if [ "$current_branch" = "$base_branch" ]; then
|
||||
echo "Cannot create PR from the default branch ($base_branch)." >&2
|
||||
echo "Create a feature branch first." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if PR already exists
|
||||
local existing_pr
|
||||
existing_pr=$(gh pr view --json number,url 2>/dev/null || true)
|
||||
if [ -n "$existing_pr" ]; then
|
||||
local pr_url
|
||||
pr_url=$(printf "%s" "$existing_pr" | jq -r '.url')
|
||||
echo "A PR already exists for this branch: $pr_url" >&2
|
||||
echo "Use 'gpr update' to update the description instead." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if branch is pushed to remote
|
||||
local remote_branch
|
||||
remote_branch=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || true)
|
||||
if [ -z "$remote_branch" ]; then
|
||||
echo "Branch not pushed to remote. Pushing now..."
|
||||
git push -u origin "$current_branch" || return 1
|
||||
fi
|
||||
|
||||
echo "Generating PR description with LLM..."
|
||||
local description
|
||||
description=$(_gpr_generate_description "$base_branch") || return 1
|
||||
|
||||
echo "Generating PR title..."
|
||||
local title
|
||||
title=$(_gpr_generate_title "$base_branch") || return 1
|
||||
|
||||
# Create temp file for editing
|
||||
local tmpfile
|
||||
tmpfile=$(mktemp --suffix=.md)
|
||||
trap "rm -f '$tmpfile'" EXIT
|
||||
|
||||
cat > "$tmpfile" <<EOF
|
||||
${title}
|
||||
---
|
||||
${description}
|
||||
EOF
|
||||
|
||||
echo "Opening editor for review..."
|
||||
${EDITOR:-vi} "$tmpfile"
|
||||
|
||||
# Check if user saved (file not empty after title line)
|
||||
if [ ! -s "$tmpfile" ]; then
|
||||
echo "Aborted: empty file." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parse title (first line) and body (after ---)
|
||||
local final_title final_body
|
||||
final_title=$(head -n 1 "$tmpfile")
|
||||
final_body=$(tail -n +3 "$tmpfile")
|
||||
|
||||
if [ -z "$final_title" ]; then
|
||||
echo "Aborted: no title provided." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Creating PR..."
|
||||
gh pr create --title "$final_title" --body "$final_body" --base "$base_branch"
|
||||
}
|
||||
|
||||
gpr_update() {
|
||||
_gpr_check_deps || return 1
|
||||
|
||||
local current_branch
|
||||
current_branch=$(_gpr_get_current_branch)
|
||||
|
||||
local base_branch
|
||||
base_branch=$(_gpr_get_base_branch)
|
||||
|
||||
# Check if PR exists
|
||||
local existing_pr
|
||||
existing_pr=$(gh pr view --json number,title,body,url 2>/dev/null || true)
|
||||
if [ -z "$existing_pr" ]; then
|
||||
echo "No PR exists for this branch." >&2
|
||||
echo "Use 'gpr create' to create one first." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local pr_url pr_title pr_body
|
||||
pr_url=$(printf "%s" "$existing_pr" | jq -r '.url')
|
||||
pr_title=$(printf "%s" "$existing_pr" | jq -r '.title')
|
||||
pr_body=$(printf "%s" "$existing_pr" | jq -r '.body')
|
||||
|
||||
echo "Updating PR: $pr_url"
|
||||
echo "Generating updated description with LLM..."
|
||||
|
||||
local description
|
||||
description=$(_gpr_generate_description "$base_branch" "$pr_body") || return 1
|
||||
|
||||
# Create temp file for editing
|
||||
local tmpfile
|
||||
tmpfile=$(mktemp --suffix=.md)
|
||||
trap "rm -f '$tmpfile'" EXIT
|
||||
|
||||
cat > "$tmpfile" <<EOF
|
||||
${pr_title}
|
||||
---
|
||||
${description}
|
||||
EOF
|
||||
|
||||
echo "Opening editor for review..."
|
||||
${EDITOR:-vi} "$tmpfile"
|
||||
|
||||
# Check if user saved
|
||||
if [ ! -s "$tmpfile" ]; then
|
||||
echo "Aborted: empty file." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parse title and body
|
||||
local final_title final_body
|
||||
final_title=$(head -n 1 "$tmpfile")
|
||||
final_body=$(tail -n +3 "$tmpfile")
|
||||
|
||||
if [ -z "$final_title" ]; then
|
||||
echo "Aborted: no title provided." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Updating PR..."
|
||||
gh pr edit --title "$final_title" --body "$final_body"
|
||||
|
||||
echo "PR updated: $pr_url"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue