Repository: orhun/git-cliff
Pull request #1116 (2025-03-23 10:45:34)

chore(deps): bump the minor group in /website with 2 updates
Bumps the minor group in /website with 2 updates: [@easyops-cn/docusaurus-search-local](https://github.com/easyops-cn/docusaurus-search-local/tree/HEAD/packages/docusaurus-search-local) and [typescript](https://github.com/microsoft/TypeScript).

Updates `@easyops-cn/docusaurus-search-local` from 0.48.4 to 0.49.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/easyops-cn/docusaurus-search-local/releases"><code>@​easyops-cn/docusaurus-search-local</code>'s releases</a>.</em></p>
<blockquote>
<h2>v0.49.1</h2>
<h2><a href="https://github.com/easyops-cn/docusaurus-search-local/compare/v0.49.0...v0.49.1">0.49.1</a> (2025-03-12)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>refine fuzzy matching order (<a href="https://github.com/easyops-cn/docusaurus-search-local/commit/60d2b0bab03ebcb9d969ac0fabaf2deaa76eef13">60d2b0b</a>), closes <a href="https://redirect.github.com/easyops-cn/docusaurus-search-local/issues/504">#504</a></li>
<li>refine fuzzy matching order (<a href="https://redirect.github.com/easyops-cn/docusaurus-search-local/issues/507">#507</a>) (<a href="https://github.com/easyops-cn/docusaurus-search-local/commit/646b5a0fb20933c86972047ec940a51270bf96ba">646b5a0</a>), closes <a href="https://redirect.github.com/easyops-cn/docusaurus-search-local/issues/504">#504</a></li>
</ul>
<h2>v0.49.0</h2>
<h2><a href="https://github.com/easyops-cn/docusaurus-search-local/compare/v0.48.5...v0.49.0">0.49.0</a> (2025-03-12)</h2>
<h3>Features</h3>
<ul>
<li>support fuzzy matching, closes <a href="https://redirect.github.com/easyops-cn/docusaurus-search-local/issues/504">#504</a> (<a href="https://redirect.github.com/easyops-cn/docusaurus-search-local/issues/505">#505</a>) (<a href="https://github.com/easyops-cn/docusaurus-search-local/commit/c8310f39c05f0409ba227318a70dba828ca65aa0">c8310f3</a>)</li>
</ul>
<h2>v0.48.5</h2>
<h2><a href="https://github.com/easyops-cn/docusaurus-search-local/compare/v0.48.4...v0.48.5">0.48.5</a> (2025-02-05)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>fix contextual search, closes <a href="https://redirect.github.com/easyops-cn/docusaurus-search-local/issues/380">#380</a> (<a href="https://github.com/easyops-cn/docusaurus-search-local/commit/2a6d9d4f6ad7daea4f2c3c41604494a6a94492f3">2a6d9d4</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a href="https://github.com/easyops-cn/docusaurus-search-local/commits/v0.49.1/packages/docusaurus-search-local">compare view</a></li>
</ul>
</details>
<br />

Updates `typescript` from 5.7.3 to 5.8.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/microsoft/TypeScript/releases">typescript's releases</a>.</em></p>
<blockquote>
<h2>TypeScript 5.8</h2>
<p>For release notes, check out the <a href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-8/">release announcement</a>.</p>
<ul>
<li><a href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+5.8.0%22+is%3Aclosed+">fixed issues query for Typescript 5.8.0 (Beta)</a>.</li>
<li><a href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+5.8.1%22+is%3Aclosed+">fixed issues query for Typescript 5.8.1 (RC)</a>.</li>
<li><a href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+5.8.2%22+is%3Aclosed+">fixed issues query for Typescript 5.8.2 (Stable)</a>.</li>
</ul>
<p>Downloads are available on:</p>
<ul>
<li><a href="https://www.npmjs.com/package/typescript">npm</a></li>
</ul>
<h2>TypeScript 5.8 RC</h2>
<p>For release notes, check out the <a href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-8-rc/">release announcement</a>.</p>
<ul>
<li><a href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+5.8.0%22+is%3Aclosed+">fixed issues query for Typescript 5.8.0 (Beta)</a>.</li>
<li><a href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+5.8.1%22+is%3Aclosed+">fixed issues query for Typescript 5.8.1 (RC)</a>.</li>
</ul>
<p>Downloads are available on:</p>
<ul>
<li><a href="https://www.npmjs.com/package/typescript">npm</a></li>
</ul>
<h2>TypeScript 5.8 Beta</h2>
<p>For release notes, check out the <a href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-8-beta/">release announcement</a>.</p>
<ul>
<li><a href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+5.8.0%22+is%3Aclosed+">fixed issues query for Typescript 5.8.0 (Beta)</a>.</li>
</ul>
<p>Downloads are available on:</p>
<ul>
<li><a href="https://www.npmjs.com/package/typescript">npm</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/microsoft/TypeScript/commit/beb69e4cdd61b1a0fd9ae21ae58bd4bd409d7217"><code>beb69e4</code></a> Bump version to 5.8.2 and LKG</li>
<li><a href="https://github.com/microsoft/TypeScript/commit/8fdbd54160e5a6796a2c2522222553e79d61ec7c"><code>8fdbd54</code></a> 🤖 Pick PR <a href="https://redirect.github.com/microsoft/TypeScript/issues/61210">#61210</a> (Fix mistakenly disallowed default e...) into release-5.8 (#...</li>
<li><a href="https://github.com/microsoft/TypeScript/commit/f4a3a8ae67f2bb437cceec710979c3c107cff177"><code>f4a3a8a</code></a> 🤖 Pick PR <a href="https://redirect.github.com/microsoft/TypeScript/issues/61175">#61175</a> (Ban import=require and export= unde...) into release-5.8 (#...</li>
<li><a href="https://github.com/microsoft/TypeScript/commit/420ff069f7e1844c0b94f591acbd4aa68846e295"><code>420ff06</code></a> Bump version to 5.8.1-rc and LKG</li>
<li><a href="https://github.com/microsoft/TypeScript/commit/48eb13fd4ac9cfa1ef67b688e735aae48a24d300"><code>48eb13f</code></a> Update LKG</li>
<li><a href="https://github.com/microsoft/TypeScript/commit/fb59c19064a547dd08e02b58fec0c6213711a1f3"><code>fb59c19</code></a> Merge remote-tracking branch 'origin/main' into release-5.8</li>
<li><a href="https://github.com/microsoft/TypeScript/commit/df342b7206cb56b56bb3b3aecbb2ee2d2ff7b217"><code>df342b7</code></a> Fixed <code>rewriteRelativeImportExtensions</code> for <code>import()</code> within call expression...</li>
<li><a href="https://github.com/microsoft/TypeScript/commit/775412a81af187e67371cd31719d19ab075b4ef3"><code>775412a</code></a> Bump github/codeql-action from 3.28.8 to 3.28.9 in the github-actions group (...</li>
<li><a href="https://github.com/microsoft/TypeScript/commit/e1629e540ef361167e0c96c8703728a419b6e8e9"><code>e1629e5</code></a> Pass ignoreErrors=true to more resolveEntityName callers (<a href="https://redirect.github.com/microsoft/TypeScript/issues/61144">#61144</a>)</li>
<li><a href="https://github.com/microsoft/TypeScript/commit/6fd17992b00a6e64b51fef60e334b71904d06a54"><code>6fd1799</code></a> Update LKG</li>
<li>Additional commits viewable in <a href="https://github.com/microsoft/TypeScript/compare/v5.7.3...v5.8.2">compare view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions


</details>


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index f0e7dba..873f937 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -30,7 +30,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
 dependencies = [
  "cfg-if",
- "getrandom",
+ "getrandom 0.2.15",
  "once_cell",
  "version_check",
  "zerocopy",
@@ -126,9 +126,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.89"
+version = "1.0.98"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
+checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
 
 [[package]]
 name = "arraydeque"
@@ -158,9 +158,9 @@ dependencies = [
 
 [[package]]
 name = "async-trait"
-version = "0.1.82"
+version = "0.1.88"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
+checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -211,19 +211,20 @@ dependencies = [
 
 [[package]]
 name = "bincode"
-version = "2.0.0-rc.3"
+version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95"
+checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
 dependencies = [
  "bincode_derive",
  "serde",
+ "unty",
 ]
 
 [[package]]
 name = "bincode_derive"
-version = "2.0.0-rc.3"
+version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e30759b3b99a1b802a7a3aa21c85c3ded5c28e1c83170d82d70f08bbf7f3e4c"
+checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
 dependencies = [
  "virtue",
 ]
@@ -369,9 +370,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.27"
+version = "4.5.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796"
+checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -379,9 +380,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.27"
+version = "4.5.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
+checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
 dependencies = [
  "anstream",
  "anstyle",
@@ -392,18 +393,18 @@ dependencies = [
 
 [[package]]
 name = "clap_complete"
-version = "4.5.42"
+version = "4.5.47"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33a7e468e750fa4b6be660e8b5651ad47372e8fb114030b594c2d75d48c5ffd0"
+checksum = "c06f5378ea264ad4f82bbc826628b5aad714a75abf6ece087e923010eb937fb6"
 dependencies = [
  "clap",
 ]
 
 [[package]]
 name = "clap_derive"
-version = "4.5.24"
+version = "4.5.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c"
+checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
 dependencies = [
  "heck",
  "proc-macro2",
@@ -435,9 +436,9 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
 
 [[package]]
 name = "config"
-version = "0.15.6"
+version = "0.15.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e329294a796e9b22329669c1f433a746983f9e324e07f4ef135be81bb2262de4"
+checksum = "595aae20e65c3be792d05818e8c63025294ac3cb7e200f11459063a352a6ef80"
 dependencies = [
  "pathdiff",
  "serde",
@@ -459,16 +460,6 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
-[[package]]
-name = "conventional_commit_parser"
-version = "0.9.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58660f9e1d5eeeeec9c33d1473ea8bba000c673a2189edaeedb4523ec7d6f7cb"
-dependencies = [
- "pest",
- "pest_derive",
-]
-
 [[package]]
 name = "core-foundation"
 version = "0.10.0"
@@ -573,9 +564,9 @@ dependencies = [
 
 [[package]]
 name = "deranged"
-version = "0.3.11"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
 dependencies = [
  "powerfmt",
 ]
@@ -640,26 +631,20 @@ version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d"
 
-[[package]]
-name = "doc-comment"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
-
 [[package]]
 name = "document-features"
-version = "0.2.10"
+version = "0.2.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0"
+checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
 dependencies = [
  "litrs",
 ]
 
 [[package]]
 name = "dyn-clone"
-version = "1.0.17"
+version = "1.0.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
+checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
 
 [[package]]
 name = "either"
@@ -786,6 +771,12 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
 [[package]]
 name = "form_urlencoded"
 version = "1.2.1"
@@ -903,10 +894,22 @@ dependencies = [
  "cfg-if",
  "js-sys",
  "libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "getrandom"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
+]
+
 [[package]]
 name = "gimli"
 version = "0.31.1"
@@ -930,7 +933,7 @@ dependencies = [
  "pathdiff",
  "pprof",
  "pretty_assertions",
- "rand",
+ "rand 0.9.1",
  "regex",
  "reqwest",
  "secrecy",
@@ -943,7 +946,7 @@ dependencies = [
 name = "git-cliff-core"
 version = "2.8.0"
 dependencies = [
- "bincode 2.0.0-rc.3",
+ "bincode 2.0.1",
  "cacache",
  "config",
  "dirs",
@@ -972,7 +975,7 @@ dependencies = [
  "serde_regex",
  "temp-dir",
  "tera",
- "thiserror 2.0.11",
+ "thiserror 2.0.12",
  "time",
  "tokio",
  "toml",
@@ -982,11 +985,10 @@ dependencies = [
 
 [[package]]
 name = "git-conventional"
-version = "0.12.7"
+version = "0.12.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8066dc2ef3bd0e2bfb84b4a2b0b04f216ffb97390e09fab0752bf0ba943dacc6"
+checksum = "f6a949b7fcc81df22526032dcddb006e78c8575e47b0e7ba57d9960570a57bc4"
 dependencies = [
- "doc-comment",
  "serde",
  "unicase",
  "winnow",
@@ -994,9 +996,9 @@ dependencies = [
 
 [[package]]
 name = "git2"
-version = "0.20.0"
+version = "0.20.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fda788993cc341f69012feba8bf45c0ba4f3291fcc08e214b4d5a7332d88aff"
+checksum = "5220b8ba44c68a9a7f7a7659e864dd73692e417ef0211bea133c7b74e031eeb9"
 dependencies = [
  "bitflags 2.8.0",
  "libc",
@@ -1050,14 +1052,17 @@ name = "hashbrown"
 version = "0.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
+dependencies = [
+ "foldhash",
+]
 
 [[package]]
 name = "hashlink"
-version = "0.8.4"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
 dependencies = [
- "hashbrown 0.14.5",
+ "hashbrown 0.15.1",
 ]
 
 [[package]]
@@ -1463,9 +1468,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "2.7.1"
+version = "2.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
+checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
 dependencies = [
  "equivalent",
  "hashbrown 0.15.1",
@@ -1473,9 +1478,9 @@ dependencies = [
 
 [[package]]
 name = "indicatif"
-version = "0.17.9"
+version = "0.17.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281"
+checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
 dependencies = [
  "console",
  "number_prefix",
@@ -1611,9 +1616,9 @@ dependencies = [
 
 [[package]]
 name = "libgit2-sys"
-version = "0.18.0+1.9.0"
+version = "0.18.1+1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1a117465e7e1597e8febea8bb0c410f1c7fb93b1e1cddf34363f8390367ffec"
+checksum = "e1dcb20f84ffcdd825c7a311ae347cce604a6f084a767dec4a4929829645290e"
 dependencies = [
  "cc",
  "libc",
@@ -1679,9 +1684,9 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.25"
+version = "0.4.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
 
 [[package]]
 name = "memchr"
@@ -1752,17 +1757,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
 dependencies = [
  "libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
  "windows-sys 0.52.0",
 ]
 
 [[package]]
 name = "next_version"
-version = "0.2.21"
+version = "0.2.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac10cbb9ea44ccdaf522542f25c6139266b2c0d9165148cc6e1d9970f2d2eb32"
+checksum = "318b4d09572813254f5da9e4326415d834f8d27f5cfa87939320d661c4aee07b"
 dependencies = [
- "conventional_commit_parser",
+ "git-conventional",
  "regex",
  "semver",
 ]
@@ -1951,7 +1956,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
 dependencies = [
  "phf_shared",
- "rand",
+ "rand 0.8.5",
 ]
 
 [[package]]
@@ -2065,7 +2070,7 @@ dependencies = [
  "rustc-hash",
  "rustls",
  "socket2",
- "thiserror 2.0.11",
+ "thiserror 2.0.12",
  "tokio",
  "tracing",
 ]
@@ -2077,14 +2082,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d"
 dependencies = [
  "bytes",
- "getrandom",
- "rand",
+ "getrandom 0.2.15",
+ "rand 0.8.5",
  "ring",
  "rustc-hash",
  "rustls",
  "rustls-pki-types",
  "slab",
- "thiserror 2.0.11",
+ "thiserror 2.0.12",
  "tinyvec",
  "tracing",
  "web-time",
@@ -2113,6 +2118,12 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "r-efi"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
+
 [[package]]
 name = "rand"
 version = "0.8.5"
@@ -2120,8 +2131,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
  "libc",
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
 ]
 
 [[package]]
@@ -2131,7 +2152,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
 dependencies = [
  "ppv-lite86",
- "rand_core",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.3",
 ]
 
 [[package]]
@@ -2140,7 +2171,16 @@ version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.15",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.2",
 ]
 
 [[package]]
@@ -2158,7 +2198,7 @@ version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.15",
  "libredox",
  "thiserror 1.0.69",
 ]
@@ -2205,9 +2245,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
 
 [[package]]
 name = "reqwest"
-version = "0.12.12"
+version = "0.12.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
+checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
 dependencies = [
  "async-compression",
  "base64 0.22.1",
@@ -2252,9 +2292,9 @@ dependencies = [
 
 [[package]]
 name = "reqwest-middleware"
-version = "0.4.0"
+version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1ccd3b55e711f91a9885a2fa6fbbb2e39db1776420b062efc058c6410f7e5e3"
+checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -2282,7 +2322,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
 dependencies = [
  "cc",
  "cfg-if",
- "getrandom",
+ "getrandom 0.2.15",
  "libc",
  "spin",
  "untrusted",
@@ -2303,9 +2343,9 @@ checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3"
 
 [[package]]
 name = "rust-embed"
-version = "8.5.0"
+version = "8.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0"
+checksum = "e5fbc0ee50fcb99af7cebb442e5df7b5b45e9460ffa3f8f549cd26b862bec49d"
 dependencies = [
  "include-flate",
  "rust-embed-impl",
@@ -2315,9 +2355,9 @@ dependencies = [
 
 [[package]]
 name = "rust-embed-impl"
-version = "8.5.0"
+version = "8.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478"
+checksum = "6bf418c9a2e3f6663ca38b8a7134cc2c2167c9d69688860e8961e3faa731702e"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2328,9 +2368,9 @@ dependencies = [
 
 [[package]]
 name = "rust-embed-utils"
-version = "8.5.0"
+version = "8.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d"
+checksum = "08d55b95147fe01265d06b3955db798bdaed52e60e2211c41137701b3aba8e21"
 dependencies = [
  "sha2",
  "walkdir",
@@ -2488,24 +2528,24 @@ dependencies = [
 
 [[package]]
 name = "semver"
-version = "1.0.25"
+version = "1.0.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
+checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
 
 [[package]]
 name = "serde"
-version = "1.0.217"
+version = "1.0.219"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.217"
+version = "1.0.219"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2514,9 +2554,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.137"
+version = "1.0.140"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
 dependencies = [
  "itoa",
  "memchr",
@@ -2536,9 +2576,9 @@ dependencies = [
 
 [[package]]
 name = "serde_spanned"
-version = "0.6.7"
+version = "0.6.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
+checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
 dependencies = [
  "serde",
 ]
@@ -2590,9 +2630,9 @@ dependencies = [
 
 [[package]]
 name = "shellexpand"
-version = "3.1.0"
+version = "3.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
+checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
 dependencies = [
  "dirs",
 ]
@@ -2778,7 +2818,7 @@ dependencies = [
  "percent-encoding",
  "pest",
  "pest_derive",
- "rand",
+ "rand 0.8.5",
  "regex",
  "serde",
  "serde_json",
@@ -2816,11 +2856,11 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "2.0.11"
+version = "2.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
 dependencies = [
- "thiserror-impl 2.0.11",
+ "thiserror-impl 2.0.12",
 ]
 
 [[package]]
@@ -2836,9 +2876,9 @@ dependencies = [
 
 [[package]]
 name = "thiserror-impl"
-version = "2.0.11"
+version = "2.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2847,9 +2887,9 @@ dependencies = [
 
 [[package]]
 name = "time"
-version = "0.3.37"
+version = "0.3.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
+checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
 dependencies = [
  "deranged",
  "itoa",
@@ -2862,15 +2902,15 @@ dependencies = [
 
 [[package]]
 name = "time-core"
-version = "0.1.2"
+version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
 
 [[package]]
 name = "time-macros"
-version = "0.2.19"
+version = "0.2.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
+checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
 dependencies = [
  "num-conv",
  "time-core",
@@ -2903,9 +2943,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 
 [[package]]
 name = "tokio"
-version = "1.43.0"
+version = "1.44.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
+checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
 dependencies = [
  "backtrace",
  "bytes",
@@ -2964,9 +3004,9 @@ dependencies = [
 
 [[package]]
 name = "toml"
-version = "0.8.19"
+version = "0.8.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
+checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
 dependencies = [
  "serde",
  "serde_spanned",
@@ -2985,9 +3025,9 @@ dependencies = [
 
 [[package]]
 name = "toml_edit"
-version = "0.22.21"
+version = "0.22.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf"
+checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
 dependencies = [
  "indexmap",
  "serde",
@@ -3143,6 +3183,12 @@ version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
 
+[[package]]
+name = "unty"
+version = "0.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
+
 [[package]]
 name = "update-informer"
 version = "1.2.0"
@@ -3231,9 +3277,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
 
 [[package]]
 name = "virtue"
-version = "0.0.13"
+version = "0.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314"
+checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
 
 [[package]]
 name = "walkdir"
@@ -3260,6 +3306,15 @@ version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
+[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
 [[package]]
 name = "wasm-bindgen"
 version = "0.2.100"
@@ -3418,8 +3473,8 @@ checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
 dependencies = [
  "windows-implement",
  "windows-interface",
- "windows-result",
- "windows-strings",
+ "windows-result 0.2.0",
+ "windows-strings 0.1.0",
  "windows-targets 0.52.6",
 ]
 
@@ -3445,15 +3500,21 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "windows-link"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
+
 [[package]]
 name = "windows-registry"
-version = "0.2.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
+checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
 dependencies = [
- "windows-result",
- "windows-strings",
- "windows-targets 0.52.6",
+ "windows-result 0.3.2",
+ "windows-strings 0.3.1",
+ "windows-targets 0.53.0",
 ]
 
 [[package]]
@@ -3465,16 +3526,34 @@ dependencies = [
  "windows-targets 0.52.6",
 ]
 
+[[package]]
+name = "windows-result"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
+dependencies = [
+ "windows-link",
+]
+
 [[package]]
 name = "windows-strings"
 version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
 dependencies = [
- "windows-result",
+ "windows-result 0.2.0",
  "windows-targets 0.52.6",
 ]
 
+[[package]]
+name = "windows-strings"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
+dependencies = [
+ "windows-link",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.48.0"
@@ -3526,13 +3605,29 @@ dependencies = [
  "windows_aarch64_gnullvm 0.52.6",
  "windows_aarch64_msvc 0.52.6",
  "windows_i686_gnu 0.52.6",
- "windows_i686_gnullvm",
+ "windows_i686_gnullvm 0.52.6",
  "windows_i686_msvc 0.52.6",
  "windows_x86_64_gnu 0.52.6",
  "windows_x86_64_gnullvm 0.52.6",
  "windows_x86_64_msvc 0.52.6",
 ]
 
+[[package]]
+name = "windows-targets"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
+dependencies = [
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
 [[package]]
 name = "windows_aarch64_gnullvm"
 version = "0.48.5"
@@ -3545,6 +3640,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.48.5"
@@ -3557,6 +3658,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.48.5"
@@ -3569,12 +3676,24 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
 [[package]]
 name = "windows_i686_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.48.5"
@@ -3587,6 +3706,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.48.5"
@@ -3599,6 +3724,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
 [[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.48.5"
@@ -3611,6 +3742,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.48.5"
@@ -3623,15 +3760,30 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
 [[package]]
 name = "winnow"
-version = "0.6.22"
+version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980"
+checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10"
 dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags 2.8.0",
+]
+
 [[package]]
 name = "write16"
 version = "1.0.0"
@@ -3652,9 +3804,9 @@ checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984"
 
 [[package]]
 name = "yaml-rust2"
-version = "0.9.0"
+version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a1a1c0bc9823338a3bdf8c61f994f23ac004c6fa32c08cd152984499b445e8d"
+checksum = "818913695e83ece1f8d2a1c52d54484b7b46d0f9c06beeb2649b9da50d9b512d"
 dependencies = [
  "arraydeque",
  "encoding_rs",
diff --git a/Dockerfile b/Dockerfile
index 8e2d83c..4630d7e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # syntax=docker/dockerfile:1.4.3-labs
-FROM lukemathwalker/cargo-chef:0.1.70-rust-1.84.0-slim AS chef
+FROM lukemathwalker/cargo-chef:0.1.71-rust-1.85.0-slim-bookworm AS chef
 WORKDIR app
 
 FROM chef AS planner
diff --git a/config/cliff.toml b/config/cliff.toml
index b95f080..c81800c 100644
--- a/config/cliff.toml
+++ b/config/cliff.toml
@@ -37,8 +37,8 @@ trim = true
 render_always = true
 # An array of regex based postprocessors to modify the changelog.
 postprocessors = [
-  # Replace the placeholder <REPO> with a URL.
-  #{ pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" },
+    # Replace the placeholder <REPO> with a URL.
+    #{ pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" },
 ]
 # render body even when there are no releases to process
 # render_always = true
@@ -58,11 +58,11 @@ require_conventional = false
 split_commits = false
 # An array of regex based parsers to modify commit messages prior to further processing.
 commit_preprocessors = [
-  # Replace issue numbers with link templates to be updated in `changelog.postprocessors`.
-  #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
-  # Check spelling of the commit message using https://github.com/crate-ci/typos.
-  # If the spelling is incorrect, it will be fixed automatically.
-  #{ pattern = '.*', replace_command = 'typos --write-changes -' },
+    # Replace issue numbers with link templates to be updated in `changelog.postprocessors`.
+    #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
+    # Check spelling of the commit message using https://github.com/crate-ci/typos.
+    # If the spelling is incorrect, it will be fixed automatically.
+    #{ pattern = '.*', replace_command = 'typos --write-changes -' },
 ]
 # Prevent commits that are breaking from being excluded by commit parsers.
 protect_breaking_commits = false
@@ -70,21 +70,21 @@ protect_breaking_commits = false
 # Assigns commits to groups.
 # Optionally sets the commit's scope and can decide to exclude commits from further processing.
 commit_parsers = [
-  { message = "^feat", group = "<!-- 0 -->🚀 Features" },
-  { message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
-  { message = "^doc", group = "<!-- 3 -->📚 Documentation" },
-  { message = "^perf", group = "<!-- 4 -->⚡ Performance" },
-  { message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
-  { message = "^style", group = "<!-- 5 -->🎨 Styling" },
-  { message = "^test", group = "<!-- 6 -->🧪 Testing" },
-  { message = "^chore\\(release\\): prepare for", skip = true },
-  { message = "^chore\\(deps.*\\)", skip = true },
-  { message = "^chore\\(pr\\)", skip = true },
-  { message = "^chore\\(pull\\)", skip = true },
-  { message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
-  { body = ".*security", group = "<!-- 8 -->🛡️ Security" },
-  { message = "^revert", group = "<!-- 9 -->◀️ Revert" },
-  { message = ".*", group = "<!-- 10 -->💼 Other" },
+    { message = "^feat", group = "<!-- 0 -->🚀 Features" },
+    { message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
+    { message = "^doc", group = "<!-- 3 -->📚 Documentation" },
+    { message = "^perf", group = "<!-- 4 -->⚡ Performance" },
+    { message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
+    { message = "^style", group = "<!-- 5 -->🎨 Styling" },
+    { message = "^test", group = "<!-- 6 -->🧪 Testing" },
+    { message = "^chore\\(release\\): prepare for", skip = true },
+    { message = "^chore\\(deps.*\\)", skip = true },
+    { message = "^chore\\(pr\\)", skip = true },
+    { message = "^chore\\(pull\\)", skip = true },
+    { message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
+    { body = ".*security", group = "<!-- 8 -->🛡️ Security" },
+    { message = "^revert", group = "<!-- 9 -->◀️ Revert" },
+    { message = ".*", group = "<!-- 10 -->💼 Other" },
 ]
 # Exclude commits that are not matched by any commit parser.
 filter_commits = false
@@ -94,6 +94,10 @@ link_parsers = []
 use_branch_tags = false
 # Order releases topologically instead of chronologically.
 topo_order = false
+# Order releases topologically instead of chronologically.
+topo_order_commits = true
 # Order of commits in each group/release within the changelog.
 # Allowed values: newest, oldest
 sort_commits = "oldest"
+# Process submodules commits
+recurse_submodules = false
diff --git a/flake.lock b/flake.lock
index 8673ad8..9208fc2 100644
--- a/flake.lock
+++ b/flake.lock
@@ -20,11 +20,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1737469691,
-        "narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=",
+        "lastModified": 1744932701,
+        "narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab",
+        "rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef",
         "type": "github"
       },
       "original": {
@@ -36,11 +36,11 @@
     },
     "nixpkgs_2": {
       "locked": {
-        "lastModified": 1736320768,
-        "narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
+        "lastModified": 1744536153,
+        "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
+        "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
         "type": "github"
       },
       "original": {
@@ -62,11 +62,11 @@
         "nixpkgs": "nixpkgs_2"
       },
       "locked": {
-        "lastModified": 1737512878,
-        "narHash": "sha256-dgF6htdmfNnZzVInifks6npnCAyVsIHWSpWNs10RSW0=",
+        "lastModified": 1745116541,
+        "narHash": "sha256-5xzA6dTfqCfTTDCo3ipPZzrg3wp01xmcr73y4cTNMP8=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "06b8ed0eee289fe94c66f1202ced9a6a2c59a14c",
+        "rev": "e2142ef330a61c02f274ac9a9cb6f8487a5d0080",
         "type": "github"
       },
       "original": {
diff --git a/git-cliff-core/Cargo.toml b/git-cliff-core/Cargo.toml
index 343cec7..94edf36 100644
--- a/git-cliff-core/Cargo.toml
+++ b/git-cliff-core/Cargo.toml
@@ -8,8 +8,8 @@ readme = "../README.md"
 homepage = "https://github.com/orhun/git-cliff"
 repository = "https://github.com/orhun/git-cliff"
 keywords = ["changelog", "generator", "conventional", "commit"]
-edition = "2021"
-rust-version = "1.83.0"
+edition = "2024"
+rust-version = "1.85.0"
 
 [features]
 default = ["repo"]
@@ -49,16 +49,16 @@ log.workspace = true
 secrecy.workspace = true
 dirs.workspace = true
 lazy_static.workspace = true
-thiserror = "2.0.11"
+thiserror = "2.0.12"
 serde = { version = "1.0.217", features = ["derive"] }
 serde_json = "1.0.137"
-bincode = "2.0.0-rc.3"
+bincode = "2.0.1"
 serde_regex = "1.1.0"
 tera = "1.20.0"
 indexmap = { version = "2.7.1", optional = true }
 toml = "0.8.19"
 lazy-regex = "3.4.1"
-next_version = "0.2.21"
+next_version = "0.2.24"
 semver = "1.0.25"
 document-features = { version = "0.2.10", optional = true }
 reqwest = { workspace = true, optional = true }
diff --git a/git-cliff-core/src/changelog.rs b/git-cliff-core/src/changelog.rs
index 6bfb901..125b18b 100644
--- a/git-cliff-core/src/changelog.rs
+++ b/git-cliff-core/src/changelog.rs
@@ -44,9 +44,13 @@ pub struct Changelog<'a> {
 
 impl<'a> Changelog<'a> {
 	/// Constructs a new instance.
-	pub fn new(releases: Vec<Release<'a>>, config: &'a Config) -> Result<Self> {
+	pub fn new(
+		releases: Vec<Release<'a>>,
+		config: &'a Config,
+		range: Option<&str>,
+	) -> Result<Self> {
 		let mut changelog = Changelog::build(releases, config)?;
-		changelog.add_remote_data()?;
+		changelog.add_remote_data(range)?;
 		changelog.process_commits()?;
 		changelog.process_releases();
 		Ok(changelog)
@@ -116,67 +120,26 @@ impl<'a> Changelog<'a> {
 		}
 	}
 
-	/// Processes the commits and omits the ones that doesn't match the
-	/// criteria set by configuration file.
-	fn process_commits(&mut self) -> Result<()> {
-		debug!("Processing the commits...");
-		self.releases.iter_mut().for_each(|release| {
-			release.commits = release
-				.commits
-				.iter()
-				.cloned()
-				.filter_map(|commit| Self::process_commit(&commit, &self.config.git))
-				.flat_map(|commit| {
-					if self.config.git.split_commits {
-						commit
-							.message
-							.lines()
-							.filter_map(|line| {
-								let mut c = commit.clone();
-								c.message = line.to_string();
-								if c.message.is_empty() {
-									None
-								} else {
-									Self::process_commit(&c, &self.config.git)
-								}
-							})
-							.collect()
-					} else {
-						vec![commit]
-					}
-				})
-				.collect::<Vec<Commit>>();
-		});
-
-		if self.config.git.require_conventional {
-			self.check_conventional_commits()?;
-		}
-
-		Ok(())
-	}
-
 	/// Checks the commits and returns an error if any unconventional commits
 	/// are found.
-	fn check_conventional_commits(&mut self) -> Result<()> {
+	fn check_conventional_commits(commits: &Vec<Commit<'a>>) -> Result<()> {
 		debug!("Verifying that all commits are conventional.");
 		let mut unconventional_count = 0;
 
-		self.releases.iter().for_each(|release| {
-			release.commits.iter().for_each(|commit| {
-				if commit.conv.is_none() {
-					error!(
-						"Commit {id} is not conventional:\n{message}",
-						id = &commit.id[..7],
-						message = commit
-							.message
-							.lines()
-							.map(|line| { format!("    | {}", line.trim()) })
-							.collect::<Vec<String>>()
-							.join("\n")
-					);
-					unconventional_count += 1;
-				}
-			});
+		commits.iter().for_each(|commit| {
+			if commit.conv.is_none() {
+				error!(
+					"Commit {id} is not conventional:\n{message}",
+					id = &commit.id[..7],
+					message = commit
+						.message
+						.lines()
+						.map(|line| { format!("    | {}", line.trim()) })
+						.collect::<Vec<String>>()
+						.join("\n")
+				);
+				unconventional_count += 1;
+			}
 		});
 
 		if unconventional_count > 0 {
@@ -186,6 +149,54 @@ impl<'a> Changelog<'a> {
 		Ok(())
 	}
 
+	fn process_commit_list(
+		commits: &mut Vec<Commit<'a>>,
+		git_config: &GitConfig,
+	) -> Result<()> {
+		*commits = commits
+			.iter()
+			.filter_map(|commit| Self::process_commit(commit, git_config))
+			.flat_map(|commit| {
+				if git_config.split_commits {
+					commit
+						.message
+						.lines()
+						.filter_map(|line| {
+							let mut c = commit.clone();
+							c.message = line.to_string();
+							if c.message.is_empty() {
+								None
+							} else {
+								Self::process_commit(&c, git_config)
+							}
+						})
+						.collect()
+				} else {
+					vec![commit]
+				}
+			})
+			.collect::<Vec<Commit>>();
+
+		if git_config.require_conventional {
+			Self::check_conventional_commits(commits)?;
+		}
+
+		Ok(())
+	}
+
+	/// Processes the commits and omits the ones that doesn't match the
+	/// criteria set by configuration file.
+	fn process_commits(&mut self) -> Result<()> {
+		debug!("Processing the commits...");
+		for release in self.releases.iter_mut() {
+			Self::process_commit_list(&mut release.commits, &self.config.git)?;
+			for submodule_commits in release.submodule_commits.values_mut() {
+				Self::process_commit_list(submodule_commits, &self.config.git)?;
+			}
+		}
+		Ok(())
+	}
+
 	/// Processes the releases and filters them out based on the configuration.
 	fn process_releases(&mut self) {
 		debug!("Processing {} release(s)...", self.releases.len());
@@ -256,7 +267,10 @@ impl<'a> Changelog<'a> {
 	/// If no GitHub related variable is used in the template then this function
 	/// returns empty vectors.
 	#[cfg(feature = "github")]
-	fn get_github_metadata(&self) -> Result<crate::remote::RemoteMetadata> {
+	fn get_github_metadata(
+		&self,
+		ref_name: Option<&str>,
+	) -> Result<crate::remote::RemoteMetadata> {
 		use crate::remote::github;
 		if self.config.remote.github.is_custom ||
 			self.body_template
@@ -279,8 +293,8 @@ impl<'a> Changelog<'a> {
 				.build()?
 				.block_on(async {
 					let (commits, pull_requests) = tokio::try_join!(
-						github_client.get_commits(),
-						github_client.get_pull_requests(),
+						github_client.get_commits(ref_name),
+						github_client.get_pull_requests(ref_name),
 					)?;
 					debug!("Number of GitHub commits: {}", commits.len());
 					debug!(
@@ -312,7 +326,10 @@ impl<'a> Changelog<'a> {
 	/// If no GitLab related variable is used in the template then this function
 	/// returns empty vectors.
 	#[cfg(feature = "gitlab")]
-	fn get_gitlab_metadata(&self) -> Result<crate::remote::RemoteMetadata> {
+	fn get_gitlab_metadata(
+		&self,
+		ref_name: Option<&str>,
+	) -> Result<crate::remote::RemoteMetadata> {
 		use crate::remote::gitlab;
 		if self.config.remote.gitlab.is_custom ||
 			self.body_template
@@ -335,18 +352,18 @@ impl<'a> Changelog<'a> {
 				.build()?
 				.block_on(async {
 					// Map repo/owner to gitlab id
-					let project_id = match tokio::join!(gitlab_client.get_project())
-					{
-						(Ok(project),) => project.id,
-						(Err(err),) => {
-							error!("Failed to lookup project! {}", err);
-							return Err(err);
-						}
-					};
+					let project_id =
+						match tokio::join!(gitlab_client.get_project(ref_name)) {
+							(Ok(project),) => project.id,
+							(Err(err),) => {
+								error!("Failed to lookup project! {}", err);
+								return Err(err);
+							}
+						};
 					let (commits, merge_requests) = tokio::try_join!(
 						// Send id to these functions
-						gitlab_client.get_commits(project_id),
-						gitlab_client.get_merge_requests(project_id),
+						gitlab_client.get_commits(project_id, ref_name),
+						gitlab_client.get_merge_requests(project_id, ref_name),
 					)?;
 					debug!("Number of GitLab commits: {}", commits.len());
 					debug!(
@@ -376,7 +393,10 @@ impl<'a> Changelog<'a> {
 	/// If no Gitea related variable is used in the template then this function
 	/// returns empty vectors.
 	#[cfg(feature = "gitea")]
-	fn get_gitea_metadata(&self) -> Result<crate::remote::RemoteMetadata> {
+	fn get_gitea_metadata(
+		&self,
+		ref_name: Option<&str>,
+	) -> Result<crate::remote::RemoteMetadata> {
 		use crate::remote::gitea;
 		if self.config.remote.gitea.is_custom ||
 			self.body_template
@@ -399,8 +419,8 @@ impl<'a> Changelog<'a> {
 				.build()?
 				.block_on(async {
 					let (commits, pull_requests) = tokio::try_join!(
-						gitea_client.get_commits(),
-						gitea_client.get_pull_requests(),
+						gitea_client.get_commits(ref_name),
+						gitea_client.get_pull_requests(ref_name),
 					)?;
 					debug!("Number of Gitea commits: {}", commits.len());
 					debug!("Number of Gitea pull requests: {}", pull_requests.len());
@@ -429,7 +449,10 @@ impl<'a> Changelog<'a> {
 	/// If no bitbucket related variable is used in the template then this
 	/// function returns empty vectors.
 	#[cfg(feature = "bitbucket")]
-	fn get_bitbucket_metadata(&self) -> Result<crate::remote::RemoteMetadata> {
+	fn get_bitbucket_metadata(
+		&self,
+		ref_name: Option<&str>,
+	) -> Result<crate::remote::RemoteMetadata> {
 		use crate::remote::bitbucket;
 		if self.config.remote.bitbucket.is_custom ||
 			self.body_template
@@ -452,8 +475,8 @@ impl<'a> Changelog<'a> {
 				.build()?
 				.block_on(async {
 					let (commits, pull_requests) = tokio::try_join!(
-						bitbucket_client.get_commits(),
-						bitbucket_client.get_pull_requests()
+						bitbucket_client.get_commits(ref_name),
+						bitbucket_client.get_pull_requests(ref_name)
 					)?;
 					debug!("Number of Bitbucket commits: {}", commits.len());
 					debug!(
@@ -479,14 +502,22 @@ impl<'a> Changelog<'a> {
 	}
 
 	/// Adds remote data (e.g. GitHub commits) to the releases.
-	pub fn add_remote_data(&mut self) -> Result<()> {
+	pub fn add_remote_data(&mut self, range: Option<&str>) -> Result<()> {
 		debug!("Adding remote data...");
 		self.add_remote_context()?;
 
+		// Determine the ref at which to fetch remote commits, based on the commit
+		// range
+		let range_head = range.and_then(|r| r.split("..").last());
+		let ref_name = match range_head {
+			Some("HEAD") => None,
+			other => other,
+		};
+
 		#[cfg(feature = "github")]
 		let (github_commits, github_pull_requests) = if self.config.remote.github.is_set()
 		{
-			self.get_github_metadata()
+			self.get_github_metadata(ref_name)
 				.expect("Could not get github metadata")
 		} else {
 			(vec![], vec![])
@@ -494,14 +525,14 @@ impl<'a> Changelog<'a> {
 		#[cfg(feature = "gitlab")]
 		let (gitlab_commits, gitlab_merge_request) = if self.config.remote.gitlab.is_set()
 		{
-			self.get_gitlab_metadata()
+			self.get_gitlab_metadata(ref_name)
 				.expect("Could not get gitlab metadata")
 		} else {
 			(vec![], vec![])
 		};
 		#[cfg(feature = "gitea")]
 		let (gitea_commits, gitea_merge_request) = if self.config.remote.gitea.is_set() {
-			self.get_gitea_metadata()
+			self.get_gitea_metadata(ref_name)
 				.expect("Could not get gitea metadata")
 		} else {
 			(vec![], vec![])
@@ -509,7 +540,7 @@ impl<'a> Changelog<'a> {
 		#[cfg(feature = "bitbucket")]
 		let (bitbucket_commits, bitbucket_pull_request) =
 			if self.config.remote.bitbucket.is_set() {
-				self.get_bitbucket_metadata()
+				self.get_bitbucket_metadata(ref_name)
 					.expect("Could not get bitbucket metadata")
 			} else {
 				(vec![], vec![])
@@ -862,9 +893,11 @@ mod test {
 				count_tags:               None,
 				use_branch_tags:          false,
 				topo_order:               false,
+				topo_order_commits:       true,
 				sort_commits:             String::from("oldest"),
 				link_parsers:             [].to_vec(),
 				limit_commits:            None,
+				recurse_submodules:       None,
 			},
 			remote:    RemoteConfig {
 				github:    Remote {
@@ -968,10 +1001,37 @@ mod test {
 					String::from("misc: use footer\n\nFooter: footer text"),
 				),
 			],
+			commit_range: None,
 			commit_id: Some(String::from("0bc123")),
 			timestamp: 50000000,
 			previous: None,
 			repository: Some(String::from("/root/repo")),
+			submodule_commits: HashMap::from([(
+				String::from("submodule_one"),
+				vec![
+					Commit::new(
+						String::from("sub0jkl12"),
+						String::from("chore(app): submodule_one do nothing"),
+					),
+					Commit::new(
+						String::from("subqwerty"),
+						String::from("chore: submodule_one <preprocess>"),
+					),
+					Commit::new(
+						String::from("subqwertz"),
+						String::from(
+							"feat!: submodule_one support breaking commits",
+						),
+					),
+					Commit::new(
+						String::from("subqwert0"),
+						String::from(
+							"match(group): submodule_one support regex-replace for \
+							 groups",
+						),
+					),
+				],
+			)]),
 			#[cfg(feature = "github")]
 			github: crate::remote::RemoteReleaseMetadata {
 				contributors: vec![],
@@ -1030,10 +1090,27 @@ mod test {
 						String::from("revert(app): skip this commit"),
 					),
 				],
+				commit_range: None,
 				commit_id: None,
 				timestamp: 1000,
 				previous: Some(Box::new(test_release)),
 				repository: Some(String::from("/root/repo")),
+				submodule_commits: HashMap::from([
+					(String::from("submodule_one"), vec![
+						Commit::new(
+							String::from("def349"),
+							String::from("sub_one merge #4"),
+						),
+						Commit::new(
+							String::from("da8912"),
+							String::from("sub_one merge #5"),
+						),
+					]),
+					(String::from("submodule_two"), vec![Commit::new(
+						String::from("ab76ef"),
+						String::from("sub_two bump"),
+					)]),
+				]),
 				#[cfg(feature = "github")]
 				github: crate::remote::RemoteReleaseMetadata {
 					contributors: vec![],
@@ -1058,7 +1135,7 @@ mod test {
 	#[test]
 	fn changelog_generator() -> Result<()> {
 		let (config, releases) = get_test_data();
-		let mut changelog = Changelog::new(releases, &config)?;
+		let mut changelog = Changelog::new(releases, &config, None)?;
 		changelog.bump_version()?;
 		changelog.releases[0].timestamp = 0;
 		let mut out = Vec::new();
@@ -1180,7 +1257,7 @@ chore(deps): fix broken deps
 ",
 			),
 		));
-		let changelog = Changelog::new(releases, &config)?;
+		let changelog = Changelog::new(releases, &config, None)?;
 		let mut out = Vec::new();
 		changelog.generate(&mut out)?;
 		assert_eq!(
@@ -1282,7 +1359,7 @@ chore(deps): fix broken deps
 				- {{ commit.message }}{% endfor %}
 				{% endfor %}{% endfor %}"#
 			.to_string();
-		let mut changelog = Changelog::new(releases, &config)?;
+		let mut changelog = Changelog::new(releases, &config, None)?;
 		changelog.add_context("custom_field", "Hello")?;
 		let mut out = Vec::new();
 		changelog.generate(&mut out)?;
diff --git a/git-cliff-core/src/commit.rs b/git-cliff-core/src/commit.rs
index 17d68f3..130c1b6 100644
--- a/git-cliff-core/src/commit.rs
+++ b/git-cliff-core/src/commit.rs
@@ -8,19 +8,19 @@ use crate::error::{
 	Error as AppError,
 	Result,
 };
+use git_conventional::{
+	Commit as ConventionalCommit,
+	Footer as ConventionalFooter,
+};
 #[cfg(feature = "repo")]
 use git2::{
 	Commit as GitCommit,
 	Signature as CommitSignature,
 };
-use git_conventional::{
-	Commit as ConventionalCommit,
-	Footer as ConventionalFooter,
-};
 use lazy_regex::{
-	lazy_regex,
 	Lazy,
 	Regex,
+	lazy_regex,
 };
 use serde::ser::{
 	SerializeStruct,
@@ -98,6 +98,25 @@ impl<'a> From<CommitSignature<'a>> for Signature {
 	}
 }
 
+/// Commit range (from..to)
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct Range {
+	/// Full commit SHA the range starts at
+	from: String,
+	/// Full commit SHA the range ends at
+	to:   String,
+}
+
+impl Range {
+	/// Creates a new [`Range`] from [`crate::commit::Commit`].
+	pub fn new(from: &Commit, to: &Commit) -> Self {
+		Self {
+			from: from.id.clone(),
+			to:   to.id.clone(),
+		}
+	}
+}
+
 /// Common commit object that is parsed from a repository.
 #[derive(Debug, Default, Clone, PartialEq, Deserialize)]
 #[serde(rename_all(serialize = "camelCase"))]
@@ -177,8 +196,8 @@ impl From<String> for Commit<'_> {
 }
 
 #[cfg(feature = "repo")]
-impl<'a> From<&GitCommit<'a>> for Commit<'a> {
-	fn from(commit: &GitCommit<'a>) -> Self {
+impl From<&GitCommit<'_>> for Commit<'_> {
+	fn from(commit: &GitCommit<'_>) -> Self {
 		Commit {
 			id: commit.id().to_string(),
 			message: commit.message().unwrap_or_default().trim_end().to_string(),
diff --git a/git-cliff-core/src/config.rs b/git-cliff-core/src/config.rs
index 724e986..9c138e2 100644
--- a/git-cliff-core/src/config.rs
+++ b/git-cliff-core/src/config.rs
@@ -130,10 +130,14 @@ pub struct GitConfig {
 	pub use_branch_tags:          bool,
 	/// Order releases topologically instead of chronologically.
 	pub topo_order:               bool,
+	/// Order commits chronologically instead of topologically.
+	pub topo_order_commits:       bool,
 	/// How to order commits in each group/release within the changelog.
 	pub sort_commits:             String,
 	/// Limit the total number of commits included in the changelog.
 	pub limit_commits:            Option<usize>,
+	/// Read submodule commits.
+	pub recurse_submodules:       Option<bool>,
 }
 
 /// Remote configuration.
@@ -486,9 +490,11 @@ mod test {
 		const TAG_PATTERN_VALUE: &str = ".*[0-9].*";
 		const IGNORE_TAGS_VALUE: &str = "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+";
 
-		env::set_var("GIT_CLIFF__CHANGELOG__FOOTER", FOOTER_VALUE);
-		env::set_var("GIT_CLIFF__GIT__TAG_PATTERN", TAG_PATTERN_VALUE);
-		env::set_var("GIT_CLIFF__GIT__IGNORE_TAGS", IGNORE_TAGS_VALUE);
+		unsafe {
+			env::set_var("GIT_CLIFF__CHANGELOG__FOOTER", FOOTER_VALUE);
+			env::set_var("GIT_CLIFF__GIT__TAG_PATTERN", TAG_PATTERN_VALUE);
+			env::set_var("GIT_CLIFF__GIT__IGNORE_TAGS", IGNORE_TAGS_VALUE);
+		};
 
 		let config = Config::parse(&path)?;
 
diff --git a/git-cliff-core/src/release.rs b/git-cliff-core/src/release.rs
index 9930851..47fbfb2 100644
--- a/git-cliff-core/src/release.rs
+++ b/git-cliff-core/src/release.rs
@@ -1,7 +1,12 @@
+use std::collections::HashMap;
+
 use crate::commit::commits_to_conventional_commits;
 use crate::error::Result;
 use crate::{
-	commit::Commit,
+	commit::{
+		Commit,
+		Range,
+	},
 	config::Bump,
 	config::BumpType,
 };
@@ -31,35 +36,43 @@ use serde_json::value::Value;
 #[serde(rename_all(serialize = "camelCase"))]
 pub struct Release<'a> {
 	/// Release version, git tag.
-	pub version:    Option<String>,
+	pub version:           Option<String>,
 	/// git tag's message.
-	pub message:    Option<String>,
+	pub message:           Option<String>,
 	/// Commits made for the release.
 	#[serde(deserialize_with = "commits_to_conventional_commits")]
-	pub commits:    Vec<Commit<'a>>,
+	pub commits:           Vec<Commit<'a>>,
 	/// Commit ID of the tag.
 	#[serde(rename = "commit_id")]
-	pub commit_id:  Option<String>,
+	pub commit_id:         Option<String>,
 	/// Timestamp of the release in seconds, from epoch.
-	pub timestamp:  i64,
+	pub timestamp:         i64,
 	/// Previous release.
-	pub previous:   Option<Box<Release<'a>>>,
+	pub previous:          Option<Box<Release<'a>>>,
 	/// Repository path.
-	pub repository: Option<String>,
+	pub repository:        Option<String>,
+	/// Commit range.
+	#[serde(rename = "commit_range")]
+	pub commit_range:      Option<Range>,
+	/// Submodule commits.
+	///
+	/// Maps submodule path to a list of commits.
+	#[serde(rename = "submodule_commits")]
+	pub submodule_commits: HashMap<String, Vec<Commit<'a>>>,
 	/// Arbitrary data to be used with the `--from-context` CLI option.
-	pub extra:      Option<Value>,
+	pub extra:             Option<Value>,
 	/// Contributors.
 	#[cfg(feature = "github")]
-	pub github:     RemoteReleaseMetadata,
+	pub github:            RemoteReleaseMetadata,
 	/// Contributors.
 	#[cfg(feature = "gitlab")]
-	pub gitlab:     RemoteReleaseMetadata,
+	pub gitlab:            RemoteReleaseMetadata,
 	/// Contributors.
 	#[cfg(feature = "gitea")]
-	pub gitea:      RemoteReleaseMetadata,
+	pub gitea:             RemoteReleaseMetadata,
 	/// Contributors.
 	#[cfg(feature = "bitbucket")]
-	pub bitbucket:  RemoteReleaseMetadata,
+	pub bitbucket:         RemoteReleaseMetadata,
 }
 
 #[cfg(feature = "github")]
@@ -101,7 +114,7 @@ impl Release<'_> {
 				let mut prefix = None;
 				if semver.is_err() && version.split('.').count() >= 2 {
 					let mut found_numeric = false;
-					for (i, c) in version.chars().enumerate() {
+					for (i, c) in version.char_indices() {
 						if c.is_numeric() && !found_numeric {
 							found_numeric = true;
 							let version_prefix = version[..i].to_string();
@@ -195,6 +208,7 @@ mod test {
 					.iter()
 					.map(|v| Commit::from(v.to_string()))
 					.collect(),
+				commit_range: None,
 				commit_id: None,
 				timestamp: 0,
 				previous: Some(Box::new(Release {
@@ -202,6 +216,7 @@ mod test {
 					..Default::default()
 				})),
 				repository: Some(String::from("/root/repo")),
+				submodule_commits: HashMap::new(),
 				#[cfg(feature = "github")]
 				github: crate::remote::RemoteReleaseMetadata {
 					contributors: vec![],
@@ -411,6 +426,7 @@ mod test {
 					"6c34967147560ea09658776d4901709139b4ad66 should be fine",
 				)),
 			],
+			commit_range: None,
 			commit_id: None,
 			timestamp: 0,
 			previous: Some(Box::new(Release {
@@ -418,6 +434,7 @@ mod test {
 				..Default::default()
 			})),
 			repository: Some(String::from("/root/repo")),
+			submodule_commits: HashMap::new(),
 			github: RemoteReleaseMetadata {
 				contributors: vec![],
 			},
@@ -779,6 +796,7 @@ mod test {
 					"6c34967147560ea09658776d4901709139b4ad66 should be fine",
 				)),
 			],
+			commit_range: None,
 			commit_id: None,
 			timestamp: 0,
 			previous: Some(Box::new(Release {
@@ -786,6 +804,7 @@ mod test {
 				..Default::default()
 			})),
 			repository: Some(String::from("/root/repo")),
+			submodule_commits: HashMap::new(),
 			#[cfg(feature = "github")]
 			github: RemoteReleaseMetadata {
 				contributors: vec![],
@@ -1169,6 +1188,7 @@ mod test {
 					"6c34967147560ea09658776d4901709139b4ad66 should be fine",
 				)),
 			],
+			commit_range: None,
 			commit_id: None,
 			timestamp: 0,
 			previous: Some(Box::new(Release {
@@ -1176,6 +1196,7 @@ mod test {
 				..Default::default()
 			})),
 			repository: Some(String::from("/root/repo")),
+			submodule_commits: HashMap::new(),
 			#[cfg(feature = "github")]
 			github: RemoteReleaseMetadata {
 				contributors: vec![],
@@ -1527,6 +1548,7 @@ mod test {
 					"6c34967147560ea09658776d4901709139b4ad66 should be fine",
 				)),
 			],
+			commit_range: None,
 			commit_id: None,
 			timestamp: 0,
 			previous: Some(Box::new(Release {
@@ -1534,6 +1556,7 @@ mod test {
 				..Default::default()
 			})),
 			repository: Some(String::from("/root/repo")),
+			submodule_commits: HashMap::new(),
 			#[cfg(feature = "github")]
 			github: RemoteReleaseMetadata {
 				contributors: vec![],
diff --git a/git-cliff-core/src/remote/bitbucket.rs b/git-cliff-core/src/remote/bitbucket.rs
index a8c2181..53ff154 100644
--- a/git-cliff-core/src/remote/bitbucket.rs
+++ b/git-cliff-core/src/remote/bitbucket.rs
@@ -48,12 +48,24 @@ impl RemoteCommit for BitbucketCommit {
 
 /// <https://developer.atlassian.com/cloud/bitbucket/rest/api-group-commits/#api-repositories-workspace-repo-slug-commits-get>
 impl RemoteEntry for BitbucketPagination<BitbucketCommit> {
-	fn url(_id: i64, api_url: &str, remote: &Remote, page: i32) -> String {
+	fn url(
+		_id: i64,
+		api_url: &str,
+		remote: &Remote,
+		ref_name: Option<&str>,
+		page: i32,
+	) -> String {
 		let commit_page = page + 1;
-		format!(
+		let mut url = format!(
 			"{}/{}/{}/commits?pagelen={MAX_PAGE_SIZE}&page={commit_page}",
 			api_url, remote.owner, remote.repo
-		)
+		);
+
+		if let Some(ref_name) = ref_name {
+			url.push_str(&format!("&include={}", ref_name));
+		}
+
+		url
 	}
 
 	fn buffer_size() -> usize {
@@ -141,7 +153,13 @@ impl RemotePullRequest for BitbucketPullRequest {
 
 /// <https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/#api-repositories-workspace-repo-slug-pullrequests-get>
 impl RemoteEntry for BitbucketPagination<BitbucketPullRequest> {
-	fn url(_id: i64, api_url: &str, remote: &Remote, page: i32) -> String {
+	fn url(
+		_id: i64,
+		api_url: &str,
+		remote: &Remote,
+		_ref_name: Option<&str>,
+		page: i32,
+	) -> String {
 		let pr_page = page + 1;
 		format!(
 			"{}/{}/{}/pullrequests?&pagelen={BITBUCKET_MAX_PAGE_PRS}&\
@@ -194,9 +212,14 @@ impl RemoteClient for BitbucketClient {
 
 impl BitbucketClient {
 	/// Fetches the Bitbucket API and returns the commits.
-	pub async fn get_commits(&self) -> Result<Vec<Box<dyn RemoteCommit>>> {
+	pub async fn get_commits(
+		&self,
+		ref_name: Option<&str>,
+	) -> Result<Vec<Box<dyn RemoteCommit>>> {
 		Ok(self
-			.fetch_with_early_exit::<BitbucketPagination<BitbucketCommit>>(0)
+			.fetch_with_early_exit::<BitbucketPagination<BitbucketCommit>>(
+				0, ref_name,
+			)
 			.await?
 			.into_iter()
 			.flat_map(|v| v.values)
@@ -207,9 +230,12 @@ impl BitbucketClient {
 	/// Fetches the Bitbucket API and returns the pull requests.
 	pub async fn get_pull_requests(
 		&self,
+		ref_name: Option<&str>,
 	) -> Result<Vec<Box<dyn RemotePullRequest>>> {
 		Ok(self
-			.fetch_with_early_exit::<BitbucketPagination<BitbucketPullRequest>>(0)
+			.fetch_with_early_exit::<BitbucketPagination<BitbucketPullRequest>>(
+				0, ref_name,
+			)
 			.await?
 			.into_iter()
 			.flat_map(|v| v.values)
diff --git a/git-cliff-core/src/remote/gitea.rs b/git-cliff-core/src/remote/gitea.rs
index 2b4e6cc..8586ec5 100644
--- a/git-cliff-core/src/remote/gitea.rs
+++ b/git-cliff-core/src/remote/gitea.rs
@@ -44,12 +44,25 @@ impl RemoteCommit for GiteaCommit {
 }
 
 impl RemoteEntry for GiteaCommit {
-	fn url(_id: i64, api_url: &str, remote: &Remote, page: i32) -> String {
-		format!(
+	fn url(
+		_id: i64,
+		api_url: &str,
+		remote: &Remote,
+		ref_name: Option<&str>,
+		page: i32,
+	) -> String {
+		let mut url = format!(
 			"{}/api/v1/repos/{}/{}/commits?limit={MAX_PAGE_SIZE}&page={page}",
 			api_url, remote.owner, remote.repo
-		)
+		);
+
+		if let Some(ref_name) = ref_name {
+			url.push_str(&format!("&sha={}", ref_name));
+		}
+
+		url
 	}
+
 	fn buffer_size() -> usize {
 		10
 	}
@@ -106,7 +119,13 @@ impl RemotePullRequest for GiteaPullRequest {
 }
 
 impl RemoteEntry for GiteaPullRequest {
-	fn url(_id: i64, api_url: &str, remote: &Remote, page: i32) -> String {
+	fn url(
+		_id: i64,
+		api_url: &str,
+		remote: &Remote,
+		_ref_name: Option<&str>,
+		page: i32,
+	) -> String {
 		format!(
 			"{}/api/v1/repos/{}/{}/pulls?limit={MAX_PAGE_SIZE}&page={page}&\
 			 state=closed",
@@ -158,9 +177,12 @@ impl RemoteClient for GiteaClient {
 
 impl GiteaClient {
 	/// Fetches the Gitea API and returns the commits.
-	pub async fn get_commits(&self) -> Result<Vec<Box<dyn RemoteCommit>>> {
+	pub async fn get_commits(
+		&self,
+		ref_name: Option<&str>,
+	) -> Result<Vec<Box<dyn RemoteCommit>>> {
 		Ok(self
-			.fetch::<GiteaCommit>(0)
+			.fetch::<GiteaCommit>(0, ref_name)
 			.await?
 			.into_iter()
 			.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
@@ -170,9 +192,10 @@ impl GiteaClient {
 	/// Fetches the Gitea API and returns the pull requests.
 	pub async fn get_pull_requests(
 		&self,
+		ref_name: Option<&str>,
 	) -> Result<Vec<Box<dyn RemotePullRequest>>> {
 		Ok(self
-			.fetch::<GiteaPullRequest>(0)
+			.fetch::<GiteaPullRequest>(0, ref_name)
 			.await?
 			.into_iter()
 			.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
diff --git a/git-cliff-core/src/remote/github.rs b/git-cliff-core/src/remote/github.rs
index e3cb4b3..b5e3c97 100644
--- a/git-cliff-core/src/remote/github.rs
+++ b/git-cliff-core/src/remote/github.rs
@@ -60,12 +60,25 @@ impl RemoteCommit for GitHubCommit {
 }
 
 impl RemoteEntry for GitHubCommit {
-	fn url(_id: i64, api_url: &str, remote: &Remote, page: i32) -> String {
-		format!(
+	fn url(
+		_id: i64,
+		api_url: &str,
+		remote: &Remote,
+		ref_name: Option<&str>,
+		page: i32,
+	) -> String {
+		let mut url = format!(
 			"{}/repos/{}/{}/commits?per_page={MAX_PAGE_SIZE}&page={page}",
 			api_url, remote.owner, remote.repo
-		)
+		);
+
+		if let Some(ref_name) = ref_name {
+			url.push_str(&format!("&sha={}", ref_name));
+		}
+
+		url
 	}
+
 	fn buffer_size() -> usize {
 		10
 	}
@@ -122,7 +135,13 @@ impl RemotePullRequest for GitHubPullRequest {
 }
 
 impl RemoteEntry for GitHubPullRequest {
-	fn url(_id: i64, api_url: &str, remote: &Remote, page: i32) -> String {
+	fn url(
+		_id: i64,
+		api_url: &str,
+		remote: &Remote,
+		_ref_name: Option<&str>,
+		page: i32,
+	) -> String {
 		format!(
 			"{}/repos/{}/{}/pulls?per_page={MAX_PAGE_SIZE}&page={page}&state=closed",
 			api_url, remote.owner, remote.repo
@@ -173,9 +192,12 @@ impl RemoteClient for GitHubClient {
 
 impl GitHubClient {
 	/// Fetches the GitHub API and returns the commits.
-	pub async fn get_commits(&self) -> Result<Vec<Box<dyn RemoteCommit>>> {
+	pub async fn get_commits(
+		&self,
+		ref_name: Option<&str>,
+	) -> Result<Vec<Box<dyn RemoteCommit>>> {
 		Ok(self
-			.fetch::<GitHubCommit>(0)
+			.fetch::<GitHubCommit>(0, ref_name)
 			.await?
 			.into_iter()
 			.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
@@ -185,9 +207,10 @@ impl GitHubClient {
 	/// Fetches the GitHub API and returns the pull requests.
 	pub async fn get_pull_requests(
 		&self,
+		ref_name: Option<&str>,
 	) -> Result<Vec<Box<dyn RemotePullRequest>>> {
 		Ok(self
-			.fetch::<GitHubPullRequest>(0)
+			.fetch::<GitHubPullRequest>(0, ref_name)
 			.await?
 			.into_iter()
 			.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
diff --git a/git-cliff-core/src/remote/gitlab.rs b/git-cliff-core/src/remote/gitlab.rs
index c009e98..85b49ba 100644
--- a/git-cliff-core/src/remote/gitlab.rs
+++ b/git-cliff-core/src/remote/gitlab.rs
@@ -40,7 +40,13 @@ pub struct GitLabProject {
 }
 
 impl RemoteEntry for GitLabProject {
-	fn url(_id: i64, api_url: &str, remote: &Remote, _page: i32) -> String {
+	fn url(
+		_id: i64,
+		api_url: &str,
+		remote: &Remote,
+		_ref_name: Option<&str>,
+		_page: i32,
+	) -> String {
 		format!(
 			"{}/projects/{}%2F{}",
 			api_url,
@@ -106,14 +112,27 @@ impl RemoteCommit for GitLabCommit {
 }
 
 impl RemoteEntry for GitLabCommit {
-	fn url(id: i64, api_url: &str, _remote: &Remote, page: i32) -> String {
+	fn url(
+		id: i64,
+		api_url: &str,
+		_remote: &Remote,
+		ref_name: Option<&str>,
+		page: i32,
+	) -> String {
 		let commit_page = page + 1;
-		format!(
+		let mut url = format!(
 			"{}/projects/{}/repository/commits?per_page={MAX_PAGE_SIZE}&\
 			 page={commit_page}",
 			api_url, id
-		)
+		);
+
+		if let Some(ref_name) = ref_name {
+			url.push_str(&format!("&ref_name={}", ref_name));
+		}
+
+		url
 	}
+
 	fn buffer_size() -> usize {
 		10
 	}
@@ -178,7 +197,13 @@ impl RemotePullRequest for GitLabMergeRequest {
 }
 
 impl RemoteEntry for GitLabMergeRequest {
-	fn url(id: i64, api_url: &str, _remote: &Remote, page: i32) -> String {
+	fn url(
+		id: i64,
+		api_url: &str,
+		_remote: &Remote,
+		_ref_name: Option<&str>,
+		page: i32,
+	) -> String {
 		format!(
 			"{}/projects/{}/merge_requests?per_page={MAX_PAGE_SIZE}&page={page}&\
 			 state=merged",
@@ -258,17 +283,21 @@ impl RemoteClient for GitLabClient {
 
 impl GitLabClient {
 	/// Fetches the GitLab API and returns the pull requests.
-	pub async fn get_project(&self) -> Result<GitLabProject> {
-		self.get_entry::<GitLabProject>(0, 1).await
+	pub async fn get_project(
+		&self,
+		ref_name: Option<&str>,
+	) -> Result<GitLabProject> {
+		self.get_entry::<GitLabProject>(0, ref_name, 1).await
 	}
 
 	/// Fetches the GitLab API and returns the commits.
 	pub async fn get_commits(
 		&self,
 		project_id: i64,
+		ref_name: Option<&str>,
 	) -> Result<Vec<Box<dyn RemoteCommit>>> {
 		Ok(self
-			.fetch::<GitLabCommit>(project_id)
+			.fetch::<GitLabCommit>(project_id, ref_name)
 			.await?
 			.into_iter()
 			.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
@@ -279,9 +308,10 @@ impl GitLabClient {
 	pub async fn get_merge_requests(
 		&self,
 		project_id: i64,
+		ref_name: Option<&str>,
 	) -> Result<Vec<Box<dyn RemotePullRequest>>> {
 		Ok(self
-			.fetch::<GitLabMergeRequest>(project_id)
+			.fetch::<GitLabMergeRequest>(project_id, ref_name)
 			.await?
 			.into_iter()
 			.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
@@ -298,7 +328,13 @@ mod test {
 		let remote = Remote::new("abc/def", "xyz1");
 		assert_eq!(
 			"https://gitlab.test.com/api/v4/projects/abc%2Fdef%2Fxyz1",
-			GitLabProject::url(1, "https://gitlab.test.com/api/v4", &remote, 0)
+			GitLabProject::url(
+				1,
+				"https://gitlab.test.com/api/v4",
+				&remote,
+				None,
+				0
+			)
 		);
 	}
 
diff --git a/git-cliff-core/src/remote/mod.rs b/git-cliff-core/src/remote/mod.rs
index def50ee..476ee11 100644
--- a/git-cliff-core/src/remote/mod.rs
+++ b/git-cliff-core/src/remote/mod.rs
@@ -22,9 +22,9 @@ use crate::error::{
 };
 use dyn_clone::DynClone;
 use futures::{
+	StreamExt,
 	future,
 	stream,
-	StreamExt,
 };
 use http_cache_reqwest::{
 	CACacheManager,
@@ -33,11 +33,11 @@ use http_cache_reqwest::{
 	HttpCache,
 	HttpCacheOptions,
 };
+use reqwest::Client;
 use reqwest::header::{
 	HeaderMap,
 	HeaderValue,
 };
-use reqwest::Client;
 use reqwest_middleware::{
 	ClientBuilder,
 	ClientWithMiddleware,
@@ -52,8 +52,8 @@ use std::env;
 use std::fmt::Debug;
 use std::time::Duration;
 use time::{
-	format_description::well_known::Rfc3339,
 	OffsetDateTime,
+	format_description::well_known::Rfc3339,
 };
 
 /// User agent for interacting with the GitHub API.
@@ -74,7 +74,13 @@ pub(crate) const MAX_PAGE_SIZE: usize = 100;
 /// Trait for handling the different entries returned from the remote.
 pub trait RemoteEntry {
 	/// Returns the API URL for fetching the entries at the specified page.
-	fn url(project_id: i64, api_url: &str, remote: &Remote, page: i32) -> String;
+	fn url(
+		project_id: i64,
+		api_url: &str,
+		remote: &Remote,
+		ref_name: Option<&str>,
+		page: i32,
+	) -> String;
 	/// Returns the request buffer size.
 	fn buffer_size() -> usize;
 	/// Whether if exit early.
@@ -204,9 +210,11 @@ pub trait RemoteClient {
 	async fn get_entry<T: DeserializeOwned + RemoteEntry>(
 		&self,
 		project_id: i64,
+		ref_name: Option<&str>,
 		page: i32,
 	) -> Result<T> {
-		let url = T::url(project_id, &self.api_url(), &self.remote(), page);
+		let url =
+			T::url(project_id, &self.api_url(), &self.remote(), ref_name, page);
 		debug!("Sending request to: {url}");
 		let response = self.client().get(&url).send().await?;
 		let response_text = if response.status().is_success() {
@@ -225,9 +233,11 @@ pub trait RemoteClient {
 	async fn get_entries_with_page<T: DeserializeOwned + RemoteEntry>(
 		&self,
 		project_id: i64,
+		ref_name: Option<&str>,
 		page: i32,
 	) -> Result<Vec<T>> {
-		let url = T::url(project_id, &self.api_url(), &self.remote(), page);
+		let url =
+			T::url(project_id, &self.api_url(), &self.remote(), ref_name, page);
 		debug!("Sending request to: {url}");
 		let response = self.client().get(&url).send().await?;
 		let response_text = if response.status().is_success() {
@@ -253,9 +263,10 @@ pub trait RemoteClient {
 	async fn fetch<T: DeserializeOwned + RemoteEntry>(
 		&self,
 		project_id: i64,
+		ref_name: Option<&str>,
 	) -> Result<Vec<T>> {
 		let entries: Vec<Vec<T>> = stream::iter(0..)
-			.map(|i| self.get_entries_with_page(project_id, i))
+			.map(|i| self.get_entries_with_page(project_id, ref_name, i))
 			.buffered(T::buffer_size())
 			.take_while(|page| {
 				if let Err(e) = page {
@@ -281,9 +292,10 @@ pub trait RemoteClient {
 	async fn fetch_with_early_exit<T: DeserializeOwned + RemoteEntry>(
 		&self,
 		project_id: i64,
+		ref_name: Option<&str>,
 	) -> Result<Vec<T>> {
 		let entries: Vec<T> = stream::iter(0..)
-			.map(|i| self.get_entry::<T>(project_id, i))
+			.map(|i| self.get_entry::<T>(project_id, ref_name, i))
 			.buffered(T::buffer_size())
 			.take_while(|page| {
 				let status = match page {
diff --git a/git-cliff-core/src/repo.rs b/git-cliff-core/src/repo.rs
index 47e5888..251d281 100644
--- a/git-cliff-core/src/repo.rs
+++ b/git-cliff-core/src/repo.rs
@@ -17,12 +17,15 @@ use git2::{
 use glob::Pattern;
 use indexmap::IndexMap;
 use lazy_regex::{
-	lazy_regex,
 	Lazy,
 	Regex,
+	lazy_regex,
 };
 use std::io;
-use std::path::PathBuf;
+use std::path::{
+	Path,
+	PathBuf,
+};
 use std::result::Result as StdResult;
 use url::Url;
 
@@ -46,6 +49,15 @@ pub struct Repository {
 	changed_files_cache_path: PathBuf,
 }
 
+/// Range of commits in a submodule.
+pub struct SubmoduleRange {
+	/// Repository object to which this range belongs.
+	pub repository: Repository,
+	/// Commit range in "<first_submodule_commit>..<last_submodule_commit>" or
+	/// "<last_submodule_commit>" format.
+	pub range:      String,
+}
+
 impl Repository {
 	/// Initializes (opens) the repository.
 	pub fn init(path: PathBuf) -> Result<Self> {
@@ -77,7 +89,7 @@ impl Repository {
 	}
 
 	/// Returns the path of the repository.
-	pub fn path(&self) -> Result<PathBuf> {
+	pub fn root_path(&self) -> Result<PathBuf> {
 		let mut path = if self.inner.is_worktree() {
 			let worktree = Worktree::open_from_repository(&self.inner)?;
 			worktree.path().to_path_buf()
@@ -90,6 +102,14 @@ impl Repository {
 		Ok(path)
 	}
 
+	/// Returns the initial path of the repository.
+	///
+	/// In case of a submodule this is the relative path to the toplevel
+	/// repository.
+	pub fn path(&self) -> &PathBuf {
+		&self.path
+	}
+
 	/// Sets the range for the commit search.
 	///
 	/// When a single SHA is provided as the range, start from the
@@ -118,9 +138,15 @@ impl Repository {
 		range: Option<&str>,
 		include_path: Option<Vec<Pattern>>,
 		exclude_path: Option<Vec<Pattern>>,
+		topo_order_commits: bool,
 	) -> Result<Vec<Commit>> {
 		let mut revwalk = self.inner.revwalk()?;
-		revwalk.set_sorting(Sort::TOPOLOGICAL)?;
+		if topo_order_commits {
+			revwalk.set_sorting(Sort::TOPOLOGICAL)?;
+		} else {
+			revwalk.set_sorting(Sort::TIME)?;
+		}
+
 		Self::set_commit_range(&mut revwalk, range).map_err(|e| {
 			Error::SetCommitRangeError(
 				range.map(String::from).unwrap_or_else(|| "?".to_string()),
@@ -149,6 +175,59 @@ impl Repository {
 		Ok(commits)
 	}
 
+	/// Returns submodule repositories for a given commit range.
+	///
+	/// For one or two given commits in this repository, a list of changed
+	/// submodules is calculated. If only one commit is given, then all
+	/// submodule commits up to the referenced commit will be included. This is
+	/// usually the case if a submodule is added to the repository.
+	///
+	///  For each submodule a [`SubmoduleRange`] object is created
+	///
+	/// This can then be used to query the submodule's commits by using
+	/// [`Repository::commits`].
+	pub fn submodules_range(
+		&self,
+		old_commit: Option<Commit<'_>>,
+		new_commit: Commit<'_>,
+	) -> Result<Vec<SubmoduleRange>> {
+		let old_tree = old_commit.and_then(|commit| commit.tree().ok());
+		let new_tree = new_commit.tree().ok();
+		let diff = self.inner.diff_tree_to_tree(
+			old_tree.as_ref(),
+			new_tree.as_ref(),
+			None,
+		)?;
+		// iterate through all diffs and accumulate old/new commit ids
+		let before_and_after_deltas = diff.deltas().filter_map(|delta| {
+			let old_file_id = delta.old_file().id();
+			let new_file_id = delta.new_file().id();
+			let range = if old_file_id == new_file_id || new_file_id.is_zero() {
+				// no changes or submodule removed
+				None
+			} else if old_file_id.is_zero() {
+				// submodule added
+				Some(new_file_id.to_string())
+			} else {
+				// submodule updated
+				Some(format!("{}..{}", old_file_id, new_file_id))
+			};
+			trace!("Release commit range for submodules: {:?}", range);
+			delta.new_file().path().and_then(Path::to_str).zip(range)
+		});
+		// iterate through all path diffs and find corresponding submodule if
+		// possible
+		let submodule_range = before_and_after_deltas.filter_map(|(path, range)| {
+			let repository = self
+				.inner
+				.find_submodule(path)
+				.ok()
+				.and_then(|submodule| Self::init(submodule.path().into()).ok());
+			repository.map(|repository| SubmoduleRange { repository, range })
+		});
+		Ok(submodule_range.collect())
+	}
+
 	/// Normalizes the glob pattern to match the git diff paths.
 	///
 	/// It removes the leading `./` and adds `**` to the end if the pattern is a
@@ -159,12 +238,11 @@ impl Repository {
 				.expect("failed to add '**' to the end of glob"),
 			_ => pattern,
 		};
-		let pattern_normal = match star_added.as_str().strip_prefix("./") {
+		match star_added.as_str().strip_prefix("./") {
 			Some(stripped) => Pattern::new(stripped)
 				.expect("failed to remove leading ./ from glob"),
 			None => star_added,
-		};
-		pattern_normal
+		}
 	}
 
 	/// Calculates whether the commit should be retained or not.
@@ -621,7 +699,7 @@ mod test {
 	#[test]
 	fn get_latest_commit() -> Result<()> {
 		let repository = get_repository()?;
-		let commits = repository.commits(None, None, None)?;
+		let commits = repository.commits(None, None, None, false)?;
 		let last_commit =
 			AppCommit::from(&commits.first().expect("no commits found").clone());
 		assert_eq!(get_last_commit_hash()?, last_commit.id);
@@ -631,9 +709,11 @@ mod test {
 	#[test]
 	fn commit_search() -> Result<()> {
 		let repository = get_repository()?;
-		assert!(repository
-			.find_commit("e936ed571533ea6c41a1dd2b1a29d085c8dbada5")
-			.is_some());
+		assert!(
+			repository
+				.find_commit("e936ed571533ea6c41a1dd2b1a29d085c8dbada5")
+				.is_some()
+		);
 		Ok(())
 	}
 
@@ -739,7 +819,7 @@ mod test {
 		let repository = get_repository()?;
 		// a close descendant of the root commit
 		let range = Some("eea3914c7ab07472841aa85c36d11bdb2589a234");
-		let commits = repository.commits(range, None, None)?;
+		let commits = repository.commits(range, None, None, false)?;
 		let root_commit =
 			AppCommit::from(&commits.last().expect("no commits found").clone());
 		assert_eq!(get_root_commit_hash()?, root_commit.id);
@@ -833,10 +913,12 @@ mod test {
 
 		assert!(result.is_err());
 		if let Err(error) = result {
-			assert!(format!("{error:?}").contains(
-				format!("could not find repository at '{}'", path.display())
-					.as_str()
-			))
+			assert!(
+				format!("{error:?}").contains(
+					format!("could not find repository at '{}'", path.display())
+						.as_str()
+				)
+			)
 		}
 	}
 
@@ -866,13 +948,10 @@ mod test {
 			.expect("failed to execute git commit");
 		assert!(output.status.success(), "git commit failed {:?}", output);
 
-		let last_commit = repo
-			.inner
+		repo.inner
 			.head()
 			.and_then(|head| head.peel_to_commit())
-			.expect("failed to get the last commit");
-
-		last_commit
+			.expect("failed to get the last commit")
 	}
 
 	#[test]
diff --git a/git-cliff-core/src/template.rs b/git-cliff-core/src/template.rs
index 0d28008..186eb19 100644
--- a/git-cliff-core/src/template.rs
+++ b/git-cliff-core/src/template.rs
@@ -12,11 +12,11 @@ use std::collections::{
 };
 use std::error::Error as ErrorImpl;
 use tera::{
-	ast,
 	Context as TeraContext,
 	Result as TeraResult,
 	Tera,
 	Value,
+	ast,
 };
 
 /// Wrapper for [`Tera`].
@@ -215,10 +215,12 @@ mod test {
 			.into_iter()
 			.filter_map(|c| c.into_conventional().ok())
 			.collect(),
+			commit_range: None,
 			commit_id: None,
 			timestamp: 0,
 			previous: None,
 			repository: Some(String::from("/root/repo")),
+			submodule_commits: HashMap::new(),
 			#[cfg(feature = "github")]
 			github: crate::remote::RemoteReleaseMetadata {
 				contributors: vec![],
diff --git a/git-cliff/Cargo.toml b/git-cliff/Cargo.toml
index eb2bf00..8e2967d 100644
--- a/git-cliff/Cargo.toml
+++ b/git-cliff/Cargo.toml
@@ -10,8 +10,8 @@ repository = "https://github.com/orhun/git-cliff"
 keywords = ["changelog", "generator", "conventional", "commit"]
 categories = ["command-line-utilities"]
 default-run = "git-cliff"
-edition = "2021"
-rust-version = "1.83.0"
+edition = "2024"
+rust-version = "1.85.0"
 
 [[bin]]
 name = "git-cliff-completions"
@@ -55,7 +55,7 @@ update-informer = { version = "1.2.0", optional = true }
 indicatif = { version = "0.17.9", optional = true }
 env_logger = "=0.10.2"
 pprof = { version = "0.14", optional = true }
-rand = { version = "0.8.4", optional = true }
+rand = { version = "0.9.1", optional = true }
 reqwest = { workspace = true, optional = true }
 url.workspace = true
 pathdiff = "0.2.3"
diff --git a/git-cliff/src/args.rs b/git-cliff/src/args.rs
index 83560a4..f288137 100644
--- a/git-cliff/src/args.rs
+++ b/git-cliff/src/args.rs
@@ -1,27 +1,27 @@
 use clap::{
+	ArgAction,
+	Parser,
+	ValueEnum,
 	builder::{
+		Styles,
+		TypedValueParser,
+		ValueParserFactory,
 		styling::{
 			Ansi256Color,
 			AnsiColor,
 		},
-		Styles,
-		TypedValueParser,
-		ValueParserFactory,
 	},
 	error::{
 		ContextKind,
 		ContextValue,
 		ErrorKind,
 	},
-	ArgAction,
-	Parser,
-	ValueEnum,
 };
 use git_cliff_core::{
-	config::BumpType,
-	config::Remote,
 	DEFAULT_CONFIG,
 	DEFAULT_OUTPUT,
+	config::BumpType,
+	config::Remote,
 };
 use glob::Pattern;
 use regex::Regex;
@@ -512,12 +512,16 @@ mod tests {
 				OsStr::new("https://gitlab.archlinux.org/archlinux/packaging/packages/arch-repro-status")
 			)?
 		);
-		assert!(remote_value_parser
-			.parse_ref(&Opt::command(), None, OsStr::new("test"))
-			.is_err());
-		assert!(remote_value_parser
-			.parse_ref(&Opt::command(), None, OsStr::new(""))
-			.is_err());
+		assert!(
+			remote_value_parser
+				.parse_ref(&Opt::command(), None, OsStr::new("test"))
+				.is_err()
+		);
+		assert!(
+			remote_value_parser
+				.parse_ref(&Opt::command(), None, OsStr::new(""))
+				.is_err()
+		);
 		Ok(())
 	}
 
@@ -532,9 +536,11 @@ mod tests {
 				OsStr::new("auto")
 			)?
 		);
-		assert!(bump_option_parser
-			.parse_ref(&Opt::command(), None, OsStr::new("test"))
-			.is_err());
+		assert!(
+			bump_option_parser
+				.parse_ref(&Opt::command(), None, OsStr::new("test"))
+				.is_err()
+		);
 		assert_eq!(
 			BumpOption::Specific(BumpType::Major),
 			bump_option_parser.parse_ref(
diff --git a/git-cliff/src/lib.rs b/git-cliff/src/lib.rs
index ceff7d7..a18dd5e 100644
--- a/git-cliff/src/lib.rs
+++ b/git-cliff/src/lib.rs
@@ -21,7 +21,10 @@ use args::{
 };
 use clap::ValueEnum;
 use git_cliff_core::changelog::Changelog;
-use git_cliff_core::commit::Commit;
+use git_cliff_core::commit::{
+	Commit,
+	Range,
+};
 use git_cliff_core::config::{
 	CommitParser,
 	Config,
@@ -35,7 +38,10 @@ use git_cliff_core::error::{
 	Result,
 };
 use git_cliff_core::release::Release;
-use git_cliff_core::repo::Repository;
+use git_cliff_core::repo::{
+	Repository,
+	SubmoduleRange,
+};
 use git_cliff_core::{
 	DEFAULT_CONFIG,
 	IGNORE_FILE,
@@ -77,6 +83,130 @@ fn check_new_version() {
 	}
 }
 
+/// Produces a commit range on the format `BASE..HEAD`, derived from the
+/// command line arguments and repository tags.
+///
+/// If no commit range could be determined, `None` is returned.
+fn determine_commit_range(
+	args: &Opt,
+	config: &Config,
+	repository: &Repository,
+) -> Result<Option<String>> {
+	let tags = repository.tags(
+		&config.git.tag_pattern,
+		args.topo_order,
+		args.use_branch_tags,
+	)?;
+
+	let mut commit_range = args.range.clone();
+	if args.unreleased {
+		if let Some(last_tag) = tags.last().map(|(k, _)| k) {
+			commit_range = Some(format!("{last_tag}..HEAD"));
+		}
+	} else if args.latest || args.current {
+		if tags.len() < 2 {
+			let commits = repository.commits(
+				None,
+				None,
+				None,
+				config.git.topo_order_commits,
+			)?;
+			if let (Some(tag1), Some(tag2)) = (
+				commits.last().map(|c| c.id().to_string()),
+				tags.get_index(0).map(|(k, _)| k),
+			) {
+				if tags.len() == 1 {
+					commit_range = Some(tag2.to_owned());
+				} else {
+					commit_range = Some(format!("{tag1}..{tag2}"));
+				}
+			}
+		} else {
+			let mut tag_index = tags.len() - 2;
+			if args.current {
+				if let Some(current_tag_index) =
+					repository.current_tag().as_ref().and_then(|tag| {
+						tags.iter()
+							.enumerate()
+							.find(|(_, (_, v))| v.name == tag.name)
+							.map(|(i, _)| i)
+					}) {
+					match current_tag_index.checked_sub(1) {
+						Some(i) => tag_index = i,
+						None => {
+							return Err(Error::ChangelogError(String::from(
+								"No suitable tags found. Maybe run with \
+								 '--topo-order'?",
+							)));
+						}
+					}
+				} else {
+					return Err(Error::ChangelogError(String::from(
+						"No tag exists for the current commit",
+					)));
+				}
+			}
+			if let (Some(tag1), Some(tag2)) = (
+				tags.get_index(tag_index).map(|(k, _)| k),
+				tags.get_index(tag_index + 1).map(|(k, _)| k),
+			) {
+				commit_range = Some(format!("{tag1}..{tag2}"));
+			}
+		}
+	}
+
+	Ok(commit_range)
+}
+
+/// Process submodules and add commits to release.
+fn process_submodules(
+	repository: &'static Repository,
+	release: &mut Release,
+	topo_order_commits: bool,
+) -> Result<()> {
+	// Retrieve first and last commit of a release to create a commit range.
+	let first_commit = release
+		.previous
+		.as_ref()
+		.and_then(|previous_release| previous_release.commit_id.clone())
+		.and_then(|commit_id| repository.find_commit(&commit_id));
+	let last_commit = release
+		.commit_id
+		.clone()
+		.and_then(|commit_id| repository.find_commit(&commit_id));
+
+	trace!("Processing submodule commits in {first_commit:?}..{last_commit:?}");
+
+	// Query repository for submodule changes. For each submodule a
+	// SubmoduleRange is created, describing the range of commits in the context
+	// of that submodule.
+	if let Some(last_commit) = last_commit {
+		let submodule_ranges =
+			repository.submodules_range(first_commit, last_commit)?;
+		let submodule_commits =
+			submodule_ranges.iter().filter_map(|submodule_range| {
+				// For each submodule, the commit range is exploded into a list of
+				// commits.
+				let SubmoduleRange {
+					repository: sub_repo,
+					range: range_str,
+				} = submodule_range;
+				let commits = sub_repo
+					.commits(Some(range_str), None, None, topo_order_commits)
+					.ok()
+					.map(|commits| commits.iter().map(Commit::from).collect());
+
+				let submodule_path = sub_repo.path().to_string_lossy().into_owned();
+				Some(submodule_path).zip(commits)
+			});
+		// Insert submodule commits into map.
+		for (submodule_path, commits) in submodule_commits {
+			release.submodule_commits.insert(submodule_path, commits);
+		}
+	}
+	Ok(())
+}
+
 /// Processes the tags and commits for creating release entries for the
 /// changelog.
 ///
@@ -95,6 +225,7 @@ fn process_repository<'a>(
 	let skip_regex = config.git.skip_tags.as_ref();
 	let ignore_regex = config.git.ignore_tags.as_ref();
 	let count_tags = config.git.count_tags.as_ref();
+	let recurse_submodules = config.git.recurse_submodules.unwrap_or(false);
 	tags.retain(|_, tag| {
 		let name = &tag.name;
 
@@ -166,62 +297,12 @@ fn process_repository<'a>(
 	log::trace!("Config: {:#?}", config);
 
 	// Parse commits.
-	let mut commit_range = args.range.clone();
-	if args.unreleased {
-		if let Some(last_tag) = tags.last().map(|(k, _)| k) {
-			commit_range = Some(format!("{last_tag}..HEAD"));
-		}
-	} else if args.latest || args.current {
-		if tags.len() < 2 {
-			let commits = repository.commits(None, None, None)?;
-			if let (Some(tag1), Some(tag2)) = (
-				commits.last().map(|c| c.id().to_string()),
-				tags.get_index(0).map(|(k, _)| k),
-			) {
-				if tags.len() == 1 {
-					commit_range = Some(tag2.to_owned());
-				} else {
-					commit_range = Some(format!("{tag1}..{tag2}"));
-				}
-			}
-		} else {
-			let mut tag_index = tags.len() - 2;
-			if args.current {
-				if let Some(current_tag_index) =
-					repository.current_tag().as_ref().and_then(|tag| {
-						tags.iter()
-							.enumerate()
-							.find(|(_, (_, v))| v.name == tag.name)
-							.map(|(i, _)| i)
-					}) {
-					match current_tag_index.checked_sub(1) {
-						Some(i) => tag_index = i,
-						None => {
-							return Err(Error::ChangelogError(String::from(
-								"No suitable tags found. Maybe run with \
-								 '--topo-order'?",
-							)));
-						}
-					}
-				} else {
-					return Err(Error::ChangelogError(String::from(
-						"No tag exists for the current commit",
-					)));
-				}
-			}
-			if let (Some(tag1), Some(tag2)) = (
-				tags.get_index(tag_index).map(|(k, _)| k),
-				tags.get_index(tag_index + 1).map(|(k, _)| k),
-			) {
-				commit_range = Some(format!("{tag1}..{tag2}"));
-			}
-		}
-	}
+	let commit_range = determine_commit_range(args, config, repository)?;
 
 	// Include only the current directory if not running from the root repository
 	let mut include_path = args.include_path.clone();
 	if let Some(mut path_diff) =
-		pathdiff::diff_paths(env::current_dir()?, repository.path()?)
+		pathdiff::diff_paths(env::current_dir()?, repository.root_path()?)
 	{
 		if args.workdir.is_none() &&
 			include_path.is_none() &&
@@ -241,6 +322,7 @@ fn process_repository<'a>(
 		commit_range.as_deref(),
 		include_path,
 		args.exclude_path.clone(),
+		config.git.topo_order_commits,
 	)?;
 	if let Some(commit_limit_value) = config.git.limit_commits {
 		commits.truncate(commit_limit_value);
@@ -272,17 +354,17 @@ fn process_repository<'a>(
 	// Process releases.
 	let mut previous_release = Release::default();
 	let mut first_processed_tag = None;
-	let repository_path = repository.path()?.to_string_lossy().into_owned();
+	let repository_path = repository.root_path()?.to_string_lossy().into_owned();
 	for git_commit in commits.iter().rev() {
 		let release = releases.last_mut().unwrap();
 		let commit = Commit::from(git_commit);
 		let commit_id = commit.id.to_string();
 		release.commits.push(commit);
 		release.repository = Some(repository_path.clone());
-		if let Some(tag) = tags.get(&commit_id) {
+		release.commit_id = Some(commit_id);
+		if let Some(tag) = tags.get(release.commit_id.as_ref().unwrap()) {
 			release.version = Some(tag.name.to_string());
 			release.message.clone_from(&tag.message);
-			release.commit_id = Some(commit_id);
 			release.timestamp = if args.tag.as_deref() == Some(tag.name.as_str()) {
 				match tag_timestamp {
 					Some(timestamp) => timestamp,
@@ -360,6 +442,25 @@ fn process_repository<'a>(
 		}
 	}
 
+	for release in &mut releases {
+		// Set the commit ranges for all releases
+		if !release.commits.is_empty() {
+			release.commit_range = Some(match args.sort {
+				Sort::Oldest => Range::new(
+					release.commits.first().unwrap(),
+					release.commits.last().unwrap(),
+				),
+				Sort::Newest => Range::new(
+					release.commits.last().unwrap(),
+					release.commits.first().unwrap(),
+				),
+			})
+		}
+		if recurse_submodules {
+			process_submodules(repository, release, config.git.topo_order_commits)?;
+		}
+	}
+
 	// Set custom message for the latest release.
 	if let Some(message) = &args.with_tag_message {
 		if let Some(latest_release) = releases
@@ -501,11 +602,7 @@ pub fn run_with_changelog_modifier(
 	} else if let Some(discovered_path) =
 		env::current_dir()?.ancestors().find_map(|dir| {
 			let path = dir.join(DEFAULT_CONFIG);
-			if path.is_file() {
-				Some(path)
-			} else {
-				None
-			}
+			if path.is_file() { Some(path) } else { None }
 		}) {
 		info!(
 			"Using configuration from parent directory: {}",
@@ -651,6 +748,7 @@ pub fn run_with_changelog_modifier(
 		let repositories =
 			args.repository.clone().unwrap_or(vec![env::current_dir()?]);
 		let mut releases = Vec::<Release>::new();
+		let mut commit_range = None;
 		for repository in repositories {
 			// Skip commits
 			let mut skip_list = Vec::new();
@@ -677,13 +775,20 @@ pub fn run_with_changelog_modifier(
 
 			// Process the repository.
 			let repository = Repository::init(repository)?;
+
+			// The commit range, used for determining the remote commits to include
+			// in the changelog, doesn't make sense if multiple repositories are
+			// specified. As such, pick the commit range from the last given
+			// repository.
+			commit_range = determine_commit_range(&args, &config, &repository)?;
+
 			releases.extend(process_repository(
 				Box::leak(Box::new(repository)),
 				&mut config,
 				&args,
 			)?);
 		}
-		Changelog::new(releases, &config)?
+		Changelog::new(releases, &config, commit_range.as_deref())?
 	};
 	changelog_modifier(&mut changelog)?;
 
diff --git a/git-cliff/src/logger.rs b/git-cliff/src/logger.rs
index 17e80d3..62cdb70 100644
--- a/git-cliff/src/logger.rs
+++ b/git-cliff/src/logger.rs
@@ -1,10 +1,10 @@
 use env_logger::{
+	Builder,
 	fmt::{
 		Color,
 		Style,
 		StyledValue,
 	},
-	Builder,
 };
 use git_cliff_core::error::{
 	Error,
diff --git a/git-cliff/src/main.rs b/git-cliff/src/main.rs
index 9073825..b034319 100644
--- a/git-cliff/src/main.rs
+++ b/git-cliff/src/main.rs
@@ -13,11 +13,11 @@ fn main() -> Result<()> {
 	// Parse the command line arguments
 	let args = Opt::parse();
 	if args.verbose == 1 {
-		env::set_var("RUST_LOG", "debug");
+		unsafe { env::set_var("RUST_LOG", "debug") };
 	} else if args.verbose > 1 {
-		env::set_var("RUST_LOG", "trace");
+		unsafe { env::set_var("RUST_LOG", "trace") };
 	} else if env::var_os("RUST_LOG").is_none() {
-		env::set_var("RUST_LOG", "info");
+		unsafe { env::set_var("RUST_LOG", "info") };
 	}
 	logger::init()?;
 
diff --git a/rustfmt.toml b/rustfmt.toml
index 3cca9ea..6a1ad92 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1,4 +1,4 @@
-edition = "2018"
+edition = "2024"
 max_width = 85
 newline_style = "Unix"
 hard_tabs = true
diff --git a/website/docs/configuration/git.md b/website/docs/configuration/git.md
index 1fa0317..b751d4d 100644
--- a/website/docs/configuration/git.md
+++ b/website/docs/configuration/git.md
@@ -24,12 +24,14 @@ tag_pattern = "v[0-9].*"
 skip_tags = "v0.1.0-beta.1"
 ignore_tags = ""
 topo_order = false
+topo_order_commits = true
 sort_commits = "oldest"
 link_parsers = [
     { pattern = "#(\\d+)", href = "https://github.com/orhun/git-cliff/issues/$1"},
     { pattern = "RFC(\\d+)", text = "ietf-rfc$1", href = "https://datatracker.ietf.org/doc/html/rfc$1"},
 ]
 limit_commits = 42
+recurse_submodules = false
 ```
 
 ### conventional_commits
@@ -252,6 +254,16 @@ If set to `true`, tags are processed in topological order instead of chronologic
 
 This can also be achieved by using the `--topo-order` command line flag.
 
+### topo_order_commits
+
+If set to `true`, commits are processed in topological order instead of chronological.
+
+```toml
+# if false, sorting commit is equivalent to git log
+# if true (default), sorting commit is equivalent to git log --topo-order
+topo_order_commits = false
+```
+
 ### sort_commits
 
 Sort the commits inside sections by specified order.
@@ -263,6 +275,8 @@ Possible values:
 
 This can also be achieved by specifying the `--sort` command line argument.
 
+The default value is `oldest`.
+
 ### link_parsers
 
 An array of link parsers for extracting external references, and turning them into URLs, using regex.
@@ -274,10 +288,16 @@ Examples:
 - `{ pattern = "RFC(\\d+)", text = "ietf-rfc$1", href = "https://datatracker.ietf.org/doc/html/rfc$1"}`,
   - Extract mentions of IETF RFCs and generate URLs linking to them. It also rewrites the text as "ietf-rfc...".
 
-These extracted links can be used in the [template](/docs/templating/context) with `commits.links` variable.
+These extracted links can be used in the [template](/docs/templating/context) with `commit.links` variable.
 
 ### limit_commits
 
 `limit_commits` is an **optional** positive integer number that limits the number of included commits in the generated changelog.
 
 `limit_commits` is not part of the default configuration.
+
+### recurse_submodules
+
+`recurse_submodules` is an _optional_ boolean value that indicates whether **git-cliff** should read and process commits of submodules.
+
+This only considers submodules at the toplevel (depth 1). These commits can then be accessed by the variable `submodule_commits` during [templating](/docs/templating/context).
diff --git a/website/docs/templating/context.md b/website/docs/templating/context.md
index a2d53c7..d3e9b2a 100644
--- a/website/docs/templating/context.md
+++ b/website/docs/templating/context.md
@@ -64,6 +64,18 @@ following context is generated to use for templating:
   "commit_id": "a440c6eb26404be4877b7e3ad592bfaa5d4eb210 (release commit)",
   "timestamp": 1625169301,
   "repository": "/path/to/repository",
+  "commit_range": {
+      "from": "(id of the first commit used for this release)",
+      "to": "(id of the last commit used for this release)"
+  "submodule_commits": {
+    "<relative/submodule_path/in/repository>": [
+      {
+        "id": "c10d0f12e85975bc1e8f41eed693c58eca1894eb",
+        "message": "release (#1671)",
+        ...
+      },
+    ],
+  },
   "previous": {
     "version": "previous release"
   }
@@ -160,6 +172,10 @@ If [`conventional_commits`](/docs/configuration/git#conventional_commits) is set
   "commit_id": "a440c6eb26404be4877b7e3ad592bfaa5d4eb210 (release commit)",
   "timestamp": 1625169301,
   "repository": "/path/to/repository",
+  "commit_range": {
+      "from": "(id of the first commit used for this release)",
+      "to": "(id of the last commit used for this release)",
+  },
   "previous": {
     "version": "previous release"
   }
diff --git a/website/docs/usage/adding-commits.md b/website/docs/usage/adding-commits.md
index 788a409..1839eee 100644
--- a/website/docs/usage/adding-commits.md
+++ b/website/docs/usage/adding-commits.md
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 8
+sidebar_position: 9
 ---
 
 # Adding custom commits
@@ -27,3 +27,9 @@ git commit -m "$commit_msg"
 ```
 
 The commit SHA will be empty as default when `--with-commit` is used. Specify the hash with a message separated by single whitespace for setting the commit SHA. e.g. `--with-commit "8f55e69eba6e6ce811ace32bd84cc82215673cb6 feat: add X"`
+
+:::info
+
+Adding custom commits without a SHA will affect the [`commit_range`](/docs/templating/context) template variable.
+
+:::
diff --git a/website/docs/usage/adding-tag-messages.md b/website/docs/usage/adding-tag-messages.md
index 7e4f569..b9bfc54 100644
--- a/website/docs/usage/adding-tag-messages.md
+++ b/website/docs/usage/adding-tag-messages.md
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 9
+sidebar_position: 10
 ---
 
 # Adding version (tag) message
diff --git a/website/docs/usage/jujutsu.md b/website/docs/usage/jujutsu.md
index f8384a5..b017251 100644
--- a/website/docs/usage/jujutsu.md
+++ b/website/docs/usage/jujutsu.md
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 12
+sidebar_position: 13
 ---
 
 # Jujutsu
diff --git a/website/docs/usage/load-context.md b/website/docs/usage/load-context.md
index fd4a5a8..2f6f2ca 100644
--- a/website/docs/usage/load-context.md
+++ b/website/docs/usage/load-context.md
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 11
+sidebar_position: 12
 ---
 
 # Load context
diff --git a/website/docs/usage/print-context.md b/website/docs/usage/print-context.md
index fc314b9..3032ca6 100644
--- a/website/docs/usage/print-context.md
+++ b/website/docs/usage/print-context.md
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 10
+sidebar_position: 11
 ---
 
 # Print context
diff --git a/website/docs/usage/skipping-commits.md b/website/docs/usage/skipping-commits.md
index a515cb2..0016643 100644
--- a/website/docs/usage/skipping-commits.md
+++ b/website/docs/usage/skipping-commits.md
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 7
+sidebar_position: 8
 ---
 
 # Skipping commits
diff --git a/website/docs/usage/submodules.md b/website/docs/usage/submodules.md
new file mode 100644
index 00000000..ccc9c9a
--- /dev/null
+++ b/website/docs/usage/submodules.md
@@ -0,0 +1,33 @@
+---
+sidebar_position: 7
+---
+
+# Submodules
+
+If [`recurse_submodules`](/docs/configuration/git#recurse_submodules) is set to `true`, you can include submodule commits into your changelog.
+
+For example:
+
+```toml
+[git]
+recurse_submodules = true
+
+[changelog]
+body = """
+{% for submodule_path, commits in submodule_commits %}
+    ### {{ submodule_path | upper_first }}
+    {% for group, commits in commits | group_by(attribute="group") %}
+        #### {{ group | upper_first }}
+        {% for commit in commits %}
+            - {{ commit.message | upper_first }}\
+        {% endfor %}
+    {% endfor %}
+{% endfor %}\n
+"""
+```
+
+:::info
+
+If a release does not contain any submodule updates, `submodule_commits` is just an empty map.
+
+:::
diff --git a/website/package-lock.json b/website/package-lock.json
index 7057518..56ae517 100644
--- a/website/package-lock.json
+++ b/website/package-lock.json
@@ -11,7 +11,7 @@
       "dependencies": {
         "@docusaurus/core": "^3.7.0",
         "@docusaurus/preset-classic": "^3.7.0",
-        "@easyops-cn/docusaurus-search-local": "^0.48.4",
+        "@easyops-cn/docusaurus-search-local": "^0.49.2",
         "@mdx-js/react": "^3.1.0",
         "clsx": "^2.1.1",
         "prism-react-renderer": "^2.4.1",
@@ -21,7 +21,7 @@
       "devDependencies": {
         "@docusaurus/module-type-aliases": "^3.4.0",
         "@docusaurus/tsconfig": "^3.7.0",
-        "typescript": "^5.7.3"
+        "typescript": "^5.8.2"
       },
       "engines": {
         "node": ">=18.0"
@@ -3821,9 +3821,9 @@
       }
     },
     "node_modules/@easyops-cn/docusaurus-search-local": {
-      "version": "0.48.4",
-      "resolved": "https://registry.npmjs.org/@easyops-cn/docusaurus-search-local/-/docusaurus-search-local-0.48.4.tgz",
-      "integrity": "sha512-QgVHbGFOASFZHZdFz9NsnzsU6VRki+Mq+qd1RIDxMK3lQ7uz2cLE5MnvBfpzTmC5Ea5JF2YdMibgPechLr5uiw==",
+      "version": "0.49.2",
+      "resolved": "https://registry.npmjs.org/@easyops-cn/docusaurus-search-local/-/docusaurus-search-local-0.49.2.tgz",
+      "integrity": "sha512-ytXxbtpJ2gVECWrsdc6Wx7bPanueLQ95lLdR9n/tHKEPBm5d1v/vAsx487iUGVXYwzeEO8W/BQJ66Fcdvpq6xg==",
       "license": "MIT",
       "dependencies": {
         "@docusaurus/plugin-content-docs": "^2 || ^3",
@@ -16326,9 +16326,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "5.7.3",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
-      "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
+      "version": "5.8.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
+      "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
       "license": "Apache-2.0",
       "bin": {
         "tsc": "bin/tsc",
diff --git a/website/package.json b/website/package.json
index f5c847a..c0e7062 100644
--- a/website/package.json
+++ b/website/package.json
@@ -18,7 +18,7 @@
   "dependencies": {
     "@docusaurus/core": "^3.7.0",
     "@docusaurus/preset-classic": "^3.7.0",
-    "@easyops-cn/docusaurus-search-local": "^0.48.4",
+    "@easyops-cn/docusaurus-search-local": "^0.49.2",
     "@mdx-js/react": "^3.1.0",
     "clsx": "^2.1.1",
     "prism-react-renderer": "^2.4.1",
@@ -28,7 +28,7 @@
   "devDependencies": {
     "@docusaurus/module-type-aliases": "^3.4.0",
     "@docusaurus/tsconfig": "^3.7.0",
-    "typescript": "^5.7.3"
+    "typescript": "^5.8.2"
   },
   "browserslist": {
     "production": [
diff --git a/website/yarn.lock b/website/yarn.lock
index 2f95cf0..24298b0 100644
--- a/website/yarn.lock
+++ b/website/yarn.lock
@@ -1975,10 +1975,10 @@
     cssesc "^3.0.0"
     immediate "^3.2.3"
 
-"@easyops-cn/docusaurus-search-local@^0.48.4":
-  version "0.48.4"
-  resolved "https://registry.yarnpkg.com/@easyops-cn/docusaurus-search-local/-/docusaurus-search-local-0.48.4.tgz#a834e57bec7946674009f3a7592d629676b2bedc"
-  integrity sha512-QgVHbGFOASFZHZdFz9NsnzsU6VRki+Mq+qd1RIDxMK3lQ7uz2cLE5MnvBfpzTmC5Ea5JF2YdMibgPechLr5uiw==
+"@easyops-cn/docusaurus-search-local@^0.49.2":
+  version "0.49.2"
+  resolved "https://registry.yarnpkg.com/@easyops-cn/docusaurus-search-local/-/docusaurus-search-local-0.49.2.tgz#20c2b0ab3f58222b7e88335121b467c0e85259a2"
+  integrity sha512-ytXxbtpJ2gVECWrsdc6Wx7bPanueLQ95lLdR9n/tHKEPBm5d1v/vAsx487iUGVXYwzeEO8W/BQJ66Fcdvpq6xg==
   dependencies:
     "@docusaurus/plugin-content-docs" "^2 || ^3"
     "@docusaurus/theme-translations" "^2 || ^3"
@@ -8827,10 +8827,10 @@ typedarray-to-buffer@^3.1.5:
   dependencies:
     is-typedarray "^1.0.0"
 
-typescript@^5.7.3:
-  version "5.7.3"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e"
-  integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==
+typescript@^5.8.2:
+  version "5.8.2"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4"
+  integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
 
 undici-types@~5.26.4:
   version "5.26.5"


Test patch:
diff --git a/.github/fixtures/test-bitbucket-integration-custom-range/cliff.toml b/.github/fixtures/test-bitbucket-integration-custom-range/cliff.toml
new file mode 100644
index 00000000..fc6cc22
--- /dev/null
+++ b/.github/fixtures/test-bitbucket-integration-custom-range/cliff.toml
@@ -0,0 +1,52 @@
+# git-cliff ~ configuration file
+# https://git-cliff.org/docs/configuration
+
+# Bitbucket integration for fetching commit metadata.
+[remote.bitbucket]
+owner = "mcwarman-playground"
+repo = "git-cliff-readme-example"
+
+[changelog]
+# A Tera template to be rendered for each release in the changelog.
+# See https://keats.github.io/tera/docs/#introduction
+body = """
+{%- macro remote_url() -%}
+  https://bitbucket.org/{{ remote.bitbucket.owner }}/{{ remote.bitbucket.repo }}
+{%- endmacro -%}
+
+## What's Changed
+{%- if version %} in {{ version }}{%- endif -%}
+{% for commit in commits %}
+  * {{ commit.message | split(pat="\n") | first | trim }}\
+    {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
+    {% if commit.remote.pr_number %} in \
+      [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull-requests/{{ commit.remote.pr_number }})\
+    {%- endif %}
+{%- endfor -%}
+
+{% if bitbucket.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
+  {% raw %}\n{% endraw -%}
+  ### New Contributors
+{%- endif %}\
+{% for contributor in bitbucket.contributors | filter(attribute="is_first_time", value=true) %}
+  * @{{ contributor.username }} made their first contribution
+    {%- if contributor.pr_number %} in \
+      [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull-requests/{{ contributor.pr_number }}) \
+    {%- endif %}
+{%- endfor -%}
+{% raw %}\n\n{% endraw -%}
+"""
+
+[git]
+# Parse commits according to the conventional commits specification.
+# See https://www.conventionalcommits.org
+conventional_commits = false
+# Exclude commits that do not match the conventional commits specification.
+filter_unconventional = true
+# Split commits on newlines, treating each line as an individual commit.
+split_commits = false
+# An array of regex based parsers to modify commit messages prior to further processing.
+commit_preprocessors = [
+  # Remove issue numbers.
+  { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
+]
diff --git a/.github/fixtures/test-bitbucket-integration-custom-range/commit.sh b/.github/fixtures/test-bitbucket-integration-custom-range/commit.sh
new file mode 100755
index 00000000..7bed941
--- /dev/null
+++ b/.github/fixtures/test-bitbucket-integration-custom-range/commit.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -e
+
+git remote add origin https://bitbucket.org/mcwarman-playground/git-cliff-readme-example
+git pull origin master
+git fetch --tags
diff --git a/.github/fixtures/test-bitbucket-integration-custom-range/expected.md b/.github/fixtures/test-bitbucket-integration-custom-range/expected.md
new file mode 100644
index 00000000..3a84224
--- /dev/null
+++ b/.github/fixtures/test-bitbucket-integration-custom-range/expected.md
@@ -0,0 +1,9 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## What's Changed in v1.0.1
+* refactor(parser): expose string functions by @orhun <orhun@archlinux.org>
+* chore(release): add release script by @orhun <orhun@archlinux.org>
+
+<!-- generated by git-cliff -->
diff --git a/.github/fixtures/test-commit-range-with-given-range/cliff.toml b/.github/fixtures/test-commit-range-with-given-range/cliff.toml
new file mode 100644
index 00000000..075847e
--- /dev/null
+++ b/.github/fixtures/test-commit-range-with-given-range/cliff.toml
@@ -0,0 +1,29 @@
+# git-cliff ~ configuration file
+# https://git-cliff.org/docs/configuration
+
+[changelog]
+# A Tera template to be rendered for each release in the changelog.
+# See https://keats.github.io/tera/docs/#introduction
+body = """
+{% if version %}\
+    ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
+{% else %}\
+    ## [unreleased]
+{% endif %}
+{{ commit_range.from }}..{{ commit_range.to }}
+{% for group, commits in commits | group_by(attribute="group") %}
+    ### {{ group | upper_first }}
+    {% for commit in commits %}
+        - {{ commit.message | upper_first }}\
+    {% endfor %}
+{% endfor %}\n
+"""
+
+[git]
+# An array of regex based parsers for extracting data from the commit message.
+# Assigns commits to groups.
+# Optionally sets the commit's `scope` and can decide to exclude commits from further processing.
+commit_parsers = [
+    { message = "^feat", group = "Features", default_scope = "app" },
+    { message = "^fix", group = "Bug Fixes", scope = "cli" },
+]
diff --git a/.github/fixtures/test-commit-range-with-given-range/commit.sh b/.github/fixtures/test-commit-range-with-given-range/commit.sh
new file mode 100755
index 00000000..0089053
--- /dev/null
+++ b/.github/fixtures/test-commit-range-with-given-range/commit.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -e
+
+git remote add origin https://github.com/orhun/git-cliff-readme-example
+git pull origin master
+git fetch --tags
diff --git a/.github/fixtures/test-commit-range-with-given-range/expected.md b/.github/fixtures/test-commit-range-with-given-range/expected.md
new file mode 100644
index 00000000..e24fe72
--- /dev/null
+++ b/.github/fixtures/test-commit-range-with-given-range/expected.md
@@ -0,0 +1,30 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [unreleased]
+
+a140cef0405e0bcbfb5de44ff59e091527d91b38..a9d4050212a18f6b3bd76e2e41fbb9045d268b80
+
+### Bug Fixes
+
+- Rename help argument due to conflict
+
+### Features
+
+- Add ability to parse arrays
+- Support multiple file formats
+
+### Chore
+
+- Add release script
+
+### Docs
+
+- Add tested usage example
+
+### Refactor
+
+- Expose string functions
+
+<!-- generated by git-cliff -->
diff --git a/.github/fixtures/test-commit-range-with-sort-commits/cliff.toml b/.github/fixtures/test-commit-range-with-sort-commits/cliff.toml
new file mode 100644
index 00000000..9f551bc
--- /dev/null
+++ b/.github/fixtures/test-commit-range-with-sort-commits/cliff.toml
@@ -0,0 +1,30 @@
+# git-cliff ~ configuration file
+# https://git-cliff.org/docs/configuration
+
+[changelog]
+# A Tera template to be rendered for each release in the changelog.
+# See https://keats.github.io/tera/docs/#introduction
+body = """
+{% if version %}\
+    ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
+{% else %}\
+    ## [unreleased]
+{% endif %}
+{{ commit_range.from }}..{{ commit_range.to }}
+{% for group, commits in commits | group_by(attribute="group") %}
+    ### {{ group | upper_first }}
+    {% for commit in commits %}
+        - {{ commit.message | upper_first }}\
+    {% endfor %}
+{% endfor %}\n
+"""
+
+[git]
+# An array of regex based parsers for extracting data from the commit message.
+# Assigns commits to groups.
+# Optionally sets the commit's `scope` and can decide to exclude commits from further processing.
+commit_parsers = [
+    { message = "^feat", group = "Features", default_scope = "app" },
+    { message = "^fix", group = "Bug Fixes", scope = "cli" },
+]
+sort_commits = "newest"
diff --git a/.github/fixtures/test-commit-range-with-sort-commits/commit.sh b/.github/fixtures/test-commit-range-with-sort-commits/commit.sh
new file mode 100755
index 00000000..0089053
--- /dev/null
+++ b/.github/fixtures/test-commit-range-with-sort-commits/commit.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -e
+
+git remote add origin https://github.com/orhun/git-cliff-readme-example
+git pull origin master
+git fetch --tags
diff --git a/.github/fixtures/test-commit-range-with-sort-commits/expected.md b/.github/fixtures/test-commit-range-with-sort-commits/expected.md
new file mode 100644
index 00000000..ab0ea53
--- /dev/null
+++ b/.github/fixtures/test-commit-range-with-sort-commits/expected.md
@@ -0,0 +1,43 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [unreleased]
+
+a9d4050212a18f6b3bd76e2e41fbb9045d268b80..df6aef41292f3ffe5887754232e6ea7831c50ba5
+
+### Features
+
+- Use cache while fetching pages
+- Support multiple file formats
+
+## [1.0.1] - 2021-07-18
+
+e4fd3cf8e2e6f49c0b57f66416e886c37cbb3715..06412ac1dd4071006c465dde6597a21d4367a158
+
+### Chore
+
+- Add release script
+
+### Refactor
+
+- Expose string functions
+
+## [1.0.0] - 2021-07-18
+
+a78bc368e9ee382a3016c0c4bab41f7de4503bcd..ad27b43e8032671afb4809a1a3ecf12f45c60e0e
+
+### Bug Fixes
+
+- Rename help argument due to conflict
+
+### Features
+
+- Add ability to parse arrays
+
+### Docs
+
+- Add tested usage example
+- Add README.md
+
+<!-- generated by git-cliff -->
diff --git a/.github/fixtures/test-commit-range/cliff.toml b/.github/fixtures/test-commit-range/cliff.toml
new file mode 100644
index 00000000..075847e
--- /dev/null
+++ b/.github/fixtures/test-commit-range/cliff.toml
@@ -0,0 +1,29 @@
+# git-cliff ~ configuration file
+# https://git-cliff.org/docs/configuration
+
+[changelog]
+# A Tera template to be rendered for each release in the changelog.
+# See https://keats.github.io/tera/docs/#introduction
+body = """
+{% if version %}\
+    ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
+{% else %}\
+    ## [unreleased]
+{% endif %}
+{{ commit_range.from }}..{{ commit_range.to }}
+{% for group, commits in commits | group_by(attribute="group") %}
+    ### {{ group | upper_first }}
+    {% for commit in commits %}
+        - {{ commit.message | upper_first }}\
+    {% endfor %}
+{% endfor %}\n
+"""
+
+[git]
+# An array of regex based parsers for extracting data from the commit message.
+# Assigns commits to groups.
+# Optionally sets the commit's `scope` and can decide to exclude commits from further processing.
+commit_parsers = [
+    { message = "^feat", group = "Features", default_scope = "app" },
+    { message = "^fix", group = "Bug Fixes", scope = "cli" },
+]
diff --git a/.github/fixtures/test-commit-range/commit.sh b/.github/fixtures/test-commit-range/commit.sh
new file mode 100755
index 00000000..0089053
--- /dev/null
+++ b/.github/fixtures/test-commit-range/commit.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -e
+
+git remote add origin https://github.com/orhun/git-cliff-readme-example
+git pull origin master
+git fetch --tags
diff --git a/.github/fixtures/test-commit-range/expected.md b/.github/fixtures/test-commit-range/expected.md
new file mode 100644
index 00000000..9caa12a
--- /dev/null
+++ b/.github/fixtures/test-commit-range/expected.md
@@ -0,0 +1,43 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [unreleased]
+
+a9d4050212a18f6b3bd76e2e41fbb9045d268b80..df6aef41292f3ffe5887754232e6ea7831c50ba5
+
+### Features
+
+- Support multiple file formats
+- Use cache while fetching pages
+
+## [1.0.1] - 2021-07-18
+
+e4fd3cf8e2e6f49c0b57f66416e886c37cbb3715..06412ac1dd4071006c465dde6597a21d4367a158
+
+### Chore
+
+- Add release script
+
+### Refactor
+
+- Expose string functions
+
+## [1.0.0] - 2021-07-18
+
+a78bc368e9ee382a3016c0c4bab41f7de4503bcd..ad27b43e8032671afb4809a1a3ecf12f45c60e0e
+
+### Bug Fixes
+
+- Rename help argument due to conflict
+
+### Features
+
+- Add ability to parse arrays
+
+### Docs
+
+- Add README.md
+- Add tested usage example
+
+<!-- generated by git-cliff -->
diff --git a/.github/fixtures/test-from-context-does-not-discard-fields/context.json b/.github/fixtures/test-from-context-does-not-discard-fields/context.json
index e7750b3..1029b0d 100644
--- a/.github/fixtures/test-from-context-does-not-discard-fields/context.json
+++ b/.github/fixtures/test-from-context-does-not-discard-fields/context.json
@@ -200,7 +200,12 @@
         "message": "fix 1, use scope as group",
         "body": "Body fix 1",
         "footers": [
-          { "token": "Fix", "separator": ":", "value": "#1", "breaking": false }
+          {
+            "token": "Fix",
+            "separator": ":",
+            "value": "#1",
+            "breaking": false
+          }
         ],
         "group": "fix",
         "breaking_description": null,
@@ -314,7 +319,12 @@
             "value": "#3",
             "breaking": false
           },
-          { "token": "Fix", "separator": ":", "value": "#4", "breaking": false }
+          {
+            "token": "Fix",
+            "separator": ":",
+            "value": "#4",
+            "breaking": false
+          }
         ],
         "group": "fix",
         "breaking_description": null,
@@ -375,17 +385,37 @@
       "timestamp": 0,
       "previous": null,
       "repository": null,
+      "commit_range": null,
+      "submodule_commits": {},
       "extra": null,
-      "github": { "contributors": [] },
-      "gitlab": { "contributors": [] },
-      "gitea": { "contributors": [] },
-      "bitbucket": { "contributors": [] }
+      "github": {
+        "contributors": []
+      },
+      "gitlab": {
+        "contributors": []
+      },
+      "gitea": {
+        "contributors": []
+      },
+      "bitbucket": {
+        "contributors": []
+      }
     },
     "repository": "/path/to/repository",
+    "commit_range": null,
+    "submodule_commits": {},
     "extra": null,
-    "github": { "contributors": [] },
-    "gitlab": { "contributors": [] },
-    "gitea": { "contributors": [] },
-    "bitbucket": { "contributors": [] }
+    "github": {
+      "contributors": []
+    },
+    "gitlab": {
+      "contributors": []
+    },
+    "gitea": {
+      "contributors": []
+    },
+    "bitbucket": {
+      "contributors": []
+    }
   }
-]
+]
\ No newline at end of file
diff --git a/.github/fixtures/test-from-context/context.json b/.github/fixtures/test-from-context/context.json
index 5dee19a..d7dd053 100644
--- a/.github/fixtures/test-from-context/context.json
+++ b/.github/fixtures/test-from-context/context.json
@@ -1,626 +1,638 @@
 [
-    {
-        "version": null,
-        "message": null,
-        "commits": [
-            {
-                "id": "c7541276668616c136d12a9ccd87087314d66fb6",
-                "message": "add tests",
-                "body": null,
-                "footers": [],
-                "group": "test",
-                "breaking_description": null,
-                "breaking": false,
-                "scope": null,
-                "links": [],
-                "author": {
-                    "name": "John Doe",
-                    "email": "john@doe.com",
-                    "timestamp": 1723730666
-                },
-                "committer": {
-                    "name": "John Doe",
-                    "email": "john@doe.com",
-                    "timestamp": 1649201113
-                },
-                "conventional": true,
-                "merge_commit": false,
-                "github": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                },
-                "gitlab": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                },
-                "gitea": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                },
-                "bitbucket": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                }
-            }
-        ],
-        "commit_id": null,
-        "timestamp": 0,
-        "previous": {
-            "version": "v0.2.0",
-            "message": null,
-            "commits": [
-                {
-                    "id": "ce8cecc9834e5cbdb5b4e79c8260cace3c0dd949",
-                    "message": "feat(gui): add feature 2\n",
-                    "group": null,
-                    "scope": null,
-                    "links": [],
-                    "author": {
-                        "name": "John Doe",
-                        "email": "john@doe.com",
-                        "timestamp": 1723730666
-                    },
-                    "committer": {
-                        "name": "John Doe",
-                        "email": "john@doe.com",
-                        "timestamp": 1649201111
-                    },
-                    "conventional": false,
-                    "merge_commit": false,
-                    "github": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    },
-                    "gitlab": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    },
-                    "gitea": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    },
-                    "bitbucket": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    }
-                },
-                {
-                    "id": "5061081d6272b1da2146fab49d803c193db309d9",
-                    "message": "fix(gui): fix feature 2\n",
-                    "group": null,
-                    "scope": null,
-                    "links": [],
-                    "author": {
-                        "name": "John Doe",
-                        "email": "john@doe.com",
-                        "timestamp": 1723730666
-                    },
-                    "committer": {
-                        "name": "John Doe",
-                        "email": "john@doe.com",
-                        "timestamp": 1649201112
-                    },
-                    "conventional": false,
-                    "merge_commit": false,
-                    "github": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    },
-                    "gitlab": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    },
-                    "gitea": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    },
-                    "bitbucket": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    }
-                }
-            ],
-            "commit_id": "5061081d6272b1da2146fab49d803c193db309d9",
-            "timestamp": 1649201112,
-            "previous": null,
-            "repository": "/home/johndoe/repo/",
-            "github": {
-                "contributors": []
-            },
-            "gitlab": {
-                "contributors": []
-            },
-            "gitea": {
-                "contributors": []
-            },
-            "bitbucket": {
-                "contributors": []
-            }
+  {
+    "version": null,
+    "message": null,
+    "commits": [
+      {
+        "id": "c7541276668616c136d12a9ccd87087314d66fb6",
+        "message": "add tests",
+        "body": null,
+        "footers": [],
+        "group": "test",
+        "breaking_description": null,
+        "breaking": false,
+        "scope": null,
+        "links": [],
+        "author": {
+          "name": "John Doe",
+          "email": "john@doe.com",
+          "timestamp": 1723730666
         },
-        "repository": "/home/johndoe/repo/",
+        "committer": {
+          "name": "John Doe",
+          "email": "john@doe.com",
+          "timestamp": 1649201113
+        },
+        "conventional": true,
+        "merge_commit": false,
         "github": {
-            "contributors": []
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
         },
         "gitlab": {
-            "contributors": []
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
         },
         "gitea": {
-            "contributors": []
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
         },
         "bitbucket": {
-            "contributors": []
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
+        }
+      }
+    ],
+    "commit_id": null,
+    "timestamp": 0,
+    "previous": {
+      "version": "v0.2.0",
+      "message": null,
+      "commits": [
+        {
+          "id": "ce8cecc9834e5cbdb5b4e79c8260cace3c0dd949",
+          "message": "feat(gui): add feature 2\n",
+          "group": null,
+          "scope": null,
+          "links": [],
+          "author": {
+            "name": "John Doe",
+            "email": "john@doe.com",
+            "timestamp": 1723730666
+          },
+          "committer": {
+            "name": "John Doe",
+            "email": "john@doe.com",
+            "timestamp": 1649201111
+          },
+          "conventional": false,
+          "merge_commit": false,
+          "github": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          },
+          "gitlab": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          },
+          "gitea": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          },
+          "bitbucket": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          }
+        },
+        {
+          "id": "5061081d6272b1da2146fab49d803c193db309d9",
+          "message": "fix(gui): fix feature 2\n",
+          "group": null,
+          "scope": null,
+          "links": [],
+          "author": {
+            "name": "John Doe",
+            "email": "john@doe.com",
+            "timestamp": 1723730666
+          },
+          "committer": {
+            "name": "John Doe",
+            "email": "john@doe.com",
+            "timestamp": 1649201112
+          },
+          "conventional": false,
+          "merge_commit": false,
+          "github": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          },
+          "gitlab": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          },
+          "gitea": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          },
+          "bitbucket": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          }
         }
+      ],
+      "commit_id": "5061081d6272b1da2146fab49d803c193db309d9",
+      "timestamp": 1649201112,
+      "previous": null,
+      "repository": "/home/johndoe/repo/",
+      "commit_range": null,
+      "submodule_commits": {},
+      "github": {
+        "contributors": []
+      },
+      "gitlab": {
+        "contributors": []
+      },
+      "gitea": {
+        "contributors": []
+      },
+      "bitbucket": {
+        "contributors": []
+      }
+    },
+    "repository": "/home/johndoe/repo/",
+    "commit_range": null,
+    "submodule_commits": {},
+    "github": {
+      "contributors": []
+    },
+    "gitlab": {
+      "contributors": []
+    },
+    "gitea": {
+      "contributors": []
     },
-    {
-        "version": "v0.2.0",
-        "message": null,
-        "commits": [
-            {
-                "id": "ce8cecc9834e5cbdb5b4e79c8260cace3c0dd949",
-                "message": "add feature 2",
-                "body": null,
-                "footers": [],
-                "group": "Features",
-                "breaking_description": null,
-                "breaking": false,
-                "scope": "gui",
-                "links": [],
-                "author": {
-                    "name": "John Doe",
-                    "email": "john@doe.com",
-                    "timestamp": 1723730666
-                },
-                "committer": {
-                    "name": "John Doe",
-                    "email": "john@doe.com",
-                    "timestamp": 1649201111
-                },
-                "conventional": true,
-                "merge_commit": false,
-                "github": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                },
-                "gitlab": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                },
-                "gitea": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                },
-                "bitbucket": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                }
-            },
-            {
-                "id": "5061081d6272b1da2146fab49d803c193db309d9",
-                "message": "fix feature 2",
-                "body": null,
-                "footers": [],
-                "group": "Bug Fixes",
-                "breaking_description": null,
-                "breaking": false,
-                "scope": "cli",
-                "links": [],
-                "author": {
-                    "name": "John Doe",
-                    "email": "john@doe.com",
-                    "timestamp": 1723730666
-                },
-                "committer": {
-                    "name": "John Doe",
-                    "email": "john@doe.com",
-                    "timestamp": 1649201112
-                },
-                "conventional": true,
-                "merge_commit": false,
-                "extra": {
-                    "note": "that was a tough one"
-                },
-                "github": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                },
-                "gitlab": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                },
-                "gitea": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                },
-                "bitbucket": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                }
-            }
-        ],
-        "commit_id": "5061081d6272b1da2146fab49d803c193db309d9",
-        "timestamp": 1649201112,
-        "previous": {
-            "version": "v0.1.0",
-            "message": null,
-            "commits": [
-                {
-                    "id": "c6ba7fe73c3b639ff2f934f3f98e5f50350ab463",
-                    "message": "Initial commit\n",
-                    "group": null,
-                    "scope": null,
-                    "links": [],
-                    "author": {
-                        "name": "John Doe",
-                        "email": "john@doe.com",
-                        "timestamp": 1723730666
-                    },
-                    "committer": {
-                        "name": "John Doe",
-                        "email": "john@doe.com",
-                        "timestamp": 1649201108
-                    },
-                    "conventional": false,
-                    "merge_commit": false,
-                    "github": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    },
-                    "gitlab": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    },
-                    "gitea": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    },
-                    "bitbucket": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    }
-                },
-                {
-                    "id": "9c10e506bf103998e303d8164c7b4a1a841e2471",
-                    "message": "feat: add feature 1\n",
-                    "group": null,
-                    "scope": null,
-                    "links": [],
-                    "author": {
-                        "name": "John Doe",
-                        "email": "john@doe.com",
-                        "timestamp": 1723730666
-                    },
-                    "committer": {
-                        "name": "John Doe",
-                        "email": "john@doe.com",
-                        "timestamp": 1649201109
-                    },
-                    "conventional": false,
-                    "merge_commit": false,
-                    "github": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    },
-                    "gitlab": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    },
-                    "gitea": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    },
-                    "bitbucket": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    }
-                },
-                {
-                    "id": "98b1464c2f99a0c6812ce6d3ea4a5b953dd35fe2",
-                    "message": "fix: fix feature 1\n",
-                    "group": null,
-                    "scope": null,
-                    "links": [],
-                    "author": {
-                        "name": "John Doe",
-                        "email": "john@doe.com",
-                        "timestamp": 1723730666
-                    },
-                    "committer": {
-                        "name": "John Doe",
-                        "email": "john@doe.com",
-                        "timestamp": 1649201110
-                    },
-                    "conventional": false,
-                    "merge_commit": false,
-                    "github": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    },
-                    "gitlab": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    },
-                    "gitea": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    },
-                    "bitbucket": {
-                        "username": null,
-                        "pr_title": null,
-                        "pr_number": null,
-                        "pr_labels": [],
-                        "is_first_time": false
-                    }
-                }
-            ],
-            "commit_id": "98b1464c2f99a0c6812ce6d3ea4a5b953dd35fe2",
-            "timestamp": 1649201110,
-            "previous": null,
-            "repository": "/home/johndoe/repo/",
-            "github": {
-                "contributors": []
-            },
-            "gitlab": {
-                "contributors": []
-            },
-            "gitea": {
-                "contributors": []
-            },
-            "bitbucket": {
-                "contributors": []
-            }
+    "bitbucket": {
+      "contributors": []
+    }
+  },
+  {
+    "version": "v0.2.0",
+    "message": null,
+    "commits": [
+      {
+        "id": "ce8cecc9834e5cbdb5b4e79c8260cace3c0dd949",
+        "message": "add feature 2",
+        "body": null,
+        "footers": [],
+        "group": "Features",
+        "breaking_description": null,
+        "breaking": false,
+        "scope": "gui",
+        "links": [],
+        "author": {
+          "name": "John Doe",
+          "email": "john@doe.com",
+          "timestamp": 1723730666
+        },
+        "committer": {
+          "name": "John Doe",
+          "email": "john@doe.com",
+          "timestamp": 1649201111
         },
+        "conventional": true,
+        "merge_commit": false,
+        "github": {
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
+        },
+        "gitlab": {
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
+        },
+        "gitea": {
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
+        },
+        "bitbucket": {
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
+        }
+      },
+      {
+        "id": "5061081d6272b1da2146fab49d803c193db309d9",
+        "message": "fix feature 2",
+        "body": null,
+        "footers": [],
+        "group": "Bug Fixes",
+        "breaking_description": null,
+        "breaking": false,
+        "scope": "cli",
+        "links": [],
+        "author": {
+          "name": "John Doe",
+          "email": "john@doe.com",
+          "timestamp": 1723730666
+        },
+        "committer": {
+          "name": "John Doe",
+          "email": "john@doe.com",
+          "timestamp": 1649201112
+        },
+        "conventional": true,
+        "merge_commit": false,
         "extra": {
-            "note": "This is so awesome!"
+          "note": "that was a tough one"
         },
-        "repository": "/home/johndoe/repo/",
         "github": {
-            "contributors": []
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
         },
         "gitlab": {
-            "contributors": []
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
         },
         "gitea": {
-            "contributors": []
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
         },
         "bitbucket": {
-            "contributors": []
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
+        }
+      }
+    ],
+    "commit_id": "5061081d6272b1da2146fab49d803c193db309d9",
+    "timestamp": 1649201112,
+    "previous": {
+      "version": "v0.1.0",
+      "message": null,
+      "commits": [
+        {
+          "id": "c6ba7fe73c3b639ff2f934f3f98e5f50350ab463",
+          "message": "Initial commit\n",
+          "group": null,
+          "scope": null,
+          "links": [],
+          "author": {
+            "name": "John Doe",
+            "email": "john@doe.com",
+            "timestamp": 1723730666
+          },
+          "committer": {
+            "name": "John Doe",
+            "email": "john@doe.com",
+            "timestamp": 1649201108
+          },
+          "conventional": false,
+          "merge_commit": false,
+          "github": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          },
+          "gitlab": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          },
+          "gitea": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          },
+          "bitbucket": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          }
+        },
+        {
+          "id": "9c10e506bf103998e303d8164c7b4a1a841e2471",
+          "message": "feat: add feature 1\n",
+          "group": null,
+          "scope": null,
+          "links": [],
+          "author": {
+            "name": "John Doe",
+            "email": "john@doe.com",
+            "timestamp": 1723730666
+          },
+          "committer": {
+            "name": "John Doe",
+            "email": "john@doe.com",
+            "timestamp": 1649201109
+          },
+          "conventional": false,
+          "merge_commit": false,
+          "github": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          },
+          "gitlab": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          },
+          "gitea": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          },
+          "bitbucket": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          }
+        },
+        {
+          "id": "98b1464c2f99a0c6812ce6d3ea4a5b953dd35fe2",
+          "message": "fix: fix feature 1\n",
+          "group": null,
+          "scope": null,
+          "links": [],
+          "author": {
+            "name": "John Doe",
+            "email": "john@doe.com",
+            "timestamp": 1723730666
+          },
+          "committer": {
+            "name": "John Doe",
+            "email": "john@doe.com",
+            "timestamp": 1649201110
+          },
+          "conventional": false,
+          "merge_commit": false,
+          "github": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          },
+          "gitlab": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          },
+          "gitea": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          },
+          "bitbucket": {
+            "username": null,
+            "pr_title": null,
+            "pr_number": null,
+            "pr_labels": [],
+            "is_first_time": false
+          }
         }
+      ],
+      "commit_id": "98b1464c2f99a0c6812ce6d3ea4a5b953dd35fe2",
+      "timestamp": 1649201110,
+      "previous": null,
+      "repository": "/home/johndoe/repo/",
+      "commit_range": null,
+      "submodule_commits": {},
+      "github": {
+        "contributors": []
+      },
+      "gitlab": {
+        "contributors": []
+      },
+      "gitea": {
+        "contributors": []
+      },
+      "bitbucket": {
+        "contributors": []
+      }
     },
-    {
-        "version": "v0.1.0",
-        "message": null,
-        "commits": [
-            {
-                "id": "9c10e506bf103998e303d8164c7b4a1a841e2471",
-                "message": "add feature 1",
-                "body": null,
-                "footers": [],
-                "group": "Features",
-                "breaking_description": null,
-                "breaking": false,
-                "scope": "app",
-                "links": [],
-                "author": {
-                    "name": "John Doe",
-                    "email": "john@doe.com",
-                    "timestamp": 1723730666
-                },
-                "committer": {
-                    "name": "John Doe",
-                    "email": "john@doe.com",
-                    "timestamp": 1649201109
-                },
-                "conventional": true,
-                "merge_commit": false,
-                "github": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                },
-                "gitlab": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                },
-                "gitea": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                },
-                "bitbucket": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                }
-            },
-            {
-                "id": "98b1464c2f99a0c6812ce6d3ea4a5b953dd35fe2",
-                "message": "fix feature 1",
-                "body": null,
-                "footers": [],
-                "group": "Bug Fixes",
-                "breaking_description": null,
-                "breaking": false,
-                "scope": "cli",
-                "links": [],
-                "author": {
-                    "name": "John Doe",
-                    "email": "john@doe.com",
-                    "timestamp": 1723730666
-                },
-                "committer": {
-                    "name": "John Doe",
-                    "email": "john@doe.com",
-                    "timestamp": 1649201110
-                },
-                "conventional": true,
-                "merge_commit": false,
-                "github": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                },
-                "gitlab": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                },
-                "gitea": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                },
-                "bitbucket": {
-                    "username": null,
-                    "pr_title": null,
-                    "pr_number": null,
-                    "pr_labels": [],
-                    "is_first_time": false
-                }
-            }
-        ],
-        "commit_id": "98b1464c2f99a0c6812ce6d3ea4a5b953dd35fe2",
-        "timestamp": 1649201110,
-        "previous": {
-            "version": null,
-            "message": null,
-            "commits": [],
-            "commit_id": null,
-            "timestamp": 0,
-            "previous": null,
-            "repository": null,
-            "github": {
-                "contributors": []
-            },
-            "gitlab": {
-                "contributors": []
-            },
-            "gitea": {
-                "contributors": []
-            },
-            "bitbucket": {
-                "contributors": []
-            }
+    "extra": {
+      "note": "This is so awesome!"
+    },
+    "repository": "/home/johndoe/repo/",
+    "commit_range": null,
+    "submodule_commits": {},
+    "github": {
+      "contributors": []
+    },
+    "gitlab": {
+      "contributors": []
+    },
+    "gitea": {
+      "contributors": []
+    },
+    "bitbucket": {
+      "contributors": []
+    }
+  },
+  {
+    "version": "v0.1.0",
+    "message": null,
+    "commits": [
+      {
+        "id": "9c10e506bf103998e303d8164c7b4a1a841e2471",
+        "message": "add feature 1",
+        "body": null,
+        "footers": [],
+        "group": "Features",
+        "breaking_description": null,
+        "breaking": false,
+        "scope": "app",
+        "links": [],
+        "author": {
+          "name": "John Doe",
+          "email": "john@doe.com",
+          "timestamp": 1723730666
+        },
+        "committer": {
+          "name": "John Doe",
+          "email": "john@doe.com",
+          "timestamp": 1649201109
+        },
+        "conventional": true,
+        "merge_commit": false,
+        "github": {
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
+        },
+        "gitlab": {
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
         },
-        "repository": "/home/johndoe/repo/",
+        "gitea": {
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
+        },
+        "bitbucket": {
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
+        }
+      },
+      {
+        "id": "98b1464c2f99a0c6812ce6d3ea4a5b953dd35fe2",
+        "message": "fix feature 1",
+        "body": null,
+        "footers": [],
+        "group": "Bug Fixes",
+        "breaking_description": null,
+        "breaking": false,
+        "scope": "cli",
+        "links": [],
+        "author": {
+          "name": "John Doe",
+          "email": "john@doe.com",
+          "timestamp": 1723730666
+        },
+        "committer": {
+          "name": "John Doe",
+          "email": "john@doe.com",
+          "timestamp": 1649201110
+        },
+        "conventional": true,
+        "merge_commit": false,
         "github": {
-            "contributors": []
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
         },
         "gitlab": {
-            "contributors": []
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
         },
         "gitea": {
-            "contributors": []
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
         },
         "bitbucket": {
-            "contributors": []
+          "username": null,
+          "pr_title": null,
+          "pr_number": null,
+          "pr_labels": [],
+          "is_first_time": false
         }
+      }
+    ],
+    "commit_id": "98b1464c2f99a0c6812ce6d3ea4a5b953dd35fe2",
+    "timestamp": 1649201110,
+    "previous": {
+      "version": null,
+      "message": null,
+      "commits": [],
+      "commit_id": null,
+      "timestamp": 0,
+      "previous": null,
+      "repository": null,
+      "commit_range": null,
+      "submodule_commits": {},
+      "github": {
+        "contributors": []
+      },
+      "gitlab": {
+        "contributors": []
+      },
+      "gitea": {
+        "contributors": []
+      },
+      "bitbucket": {
+        "contributors": []
+      }
+    },
+    "repository": "/home/johndoe/repo/",
+    "commit_range": null,
+    "submodule_commits": {},
+    "github": {
+      "contributors": []
+    },
+    "gitlab": {
+      "contributors": []
+    },
+    "gitea": {
+      "contributors": []
+    },
+    "bitbucket": {
+      "contributors": []
     }
-]
+  }
+]
\ No newline at end of file
diff --git a/.github/fixtures/test-gitea-integration-custom-range/cliff.toml b/.github/fixtures/test-gitea-integration-custom-range/cliff.toml
new file mode 100644
index 00000000..90e328a
--- /dev/null
+++ b/.github/fixtures/test-gitea-integration-custom-range/cliff.toml
@@ -0,0 +1,46 @@
+# git-cliff ~ configuration file
+# https://git-cliff.org/docs/configuration
+
+# Gitea integration for fetching commit metadata.
+[remote.gitea]
+owner = "ThetaDev"
+repo = "git-cliff-readme-example"
+
+[changelog]
+# A Tera template to be rendered for each release in the changelog.
+# See https://keats.github.io/tera/docs/#introduction
+body = """
+## What's Changed
+{%- if version %} in {{ version }}{%- endif -%}
+{% for commit in commits %}
+  * {{ commit.message | split(pat="\n") | first | trim }}\
+    {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
+    {% if commit.remote.pr_number %} in #{{ commit.remote.pr_number }}{%- endif %}
+{%- endfor -%}
+
+{% if gitea.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
+  {% raw %}\n{% endraw -%}
+  ### New Contributors
+{%- endif %}\
+{% for contributor in gitea.contributors | filter(attribute="is_first_time", value=true) %}
+  * @{{ contributor.username }} made their first contribution
+    {%- if contributor.pr_number %} in \
+      [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
+    {%- endif %}
+{%- endfor -%}
+{% raw %}\n\n{% endraw -%}
+"""
+
+[git]
+# Parse commits according to the conventional commits specification.
+# See https://www.conventionalcommits.org
+conventional_commits = false
+# Exclude commits that do not match the conventional commits specification.
+filter_unconventional = true
+# Split commits on newlines, treating each line as an individual commit.
+split_commits = false
+# An array of regex based parsers to modify commit messages prior to further processing.
+commit_preprocessors = [
+  # Remove issue numbers.
+  { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
+]
diff --git a/.github/fixtures/test-gitea-integration-custom-range/commit.sh b/.github/fixtures/test-gitea-integration-custom-range/commit.sh
new file mode 100755
index 00000000..d15ef15
--- /dev/null
+++ b/.github/fixtures/test-gitea-integration-custom-range/commit.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -e
+
+git remote add origin https://codeberg.org/ThetaDev/git-cliff-readme-example.git
+git pull origin master
+git fetch --tags
diff --git a/.github/fixtures/test-gitea-integration-custom-range/expected.md b/.github/fixtures/test-gitea-integration-custom-range/expected.md
new file mode 100644
index 00000000..b48d592
--- /dev/null
+++ b/.github/fixtures/test-gitea-integration-custom-range/expected.md
@@ -0,0 +1,9 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## What's Changed
+* feat(config): support multiple file formats by @ThetaDev
+* feat(cache): use cache while fetching pages by @ThetaDev
+
+<!-- generated by git-cliff -->
diff --git a/.github/fixtures/test-github-integration-custom-range/cliff.toml b/.github/fixtures/test-github-integration-custom-range/cliff.toml
new file mode 100644
index 00000000..36f9aca
--- /dev/null
+++ b/.github/fixtures/test-github-integration-custom-range/cliff.toml
@@ -0,0 +1,54 @@
+# git-cliff ~ configuration file
+# https://git-cliff.org/docs/configuration
+
+# GitHub integration for fetching commit metadata.
+[remote.github]
+owner = "orhun"
+repo = "git-cliff-readme-example"
+
+[changelog]
+# A Tera template to be rendered for each release in the changelog.
+# See https://keats.github.io/tera/docs/#introduction
+body = """
+## What's Changed
+
+{%- if version %} in {{ version }}{%- endif -%}
+{% for commit in commits %}
+  * {{ commit.message | split(pat="\n") | first | trim }}\
+    {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
+    {% if commit.remote.pr_number %} in #{{ commit.remote.pr_number }}{%- endif %}
+{%- endfor -%}
+
+{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
+  {% raw %}\n{% endraw -%}
+  ### New Contributors
+{%- endif %}\
+{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
+  * @{{ contributor.username }} made their first contribution
+    {%- if contributor.pr_number %} in \
+      [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
+    {%- endif %}
+{%- endfor -%}
+
+{% if version %}
+    {% if previous.version %}
+      **Full Changelog**: https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/compare/{{ previous.version }}...{{ version }}
+    {% endif %}
+{% else -%}
+  {% raw %}\n{% endraw %}
+{% endif %}
+"""
+
+[git]
+# Parse commits according to the conventional commits specification.
+# See https://www.conventionalcommits.org
+conventional_commits = false
+# Exclude commits that do not match the conventional commits specification.
+filter_unconventional = true
+# Split commits on newlines, treating each line as an individual commit.
+split_commits = false
+# An array of regex based parsers to modify commit messages prior to further processing.
+commit_preprocessors = [
+  # Remove issue numbers.
+  { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
+]
diff --git a/.github/fixtures/test-github-integration-custom-range/commit.sh b/.github/fixtures/test-github-integration-custom-range/commit.sh
new file mode 100755
index 00000000..0089053
--- /dev/null
+++ b/.github/fixtures/test-github-integration-custom-range/commit.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -e
+
+git remote add origin https://github.com/orhun/git-cliff-readme-example
+git pull origin master
+git fetch --tags
diff --git a/.github/fixtures/test-github-integration-custom-range/expected.md b/.github/fixtures/test-github-integration-custom-range/expected.md
new file mode 100644
index 00000000..65bc533
--- /dev/null
+++ b/.github/fixtures/test-github-integration-custom-range/expected.md
@@ -0,0 +1,11 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## What's Changed in v1.0.1
+* refactor(parser): expose string functions by @orhun
+* chore(release): add release script by @orhun
+
+**Full Changelog**: https://github.com/orhun/git-cliff-readme-example/compare/v1.0.0...v1.0.1
+
+<!-- generated by git-cliff -->
diff --git a/.github/fixtures/test-gitlab-integration-custom-range/cliff.toml b/.github/fixtures/test-gitlab-integration-custom-range/cliff.toml
new file mode 100644
index 00000000..64e297e
--- /dev/null
+++ b/.github/fixtures/test-gitlab-integration-custom-range/cliff.toml
@@ -0,0 +1,54 @@
+# git-cliff ~ configuration file
+# https://git-cliff.org/docs/configuration
+
+# GitLab integration for fetching commit metadata.
+[remote.gitlab]
+owner = "dark0dave"
+repo = "dotfiles"
+
+[changelog]
+# A Tera template to be rendered for each release in the changelog.
+# See https://keats.github.io/tera/docs/#introduction
+body = """
+## What's Changed
+
+{%- if version %} in {{ version }}{%- endif -%}
+{% for commit in commits %}
+  * {{ commit.message | split(pat="\n") | first | trim }}\
+    {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
+    {% if commit.remote.pr_number %} in #{{ commit.remote.pr_number }}{%- endif %}
+{%- endfor -%}
+
+{% if gitlab.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
+  {% raw %}\n{% endraw -%}
+  ### New Contributors
+{%- endif %}\
+{% for contributor in gitlab.contributors | filter(attribute="is_first_time", value=true) %}
+  * @{{ contributor.username }} made their first contribution
+    {%- if contributor.pr_number %} in \
+      [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
+    {%- endif %}
+{%- endfor -%}
+
+{% if version %}
+    {% if previous.version %}
+      **Full Changelog**: https://gitlab.com/{{ remote.gitlab.owner }}/{{ remote.gitlab.repo }}/compare/{{ previous.version }}...{{ version }}
+    {% endif %}
+{% else -%}
+  {% raw %}\n{% endraw %}
+{% endif %}
+"""
+
+[git]
+# Parse commits according to the conventional commits specification.
+# See https://www.conventionalcommits.org
+conventional_commits = false
+# Exclude commits that do not match the conventional commits specification.
+filter_unconventional = true
+# Split commits on newlines, treating each line as an individual commit.
+split_commits = false
+# An array of regex based parsers to modify commit messages prior to further processing.
+commit_preprocessors = [
+  # Remove issue numbers.
+  { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
+]
diff --git a/.github/fixtures/test-gitlab-integration-custom-range/commit.sh b/.github/fixtures/test-gitlab-integration-custom-range/commit.sh
new file mode 100755
index 00000000..8cbedca
--- /dev/null
+++ b/.github/fixtures/test-gitlab-integration-custom-range/commit.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -e
+
+git remote add origin https://gitlab.com/dark0dave/dotfiles
+git fetch
+git checkout 3c048a1b6a32e6d1076581225b06dd41ee5a02ff
diff --git a/.github/fixtures/test-gitlab-integration-custom-range/expected.md b/.github/fixtures/test-gitlab-integration-custom-range/expected.md
new file mode 100644
index 00000000..f6c9a92
--- /dev/null
+++ b/.github/fixtures/test-gitlab-integration-custom-range/expected.md
@@ -0,0 +1,12 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## What's Changed
+* Added curl files by @dark0dave
+* Zsh update by @dark0dave
+* Update README.md by @dark0dave
+* Fix for path by @dark0dave
+* Update README.md by @dark0dave
+
+<!-- generated by git-cliff -->
diff --git a/.github/fixtures/test-submodules-include-path/cliff.toml b/.github/fixtures/test-submodules-include-path/cliff.toml
new file mode 100644
index 00000000..3a26249
--- /dev/null
+++ b/.github/fixtures/test-submodules-include-path/cliff.toml
@@ -0,0 +1,38 @@
+# git-cliff ~ configuration file
+# https://git-cliff.org/docs/configuration
+
+[changelog]
+# A Tera template to be rendered for each release in the changelog.
+# See https://keats.github.io/tera/docs/#introduction
+body = """
+{% if version %}\
+    ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
+{% else %}\
+    ## [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+    ### {{ group | upper_first }}
+    {% for commit in commits %}
+        - {{ commit.message | upper_first }}\
+    {% endfor %}
+{% endfor %}\
+{% for submodule_path, commits in submodule_commits %}
+    ### {{ submodule_path | upper_first }}
+    {% for group, commits in commits | group_by(attribute="group") %}
+        #### {{ group | upper_first }}
+        {% for commit in commits %}
+            - {{ commit.message | upper_first }}\
+        {% endfor %}
+    {% endfor %}
+{% endfor %}\n
+"""
+
+[git]
+# An array of regex based parsers for extracting data from the commit message.
+# Assigns commits to groups.
+# Optionally sets the commit's `scope` and can decide to exclude commits from further processing.
+commit_parsers = [
+    { message = "^feat", group = "Features", default_scope = "app" },
+    { message = "^fix", group = "Bug Fixes", scope = "cli" },
+]
+recurse_submodules = true
diff --git a/.github/fixtures/test-submodules-include-path/commit.sh b/.github/fixtures/test-submodules-include-path/commit.sh
new file mode 100755
index 00000000..05c2031
--- /dev/null
+++ b/.github/fixtures/test-submodules-include-path/commit.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+set -e
+
+current_dir="$(pwd)"
+submodule_one_dir="$(mktemp -d)"
+submodule_two_dir="$(mktemp -d)"
+
+cd $submodule_one_dir && git init >&2
+GIT_COMMITTER_DATE="2022-04-05 01:00:8" git commit --allow-empty -m "feat: submodule_one initial commit"
+
+cd $submodule_two_dir && git init >&2
+GIT_COMMITTER_DATE="2022-04-05 01:00:12" git commit --allow-empty -m "feat: submodule_two feature B"
+
+cd $current_dir
+GIT_COMMITTER_DATE="2022-04-06 01:25:08" git commit --allow-empty -m "Initial commit"
+
+git -c protocol.file.allow=always submodule add $submodule_one_dir submodule_one
+GIT_COMMITTER_DATE="2022-04-06 01:25:09" git commit -a -m "feat: add submodule_one"
+
+cd submodule_one
+GIT_COMMITTER_DATE="2022-04-05 01:00:10" git commit --allow-empty -m "feat: submodule_one feature A"
+GIT_COMMITTER_DATE="2022-04-05 01:00:11" git commit --allow-empty -m "fix: submodule_one fix A"
+cd ..
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:10" git commit -a -m "feat: submodule_one update 1"
+git tag v0.1.0
+
+git -c protocol.file.allow=always submodule add $submodule_two_dir submodule_two
+GIT_COMMITTER_DATE="2022-04-06 01:25:11" git commit -a -m "feat: submodule_two with initial commits"
+
+cd submodule_two
+GIT_COMMITTER_DATE="2022-04-05 01:00:13" git commit --allow-empty -m "fix: submodule_two fix B"
+cd ..
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:12" git commit -a -m "feat: submodule_two update 1"
+git tag v0.2.0
+
+cd submodule_two
+GIT_COMMITTER_DATE="2022-04-05 01:00:14" git commit --allow-empty -m "fix: submodule_two fix C"
+cd ..
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:13" git commit -a -m "feat: submodule_two update 2"
diff --git a/.github/fixtures/test-submodules-include-path/expected.md b/.github/fixtures/test-submodules-include-path/expected.md
new file mode 100644
index 00000000..907942f
--- /dev/null
+++ b/.github/fixtures/test-submodules-include-path/expected.md
@@ -0,0 +1,36 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [unreleased]
+
+### Features
+
+- Submodule_two update 2
+
+### Submodule_two
+
+#### Bug Fixes
+
+- Submodule_two fix C
+
+
+## [0.2.0] - 2022-04-06
+
+### Features
+
+- Submodule_two with initial commits
+- Submodule_two update 1
+
+### Submodule_two
+
+#### Bug Fixes
+
+- Submodule_two fix B
+
+#### Features
+
+- Submodule_two feature B
+
+
+<!-- generated by git-cliff -->
diff --git a/.github/fixtures/test-submodules-range/cliff.toml b/.github/fixtures/test-submodules-range/cliff.toml
new file mode 100644
index 00000000..3a26249
--- /dev/null
+++ b/.github/fixtures/test-submodules-range/cliff.toml
@@ -0,0 +1,38 @@
+# git-cliff ~ configuration file
+# https://git-cliff.org/docs/configuration
+
+[changelog]
+# A Tera template to be rendered for each release in the changelog.
+# See https://keats.github.io/tera/docs/#introduction
+body = """
+{% if version %}\
+    ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
+{% else %}\
+    ## [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+    ### {{ group | upper_first }}
+    {% for commit in commits %}
+        - {{ commit.message | upper_first }}\
+    {% endfor %}
+{% endfor %}\
+{% for submodule_path, commits in submodule_commits %}
+    ### {{ submodule_path | upper_first }}
+    {% for group, commits in commits | group_by(attribute="group") %}
+        #### {{ group | upper_first }}
+        {% for commit in commits %}
+            - {{ commit.message | upper_first }}\
+        {% endfor %}
+    {% endfor %}
+{% endfor %}\n
+"""
+
+[git]
+# An array of regex based parsers for extracting data from the commit message.
+# Assigns commits to groups.
+# Optionally sets the commit's `scope` and can decide to exclude commits from further processing.
+commit_parsers = [
+    { message = "^feat", group = "Features", default_scope = "app" },
+    { message = "^fix", group = "Bug Fixes", scope = "cli" },
+]
+recurse_submodules = true
diff --git a/.github/fixtures/test-submodules-range/commit.sh b/.github/fixtures/test-submodules-range/commit.sh
new file mode 100755
index 00000000..05c2031
--- /dev/null
+++ b/.github/fixtures/test-submodules-range/commit.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+set -e
+
+current_dir="$(pwd)"
+submodule_one_dir="$(mktemp -d)"
+submodule_two_dir="$(mktemp -d)"
+
+cd $submodule_one_dir && git init >&2
+GIT_COMMITTER_DATE="2022-04-05 01:00:8" git commit --allow-empty -m "feat: submodule_one initial commit"
+
+cd $submodule_two_dir && git init >&2
+GIT_COMMITTER_DATE="2022-04-05 01:00:12" git commit --allow-empty -m "feat: submodule_two feature B"
+
+cd $current_dir
+GIT_COMMITTER_DATE="2022-04-06 01:25:08" git commit --allow-empty -m "Initial commit"
+
+git -c protocol.file.allow=always submodule add $submodule_one_dir submodule_one
+GIT_COMMITTER_DATE="2022-04-06 01:25:09" git commit -a -m "feat: add submodule_one"
+
+cd submodule_one
+GIT_COMMITTER_DATE="2022-04-05 01:00:10" git commit --allow-empty -m "feat: submodule_one feature A"
+GIT_COMMITTER_DATE="2022-04-05 01:00:11" git commit --allow-empty -m "fix: submodule_one fix A"
+cd ..
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:10" git commit -a -m "feat: submodule_one update 1"
+git tag v0.1.0
+
+git -c protocol.file.allow=always submodule add $submodule_two_dir submodule_two
+GIT_COMMITTER_DATE="2022-04-06 01:25:11" git commit -a -m "feat: submodule_two with initial commits"
+
+cd submodule_two
+GIT_COMMITTER_DATE="2022-04-05 01:00:13" git commit --allow-empty -m "fix: submodule_two fix B"
+cd ..
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:12" git commit -a -m "feat: submodule_two update 1"
+git tag v0.2.0
+
+cd submodule_two
+GIT_COMMITTER_DATE="2022-04-05 01:00:14" git commit --allow-empty -m "fix: submodule_two fix C"
+cd ..
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:13" git commit -a -m "feat: submodule_two update 2"
diff --git a/.github/fixtures/test-submodules-range/expected.md b/.github/fixtures/test-submodules-range/expected.md
new file mode 100644
index 00000000..907942f
--- /dev/null
+++ b/.github/fixtures/test-submodules-range/expected.md
@@ -0,0 +1,36 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [unreleased]
+
+### Features
+
+- Submodule_two update 2
+
+### Submodule_two
+
+#### Bug Fixes
+
+- Submodule_two fix C
+
+
+## [0.2.0] - 2022-04-06
+
+### Features
+
+- Submodule_two with initial commits
+- Submodule_two update 1
+
+### Submodule_two
+
+#### Bug Fixes
+
+- Submodule_two fix B
+
+#### Features
+
+- Submodule_two feature B
+
+
+<!-- generated by git-cliff -->
diff --git a/.github/fixtures/test-submodules/cliff.toml b/.github/fixtures/test-submodules/cliff.toml
new file mode 100644
index 00000000..3a26249
--- /dev/null
+++ b/.github/fixtures/test-submodules/cliff.toml
@@ -0,0 +1,38 @@
+# git-cliff ~ configuration file
+# https://git-cliff.org/docs/configuration
+
+[changelog]
+# A Tera template to be rendered for each release in the changelog.
+# See https://keats.github.io/tera/docs/#introduction
+body = """
+{% if version %}\
+    ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
+{% else %}\
+    ## [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+    ### {{ group | upper_first }}
+    {% for commit in commits %}
+        - {{ commit.message | upper_first }}\
+    {% endfor %}
+{% endfor %}\
+{% for submodule_path, commits in submodule_commits %}
+    ### {{ submodule_path | upper_first }}
+    {% for group, commits in commits | group_by(attribute="group") %}
+        #### {{ group | upper_first }}
+        {% for commit in commits %}
+            - {{ commit.message | upper_first }}\
+        {% endfor %}
+    {% endfor %}
+{% endfor %}\n
+"""
+
+[git]
+# An array of regex based parsers for extracting data from the commit message.
+# Assigns commits to groups.
+# Optionally sets the commit's `scope` and can decide to exclude commits from further processing.
+commit_parsers = [
+    { message = "^feat", group = "Features", default_scope = "app" },
+    { message = "^fix", group = "Bug Fixes", scope = "cli" },
+]
+recurse_submodules = true
diff --git a/.github/fixtures/test-submodules/commit.sh b/.github/fixtures/test-submodules/commit.sh
new file mode 100755
index 00000000..05c2031
--- /dev/null
+++ b/.github/fixtures/test-submodules/commit.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+set -e
+
+current_dir="$(pwd)"
+submodule_one_dir="$(mktemp -d)"
+submodule_two_dir="$(mktemp -d)"
+
+cd $submodule_one_dir && git init >&2
+GIT_COMMITTER_DATE="2022-04-05 01:00:8" git commit --allow-empty -m "feat: submodule_one initial commit"
+
+cd $submodule_two_dir && git init >&2
+GIT_COMMITTER_DATE="2022-04-05 01:00:12" git commit --allow-empty -m "feat: submodule_two feature B"
+
+cd $current_dir
+GIT_COMMITTER_DATE="2022-04-06 01:25:08" git commit --allow-empty -m "Initial commit"
+
+git -c protocol.file.allow=always submodule add $submodule_one_dir submodule_one
+GIT_COMMITTER_DATE="2022-04-06 01:25:09" git commit -a -m "feat: add submodule_one"
+
+cd submodule_one
+GIT_COMMITTER_DATE="2022-04-05 01:00:10" git commit --allow-empty -m "feat: submodule_one feature A"
+GIT_COMMITTER_DATE="2022-04-05 01:00:11" git commit --allow-empty -m "fix: submodule_one fix A"
+cd ..
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:10" git commit -a -m "feat: submodule_one update 1"
+git tag v0.1.0
+
+git -c protocol.file.allow=always submodule add $submodule_two_dir submodule_two
+GIT_COMMITTER_DATE="2022-04-06 01:25:11" git commit -a -m "feat: submodule_two with initial commits"
+
+cd submodule_two
+GIT_COMMITTER_DATE="2022-04-05 01:00:13" git commit --allow-empty -m "fix: submodule_two fix B"
+cd ..
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:12" git commit -a -m "feat: submodule_two update 1"
+git tag v0.2.0
+
+cd submodule_two
+GIT_COMMITTER_DATE="2022-04-05 01:00:14" git commit --allow-empty -m "fix: submodule_two fix C"
+cd ..
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:13" git commit -a -m "feat: submodule_two update 2"
diff --git a/.github/fixtures/test-submodules/expected.md b/.github/fixtures/test-submodules/expected.md
new file mode 100644
index 00000000..9fb8652
--- /dev/null
+++ b/.github/fixtures/test-submodules/expected.md
@@ -0,0 +1,55 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [unreleased]
+
+### Features
+
+- Submodule_two update 2
+
+### Submodule_two
+
+#### Bug Fixes
+
+- Submodule_two fix C
+
+
+## [0.2.0] - 2022-04-06
+
+### Features
+
+- Submodule_two with initial commits
+- Submodule_two update 1
+
+### Submodule_two
+
+#### Bug Fixes
+
+- Submodule_two fix B
+
+#### Features
+
+- Submodule_two feature B
+
+
+## [0.1.0] - 2022-04-06
+
+### Features
+
+- Add submodule_one
+- Submodule_one update 1
+
+### Submodule_one
+
+#### Bug Fixes
+
+- Submodule_one fix A
+
+#### Features
+
+- Submodule_one feature A
+- Submodule_one initial commit
+
+
+<!-- generated by git-cliff -->
diff --git a/.github/fixtures/test-topo-order-commits/cliff.toml b/.github/fixtures/test-topo-order-commits/cliff.toml
new file mode 100644
index 00000000..b8139b1
--- /dev/null
+++ b/.github/fixtures/test-topo-order-commits/cliff.toml
@@ -0,0 +1,7 @@
+# git-cliff ~ configuration file
+# https://git-cliff.org/docs/configuration
+
+
+[git]
+
+topo_order_commits = false
\ No newline at end of file
diff --git a/.github/fixtures/test-topo-order-commits/commit.sh b/.github/fixtures/test-topo-order-commits/commit.sh
new file mode 100755
index 00000000..f4b6c67
--- /dev/null
+++ b/.github/fixtures/test-topo-order-commits/commit.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+set -e
+
+GIT_COMMITTER_DATE="2022-04-06 03:25:08" git commit --allow-empty -m "Initial commit"
+GIT_COMMITTER_DATE="2022-04-06 03:25:09" git commit --allow-empty -m "feat: add feature 1"
+GIT_COMMITTER_DATE="2022-04-06 03:25:10" git commit --allow-empty -m "fix: fix feature 1"
+git tag v0.1.0
+GIT_COMMITTER_DATE="2022-04-06 03:25:11" git commit --allow-empty -m "feat(gui): add feature 2"
+GIT_COMMITTER_DATE="2022-04-06 03:25:12" git commit --allow-empty -m "fix(gui): fix feature 2"
+git tag v0.2.0
+GIT_COMMITTER_DATE="2022-04-06 03:25:25" git commit --allow-empty -m "test: add tests"
+GIT_COMMITTER_DATE="2022-04-06 03:25:25" git checkout v0.1.0
+GIT_COMMITTER_DATE="2022-04-06 03:25:13" git commit --allow-empty -m "fix: fix again feature 1"
+git tag v0.1.1
+git checkout master
+git merge v0.1.1 --no-edit
diff --git a/.github/fixtures/test-topo-order-commits/expected.md b/.github/fixtures/test-topo-order-commits/expected.md
new file mode 100644
index 00000000..124385f
--- /dev/null
+++ b/.github/fixtures/test-topo-order-commits/expected.md
@@ -0,0 +1,37 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [unreleased]
+
+### 🧪 Testing
+
+- Add tests
+
+## [0.1.1] - 2022-04-06
+
+### 🐛 Bug Fixes
+
+- Fix again feature 1
+
+## [0.2.0] - 2022-04-06
+
+### 🚀 Features
+
+- *(gui)* Add feature 2
+
+### 🐛 Bug Fixes
+
+- *(gui)* Fix feature 2
+
+## [0.1.0] - 2022-04-06
+
+### 🚀 Features
+
+- Add feature 1
+
+### 🐛 Bug Fixes
+
+- Fix feature 1
+
+<!-- generated by git-cliff -->
diff --git a/.github/workflows/test-fixtures.yml b/.github/workflows/test-fixtures.yml
index 567fba3..4ba1ea1 100644
--- a/.github/workflows/test-fixtures.yml
+++ b/.github/workflows/test-fixtures.yml
@@ -18,9 +18,17 @@ jobs:
         include:
           - fixtures-name: new-fixture-template
           - fixtures-name: test-github-integration
+          - fixtures-name: test-github-integration-custom-range
+            command: v1.0.0..v1.0.1
           - fixtures-name: test-gitlab-integration
+          - fixtures-name: test-gitlab-integration-custom-range
+            command: 9f66ac0f76..89de5e8e50
           - fixtures-name: test-gitea-integration
+          - fixtures-name: test-gitea-integration-custom-range
+            command: 5ed596d935..c074c439b5
           - fixtures-name: test-bitbucket-integration
+          - fixtures-name: test-bitbucket-integration-custom-range
+            command: v1.0.0..v1.0.1
           - fixtures-name: test-ignore-tags
           - fixtures-name: test-invert-ignore-tags
           - fixtures-name: test-topo-order
@@ -108,8 +116,18 @@ jobs:
             command: v2.6.1..v2.7.0 --include-path .github/fixtures/
           - fixtures-name: test-require-conventional-negative
           - fixtures-name: test-require-conventional-skipped
+          - fixtures-name: test-submodules
+          - fixtures-name: test-submodules-range
+            command: v0.1.0..HEAD
+          - fixtures-name: test-submodules-include-path
+            command: --include-path submodule_two
           - fixtures-name: test-remote-config
             command: --config-url "https://github.com/orhun/git-cliff/blob/main/.github/fixtures/new-fixture-template/cliff.toml?raw=true"
+          - fixtures-name: test-topo-order-commits
+          - fixtures-name: test-commit-range
+          - fixtures-name: test-commit-range-with-sort-commits
+          - fixtures-name: test-commit-range-with-given-range
+            command: a140cef^..a9d4050 --ignore-tags "."
 
     steps:
       - name: Checkout
diff --git a/git-cliff-core/tests/integration_test.rs b/git-cliff-core/tests/integration_test.rs
index eb9dfee..96e53c0 100644
--- a/git-cliff-core/tests/integration_test.rs
+++ b/git-cliff-core/tests/integration_test.rs
@@ -1,5 +1,6 @@
 use git_cliff_core::commit::{
 	Commit,
+	Range,
 	Signature,
 };
 use git_cliff_core::config::{
@@ -24,6 +25,8 @@ fn generate_changelog() -> Result<()> {
 		body:           String::from(
 			r#"
 ## Release {{ version }} - <DATE>
+
+{{ commit_range.from }}..{{ commit_range.to }}
 {% for group, commits in commits | group_by(attribute="group") %}
 ### {{ group }}
 {% for commit in commits %}
@@ -124,6 +127,7 @@ fn generate_changelog() -> Result<()> {
 		count_tags:               None,
 		use_branch_tags:          false,
 		topo_order:               false,
+		topo_order_commits:       true,
 		sort_commits:             String::from("oldest"),
 		link_parsers:             vec![
 			LinkParser {
@@ -138,6 +142,7 @@ fn generate_changelog() -> Result<()> {
 			},
 		],
 		limit_commits:            None,
+		recurse_submodules:       None,
 	};
 
 	let mut commit_with_author = Commit::new(
@@ -151,13 +156,25 @@ fn generate_changelog() -> Result<()> {
 		timestamp: 0x0,
 	};
 
-	let releases = vec![
-		Release {
-			version:   Some(String::from("v2.0.0")),
-			message: None,
-            extra: None,
-			commits:   vec![
+	let release_v1_commits = vec![
+		Commit::new(
+			String::from("0bc123"),
+			String::from("feat: add cool features"),
+		),
+		Commit::new(String::from("0werty"), String::from("fix: fix stuff")),
+		Commit::new(String::from("0w3rty"), String::from("fix: fix more stuff")),
+		Commit::new(String::from("0jkl12"), String::from("chore: do nothing")),
+	]
+	.into_iter()
+	.filter_map(|c| c.into_conventional().ok())
+	.collect::<Vec<Commit>>();
 
+	let release_v1_commit_range = Range::new(
+		release_v1_commits.first().unwrap(),
+		release_v1_commits.last().unwrap(),
+	);
+
+	let release_v2_commits = vec![
 				Commit::new(
 					String::from("000abc"),
 					String::from("Add unconventional commit"),
@@ -196,11 +213,25 @@ fn generate_changelog() -> Result<()> {
 			]
 			.iter()
 			.filter_map(|c| c.process(&git_config).ok())
-			.collect::<Vec<Commit>>(),
+			.collect::<Vec<Commit>>();
+
+	let release_v2_commit_range = Range::new(
+		release_v2_commits.first().unwrap(),
+		release_v2_commits.last().unwrap(),
+	);
+
+	let releases = vec![
+		Release {
+			version: Some(String::from("v2.0.0")),
+			message: None,
+			extra: None,
+			commits: release_v2_commits,
+			commit_range: Some(release_v2_commit_range),
 			commit_id: None,
 			timestamp: 0,
-			previous:  None,
+			previous: None,
 			repository: Some(String::from("/root/repo")),
+			submodule_commits: HashMap::new(),
 			#[cfg(feature = "github")]
 			github: git_cliff_core::remote::RemoteReleaseMetadata {
 				contributors: vec![],
@@ -219,31 +250,16 @@ fn generate_changelog() -> Result<()> {
 			},
 		},
 		Release {
-			version:   Some(String::from("v1.0.0")),
+			version: Some(String::from("v1.0.0")),
 			message: None,
-            extra: None,
-			commits:   vec![
-				Commit::new(
-					String::from("0bc123"),
-					String::from("feat: add cool features"),
-				),
-				Commit::new(String::from("0werty"), String::from("fix: fix stuff")),
-				Commit::new(
-					String::from("0w3rty"),
-					String::from("fix: fix more stuff"),
-				),
-				Commit::new(
-					String::from("0jkl12"),
-					String::from("chore: do nothing"),
-				),
-			]
-			.into_iter()
-			.filter_map(|c| c.into_conventional().ok())
-			.collect::<Vec<Commit>>(),
+			extra: None,
+			commits: release_v1_commits,
+			commit_range: Some(release_v1_commit_range),
 			commit_id: None,
 			timestamp: 0,
-			previous:  None,
+			previous: None,
 			repository: Some(String::from("/root/repo")),
+			submodule_commits: HashMap::new(),
 			#[cfg(feature = "github")]
 			github: git_cliff_core::remote::RemoteReleaseMetadata {
 				contributors: vec![],
@@ -291,6 +307,8 @@ fn generate_changelog() -> Result<()> {
 
 ## Release v2.0.0 - 2023
 
+abc123..hjdfas32
+
 ### docs
 - *(cool)* testing author filtering
 
@@ -310,6 +328,8 @@ fn generate_changelog() -> Result<()> {
 
 ## Release v1.0.0 - 2023
 
+0bc123..0jkl12
+
 ### chore
 - do nothing