Git – Commit Messages
A commit message is a permanent record in the project's history. It will be read during code review, during debugging, in release notes, and by engineers who haven't yet joined the team. A great commit message explains what changed and — more importantly — why. A poor one says update or fix stuff.
All Cygnus Dynamics repositories follow the Conventional Commits specification. This format is machine-readable (enabling automated changelogs and release versioning) and human-readable at a glance.
Format
<type>(<scope>): <short description>
[optional body — explain WHY, not what, wrap at 100 chars]
[optional footer — issue references, breaking change notes]
The Three Parts
| Part | Required | Purpose |
|---|---|---|
type(scope): description |
Yes | One-line summary — appears in changelogs and PR lists |
| Body | No | Explains the reasoning, context, and trade-offs |
| Footer | No | Links to tickets, flags breaking changes |
Commit Types
| Type | When to Use | Example |
|---|---|---|
feat |
A new feature visible to users or API consumers | feat(auth): add OAuth2 login with Google |
fix |
A bug fix | fix(payments): handle declined card on timeout |
refactor |
Code restructure — no behaviour change, no bug fix | refactor(order): extract validation into OrderValidator |
perf |
Performance improvement | perf(query): add index on orders.user_id |
test |
Adding or updating tests | test(user): add coverage for inactive account login |
docs |
Documentation only | docs(api): add pagination examples to orders endpoint |
chore |
Build process, dependency updates, tooling | chore(deps): upgrade Spring Boot to 3.2.1 |
style |
Formatting, whitespace — no logic change | style(user): apply spotless formatting |
ci |
CI/CD pipeline changes | ci(pipeline): add sonar quality gate to PR checks |
revert |
Reverts a previous commit | revert: feat(auth): add OAuth2 login with Google |
Scope
The scope is optional but strongly encouraged. It names the part of the codebase affected — a feature, module, layer, or service:
feat(auth): ...
fix(payments): ...
refactor(order-service): ...
chore(deps): ...
ci(azure-pipeline): ...
Keep scopes consistent across the project. If your team uses user-service in one commit and users in another, changelogs become fragmented. Agree on scope names per repo and stick to them.
Writing the Short Description
- Imperative mood — "add login" not "added login" or "adds login"
- 72 characters or fewer — it must fit in
git log --oneline, GitHub PR lists, and Azure DevOps displays - No capital first letter after the colon —
feat(auth): add OAuth2notfeat(auth): Add OAuth2 - No period at the end
- Be specific — what exactly changed?
# ✅ Good — specific, imperative, clear
feat(auth): add JWT refresh token rotation on expiry
fix(orders): prevent duplicate charge when payment gateway times out
refactor(user): extract email validation into shared EmailValidator utility
perf(dashboard): replace N+1 query with single JOIN on order summary
# ❌ Bad — vague, past tense, too generic
fix: fixed bug
update: updated stuff
WIP
feat: changes
feature work
asdfgh
Writing the Body
The body explains why the change was made — the reasoning, the problem being solved, and any trade-offs. It is not a line-by-line description of what the diff contains. The diff shows what changed; the body explains why.
Wrap lines at 100 characters. Separate the body from the subject with a blank line.
fix(payments): prevent double-charge on gateway timeout
Stripe's payment gateway occasionally returns a 504 timeout after the
charge has actually been processed. Previously, our retry logic would
attempt the charge again on any non-2xx response, causing duplicate
charges on affected customer accounts.
This fix adds idempotency key generation scoped to the order ID and
timestamp. Duplicate requests with the same key are safely rejected by
Stripe without processing a second charge.
The 504 path is now retried once with the same idempotency key,
which Stripe handles correctly by returning the original charge result.
feat(orders): add cursor-based pagination to order list endpoint
Offset pagination on the orders table was causing full-table scans at
high page numbers. For accounts with 100k+ orders, pages beyond 500
were timing out.
Replaced with keyset pagination using the created_at + id composite
cursor. This makes all pages equally fast regardless of depth and
eliminates the need for a COUNT(*) query on every request.
The existing ?page=&limit= parameters are preserved for backward
compatibility and continue to work for the first 50 pages. Beyond
that, the cursor parameter is required.
Writing the Footer
The footer references issue tracking tickets and flags breaking changes:
# ✅ Issue reference — links the commit to its ticket
Closes: PROJ-1234
Refs: PROJ-5678
# ✅ Breaking change — must be flagged explicitly
BREAKING CHANGE: The /api/v1/orders endpoint now requires the
Authorization header. Requests without a valid JWT will receive
a 401 response. Clients must be updated before deploying.
Real-World Examples
feat(auth): add rate limiting to login endpoint
Brute-force attempts on the login endpoint were detected in the
March 2026 security review. This adds a 10-attempt-per-hour limit
per IP address using Redis-backed sliding window rate limiting.
Accounts locked by this limit return a 429 with a Retry-After header
indicating when the window resets. The limit resets automatically —
no manual unlock required.
Closes: PROJ-2847
chore(deps): upgrade axios from 1.4.0 to 1.7.9
Addresses CVE-2024-39338 (SSRF vulnerability in redirect handling).
No API changes — the upgrade is backward compatible.
Refs: PROJ-3102
fix(user-profile): trim whitespace from email on save
Users pasting email addresses from clipboard sometimes include
trailing spaces, causing login failures because the stored value
doesn't match the typed value at login time.
The fix trims the email string before validation and before persistence.
Existing affected rows are cleaned up by migration V042.
Closes: PROJ-1991
Rules Summary
| Rule | ✅ Correct | ❌ Incorrect |
|---|---|---|
| Format | type(scope): description |
update user page |
| Mood | add login page |
added login page |
| Length | ≤ 72 characters | 90+ character subject |
| Capital after colon | lowercase | Uppercase |
| Period | No period | fix: handle null. |
| Body vs diff | Explains why | Restates what changed |
| Ticket reference | Closes: PROJ-1234 |
Ticket number buried in body prose |
Pre-commit enforcement
Install commitlint with the @commitlint/config-conventional ruleset to automatically reject non-conforming commit messages before they are pushed. This eliminates the most common review comment across the entire team: