Generating pull request descriptions from commits Jump to heading
A pull request description is where a reviewer learns what the change does, why, and where to look first. Too often it is empty, or a single line repeating the title, because writing it feels like duplicating work already done in the commit messages. It mostly is duplication โ which means it can be generated. The commits on the branch already contain subjects, bodies and trailers; the diff already says which areas changed. A script can assemble those into a structured draft, and the author spends a minute improving it instead of ten writing from nothing. This page builds that generator for local use and as a bot that fills empty descriptions, within pull request automation and bots.
When to use this approach Jump to heading
- Pull requests in your repositories often have empty or one-line descriptions.
- Commit messages are reasonably good โ bodies explain why, trailers link issues.
- Reviewers spend the first minutes of every review working out what a change is for.
- You use a pull request template, as in setting up PR templates for code review efficiency, and want it pre-filled.
Step 1 โ Decide what the draft should contain Jump to heading
A good description answers what, why, where to look and how it was tested. Commits supply most of it; the diff supplies the rest.
Step 2 โ Generate the draft locally Jump to heading
A short script reads the branchโs commits since the merge base and prints Markdown. Authors run it before opening a pull request.
#!/bin/sh
# scripts/pr-draft.sh [base] โ print a draft PR description for the current branch
set -eu
base=$(git merge-base "${1:-origin/main}" HEAD)
echo "## Summary"; echo
git log --reverse --no-merges --format='- %s' "$base..HEAD"
echo; echo "## Why"; echo
git log --reverse --no-merges --format='%b' "$base..HEAD" |
sed '/^[A-Za-z-]*: /d' | sed '/^$/N;/^\n$/D' # bodies without trailer lines
echo; echo "## Linked issues"; echo
git log --no-merges --format='%(trailers:key=Fixes,valueonly)' "$base..HEAD" | sed '/^$/d' | sort -u | sed 's/^/- /'
echo; echo "## Areas changed"; echo
git diff --name-only "$base" HEAD | awk -F/ '{print $1"/"$2}' | sort | uniq -c | sort -rn |
awk '{printf "- `%s` (%d files)\n", $2, $1}'
echo; echo "## Testing"; echo; echo "<!-- describe how you verified this -->" sh scripts/pr-draft.sh | gh pr create --title "$(git log -1 --format=%s)" --body-file - Step 3 โ Fill empty descriptions automatically Jump to heading
For authors who open pull requests through the web interface, a workflow can fill the description when it is empty or still the unmodified template. It must never overwrite something a person wrote.
# .github/workflows/pr-description.yml
on:
pull_request:
types: [opened]
permissions: { pull-requests: write, contents: read }
jobs:
draft:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0, ref: "${{ github.event.pull_request.head.sha }}" }
- run: |
body=$(gh pr view "$PR" --json body --jq .body)
template=$(cat .github/pull_request_template.md 2>/dev/null || true)
if [ -z "$(printf '%s' "$body" | tr -d '[:space:]')" ] || [ "$body" = "$template" ]; then
sh scripts/pr-draft.sh "origin/${{ github.base_ref }}" > draft.md
printf '\n\n<sub>Drafted from commit messages โ please edit.</sub>\n' >> draft.md
gh pr edit "$PR" --body-file draft.md
fi
env: { GH_TOKEN: "${{ github.token }}", PR: "${{ github.event.number }}" } Fork pull requests run with a read-only token under pull_request, so this workflow cannot edit their descriptions. For public repositories, use the split-workflow pattern in running CI for fork pull requests safely, or accept that fork contributors write their own.
Step 4 โ Keep the draft honest when commits are poor Jump to heading
A generator amplifies whatever the commits contain. If messages are โwipโ and โfixโ, the draft is useless. Two measures help: filter noise subjects out of the summary, and nudge authors when the draft is thin.
# Drop fixup/WIP subjects from the summary and flag a thin draft
git log --reverse --no-merges --format='%s' "$base..HEAD" |
grep -vE '^(fixup!|squash!|wip|WIP|tmp)' | sed 's/^/- /'
[ "$(git log --no-merges --format='%b' "$base..HEAD" | tr -d '[:space:]' | wc -c)" -lt 40 ] &&
echo "> โ ๏ธ Commit messages have little explanation โ please add the why here." Better commit messages make better drafts; the habits are covered in enforcing subject line length and format.
Step 5 โ Update the draft as the branch grows Jump to heading
A description drafted when the pull request opened can fall behind as commits are added. Rather than regenerating over the authorโs edits, post a short comment listing commits added since the description was last edited.
edited=$(gh pr view "$PR" --json updatedAt --jq .updatedAt)
new=$(git log --since="$edited" --no-merges --format='- %s' "origin/$BASE...HEAD")
[ -n "$new" ] && gh pr comment "$PR" --body "Commits added since the description was last edited:
$new" Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Should a language model write the description instead? Jump to heading
It can summarise diffs well, but it can also invent motivation that is not in the commits. If you use one, give it the commit messages and diff, keep the output clearly marked as a draft, and require the author to confirm the why.
Does this replace the pull request template? Jump to heading
It fills the templateโs sections. Keep the template for what cannot be generated: testing steps, screenshots, rollout notes and checklists.
What about squash merges? Jump to heading
With squash merging, the description often becomes the merged commitโs body, so a good description directly improves history. That is a strong reason to generate a good starting point.
Related Jump to heading
- Pull Request Automation & Bots โ the parent topic.
- Writing a Good Squash Commit Message โ where the description ends up.
- Requiring Linked Issues on Pull Requests โ enforcing the issue section.
- Enforcing Pull Request Size Limits โ keeping changes small enough to describe.