Conventional Commits define a lightweight way to write commit history. The format is intentionally simple:
<table> <thead> <tr> <th><type>[optional scope]: <description> [optional body] [optional footer(s)]</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
</tr>
</tbody>
</table>
A basic example looks like this:
<table> <thead> <tr> <th>feat(config): 允许 config 对象直接从其他 config 继承 BREAKING CHANGE: 在 config 对象中增加 `extends` 字段,用于从其他继承 config close issue #23</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
</tr>
</tbody>
</table>
When creating a commit in Git, use git commit -a if you want to open the editor for a multi-line message. For a single-line commit, git commit -a -m 'COMMIT MESSAGE' is enough.
How the convention maps to versioning
This style of commit message fits naturally with SemVer:
<table> <thead> <tr> <th>PATCH -> type(fix) MINOR -> type(feat) MAJOR -> BREAKING CHNAGE</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
</tr>
</tbody>
</table>
In day-to-day work, fix and feat are the types used most often, but they are far from the only ones. Other common choices include chore/docs/reflector/improvement/perf/test/style.
A few practical rules make the convention much easier to keep consistent:
- In most cases, you do not need a
bodysection unless the change is aBREAKING CHANGE - The
descriptionshould be brief and direct; state the change plainly in a short sentence - It is usually a bad idea to bundle multiple unrelated changes into one commit, especially when one of them is only half-finished
scopecan be a file path such as/lib/utils, or a feature area likeparser; keeping it within one or two words is generally best
Useful habits around commit history
Squashing related commits
If your previous commit had a bug, or the work was incomplete, and the new commit is essentially a correction or continuation of the same change, it is often better to merge them into one clean commit with git rebase -i.
For example:
<table> <thead> <tr> <th>git rebase -i HEAD~3 # 展示最近 3 次修改</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
</tr>
</tbody>
</table>
You might see something like this:
<table> <thead> <tr> <th>pick 0291959 chore(blog): 清理无关项 pick 1ef8f31 chore(blog): 清理无关项 pick 36a91db fix(post): 格式化 post 的 meta 数据格式,增加 --- 开始符</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
</tr>
</tbody>
</table>
Change the second line from pick to squash. That keeps the commit but folds its changes into the previous one, leaving a cleaner history.
Closing issues from the commit message
On GitHub and GitLab, a commit message that includes text such as Fix #23 can automatically close issue 23 once the commit is pushed to the repository.
Generating a changelog from commits
Structured commit messages are useful beyond version control. For daily or weekly reports, and especially when preparing a release, commit logs quickly show what you or your team actually changed.
A simple alias can already make the log easier to scan:
<table> <thead> <tr> <th>alias git-changelog='git log --oneline --decorate';</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
</tr>
</tbody>
</table>
If you need a more structured changelog, tools such as auto-changelog can generate one automatically and support customizable templates.
Enforcing the convention in a project
A convention without tooling is easy to ignore. In real projects, some lightweight enforcement helps turn the rule into a team habit.
1. Install the checker
A commonly used tool for validating commit messages is @commitlint/cli:
npm install @commitlint/cli --save-dev</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
</tr>
</tbody>
</table>
2. Add the ruleset
@commitlint provides a stricter preset called @commitlint/config-conventional, which can be added to the project as well:
npm install @commitlint/config-conventional --save-dev</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
</tr>
</tbody>
</table>
After installation, create commitlint.config.js and configure it explicitly:
module.exports = { extends: [ '@commitlint/config-conventional' ] };</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
</tr>
</tbody>
</table>
The config-conventional preset allows the following types:
build/chore/ci/docs/feat/fix/perf/refactor/revert/style/test
3. Run checks during commit
husky is a common way to wire commit hooks into the project. One approach is to add a .huskyrc.json file like this:
{ "hooks": { "pre-commit": "node ./node_modules/@commitlint/cli/lib/cli.js -E HUSKY_GIT_PARAMS" } }</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
</tr>
</tbody>
</table>
It can also be configured directly in package.json through a husky field.
Why this matters
A tidy commit history is not just about generating a changelog automatically. With a shared convention, the project accumulates a structured record of change over time. Add a few emoji if you like, and the result can read almost like a living timeline of the project.
That kind of history makes it easier to communicate what changed and how significant the change was. It can also benefit continuous integration workflows, since different type values can be used to trigger different build or deployment paths.