What's new in 2.14.0? 🆕

git-cliff is a command-line tool that provides a highly customizable way to generate changelogs from the Git history.
What's new? ⛰️
So many things...
The full changelog can be found here.
⚠️ Migration Guide
If you pass multiple values to --include-path, --exclude-path, --with-commit, or --skip-commit: you need to repeat the option for each value:
- git cliff --include-path "src/**" "docs/**"
+ git cliff --include-path "src/**" --include-path "docs/**"
Opt::configis now anOption<PathBuf>. The public changelog and remote context types also gained fields, so code constructing them with struct literals must initialize the new fields.git-cliff-corenow usesgit20.21. If you usegit2types exposed by its public API, update yourgit2dependency as well.
- Building git-cliff from source now requires Rust 1.88.0 or newer.
- The upgrade to
git20.21 includes a libgit2 SONAME change. Dynamically linked packages must be rebuilt against the new libgit2 version.
💻 CLI
-
Smarter config discovery: when
--configis omitted, git-cliff automatically looks for a configuration file. (#1584) -
Custom templates: you can now keep your own configuration templates in a directory and initialize them by name with
--init.Use
--templates-dir(orGIT_CLIFF_TEMPLATES_DIR) to set the directory and--list-templatesto see all built-in and custom templates. (#1583)$ tree ~/my-templates
~/my-templates
├── company.toml
└── minimal.toml
$ git cliff --templates-dir ~/my-templates --init company
$ git cliff --list-templates --templates-dir ~/my-templates
azure-devops-keepachangelog
cocogitto
company
... -
Templates from files: you can now place your changelog body template in a file and load it with
--body-file, making multiline templates easier to manage than passing them directly with--body. (#1574)$ git cliff --body-file changelog-body.tera -
Safer multi-value arguments: path and commit options now consume one value per occurrence, preventing a trailing positional range from being mistaken for another option value. (#1614)
# Repeat the option for each path
$ git cliff --include-path "src/**" --include-path "docs/**" v1.0.0..v2.0.0
# Or pass path patterns as one quoted, space-delimited value
$ git cliff --include-path "src/** docs/**" v1.0.0..v2.0.0
# The positional range can also come first
$ git cliff v1.0.0..v2.0.0 --include-path "src/**" --include-path "docs/**"
🧩 Templating
-
git-cliff can now format markdown! (#1610)
It is opt-in and can be enabled via the
formatoption:[changelog]
format = trueinfoFormatting normalizes headings, list markers and excessive blank lines. It supports GitHub-flavored Markdown features such as tables, strikethrough, task lists and footnotes and only runs for stdout, extension-less paths and
.mdoutput.Ambiguous bare brackets such as
[unreleased]are escaped according to CommonMark rules. -
New filters: two new filters give you more control over how releases and commits are grouped:
-
commit_groupspreserves first appearance order or follows the order ofcommit_parsers_groups. (#1518)tipIf you were using numbered HTML comments to control the group order, you can now remove them:
commit_parsers = [
- { message = "^feat", group = "<!-- 0 -->Features" },
- { message = "^fix", group = "<!-- 1 -->Bug Fixes" },
+ { message = "^feat", group = "Features" },
+ { message = "^fix", group = "Bug Fixes" },
]Then replace the
group_byloop withcommit_groups(groups=commit_parsers_groups):-{% for group, commits in commits | group_by(attribute="group") %}
- ### {{ group | striptags | trim | upper_first }}
- {% for commit in commits %}- {{ commit.message }}
+{% for entry in commits | commit_groups(groups=commit_parsers_groups) %}
+ ### {{ entry.group | trim | upper_first }}
+ {% for commit in entry.commits %}- {{ commit.message }}
{% endfor %}
{% endfor %}Each returned entry contains the group name in
entry.groupand its commits inentry.commits.striptagsis no longer needed either. -
group_by_scopegroups releases at a chosen semantic-version scope such asmajor,minor, orpatch, with support for version prefixes. (#1547){% for version, releases in releases | group_by_scope(scope="minor", prefix="v") %}
## {{ version }}
{% for release in releases %}
- {{ release.version }}
{% endfor %}
{% endfor %}Releases
v0.1.0,v0.1.1andv0.2.0render as:## v0.1
- v0.1.1
- v0.1.0
## v0.2
- v0.2.0
-
-
New variables: remote metadata now exposes more information for changelog templates:
-
commit.remote.pr_authorcontains the author of the matched pull request. (#1613){
"commits": [
{
"remote": {
"username": "merge-maintainer",
"pr_author": "pull-request-author",
"pr_number": 42
}
}
]
}infopr_authoris more reliable than resolving the commit author's email for squash merges, whereusernamecan point to the maintainer who merged the change. -
[github|gitlab|etc].contributors[].pr_numberscontains every pull request attributed to a contributor in the release, sorted by number. The existingpr_numberfield remains available for compatibility. (#1546){
"github": {
"contributors": [
{
"username": "contributor",
"pr_number": 42,
"pr_numbers": [42, 57, 81]
}
]
}
}
-
-
Built-in GitLab templates: (#1561)
-
gitlabgenerates concise release notes with merge request, contributor and tag links:$ git cliff --config gitlab -
gitlab-keepachangeloggenerates a more detailed Keep a Changelog layout with GitLab commit and merge request links:$ git cliff --config gitlab-keepachangelog
You can also use either preset as the starting point for your own configuration:
$ git cliff --init gitlab-keepachangelog -
⚙️ Configuration
-
Skip version bumps for selected commits: different commit types can now be excluded from version bump calculations with
no_increment_regex. (#1522)[bump]
no_increment_regex = "chore|ci|docs" -
Configure remote request timeouts: remote metadata requests now have a configurable
http_timeout. (#1580)[remote.github]
http_timeout = "60s" -
Better prepend support: the new
header_markertells git-cliff where the header ends, so it can remove the old header before writing the new one. (#1603)[changelog]
header = """
# Changelog
Tracked releases: {{ releases | length }}
"""
header_marker = "<!-- git-cliff: end of header -->"Then prepend a new release as usual:
$ git cliff --unreleased --prepend CHANGELOG.mdThe marker is written automatically after the rendered header:
# Changelog
Tracked releases: 1
<!-- git-cliff: end of header -->
## UnreleasedOn the next
--prepend, git-cliff removes everything through the marker before writing the updated header. -
Configuration schema: the git-cliff configuration schema is now available on SchemaStore, enabling validation and autocompletion in supported editors. (#1577)
tipTo select it explicitly in a Taplo-compatible editor, add this directive at the top of your configuration:
cliff.toml#:schema https://www.schemastore.org/git-cliff.json
🌳 Git
-
Correct releases across merged branches: commits are now assigned to releases using Git graph reachability. (#1601)
Why this is big?Consider a feature branch that splits before
v1.0.0but is merged afterward:F---G
/ \
A---B---C---D---M main
|
v1.0.0FandGare not part ofv1.0.0. However, a flattenedgit logcan interleave commits from both branches and make them look like they belong to that release.git-cliff now checks the actual commit graph instead. In this example,
FandGstay under Unreleased until they are included in a later tag.This fixes changelogs that list changes under a release that never shipped them, omit those changes from Unreleased or disagree with the actual
previous_tag..taghistory. It is especially useful for repositories with long-lived release branches, backports or branches that are merged after a release. -
Limit processed tags: (#1493)
$ git cliff --limit-tags 10You can also set
limit_tagsin your configuration:[git]
limit_tags = 10 -
Nested annotated tags: tags that point to other annotated tags are peeled all the way to their commit and are no longer silently omitted. (#1360)
-
Respect commits listed in
.git-blame-ignore-revs: they are now automatically excluded from the changelog. (#1585).git-blame-ignore-revs# Mass formatting
67b8f240063d0d5b8f6c58be198d31e36fbf251a
⚡ Performance
Commit statistics are no longer calculated unless a template, parser, or --context output actually needs them. This avoids walking every diff for changelogs that do not use statistics, significantly reducing unnecessary work on larger repositories. (#1543)
❤️ New Contributors
- @ChrisJr404 made their first contribution in #1610
- @tianrking made their first contribution in #1603
- @lazizbekravshanov made their first contribution in #1584
- @jimisola made their first contribution in #1613
- @Jonnobrow made their first contribution in #1601
- @YuriNachos made their first contribution in #1605
- @hasezoey made their first contribution in #1609
- @artshmelev made their first contribution in #1597
- @Cyrus580529 made their first contribution in #1594
- @Noai-oss made their first contribution in #1587
- @nabsei made their first contribution in #1585
- @JDanRibeiro made their first contribution in #1561
- @arieleli01212 made their first contribution in #1546
- @ychampion made their first contribution in #1574
- @CatBraaain made their first contribution in #1549
- @SAY-5 made their first contribution
- @GChernikov made their first contribution in #1360
- @signekb made their first contribution in #1527
- @guerda made their first contribution in #1526
Any contribution is highly appreciated! See the contribution guidelines for getting started.
Feel free to submit issues and join our Discord / Matrix for discussion!
Follow git-cliff on X & Mastodon to not miss any news!
Support 🌟
If you like git-cliff, consider:
- 💖 GitHub Sponsors: @orhun
- ☕ Buy Me A Coffee: https://www.buymeacoffee.com/orhun
Have a fantastic day! ⛰️


