Skip to main content

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>
PartRequiredDescription
WIP: NoTemporary work-in-progress prefix. See WIP commits.
<type>YesOne of the allowed types below.
<scope>NoShort context in parentheses, lowercase.
!NoMarks a breaking change. Requires a BREAKING CHANGE: footer.
<subject>YesImperative summary. Must start with an uppercase letter or digit.

Allowed types

TypeUse for
buildBuild system, compiler flags, toolchain, CMake presets
choreMaintenance that is not product code (deps, scripts, repo hygiene)
ciContinuous integration and automation pipelines
docsDocumentation only
featNew user-facing behavior or API
fixBug fixes
perfPerformance improvements
refactorCode changes that neither fix a bug nor add a feature
revertReverting a previous commit
styleFormatting, whitespace, naming-only changes
testTests 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.

Use when AI tools assisted with the commit.

Allowed values (exactly one footer):

ValueMeaning
yesAI-assisted
noNo AI assistance
perplexityPrimarily Perplexity
copilotPrimarily GitHub Copilot
mixedMore 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.

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 named release/* or hotfix/*.
  • Replace WIP: with a proper subject before merge.

Breaking changes

Two signals must agree:

  1. ! after type or scope in the title.
  2. 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
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.

RuleLimit / pattern
Title max length72
Body line max length100
Title regex^(WIP: )?((build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9._/-]+\))?(!)?): [A-Z0-9].+$
Body requiredNo
AI: footerOptional; if present, must be one of yes, no, perplexity, copilot, mixed
AI: footer countAt most one
WIP: on protected branchesNot allowed
! in titleRequires 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 when BREAKING CHANGE: is documented.
  • WIP: is not used on protected branches.
  • At most one AI: footer, with an allowed value, if you include it.