Skip to content

Commit classification

What GenerateFromRepo does to each commit message: which types it recognises, which category each becomes, and which commits never reach the output.

Only the subject line — the first line of the commit message — is classified. The body is read for one thing only: a BREAKING CHANGE: footer.

Recognised commit types

Parsing uses leodido/go-conventionalcommits with its conventional type set, which is a closed list of eleven types:

build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test

A subject whose type is not on that list is not a Conventional Commit as far as this module is concerned, however well-formed it looks. security: patch CVE-2026-1234, deps: bump go-git and wip: halfway are all unrecognised, and all dropped unless you pass WithIncludeAll.

The type match is case-insensitive: FEAT: and Feat: classify as feat. The space after the colon is required — feat:no space does not parse.

Type to category

Commit type Category Heading in the output
feat CategoryFeature ### Features
fix CategoryFix ### Bug Fixes
perf CategoryPerformance ### Performance Improvements
build, chore, docs, refactor, revert, style CategoryOther ### Other
test, ci dropped
anything else (unrecognised type) dropped
any of the above marked breaking CategoryBreaking ### Breaking Changes

CategoryBreaking is the zero value of the Category type. An Entry you construct yourself without setting Category is a breaking change.

Commits that never appear

Four kinds of commit produce no entry:

  • test and ci commits, dropped by type — unless the commit is also breaking, in which case it appears under Breaking Changes.
  • Merge commits, always. A commit with more than one parent contributes nothing, even when its message is a valid Conventional Commit and even when a release tag points at it.
  • Unrecognised types, unless WithIncludeAll is set.
  • Empty subjects. A commit message that is blank or whitespace-only is skipped.

How a breaking change is detected

Either signal marks an entry breaking, and either one overrides the category the type would otherwise have given it:

  • ! before the colonfeat!: … or feat(api)!: ….
  • A footer line, BREAKING CHANGE: or the hyphenated synonym BREAKING-CHANGE:, anywhere in the commit body. The footer must begin at the start of a line; an indented or mid-line occurrence is not detected.

A breaking test/ci commit is not dropped — the breaking classification is applied before the skip, so test(x): … with a BREAKING CHANGE: footer appears under Breaking Changes.

The footer's own text is not carried into the entry. The description stays the subject line, and the explanation of what broke stays in the commit body where nothing reads it.

Scope and description

Field Source Notes
Scope The parenthesised scope, feat(cli):cli Empty when absent. Spaces and slashes are accepted: feat(multi word scope), feat(a/b)
Description The rest of the subject line Trailing text is not truncated, cleaned or reflowed
Raw Empty during generation. Only Parse sets it, to the original bullet line

Descriptions are written into markdown verbatim. A description containing *, ** or a markdown link keeps those characters, and they render as markdown in the output. A scope containing a colon — fix(a:b): — is emitted as **a:b:** and will not parse back into a Scope field, because the scope pattern excludes colons.

Where each release's commits come from

Classification decides what an entry says; attribution decides which release it lands in. Those are separate, and attribution is by ancestry rather than by date — see How commits are attributed to releases.