
Push Pr
- 33 installs
- 269 repo stars
- Updated June 11, 2026
- gupsammy/claudest
Auto-draft a structured GitHub PR description from commits and diff stats against a base branch before you open or update a pull request.
About
push-pr wraps a small Python utility that solo builders run locally to turn git history into a polished pull request body. You point it at a base branch such as main or feat/auth, and it collects commits between base and HEAD, diff statistics, and per-file change status into markdown sections suited for GitHub. The --output json mode helps agents or scripts pipe the body into gh pr create without manual copy-paste. It assumes a normal git repo on your machine and subprocess access to the git CLI. Use it at the end of a feature branch when you want consistent PR narratives across solo projects, especially when you ship frequently from Claude Code or Cursor. It does not open the PR for you unless you combine it with your host CLI; it focuses on generating the description content reliably from facts in git.
- Python script format-pr-body.py compares HEAD to a configurable base branch
- Outputs markdown PR body with summary, changes, and commit list sections
- Supports --output text or json for CI or agent pipelines
- Uses git log oneline and diff --stat / --name-status for structured file lists
Push Pr by the numbers
- 33 all-time installs (skills.sh)
- Ranked #348 of 733 Git & Pull Requests skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/gupsammy/claudest --skill push-prAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 33 |
|---|---|
| repo stars | ★ 269 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 11, 2026 |
| Repository | gupsammy/claudest ↗ |
What it does
Auto-draft a structured GitHub PR description from commits and diff stats against a base branch before you open or update a pull request.
Files
Push & PR
Push commits and create/update pull requests with automatic branch management and scope-aware multi-PR splitting.
Arguments
Parse flexibly from $ARGUMENTS:
- status:
1=opened,2=draft,3=ready (default: new PR=opened, update=draft) - base-branch: Target branch (default:
main)
Pre-Flight Context
Injected at invocation — analyze before taking any action:
- Working tree status:
!git status --porcelain - Current branch:
!git rev-parse --abbrev-ref HEAD - Unpushed commits:
!git rev-list @{u}..HEAD --count 2>/dev/null || echo "no upstream" - Recent commits:
!git log origin/main..HEAD --oneline 2>/dev/null(assumes main base; see Step 3 if base differs) - Diff stat:
!git diff origin/main...HEAD --stat 2>/dev/null(captured before fetch; re-run if stale)
Workflow
1. Pre-Flight
Run git fetch origin to sync remote state.
If the working tree status above shows uncommitted changes, invoke Skill: commit to commit first.
Complete when: remote is fetched and working tree is clean.
2. Branch Management
If on main/master with unpushed commits, cut a feature branch before proceeding.
Branch naming: prefix from the primary commit's conventional type (feat/, fix/, docs/, chore/, refactor/); slug from the commit scope or subject, lowercase hyphens only, max 45 chars total (keeps branch names readable in GitHub's UI and avoids truncation in terminal prompts). Use the scope if present (feat(auth) → feat/auth); otherwise condense the subject to 2–4 words (fix login redirect timeout → fix/login-redirect).
git checkout -b <derived-branch-name>
git branch -f main origin/maingit branch -f moves main's pointer back to origin/main without checkout or --hard — non-destructive and never triggers permission denials.
If already on a feature branch: skip to step 3.
Complete when: HEAD is on a feature branch (not main/master).
3. Context Gathering
Derive working variables from pre-flight context and arguments:
BASE= base-branch argument, ormainif not providedBRANCH= current branch name (from pre-flight injection)
Use the pre-flight context injected above. If the base branch differs from main, re-gather against origin/$BASE:
git log origin/$BASE..HEAD --oneline --reverse
git diff origin/$BASE...HEAD --statAlways compare against origin/$BASE, not local $BASE — the PR targets the remote branch, so comparisons must match what GitHub will see.
Record: commit count, conventional-commit types and scopes present, total diff lines (approximate from --stat output).
Complete when: commit count, scope/type inventory, and approximate diff size are known.
4. Scope Analysis
Evaluate whether the changeset warrants multiple PRs. A split is warranted when either:
- Size: total diff exceeds ~400 lines (code lines; ignore lock files and generated
files) — beyond this threshold, reviewer fatigue degrades review quality and catch rate
- Diversity: commits span 3+ distinct conventional-commit scopes or types (e.g.,
feat(auth), fix(ui), chore(deps)) — multiple scopes mean the changeset lacks a single narrative, making review harder and revert riskier
If neither condition is met: proceed to step 5 as a single PR.
If either condition is met — propose stacked PRs:
Cluster commits by scope/type in the order they were made. Each cluster becomes one PR targeting the previous cluster's branch (the first targets $BASE). Present the plan:
Proposed stacked PRs (each PR targets the previous branch):
PR 1 [base: main] feat/auth — commits: abc1234, def5678
PR 2 [base: feat/auth] fix/ui-redirect — commits: ghi9012
PR 3 [base: fix/ui-...] chore/cleanup — commits: jkl3456Use AskUserQuestion: "Split into N stacked PRs as shown above, or push as a single PR?"
If user declines split: proceed to step 5 as a single PR.
If user confirms split — stacked PR execution:
For each cluster in order: 1. Create a branch from the previous cluster's branch ($BASE for cluster 1):
git checkout -b <cluster-branch> <previous-branch>
git cherry-pick <sha1> <sha2> ...2. Push: git push -u origin <cluster-branch> 3. Generate PR body using the format script with --base <previous-branch>:
python3 ${CLAUDE_PLUGIN_ROOT}/skills/push-pr/scripts/format-pr-body.py --base "<previous-branch>"4. Create PR targeting the correct base branch. 5. Repeat for next cluster.
After all PRs are created, check out the last cluster's branch and report the full stack (see Output). Exit — skip steps 5–7.
Complete when: user has chosen single-PR or stacked, and stacked flow is finished if chosen.
5. PR Status
Check for an existing PR on this branch: gh pr list --head "$BRANCH" --json number,state
Use the provided status argument, or default: new PR=opened, update=draft.
Complete when: existing PR state is known and target status is determined.
6. Push
Always push with git push -u origin "$BRANCH" — the -u flag sets tracking on new branches and is a no-op when the upstream is already correctly set, so it is always safe to use.
If push fails because the remote branch has diverged, run git pull --rebase origin $BRANCH and retry the push once. If the rebase itself has conflicts, stop and report.
Complete when: branch is pushed and tracking the remote.
7. PR Creation/Update
Generate the PR body using the format script:
python3 ${CLAUDE_PLUGIN_ROOT}/skills/push-pr/scripts/format-pr-body.py --base "origin/$BASE"Exit 1 means no changes found relative to base; report to user. On success, use stdout as the PR body directly.
New PR:
gh pr create --title "<title>" --body "<format-pr-body output>" --base "$BASE"
# If status=ready: gh pr readyExisting PR: Add a comment listing new commits since last push; update PR status if the status argument changed.
PR_NUM=$(gh pr list --head "$BRANCH" --json number -q '.[0].number')
gh pr comment $PR_NUM --body "New commits: ..."Complete when: PR URL is obtained and status matches the target.
Constraints
Produce clean, unattributed PRs that match the project's existing commit and PR style:
- No Co-authored-by or AI signatures — PRs should look like human-authored work
- No "Generated with Claude Code" — same reason; attribution is the user's choice
- No emojis in PR title or description — most project conventions use plain text
- Use existing git user config only — never modify
user.nameoruser.email
Edge Cases
- No remote → suggest
git remote add origin <url>and stop - No
ghCLI → report requirement and stop - Branch behind remote → pull/rebase before pushing
- No commits to push → report and stop
- Cherry-pick conflict during stacked flow → stop, report the cluster name, failing commit
SHA, and conflicting file(s). Suggest git cherry-pick --abort followed by manual resolution, then re-running
Output
Single PR: branch name, PR URL, PR status (opened/draft/ready).
Stacked PRs: ordered list showing each PR URL and the branch it targets, plus the name of the final branch now checked out.
#!/usr/bin/env python3
"""Generate a formatted PR body from git commit history and diff stats.
Compares HEAD against a base branch and produces a markdown PR description
with summary, changes, and commit list sections.
Usage:
format-pr-body.py [--base BRANCH] [--output text|json]
Examples:
format-pr-body.py --base main
format-pr-body.py --base feat/auth --output json
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
def run_git(args: list[str]) -> tuple[str, int]:
result = subprocess.run(["git"] + args, capture_output=True, text=True)
return result.stdout.strip(), result.returncode
def get_commits(base: str) -> list[tuple[str, str]]:
out, code = run_git(["log", f"{base}..HEAD", "--oneline"])
if code != 0 or not out:
return []
commits = []
for line in out.splitlines():
if line:
sha, _, msg = line.partition(" ")
commits.append((sha, msg))
return commits
def get_diff_stat(base: str) -> str:
out, _ = run_git(["diff", f"{base}...HEAD", "--stat"])
return out
def get_changed_files(base: str) -> list[dict]:
out, code = run_git(["diff", f"{base}...HEAD", "--name-status"])
if code != 0 or not out:
return []
files = []
for line in out.splitlines():
if not line:
continue
parts = line.split("\t", 1)
if len(parts) == 2:
files.append({"status": parts[0], "path": parts[1]})
return files
GENERATED_SUFFIXES = (".lock", ".sum", ".min.js", ".min.css", "-lock.json")
SKIP_PATHS = ("__pycache__", ".pyc", "node_modules", "dist/", "build/")
def is_significant(path: str) -> bool:
if any(path.endswith(s) for s in GENERATED_SUFFIXES):
return False
if any(p in path for p in SKIP_PATHS):
return False
return True
def format_body(
commits: list[tuple[str, str]],
diff_stat: str,
files: list[dict],
) -> str:
if commits:
summary = "\n".join(f"- {msg}" for _, msg in commits[:6])
if len(commits) > 6:
summary += f"\n- _{len(commits) - 6} more commits_"
else:
summary = "- No commits found"
significant = [f for f in files if is_significant(f["path"])][:10]
if significant:
changes = "\n".join(f"- `{f['path']}`" for f in significant)
omitted = len(files) - len(significant)
if omitted > 0:
changes += f"\n- _{omitted} generated/lock files omitted_"
else:
changes = "- See diff stat"
commit_list = "\n".join(f"- `{sha}` {msg}" for sha, msg in commits)
body = (
f"## Summary\n{summary}\n\n"
f"## Changes\n{changes}\n\n"
f"## Commits\n{commit_list}"
)
if diff_stat:
body += f"\n\n## Diff Stat\n```\n{diff_stat}\n```"
return body
def main() -> None:
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--base", default="main",
help="Base branch to compare against (default: main)",
)
parser.add_argument(
"--output", choices=["text", "json"], default="text",
help="Output format: text (default) or json with title + body fields",
)
args = parser.parse_args()
commits = get_commits(args.base)
diff_stat = get_diff_stat(args.base)
files = get_changed_files(args.base)
if not commits and not files:
print(f"No changes found relative to '{args.base}'.", file=sys.stderr)
sys.exit(1)
body = format_body(commits, diff_stat, files)
if args.output == "json":
title = commits[0][1] if commits else "Update"
print(json.dumps({"title": title, "body": body}))
else:
print(body)
sys.exit(0)
if __name__ == "__main__":
main()
Related skills
FAQ
Is Push Pr safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.