Commit message specification
This document defines the commit message format used in this repository. It is written to be portable: you can copy it into other projects and wire the same rules into gitlint, pre-commit, or CI.
The format is based on Conventional Commits with a small, fixed type set, optional scope, optional breaking-change marker, optional body, and optional footers.
Goals
- Make history readable at a glance (
git log --oneline). - Signal intent with a consistent type and scope.
- Keep titles short and scannable.
- Allow richer context in the body when a one-line title is not enough.
- Record AI assistance and breaking changes explicitly.
- Block low-quality or policy-breaking commits on protected branches.
Message structure
A commit message has up to three parts:
<title line>
<optional body>
<optional footers>
Rules:
- Separate the title from the body with one blank line.
- Separate the body from footers with one blank line.
- Wrap body and footer lines at 100 characters or fewer.
- The title line must be 72 characters or fewer.
The body is optional. A title-only commit is valid.
Title line
Grammar
[WIP: ]<type>[<scope>][!]: <subject>
| Part | Required | Description |
|---|---|---|
WIP: | No | Temporary work-in-progress prefix. See WIP commits. |
<type> | Yes | One of the allowed types below. |
<scope> | No | Short context in parentheses, lowercase. |
! | No | Marks a breaking change. Requires a BREAKING CHANGE: footer. |
<subject> | Yes | Imperative summary. Must start with an uppercase letter or digit. |
Allowed types
| Type | Use for |
|---|---|
build | Build system, compiler flags, toolchain, CMake presets |
chore | Maintenance that is not product code (deps, scripts, repo hygiene) |
ci | Continuous integration and automation pipelines |
docs | Documentation only |
feat | New user-facing behavior or API |
fix | Bug fixes |
perf | Performance improvements |
refactor | Code changes that neither fix a bug nor add a feature |
revert | Reverting a previous commit |
style | Formatting, whitespace, naming-only changes |
test | Tests only |
Do not invent new types unless you update the policy and tooling together.
Scope
Scope is optional but recommended when it clarifies blast radius.
Rules:
- Lowercase only.
- Allowed characters:
a-z,0-9,.,_,/,-. - Keep it short: one component or area, not a sentence.
Examples: core, tools, tests, cmake, repo, verify, ports/freertos.
Subject line style
- Use the imperative mood: "Add feature", not "Added feature" or "Adds feature".
- Do not end with a period.
- Start with an uppercase letter or digit after the colon.
- Describe what changed at a high level, not every file touched.
Good:
fix(tools): Configure pip SSL trust in venv setup
feat(core): Add Result wrapper type
ci(verify): Add cross-platform presets and Windows CI gate
Bad:
fixed pip ssl
Fix(tools): configure pip ssl trust in venv setup
chore: stuff
feat: add thing.
Title length
Maximum 72 characters for the full title line, including WIP: , type, scope, !, and subject.
Body
Use the body when the title alone would hide important rationale.
Guidelines:
- Explain why the change exists, not only what changed.
- Mention constraints, trade-offs, or follow-up work when relevant.
- One paragraph is often enough.
- Optional. Omit for trivial changes.
Example:
chore(tools): Drop unused C++ pre-commit hooks from config
This repo is Python-only; trim pre-commit to ruff and shared file checks so
hook installs stay fast on contributor machines.
Body lines must be 100 characters or fewer.
Footers
Footers come after the body (or directly after the title if there is no body). Separate them from the body with a blank line.
AI: footer
Use when AI tools assisted with the commit.
Allowed values (exactly one footer):
| Value | Meaning |
|---|---|
yes | AI-assisted |
no | No AI assistance |
perplexity | Primarily Perplexity |
copilot | Primarily GitHub Copilot |
mixed | More than one AI tool, or AI plus substantial human edit |
Example:
AI: mixed
Rules:
- At most one
AI:footer per commit. - Value is case-insensitive in validation, but lowercase is preferred.
BREAKING CHANGE: footer
Required when the title includes !.
Example:
feat(api)!: Remove deprecated Timer::start overload
BREAKING CHANGE: Timer::start(Duration) was removed. Use Timer::start(TimerConfig).
The footer should describe what broke and how callers should migrate.
Co-authored-by: trailer
Standard Git trailer. Use when multiple authors contributed.
Example:
Co-authored-by: Jane Doe <jane@example.com>
This is not validated by the project gitlint rules, but is compatible with the format.
WIP commits
A work-in-progress commit may prefix the title with WIP: :
WIP: feat(core): Add thread pool prototype
Rules:
- Allowed on topic branches.
- Not allowed on protected branches:
main,master,trunk,develop, or branches namedrelease/*orhotfix/*. - Replace
WIP:with a proper subject before merge.
Breaking changes
Two signals must agree:
!after type or scope in the title.- A
BREAKING CHANGE:footer in the body/footer section.
Example:
refactor(api)!: Rename MutexConfig fields
BREAKING CHANGE: `MutexConfig::recursive` is now `MutexConfig::allowRecursive`.
Update call sites before upgrading.
Complete examples
Title only
docs(repo): Add getting started guide for setup-dev
Title, body, and AI footer
chore(tools): Add Python venv setup scripts and CI integration
Add cross-platform venv setup scripts with pre-commit hook detection and run
Python tooling from .venv in GitLab CI jobs.
AI: mixed
Breaking change
feat(ports)!: Drop FreeRTOS v10 adapter
BREAKING CHANGE: Only FreeRTOS v11+ is supported. Upgrade the kernel submodule
before building firmware targets.
AI: no
WIP on a feature branch
WIP: test(host): Add timer lifecycle edge cases
Validation rules (machine-readable summary)
These rules match the gitlint configuration in this repository.
| Rule | Limit / pattern |
|---|---|
| Title max length | 72 |
| Body line max length | 100 |
| Title regex | ^(WIP: )?((build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9._/-]+\))?(!)?): [A-Z0-9].+$ |
| Body required | No |
AI: footer | Optional; if present, must be one of yes, no, perplexity, copilot, mixed |
AI: footer count | At most one |
WIP: on protected branches | Not allowed |
! in title | Requires BREAKING CHANGE: footer |
Adopting this spec in another project
1. Copy the policy
Copy this file into the target repository, for example docs/guides/commit-messages.md.
Adjust only what you truly need to differ (protected branch names, allowed types, scope naming).
2. Enforce with gitlint
Minimal .gitlint starter:
[general]
ignore=body-is-missing
extra-path=scripts
[title-max-length]
line-length=72
[title-match-regex]
regex=^(WIP: )?((build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9._/-]+\))?(!)?): [A-Z0-9].+$
[body-max-line-length]
line-length=100
Add a custom rule module (for AI:, WIP:, and breaking-change policy) under scripts/gitlint_rules.py and load it with extra-path=scripts.
3. Run on commit
Wire gitlint into pre-commit:
repos:
- repo: https://github.com/jorisroovers/gitlint
rev: v0.19.1
hooks:
- id: gitlint
stages: [commit-msg]
4. Teach contributors
Link the spec from README.md or CONTRIBUTING.md. Include two or three real examples from the target project once history exists.
Quick checklist before committing
- Title uses an allowed type and optional scope.
- Subject is imperative and starts with uppercase after
:. - Title is 72 characters or fewer.
- Body lines are 100 characters or fewer (if present).
-
!is present only whenBREAKING CHANGE:is documented. -
WIP:is not used on protected branches. - At most one
AI:footer, with an allowed value, if you include it.