Skip to content

Instantly share code, notes, and snippets.

@dashw00d
Created December 22, 2025 11:34
Show Gist options
  • Select an option

  • Save dashw00d/a78c832fca25f976d24f4cc059dd10f5 to your computer and use it in GitHub Desktop.

Select an option

Save dashw00d/a78c832fca25f976d24f4cc059dd10f5 to your computer and use it in GitHub Desktop.
Git PR Toolkit - Helpers for a Smooth PR Workflow
##### ───────────────────────── zsh git/PR toolkit ───────────────────────── #####
# ── helpers ──────────────────────────────────────────────────────────────────
_slug() {
print -r -- "$*" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-|-$//g'
}
_base_branch() {
local b
b="$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null)"
b="${b#origin/}"
[[ -n "$b" ]] && { print -r -- "$b"; return; }
git show-ref --verify --quiet refs/heads/main && { print -r -- main; return; }
git show-ref --verify --quiet refs/heads/master && { print -r -- master; return; }
print -r -- main
}
_repo_slug() {
local o r
o="$(git config --get remote.origin.url)" || return 1
o="${o%.git}"
if [[ "$o" == git@github.com:* ]]; then
r="${o#git@github.com:}"
elif [[ "$o" == https://github.com/* ]]; then
r="${o#https://github.com/}"
else
return 1
fi
print -r -- "$r"
}
_open_url() {
command -v open >/dev/null && open "$1" \
|| xdg-open "$1" >/dev/null 2>&1 \
|| print -r -- "$1"
}
_gh_api() {
[[ -z "$GITHUB_TOKEN" ]] && return 1
local method="$1" path="$2" data="$3"
curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
-X "$method" ${data:+-d "$data"} "https://api.github.com$path"
}
_json_pr_payload() {
# args: title base head [body]
local title="$1" base="$2" head="$3" body="$4"
if command -v jq >/dev/null 2>&1; then
jq -cn --arg t "$title" --arg b "$base" --arg h "$head" --arg bd "$body" \
'{title:$t, base:$b, head:$h, draft:true} + ( ($bd|length)>0 ? {body:$bd} : {} )'
else
printf '{"title":"%s","base":"%s","head":"%s","draft":true}' \
"${title//\"/\\\"}" "${base//\"/\\\"}" "${head//\"/\\\"}"
fi
}
_pr_number_for_branch() {
local repo branch owner json
repo="$(_repo_slug)" || return 1
branch="$(git branch --show-current)"
owner="${repo%%/*}"
json="$(_gh_api GET "/repos/$repo/pulls?state=open&head=$owner:$branch")" || return 1
print -r -- "$json" | sed -nE 's/.*"number":[[:space:]]*([0-9]+).*/\1/p' | head -n1
}
# helper: stage-all safely when user explicitly asked
_stage_all() {
git add -A || return $?
}
# helper: commit ONLY what is already staged
_commit_staged_only() {
local msg="$1" noverify="$2" signoff="$3"
# bail if nothing staged
git diff --cached --quiet && {
echo "No staged changes to commit."; return 1;
}
local args=()
[[ "$noverify" -eq 1 ]] && args+=("--no-verify")
[[ "$signoff" -eq 1 ]] && args+=("--signoff")
git commit -m "$msg" "${args[@]}"
}
_push_upstream() {
git rev-parse --abbrev-ref --symbolic-full-name @{u} >/dev/null 2>&1 \
&& git push || git push -u origin HEAD
}
_create_or_open_pr() {
local title="$1" body="$2" base="$3" head="$4"
local repo pr_json pr_url compare_url
repo="$(_repo_slug)" || {
echo "Pushed. No GitHub origin detected; skipping PR creation."
return 0
}
if [[ -n "$GITHUB_TOKEN" ]]; then
local payload="$(_json_pr_payload "$title" "$base" "$head" "$body")"
pr_json="$(_gh_api POST "/repos/$repo/pulls" "$payload" 2>/dev/null)" || true
pr_url="$(print -r -- "$pr_json" | sed -nE 's/.*"html_url":[[:space:]]*"([^"]+)".*/\1/p')"
if [[ -n "$pr_url" ]]; then
echo "Draft PR created: $pr_url"
_open_url "$pr_url"
else
compare_url="https://github.com/$repo/compare/$base...$head?expand=1"
echo "Pushed. Could not auto-create PR."
echo "Open compare: $compare_url"
_open_url "$compare_url"
fi
else
compare_url="https://github.com/$repo/compare/$base...$head?expand=1"
echo "Pushed. Set \$GITHUB_TOKEN to auto-create PRs."
echo "Open compare: $compare_url"
_open_url "$compare_url"
fi
}
# gbranch <new-branch-name>
# - On main: ensure clean tree, ff-only pull, branch from origin/main, rebase to origin/main
# - Not on main: require HEAD == origin/main or fail; branch from HEAD, rebase to origin/main
gbranch() {
local new="$1"
[[ -z "$new" ]] && { echo "Usage: gbranch <new-branch-name>"; return 1; }
# Helper: resolve base branch (main/master or origin HEAD)
_gb__base() {
local b
b="$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null)"
b="${b#origin/}"
[[ -n "$b" ]] && { printf "%s" "$b"; return; }
git show-ref --verify --quiet refs/heads/main && { printf "main"; return; }
git show-ref --verify --quiet refs/heads/master && { printf "master"; return; }
printf "main"
}
# Ensure we're in a git repo
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo "Not a git repo."; return 1; }
local base cur origin_base head_hash origin_base_hash
base="$(_gb__base)"
cur="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" || return 1
# Get latest refs
git fetch origin --prune || { echo "Failed to fetch origin."; return 1; }
# Verify origin/base exists
if ! git show-ref --verify --quiet "refs/remotes/origin/$base"; then
echo "Remote branch origin/$base not found."
return 1
fi
origin_base="origin/$base"
origin_base_hash="$(git rev-parse --verify "$origin_base")" || return 1
head_hash="$(git rev-parse --verify HEAD)" || return 1
if [[ "$cur" == "$base" ]]; then
# On main/master: require clean tree before pulling
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "Working tree not clean on $base. Commit/stash your changes first."
return 1
fi
# Fast-forward main
git pull --ff-only || { echo "Failed to fast-forward $base."; return 1; }
# Create from latest origin/main to be explicit
if git show-ref --verify --quiet "refs/heads/$new"; then
echo "Branch '$new' already exists."
return 1
fi
git switch -c "$new" "$origin_base" || return 1
# Safety: rebase to origin/main (should be no-op)
git rebase --rebase-merges --autostash "$origin_base" || return 1
echo "✅ Created and switched to '$new' based on $origin_base."
return 0
else
# Not on main: require exact equality with origin/main
if [[ "$head_hash" != "$origin_base_hash" ]]; then
echo "❌ Current branch '$cur' is not identical to $origin_base."
echo " HEAD: $head_hash"
echo " $origin_base: $origin_base_hash"
echo " Move to '$base' and sync (e.g., 'git switch $base && git pull --ff-only'),"
echo " or rebase your branch to $origin_base before running gbranch."
return 1
fi
# Equal to origin/main — allowed
if git show-ref --verify --quiet "refs/heads/$new"; then
echo "Branch '$new' already exists."
return 1
fi
git switch -c "$new" || return 1
# Safety: rebase to origin/main (no-op)
git rebase --rebase-merges --autostash "$origin_base" || return 1
echo "✅ Created and switched to '$new' (from $cur @ $origin_base)."
return 0
fi
}
# ── gship: create branch (from base) OR ship staged work on feature branch ────
# Usage:
# gship "Title" [Body...]
# gship --all "Title" [Body...] # stage *everything* first
#
# Behavior:
# - If you're on base (main/master): we assume you're starting new work.
# 1. sync base
# 2. make new feat/branch
# 3. stage all
# 4. commit
# 5. push + open draft PR
#
# - If you're on a feature branch:
# - default: commit ONLY staged changes
# - --all: stage everything first, then commit
#
gship() {
local add_all=0
while [[ "$1" == --* ]]; do
case "$1" in
--all) add_all=1 ;;
*) echo "Unknown flag: $1" ;;
esac
shift
done
local title="$1"; shift
local body="$*"
[[ -z "$title" ]] && { echo "Usage: gship [--all] \"Title\" [Body]"; return 1; }
local base cur ts name newbranch
base="$(_base_branch)" || return $?
cur="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" || return $?
git fetch origin --prune || return $?
if [[ "$cur" == "$base" ]]; then
# starting new work off base
git switch "$base" || return $?
git pull --ff-only || return $?
ts="$(date +%Y%m%d-%H%M)"
name="$(_slug "$title")"
newbranch="feat/${name:-change}-${ts}"
git switch -c "$newbranch" || return $?
# always stage-all on FIRST ship-from-main. This matches your habit.
git add -A || return $?
git commit -m "$title" || return $?
_push_upstream || return $?
_create_or_open_pr "$title" "$body" "$base" "$newbranch"
return 0
fi
# we're on a feature branch
# if user said --all, stage all first; otherwise leave the index alone
if [[ "$add_all" -eq 1 ]]; then
_stage_all || return $?
fi
# commit ONLY what's staged
_commit_staged_only "$title" 0 0 || return $?
# push branch
_push_upstream || return $?
# Try to open (or create) a PR if one doesn't exist
local prnum
prnum="$(_pr_number_for_branch)" || true
if [[ -n "$prnum" ]]; then
# PR already exists, just show it
local repo pr_url
repo="$(_repo_slug)" || return 0
pr_url="https://github.com/$repo/pull/$prnum"
echo "Updated branch & pushed. PR already exists: $pr_url"
_open_url "$pr_url"
else
# create new draft PR
_create_or_open_pr "$title" "$body" "$base" "$cur"
fi
}
# ── gfix: follow-up commit/push helper ────────────────────────────────────────
# gfix [-a|--amend] [--no-push] [-n|--no-verify] [-S|--signoff] [-f|--force-main] "msg"
gfix() {
local push=1 amend=0 noverify=0 signoff=0 force_main=0
while [[ "$1" == -* ]]; do
case "$1" in
-a|--amend) amend=1 ;;
--no-push) push=0 ;;
-n|--no-verify) noverify=1 ;;
-S|--signoff) signoff=1 ;;
-f|--force-main) force_main=1 ;;
--) shift; break ;;
*) break ;;
esac
shift
done
local msg="$*"
local cur base
cur="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" || { echo "Not a git repo."; return 1; }
base="$(_base_branch)"
if [[ "$cur" == "$base" && "$force_main" -ne 1 ]]; then
echo "You're on $base. Use gship to branch+PR, or --force-main to commit here."
return 1
fi
if [[ "$amend" -eq 0 && -z "$msg" ]]; then
vared -p "Commit message [chore: quick fix]: " -c msg
[[ -z "$msg" ]] && msg="chore: quick fix"
fi
git add -A || return $?
local args=()
[[ "$noverify" -eq 1 ]] && args+=("--no-verify")
[[ "$signoff" -eq 1 ]] && args+=("--signoff")
if [[ "$amend" -eq 1 ]]; then
if [[ -n "$msg" ]]; then
git commit --amend -m "$msg" "${args[@]}" || { echo "Nothing to amend."; return 1; }
else
git commit --amend --no-edit "${args[@]}" || { echo "Nothing to amend."; return 1; }
fi
else
git diff --cached --quiet && { echo "No staged changes to commit."; return 1; }
git commit -m "$msg" "${args[@]}" || return $?
fi
if [[ "$push" -eq 1 ]]; then
_push_upstream
fi
echo "✓ $( [[ "$amend" -eq 1 ]] && echo 'Amended' || echo 'Committed' ) on $cur$( [[ "$push" -eq 1 ]] && echo ' and pushed')."
}
alias gf='gfix'
# ── gmerge: squash merge & cleanup ────────────────────────────────────────────
gmerge() {
local base branch repo prnum
base="$(_base_branch)"
branch="$(git branch --show-current)"
repo="$(_repo_slug)" || true
[[ "$branch" == "$base" ]] && { echo "You're on $base; switch to a feature branch."; return 1; }
if [[ -n "$GITHUB_TOKEN" && -n "$repo" ]]; then
prnum="$(_pr_number_for_branch)" || true
if [[ -n "$prnum" ]]; then
echo "Attempting PR squash merge via API (#$prnum)…"
_gh_api PUT "/repos/$repo/pulls/$prnum/merge" '{"merge_method":"squash"}' >/dev/null && {
echo "PR merged. Deleting remote branch…"
_gh_api DELETE "/repos/$repo/git/refs/heads/$branch" >/dev/null || true
git switch "$base" && git pull --ff-only
return 0
}
echo "API merge failed; falling back to local squash."
fi
fi
echo "Local squash merge into $base…"
git switch "$base" || return $?
git pull --ff-only || return $?
git merge --squash "$branch" || return $?
git commit -m "Squash merge $branch" || return $?
git push || return $?
git push origin --delete "$branch" 2>/dev/null || true
git branch -D "$branch" 2>/dev/null || true
echo "Merged locally and cleaned up."
}
# ── gmain: sync base branch fast ──────────────────────────────────────────────
gmain() {
local base="$(_base_branch)"
git switch "$base" && git pull --ff-only && echo "➜ on $base (synced)"
}
# ── gprune: delete merged local branches ─────────────────────────────────────
gprune() {
local base="$(_base_branch)"
git fetch --prune
git branch --merged "$base" | grep -vE "^\*|$base" | xargs -r git branch -d
echo "Cleaned merged branches."
}
# ── guard: refuse commits directly on main/master unless forced ─────────────
ginstall-hook-no-main-commit() {
mkdir -p .git/hooks
cat > .git/hooks/pre-commit <<'HOOK'
#!/usr/bin/env bash
b="$(git rev-parse --abbrev-ref HEAD)"
if [ "$b" = "main" ] || [ "$b" = "master" ]; then
echo "pre-commit: refusing to commit directly to $b."
echo "Use: gship \"desc\" to branch+PR."
exit 1
fi
HOOK
chmod +x .git/hooks/pre-commit
echo "Installed pre-commit guard."
}
# ── convenience: reload zsh quickly ──────────────────────────────────────────
reload-zsh() {
source ~/.zshrc && echo "🔁 reloaded ~/.zshrc (zsh $ZSH_VERSION)"
}
alias rz='reload-zsh'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment