Repository: orhun/git-cliff (language: python)

Issue:
`--from-context` doesn't include footers
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Description of the bug

```
 ERROR git_cliff > Template render error:
Variable `commit.footers` not found in context while rendering 'template'
```

### Steps To Reproduce

You can test with my repository's template at https://github.com/muja/unrar.rs, for example where I use footers.

`git cliff --context` gives `footers`, however

`git cliff --context | git cliff --context --from-context -` has no footers.

### Expected behavior

`footers` are read from context

### Screenshots / Logs

_No response_

### Software information

- Project version: 2.5.0


### Additional context

Possibly other values are also missing? I haven't fully compared the contexts


Discussion / hints:
Good find! This is due to `footers` being serialized manually:

https://github.com/orhun/git-cliff/blob/08e761c20fb98730249aef2047da02a7dfca86ad/git-cliff-core/src/commit.rs#L427

https://github.com/orhun/git-cliff/blob/08e761c20fb98730249aef2047da02a7dfca86ad/git-cliff-core/src/commit.rs#L405-L419

We simply need to add a `Vec<Footer>` field to `Commit` for enabling the deserialization.

I might get to it soon, marking it as good first issue in case someone wants to work on it :)
FYI I compared the diffs between `git cliff --context` and `git cliff --context | git cliff --context --from-context -`. These are the differences:

1. missing fields: `footers`, `body`, `breaking_description`, `breaking`
2. `conventional` flipped from `true` to `false`
Thanks for the comparison! It will definitely come in handy while working on the fix.
@orhun 
Hi, I'm trying to fix this issue. Surely, adding `footers` into `Commit` is an easy fix for this.

But as @muja mentioned above, `body`, `breaking`, and some other fields were missing during reading and writing context. I think a proper fix should address them as well.

Looking at the serialization code, I found that we are trying to convert some fields from `conv` into `Commit`. For example, assigning `message` from `conv` to `Commit`, and since they are not identical, we lose data after converting to `context`.

Maybe we should retain some information so that after writing to `context`, we can read it back without any loss of data.
`message` should be the good one, since we can create `conv` from it. Or we can add another field (e.g `raw_message`) ?
https://github.com/orhun/git-cliff/blob/08e761c20fb98730249aef2047da02a7dfca86ad/git-cliff-core/src/commit.rs#L221-L224

What do you think?


Hey @dqkqd, that sounds reasonable 👍🏼 I'm looking at your PR right now :) Thanks

Fix patch:
diff --git a/git-cliff-core/src/commit.rs b/git-cliff-core/src/commit.rs
index 0f349efa36..31637d391e 100644
--- a/git-cliff-core/src/commit.rs
+++ b/git-cliff-core/src/commit.rs
@@ -28,6 +28,7 @@ use serde::ser::{
 };
 use serde::{
 	Deserialize,
+	Deserializer,
 	Serialize,
 };
 use serde_json::value::Value;
@@ -143,6 +144,14 @@ pub struct Commit<'a> {
 	#[cfg(feature = "bitbucket")]
 	#[deprecated(note = "Use `remote` field instead")]
 	pub bitbucket:     crate::contributor::RemoteContributor,
+
+	/// Raw message of the normal commit, works as a placeholder for converting
+	/// normal commit into conventional commit.
+	///
+	/// Despite the name, it is not actually a raw message.
+	/// In fact, it is pre-processed by [`Commit::preprocess`], and only be
+	/// generated when serializing into `context` the first time.
+	pub raw_message: Option<String>,
 }
 
 impl<'a> From<String> for Commit<'a> {
@@ -191,6 +200,11 @@ impl Commit<'_> {
 		}
 	}
 
+	/// Get raw message for converting into conventional commit.
+	pub fn raw_message(&self) -> &str {
+		self.raw_message.as_deref().unwrap_or(&self.message)
+	}
+
 	/// Processes the commit.
 	///
 	/// * converts commit to a conventional commit
