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.