Writing a good squash commit message Jump to heading

Squash merging turns a pull request’s commits into one commit on main. The default message the forge proposes is either the pull request title with a list of every original commit subject — “fix typo”, “address review”, “wip”, “actually fix it” — or just the title. Both are poor. The first buries the useful information under noise; the second loses it entirely. Six months later, someone runs git blame on a line, lands on that squash commit, and its message is the only explanation they will ever get. Writing it well takes two minutes at merge time and saves an hour of archaeology later. This page sets out what the message should contain and how to produce a good draft quickly, within squash and fixup strategies.

When to use this approach Jump to heading

  • Your team squash-merges pull requests, by policy or by habit.
  • git log on main is full of messages that list intermediate commit subjects.
  • People complain that git blame leads to commits that explain nothing.
  • You also squash locally before pushing, as in squashing without interactive rebase.

Step 1 — Know what the message is for Jump to heading

A squash commit is read in three situations: scanning git log --oneline to see what changed, landing on it from git blame to understand why a line exists, and generating release notes. Each needs something different, and a good message serves all three.

Three readers of a squash commit messageSomeone scanning the log reads only the subject, so it must say what changed. Someone arriving from blame needs the body to explain why. Release tooling needs structured trailers such as the issue reference and any breaking-change note.git log --onelinesubject onlywhat changedgit blamebodywhy it changedRelease notestrailersFixes:, BREAKINGthe list of intermediate commit subjects serves none of them

Step 2 — Write the subject as the change, not the activity Jump to heading

The subject should describe the resulting change in the imperative, short enough to read in a one-line log. If your team uses Conventional Commits, the type and scope go first; the pull request number usually goes at the end.

# Weak subjects
Update billing code
Fixes from review
JIRA-412

# Strong subjects
fix(billing): stop applying customer discount twice on credit notes (#812)
feat(export): schedule CSV exports per account (#839)

The pull request title is often a good starting point, but it is written to get a review, not to describe the final change. Edit it to match what actually merged.

Step 3 — Write a body that explains why, and what a reader would not guess Jump to heading

The body is the part that justifies the commit’s existence. Three short paragraphs cover most changes: the problem, the approach and anything surprising.

fix(billing): stop applying customer discount twice on credit notes (#812)

Credit notes reused the invoice total calculation, which applies the
customer's discount. The credit note was then discounted again when
issued, refunding less than the customer paid.

Credit notes now pass skip_discount=True so the original invoice's
already-discounted lines are used as-is.

Existing credit notes are not recalculated; a separate data fix (OPS-221)
handles the 37 affected documents.

Fixes: BILL-412
Co-authored-by: Priya Raman 
The forge's default squash message against a written oneThe default message repeats each intermediate commit subject, which records the branch's history of mistakes rather than the change. A written message states the change, explains the cause and the approach, and keeps the trailers that tools and co-authors depend on.Default concatenationWritten messagesubjectPR title, maybe vaguethe change, imperativebodylist of WIP subjectsproblem, approach, caveatsco-authorsoften droppedCo-authored-by keptissue linkin a subject, maybeFixes: trailertwo minutes at merge time, read for years afterwards

Step 4 — Keep trailers and co-authors Jump to heading

Squashing combines commits from possibly several authors into one with a single author. The Co-authored-by trailer is how other contributors keep credit, and forges recognise it. Issue references and other trailers your tooling reads should be preserved too.

# Collect co-authors from the branch's commits for the squash message
git log --format='Co-authored-by: %an <%ae>' main..feature/credit-notes | sort -u \
  | grep -v "$(git config user.email)"

The details of keeping authorship are in preserving co-authors when squashing.

Step 5 — Generate a draft, then edit it Jump to heading

A small script can produce a starting draft from the pull request: title, description, and the trailers gathered from commits. You still edit it, but you start from something structured instead of a list of “wip” subjects.

#!/bin/sh
# squash-msg.sh <pr-number> — print a draft squash message
set -eu
pr=$1
gh pr view "$pr" --json title,body,number --jq '"\(.title) (#\(.number))\n\n\(.body)"' |
  sed '/^<!--/,/-->$/d'                     # drop template comments
echo
gh pr view "$pr" --json commits --jq '.commits[].authors[] | "Co-authored-by: \(.name) <\(.email)>"' | sort -u
From pull request to squash messageA draft is generated from the pull request title, its description with template comments removed, and co-author trailers collected from its commits. A person edits the subject to describe the merged change, trims the body to the why, and confirms the trailers before merging.PR datatitle, body, commitsDraftsquash-msg.shEdit subjectthe changeEdit bodythe whyMergetrailers intacta good pull request description makes this step almost free

Forges also let you set the default squash message format — for example, to use the pull request title and description instead of the commit list. Choose that setting; it makes the default far closer to what you want.

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Should the pull request description just become the commit message? Jump to heading

Often most of it can. Descriptions are written for reviewers, though, and include screenshots, checklists and testing notes that do not belong in history. Keep the explanation and drop the review apparatus.

Is it worth enforcing message quality in CI? Jump to heading

Linting the final squash message is hard because it is written at merge time. Enforcing a good pull request title, which becomes the subject, is easy and gets most of the benefit; see enforcing subject line length and format.

What if the pull request did several unrelated things? Jump to heading

Then it should not be one commit. Squash merging works best with focused pull requests; a pull request that does three things should usually be three, or merged with its commits kept.