@@ -226,7 +240,7 @@ impl Commit<'_> {
 	/// Returns the commit with its conventional type set.
 	pub fn into_conventional(mut self) -> Result<Self> {
 		match ConventionalCommit::parse(Box::leak(
-			self.message.to_string().into_boxed_str(),
+			self.raw_message().to_string().into_boxed_str(),
 		)) {
 			Ok(conv) => {
 				self.conv = Some(conv);
@@ -423,7 +437,7 @@ impl Serialize for Commit<'_> {
 			}
 		}
 
-		let mut commit = serializer.serialize_struct("Commit", 9)?;
+		let mut commit = serializer.serialize_struct("Commit", 20)?;
 		commit.serialize_field("id", &self.id)?;
 		if let Some(conv) = &self.conv {
 			commit.serialize_field("message", conv.description())?;
@@ -472,10 +486,29 @@ impl Serialize for Commit<'_> {
 		if let Some(remote) = &self.remote {
 			commit.serialize_field("remote", remote)?;
 		}
+		commit.serialize_field("raw_message", &self.raw_message())?;
 		commit.end()
 	}
 }
 
+/// Deserialize commits into conventional commits if they are convertible.
+///
+/// Serialized commits cannot be deserialized into commits that have
+/// [`Commit::conv`]. Thus, we need to manually convert them using
+/// [`Commit::into_conventional`].
+///
+/// This function is to be used only in [`crate::release::Release::commits`].
+pub(crate) fn commits_to_conventional_commits<'de, 'a, D: Deserializer<'de>>(
+	deserializer: D,
+) -> std::result::Result<Vec<Commit<'a>>, D::Error> {
+	let commits = Vec::<Commit<'a>>::deserialize(deserializer)?;
+	let commits = commits
+		.into_iter()
+		.map(|commit| commit.clone().into_conventional().unwrap_or(commit))
+		.collect();
+	Ok(commits)
+}
+
 #[cfg(test)]
 mod test {
 	use super::*;
diff --git a/git-cliff-core/src/release.rs b/git-cliff-core/src/release.rs
index fec293f51f..dbf1d64568 100644
--- a/git-cliff-core/src/release.rs
+++ b/git-cliff-core/src/release.rs
@@ -1,3 +1,4 @@
+use crate::commit::commits_to_conventional_commits;
 use crate::error::Result;
 use crate::{
 	commit::Commit,
@@ -34,6 +35,7 @@ pub struct Release<'a> {
 	/// git tag's message.
 	pub message:    Option<String>,
 	/// Commits made for the release.
+	#[serde(deserialize_with = "commits_to_conventional_commits")]
 	pub commits:    Vec<Commit<'a>>,
 	/// Commit ID of the tag.
 	#[serde(rename = "commit_id")]
diff --git a/website/docs/templating/context.md b/website/docs/templating/context.md
index 8fe863a4ed..a2d53c7931 100644
--- a/website/docs/templating/context.md
+++ b/website/docs/templating/context.md
@@ -57,7 +57,8 @@ following context is generated to use for templating:
         "name": "User Name",
         "email": "user.email@example.com",
         "timestamp": 1660330071
-      }
+      },
+      "raw_message": "<type>[scope]: <description>\n[body]\n[footer(s)]"
     }
   ],
   "commit_id": "a440c6eb26404be4877b7e3ad592bfaa5d4eb210 (release commit)",
@@ -152,7 +153,8 @@ If [`conventional_commits`](/docs/configuration/git#conventional_commits) is set
         "name": "User Name",
         "email": "user.email@example.com",
         "timestamp": 1660330071
-      }
+      },
+      "raw_message": "(full commit message including description, footers, etc.)"
     }
   ],
   "commit_id": "a440c6eb26404be4877b7e3ad592bfaa5d4eb210 (release commit)",


Test patch:
diff --git a/.github/fixtures/test-conventional-commit/cliff.toml b/.github/fixtures/test-conventional-commit/cliff.toml
new file mode 100644
index 0000000000..847a4cc86a
--- /dev/null
+++ b/.github/fixtures/test-conventional-commit/cliff.toml
@@ -0,0 +1,35 @@
+[changelog]
+# template for the changelog footer
+header = """
+# Changelog\n
+All notable changes to this project will be documented in this file.
+"""
+# template for the changelog body
+# https://keats.github.io/tera/docs/#introduction
+body = """
+{% for group, commits in commits | group_by(attribute="group") %}
+    ## {{ group | striptags | trim | upper_first }}
+    {% for commit in commits %}
+          {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
+          {% if commit.breaking %}[**breaking**]: {{ commit.breaking_description }}{% endif %}
+          {{ commit.message }}: {{ commit.body }}\
+          {% for footer in commit.footers %}
+            - {{ footer.token }}{{ footer.separator }} {{ footer.value }}\
+          {% endfor %}
+    {% endfor %}\
+{% endfor %}\n
+"""
+# template for the changelog footer
+footer = """
+<!-- generated by git-cliff -->
+"""
+
+[git]
+# parse the commits based on https://www.conventionalcommits.org
+conventional_commits = true
+# process each line of a commit as an individual commit
+split_commits = false
+commit_parsers = [
+  { message = "^feat", group = "Features", default_scope = "app" },
+  { message = "^fix" },
+]
diff --git a/.github/fixtures/test-conventional-commit/commit.sh b/.github/fixtures/test-conventional-commit/commit.sh
new file mode 100755
index 0000000000..b55c2524a3
--- /dev/null
+++ b/.github/fixtures/test-conventional-commit/commit.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+set -e
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:08" git commit --allow-empty -m "Initial commit"
+GIT_COMMITTER_DATE="2022-04-06 01:25:09" git commit --allow-empty -m \
+    "feat(web): feature 1, breaking change in footer
+
+Body feature 1
+
+BREAKING CHANGE: breaking change description feature 1
+Signed-off-by: user1 <user1@example.com>
+Reviewed-by: user2
+"
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:10" git commit --allow-empty -m \
+    "feat(web)!: feature 2, breaking chain in description
+
+Body feature 2
+
+Signed-off-by: user3 <user3@example.com>
+"
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:11" git commit --allow-empty -m \
+    "feat!: feature 3, use default scope = app
+
+Body feature 2
+
+Signed-off-by: user3 <user3@example.com>
+"
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:12" git commit --allow-empty -m \
+    "fix(scope): fix 1, use scope as group
+
+Body fix 1
+
+Fix: #1
+"
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:13" git commit --allow-empty -m \
+    "fix(front-end): fix 2, no footer
+
+Body fix 2
+"
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:14" git commit --allow-empty -m \
+    "fix(front-end): fix 3 and 4, no body but footer
+
+Fix: #3
+Fix: #4
+"
+
+git tag v0.1.0
diff --git a/.github/fixtures/test-conventional-commit/expected.md b/.github/fixtures/test-conventional-commit/expected.md
new file mode 100644
index 0000000000..aefd3fa0c5
--- /dev/null
+++ b/.github/fixtures/test-conventional-commit/expected.md
@@ -0,0 +1,35 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## Features
+
+*(web)* [**breaking**]: breaking change description feature 1
+feature 1, breaking change in footer: Body feature 1
+- BREAKING CHANGE: breaking change description feature 1
+- Signed-off-by: user1 <user1@example.com>
+- Reviewed-by: user2
+
+*(web)* [**breaking**]: feature 2, breaking chain in description
+feature 2, breaking chain in description: Body feature 2
+- Signed-off-by: user3 <user3@example.com>
+
+*(app)* [**breaking**]: feature 3, use default scope = app
+feature 3, use default scope = app: Body feature 2
+- Signed-off-by: user3 <user3@example.com>
+
+## Fix
+
+*(scope)* 
+fix 1, use scope as group: Body fix 1
+- Fix: #1
+
+*(front-end)* 
+fix 2, no footer: Body fix 2
+
+*(front-end)* 
+fix 3 and 4, no body but footer: 
+- Fix: #3
+- Fix: #4
+
+<!-- generated by git-cliff -->
diff --git a/.github/fixtures/test-from-context-does-not-discard-fields/cliff.toml b/.github/fixtures/test-from-context-does-not-discard-fields/cliff.toml
new file mode 100644
index 0000000000..847a4cc86a
--- /dev/null
+++ b/.github/fixtures/test-from-context-does-not-discard-fields/cliff.toml
@@ -0,0 +1,35 @@
+[changelog]
+# template for the changelog footer
+header = """
+# Changelog\n
+All notable changes to this project will be documented in this file.
+"""
+# template for the changelog body
+# https://keats.github.io/tera/docs/#introduction
+body = """
+{% for group, commits in commits | group_by(attribute="group") %}
+    ## {{ group | striptags | trim | upper_first }}
+    {% for commit in commits %}
+          {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
+          {% if commit.breaking %}[**breaking**]: {{ commit.breaking_description }}{% endif %}
+          {{ commit.message }}: {{ commit.body }}\
+          {% for footer in commit.footers %}
+            - {{ footer.token }}{{ footer.separator }} {{ footer.value }}\
+          {% endfor %}
+    {% endfor %}\
+{% endfor %}\n
+"""
+# template for the changelog footer
+footer = """
+<!-- generated by git-cliff -->
+"""
+
+[git]
+# parse the commits based on https://www.conventionalcommits.org
+conventional_commits = true
+# process each line of a commit as an individual commit
+split_commits = false
+commit_parsers = [
+  { message = "^feat", group = "Features", default_scope = "app" },
+  { message = "^fix" },
+]
diff --git a/.github/fixtures/test-from-context-does-not-discard-fields/commit.sh b/.github/fixtures/test-from-context-does-not-discard-fields/commit.sh
new file mode 100755
index 0000000000..b55c2524a3
--- /dev/null
+++ b/.github/fixtures/test-from-context-does-not-discard-fields/commit.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+set -e
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:08" git commit --allow-empty -m "Initial commit"
+GIT_COMMITTER_DATE="2022-04-06 01:25:09" git commit --allow-empty -m \
+    "feat(web): feature 1, breaking change in footer
+
+Body feature 1
+
+BREAKING CHANGE: breaking change description feature 1
+Signed-off-by: user1 <user1@example.com>
+Reviewed-by: user2
+"
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:10" git commit --allow-empty -m \
+    "feat(web)!: feature 2, breaking chain in description
+
+Body feature 2
+
+Signed-off-by: user3 <user3@example.com>
+"
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:11" git commit --allow-empty -m \
+    "feat!: feature 3, use default scope = app
+
+Body feature 2
+
+Signed-off-by: user3 <user3@example.com>
+"
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:12" git commit --allow-empty -m \
+    "fix(scope): fix 1, use scope as group
+
+Body fix 1
+
+Fix: #1
+"
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:13" git commit --allow-empty -m \
+    "fix(front-end): fix 2, no footer
+
+Body fix 2
+"
+
+GIT_COMMITTER_DATE="2022-04-06 01:25:14" git commit --allow-empty -m \
+    "fix(front-end): fix 3 and 4, no body but footer
+
+Fix: #3
+Fix: #4
+"
+
+git tag v0.1.0
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
new file mode 100644
index 0000000000..e7750b3752
--- /dev/null
+++ b/.github/fixtures/test-from-context-does-not-discard-fields/context.json
@@ -0,0 +1,391 @@
+[
+  {
+    "version": "v0.1.0",
+    "message": null,
+    "commits": [
+      {
+        "id": "ab06a042344dbd1c4af974ef393049b44ad0ae28",
+        "message": "feature 1, breaking change in footer",
+        "body": "Body feature 1",
+        "footers": [
+          {
+            "token": "BREAKING CHANGE",
+            "separator": ":",
+            "value": "breaking change description feature 1",
+            "breaking": true
+          },
+          {
+            "token": "Signed-off-by",
+            "separator": ":",
+            "value": "user1 <user1@example.com>",
+            "breaking": false
+          },
+          {
+            "token": "Reviewed-by",
+            "separator": ":",
+            "value": "user2",
+            "breaking": false
+          }
+        ],
+        "group": "Features",
+        "breaking_description": "breaking change description feature 1",
+        "breaking": true,
+        "scope": "web",
+        "links": [],
+        "author": {
+          "name": "github-actions[bot]",
+          "email": "github-actions[bot]@users.noreply.github.com",
+          "timestamp": 1728938297
+        },
+        "committer": {
+          "name": "github-actions[bot]",
+          "email": "github-actions[bot]@users.noreply.github.com",
+          "timestamp": 1649175909
+        },
+        "conventional": true,
+        "merge_commit": false,
+        "extra": null,
+        "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
+        },
+        "raw_message": "feat(web): feature 1, breaking change in footer\n\nBody feature 1\n\nBREAKING CHANGE: breaking change description feature 1\nSigned-off-by: user1 <user1@example.com>\nReviewed-by: user2"
+      },
+      {
+        "id": "a87599e6ecaf05e80ac4d87edd9a852fd8cad189",
+        "message": "feature 2, breaking chain in description",
+        "body": "Body feature 2",
+        "footers": [
+          {
+            "token": "Signed-off-by",
+            "separator": ":",
+            "value": "user3 <user3@example.com>",
+            "breaking": false
+          }
+        ],
+        "group": "Features",
+        "breaking_description": "feature 2, breaking chain in description",
+        "breaking": true,
+        "scope": "web",
+        "links": [],
+        "author": {
+          "name": "github-actions[bot]",
+          "email": "github-actions[bot]@users.noreply.github.com",
+          "timestamp": 1728938297
+        },
+        "committer": {
+          "name": "github-actions[bot]",
+          "email": "github-actions[bot]@users.noreply.github.com",
+          "timestamp": 1649175910
+        },
+        "conventional": true,
+        "merge_commit": false,
+        "extra": null,
+        "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
+        },
+        "raw_message": "feat(web)!: feature 2, breaking chain in description\n\nBody feature 2\n\nSigned-off-by: user3 <user3@example.com>"
+      },
+      {
+        "id": "9d83e631a1c57eec825f724283041662edd9230b",
+        "message": "feature 3, use default scope = app",
+        "body": "Body feature 2",
+        "footers": [
+          {
+            "token": "Signed-off-by",
+            "separator": ":",
+            "value": "user3 <user3@example.com>",
+            "breaking": false
+          }
+        ],
+        "group": "Features",
+        "breaking_description": "feature 3, use default scope = app",
+        "breaking": true,
+        "scope": "app",
+        "links": [],
+        "author": {
+          "name": "github-actions[bot]",
+          "email": "github-actions[bot]@users.noreply.github.com",
+          "timestamp": 1728938297
+        },
+        "committer": {
+          "name": "github-actions[bot]",
+          "email": "github-actions[bot]@users.noreply.github.com",
+          "timestamp": 1649175911
+        },
+        "conventional": true,
+        "merge_commit": false,
+        "extra": null,
+        "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
+        },
+        "raw_message": "feat!: feature 3, use default scope = app\n\nBody feature 2\n\nSigned-off-by: user3 <user3@example.com>"
+      },
+      {
+        "id": "3bb5b6f741fd40cc71f09d3c6a339e4b5d0843cc",
+        "message": "fix 1, use scope as group",
+        "body": "Body fix 1",
+        "footers": [
+          { "token": "Fix", "separator": ":", "value": "#1", "breaking": false }
+        ],
+        "group": "fix",
+        "breaking_description": null,
+        "breaking": false,
+        "scope": "scope",
+        "links": [],
+        "author": {
+          "name": "github-actions[bot]",
+          "email": "github-actions[bot]@users.noreply.github.com",
+          "timestamp": 1728938297
+        },
+        "committer": {
+          "name": "github-actions[bot]",
+          "email": "github-actions[bot]@users.noreply.github.com",
+          "timestamp": 1649175912
+        },
+        "conventional": true,
+        "merge_commit": false,
+        "extra": null,
+        "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
+        },
+        "raw_message": "fix(scope): fix 1, use scope as group\n\nBody fix 1\n\nFix: #1"
+      },
+      {
+        "id": "856472f0e5b238278fe331dcc151a96982c5bbad",
+        "message": "fix 2, no footer",
+        "body": "Body fix 2",
+        "footers": [],
+        "group": "fix",
+        "breaking_description": null,
+        "breaking": false,
+        "scope": "front-end",
+        "links": [],
+        "author": {
+          "name": "github-actions[bot]",
+          "email": "github-actions[bot]@users.noreply.github.com",
+          "timestamp": 1728938297
+        },
+        "committer": {
+          "name": "github-actions[bot]",
+          "email": "github-actions[bot]@users.noreply.github.com",
+          "timestamp": 1649175913
+        },
+        "conventional": true,
+        "merge_commit": false,
+        "extra": null,
+        "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
+        },
+        "raw_message": "fix(front-end): fix 2, no footer\n\nBody fix 2"
+      },
+      {
+        "id": "6e7a2b8e670a348ccfbae696fd850d8769c54360",
+        "message": "fix 3 and 4, no body but footer",
+        "body": null,
+        "footers": [
+          {
+            "token": "Fix",
+            "separator": ":",
+            "value": "#3",
+            "breaking": false
+          },
+          { "token": "Fix", "separator": ":", "value": "#4", "breaking": false }
+        ],
+        "group": "fix",
+        "breaking_description": null,
+        "breaking": false,
+        "scope": "front-end",
+        "links": [],
+        "author": {
+          "name": "github-actions[bot]",
+          "email": "github-actions[bot]@users.noreply.github.com",
+          "timestamp": 1728938297
+        },
+        "committer": {
+          "name": "github-actions[bot]",
+          "email": "github-actions[bot]@users.noreply.github.com",
+          "timestamp": 1649175914
+        },
+        "conventional": true,
+        "merge_commit": false,
+        "extra": null,
+        "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
+        },
+        "raw_message": "fix(front-end): fix 3 and 4, no body but footer\n\nFix: #3\nFix: #4"
+      }
+    ],
+    "commit_id": "6e7a2b8e670a348ccfbae696fd850d8769c54360",
+    "timestamp": 1649175914,
+    "previous": {
+      "version": null,
+      "message": null,
+      "commits": [],
+      "commit_id": null,
+      "timestamp": 0,
+      "previous": null,
+      "repository": null,
+      "extra": null,
+      "github": { "contributors": [] },
+      "gitlab": { "contributors": [] },
+      "gitea": { "contributors": [] },
+      "bitbucket": { "contributors": [] }
+    },
+    "repository": "/path/to/repository",
+    "extra": null,
+    "github": { "contributors": [] },
+    "gitlab": { "contributors": [] },
+    "gitea": { "contributors": [] },
+    "bitbucket": { "contributors": [] }
+  }
+]
diff --git a/.github/fixtures/test-from-context-does-not-discard-fields/expected.md b/.github/fixtures/test-from-context-does-not-discard-fields/expected.md
new file mode 100644
index 0000000000..aefd3fa0c5
--- /dev/null
+++ b/.github/fixtures/test-from-context-does-not-discard-fields/expected.md
@@ -0,0 +1,35 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## Features
+
+*(web)* [**breaking**]: breaking change description feature 1
+feature 1, breaking change in footer: Body feature 1
+- BREAKING CHANGE: breaking change description feature 1
+- Signed-off-by: user1 <user1@example.com>
+- Reviewed-by: user2
+
+*(web)* [**breaking**]: feature 2, breaking chain in description
+feature 2, breaking chain in description: Body feature 2
+- Signed-off-by: user3 <user3@example.com>
+
+*(app)* [**breaking**]: feature 3, use default scope = app
+feature 3, use default scope = app: Body feature 2
+- Signed-off-by: user3 <user3@example.com>
+
+## Fix
+
+*(scope)* 
+fix 1, use scope as group: Body fix 1
+- Fix: #1
+
+*(front-end)* 
+fix 2, no footer: Body fix 2
+
+*(front-end)* 
+fix 3 and 4, no body but footer: 
+- Fix: #3
+- Fix: #4
+
+<!-- generated by git-cliff -->
diff --git a/.github/workflows/test-fixtures.yml b/.github/workflows/test-fixtures.yml
index 6512af6280..09e7d138d8 100644
--- a/.github/workflows/test-fixtures.yml
+++ b/.github/workflows/test-fixtures.yml
@@ -33,6 +33,7 @@ jobs:
             command: --latest
           - fixtures-name: test-commit-footers
           - fixtures-name: test-commit-preprocessors
+          - fixtures-name: test-conventional-commit
           - fixtures-name: test-custom-scope
           - fixtures-name: test-limit-commits
           - fixtures-name: test-skip-breaking-changes
@@ -93,6 +94,8 @@ jobs:
             command: --bump --unreleased --with-tag-message "Some text"
           - fixtures-name: test-from-context
             command: --from-context context.json
+          - fixtures-name: test-from-context-does-not-discard-fields
+            command: --from-context context.json
           - fixtures-name: test-always-render-unreleased
             command: --unreleased
           - fixtures-name: test-always-render