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.

Where each part of the draft comes fromThe summary comes from commit subjects. The motivation comes from commit bodies. Linked issues come from trailers. The areas touched come from the diff's paths. Testing notes are the one part the author must still write.Summarycommit subjectsWhycommit bodiesIssuesFixes: trailersWhere to lookdiff by areaTestingauthor writesfour of five sections can be drafted; the fifth is the author's judgement

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 }}" }
Filling a description without overwriting oneWhen a pull request is opened, the workflow reads its description. If it is empty or still the untouched template, the workflow generates a draft from the commits and writes it with a note asking the author to edit. Any description a person wrote is left alone.PR openedeventRead bodyempty or template?Generatepr-draft.shWrite draft+ 'please edit' noteElseleave it alonerunning only on 'opened' avoids rewriting a description the author later cleared on purpose

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"
Empty descriptions against generated draftsWith empty descriptions, reviewers reconstruct the change from the diff and ask questions the commits already answered. With generated drafts, reviewers start from a summary, the motivation and the areas changed, and authors only add testing notes and polish.Empty descriptionGenerated draftreviewer's first minutesreconstructing intentreading intentlinked issuesoften missingfrom trailersauthor effortnone, then questionsone minute of editingthe draft is a starting point the author owns, not a substitute for them

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.