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

Issue:
Support generating changelog from a JSON context
### Is there an existing issue or pull request for this?

- [X] I have searched the existing issues and pull requests

### Feature description

`git-cliff` currently supports printing out the context via `-x` argument:

```sh
$ git cliff -x | jq . | head -n 5

[
  {
    "version": null,
    "commits": [
      {
```

It would be nice to generate a changelog from this JSON context as well.

### Desired solution

A new command-line argument might be introduced or we can take the context from `stdin`:

```sh
$ git-cliff -x > context.json

$ git-cliff --from-context context.json -o CHANGELOG.md

$ cat context.json | git-cliff
```

### Alternatives considered

None.

### Additional context

https://git-cliff.org/docs/templating/context

closes #773 


Discussion / hints:

Fix patch:
diff --git a/git-cliff-core/src/changelog.rs b/git-cliff-core/src/changelog.rs
index 2961872438..3a42697dcc 100644
--- a/git-cliff-core/src/changelog.rs
+++ b/git-cliff-core/src/changelog.rs
@@ -18,7 +18,10 @@ use crate::remote::github::GitHubClient;
 use crate::remote::gitlab::GitLabClient;
 use crate::template::Template;
 use std::collections::HashMap;
-use std::io::Write;
+use std::io::{
+	Read,
+	Write,
+};
 use std::time::{
 	SystemTime,
 	UNIX_EPOCH,
@@ -39,8 +42,17 @@ pub struct Changelog<'a> {
 impl<'a> Changelog<'a> {
 	/// Constructs a new instance.
 	pub fn new(releases: Vec<Release<'a>>, config: &'a Config) -> Result<Self> {
+		let mut changelog = Changelog::build(releases, config)?;
+		changelog.add_remote_data()?;
+		changelog.process_commits();
+		changelog.process_releases();
+		Ok(changelog)
+	}
+
+	/// Builds a changelog from releases and config.
+	fn build(releases: Vec<Release<'a>>, config: &'a Config) -> Result<Self> {
 		let trim = config.changelog.trim.unwrap_or(true);
-		let mut changelog = Self {
+		Ok(Self {
 			releases,
 			header_template: match &config.changelog.header {
 				Some(header) => Some(Template::new(header.to_string(), trim)?),
@@ -61,11 +73,12 @@ impl<'a> Changelog<'a> {
 			},
 			config,
 			additional_context: HashMap::new(),
-		};
-		changelog.add_remote_data()?;
-		changelog.process_commits();
-		changelog.process_releases();
-		Ok(changelog)
+		})
+	}
+
+	/// Constructs an instance from a serialized context object.
+	pub fn from_context<R: Read>(input: &mut R, config: &'a Config) -> Result<Self> {
+		Changelog::build(serde_json::from_reader(input)?, config)
 	}
 
 	/// Adds a key value pair to the template context.
@@ -821,6 +834,7 @@ mod test {
 		let test_release = Release {
 			version: Some(String::from("v1.0.0")),
 			message: None,
+			extra: None,
 			commits: vec![
 				Commit::new(
 					String::from("coffee"),
@@ -917,6 +931,7 @@ mod test {
 			Release {
 				version: None,
 				message: None,
+				extra: None,
 				commits: vec![
 					Commit::new(
 						String::from("abc123"),
diff --git a/git-cliff-core/src/commit.rs b/git-cliff-core/src/commit.rs
index 047477250a..ba5139695d 100644
--- a/git-cliff-core/src/commit.rs
+++ b/git-cliff-core/src/commit.rs
@@ -30,6 +30,7 @@ use serde::{
 	Deserialize,
 	Serialize,
 };
+use serde_json::value::Value;
 
 /// Regular expression for matching SHA1 and a following commit message
 /// separated by a whitespace.
@@ -37,7 +38,7 @@ static SHA1_REGEX: Lazy<Regex> = lazy_regex!(r#"^\b([a-f0-9]{40})\b (.*)$"#);
 
 /// Object representing a link
 #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
+#[serde(rename_all(serialize = "camelCase"))]
 pub struct Link {
 	/// Text of the link.
 	pub text: String,
@@ -98,7 +99,7 @@ impl<'a> From<CommitSignature<'a>> for Signature {
 
 /// Common commit object that is parsed from a repository.
 #[derive(Debug, Default, Clone, PartialEq, Deserialize)]
-#[serde(rename_all = "camelCase")]
+#[serde(rename_all(serialize = "camelCase"))]
 pub struct Commit<'a> {
 	/// Commit ID.
 	pub id:            String,
@@ -122,6 +123,8 @@ pub struct Commit<'a> {
 	pub committer:     Signature,
 	/// Whether if the commit has two or more parents.
 	pub merge_commit:  bool,
+	/// Arbitrary data to be used with the `--from-context` CLI option.
+	pub extra:         Option<Value>,
 	/// GitHub metadata of the commit.
 	#[cfg(feature = "github")]
 	pub github:        crate::remote::RemoteContributor,
@@ -454,6 +457,7 @@ impl Serialize for Commit<'_> {
 		commit.serialize_field("committer", &self.committer)?;
 		commit.serialize_field("conventional", &self.conv.is_some())?;
 		commit.serialize_field("merge_commit", &self.merge_commit)?;
+		commit.serialize_field("extra", &self.extra)?;
 		#[cfg(feature = "github")]
 		commit.serialize_field("github", &self.github)?;
 		#[cfg(feature = "gitlab")]
diff --git a/git-cliff-core/src/release.rs b/git-cliff-core/src/release.rs
index 2e681bf51b..56fb8967a5 100644
--- a/git-cliff-core/src/release.rs
+++ b/git-cliff-core/src/release.rs
@@ -21,10 +21,11 @@ use serde::{
 	Deserialize,
 	Serialize,
 };
+use serde_json::value::Value;
 
 /// Representation of a release.
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
+#[serde(rename_all(serialize = "camelCase"))]
 pub struct Release<'a> {
 	/// Release version, git tag.
 	pub version:    Option<String>,
@@ -41,6 +42,8 @@ pub struct Release<'a> {
 	pub previous:   Option<Box<Release<'a>>>,
 	/// Repository path.
 	pub repository: Option<String>,
+	/// Arbitrary data to be used with the `--from-context` CLI option.
+	pub extra:      Option<Value>,
 	/// Contributors.
 	#[cfg(feature = "github")]
 	pub github:     RemoteReleaseMetadata,
@@ -194,6 +197,7 @@ mod test {
 			Release {
 				version: None,
 				message: None,
+				extra: None,
 				commits: commits
 					.iter()
 					.map(|v| Commit::from(v.to_string()))
@@ -389,6 +393,7 @@ mod test {
 		let mut release = Release {
 			version: None,
 			message: None,
+			extra: None,
 			commits: vec![
 				Commit::from(String::from(
 					"1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github \
@@ -676,6 +681,7 @@ mod test {
 		let mut release = Release {
 			version: None,
 			message: None,
+			extra: None,
 			commits: vec![
 				Commit::from(String::from(
 					"1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github \
@@ -1021,6 +1027,7 @@ mod test {
 		let mut release = Release {
 			version: None,
 			message: None,
+			extra: None,
 			commits: vec![
 				Commit::from(String::from(
 					"1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github \
diff --git a/git-cliff-core/src/template.rs b/git-cliff-core/src/template.rs
index 6d2e611805..d9ced84abf 100644
--- a/git-cliff-core/src/template.rs
+++ b/git-cliff-core/src/template.rs
@@ -190,6 +190,7 @@ mod test {
 		Release {
 			version: Some(String::from("1.0")),
 			message: None,
+			extra: None,
 			commits: vec![
 				Commit::new(
 					String::from("123123"),
diff --git a/git-cliff/src/args.rs b/git-cliff/src/args.rs
index 444af6d3e8..1b8beb6a30 100644
--- a/git-cliff/src/args.rs
+++ b/git-cliff/src/args.rs
@@ -234,6 +234,14 @@ pub struct Opt {
 	/// Prints changelog context as JSON.
 	#[arg(short = 'x', long, help_heading = Some("FLAGS"))]
 	pub context:          bool,
+	/// Generates changelog from a JSON context.
+	#[arg(
+        long,
+	    value_name = "PATH",
+	    value_parser = Opt::parse_dir,
+		env = "GIT_CLIFF_CONTEXT",
+    )]
+	pub from_context:     Option<PathBuf>,
 	/// Strips the given parts from the changelog.
 	#[arg(short, long, value_name = "PART", value_enum)]
 	pub strip:            Option<Strip>,
diff --git a/git-cliff/src/lib.rs b/git-cliff/src/lib.rs
index ed88cce637..cfe6cff1ba 100644
--- a/git-cliff/src/lib.rs
+++ b/git-cliff/src/lib.rs
@@ -521,49 +521,63 @@ pub fn run(mut args: Opt) -> Result<()> {
 	if args.count_tags.is_some() {
 		config.git.count_tags.clone_from(&args.count_tags);
 	}
-	// Process the repositories.
-	let repositories = args.repository.clone().unwrap_or(vec![env::current_dir()?]);
-	let mut releases = Vec::<Release>::new();
-	for repository in repositories {
-		// Skip commits
-		let mut skip_list = Vec::new();
-		let ignore_file = repository.join(IGNORE_FILE);
-		if ignore_file.exists() {
-			let contents = fs::read_to_string(ignore_file)?;
-			let commits = contents
-				.lines()
-				.filter(|v| !(v.starts_with('#') || v.trim().is_empty()))
-				.map(|v| String::from(v.trim()))
-				.collect::<Vec<String>>();
-			skip_list.extend(commits);
-		}
-		if let Some(ref skip_commit) = args.skip_commit {
-			skip_list.extend(skip_commit.clone());
-		}
-		if let Some(commit_parsers) = config.git.commit_parsers.as_mut() {
-			for sha1 in skip_list {
-				commit_parsers.insert(0, CommitParser {
-					sha: Some(sha1.to_string()),
-					skip: Some(true),
-					..Default::default()
-				})
-			}
-		}
-
-		// Process the repository.
-		let repository = Repository::init(repository)?;
-		releases.extend(process_repository(
-			Box::leak(Box::new(repository)),
-			&mut config,
-			&args,
-		)?);
-	}
 
 	// Process commits and releases for the changelog.
 	if let Some(BumpOption::Specific(bump_type)) = args.bump {
 		config.bump.bump_type = Some(bump_type)
 	}
-	let mut changelog = Changelog::new(releases, &config)?;
+
+	// Generate changelog from context.
+	let mut changelog: Changelog = if let Some(context_path) = args.from_context {
+		let mut input: Box<dyn io::Read> = if context_path == Path::new("-") {
+			Box::new(io::stdin())
+		} else {
+			Box::new(File::open(context_path)?)
+		};
+		let mut changelog = Changelog::from_context(&mut input, &config)?;
+		changelog.add_remote_data()?;
+		changelog
+	} else {
+		// Process the repositories.
+		let repositories =
+			args.repository.clone().unwrap_or(vec![env::current_dir()?]);
+		let mut releases = Vec::<Release>::new();
+		for repository in repositories {
+			// Skip commits
+			let mut skip_list = Vec::new();
+			let ignore_file = repository.join(IGNORE_FILE);
+			if ignore_file.exists() {
+				let contents = fs::read_to_string(ignore_file)?;
+				let commits = contents
+					.lines()
+					.filter(|v| !(v.starts_with('#') || v.trim().is_empty()))
+					.map(|v| String::from(v.trim()))
+					.collect::<Vec<String>>();
+				skip_list.extend(commits);
+			}
+			if let Some(ref skip_commit) = args.skip_commit {
+				skip_list.extend(skip_commit.clone());
+			}
+			if let Some(commit_parsers) = config.git.commit_parsers.as_mut() {
+				for sha1 in skip_list {
+					commit_parsers.insert(0, CommitParser {
+						sha: Some(sha1.to_string()),
+						skip: Some(true),
+						..Default::default()
+					})
+				}
+			}
+
+			// Process the repository.
+			let repository = Repository::init(repository)?;
+			releases.extend(process_repository(
+				Box::leak(Box::new(repository)),
+				&mut config,
+				&args,
+			)?);
+		}
+		Changelog::new(releases, &config)?
+	};
 
 	// Print the result.
 	let mut out: Box<dyn io::Write> = if let Some(path) = &args.output {
diff --git a/website/docs/usage/args.md b/website/docs/usage/args.md
index 9c500952a7..6b3f6570a4 100644
--- a/website/docs/usage/args.md
+++ b/website/docs/usage/args.md
@@ -43,6 +43,7 @@ git-cliff [FLAGS] [OPTIONS] [--] [RANGE]
 -o, --output [<PATH>]              Writes output to the given file [env: GIT_CLIFF_OUTPUT=]
 -t, --tag <TAG>                    Sets the tag for the latest version [env: GIT_CLIFF_TAG=]
 -b, --body <TEMPLATE>              Sets the template for the changelog body [env: GIT_CLIFF_TEMPLATE=]
+    --from-context <PATH>          Generates changelog from a JSON context [env: GIT_CLIFF_CONTEXT=]
 -s, --strip <PART>                 Strips the given parts from the changelog [possible values: header, footer, all]
     --sort <SORT>                  Sets sorting of the commits inside sections [default: oldest] [possible values: oldest, newest]
     --github-token <TOKEN>         Sets the GitHub API token [env: GITHUB_TOKEN]
diff --git a/website/docs/usage/load-context.md b/website/docs/usage/load-context.md
new file mode 100644
index 0000000000..fd4a5a8cd1
--- /dev/null
+++ b/website/docs/usage/load-context.md
@@ -0,0 +1,36 @@
+---
+sidebar_position: 11
+---
+
+# Load context
+
+:::tip
+
+This is useful if you want to [print context](/docs/usage/print-context), modify it with an external tool and then "pipe" it back into **git-cliff**.
+
+:::
+
+To load a context from a file and output the generated changelog:
+
+```bash
+# create a context
+git cliff --context -o context.json
+
+# process context and generate a changelog
+git cliff --from-context context.json
+
+# process context loaded from stdin
+git cliff --from-context -
+```
+
+Free-form metadata can be added to release objects and commit objects in the context using the `extra` field:
+
+```json
+{
+  "id": "5061081d6272b1da2146fab49d803c193db309d9",
+  "message": "commit message",
+  "extra": {
+    "note": "this is some arbitrary data"
+  }
+}
+```


Test patch:
diff --git a/.github/fixtures/test-from-context/cliff.toml b/.github/fixtures/test-from-context/cliff.toml
new file mode 100644
index 0000000000..c533b79632
--- /dev/null
+++ b/.github/fixtures/test-from-context/cliff.toml
@@ -0,0 +1,34 @@
+[changelog]
+# template for the changelog footer
+header = """
+# Changelog\n
+All notable changes to this project will be documented in this file.\n
+"""
+# template for the changelog body
+# https://keats.github.io/tera/docs/#introduction
+body = """
+{% if version %}\
+    ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}{% if extra.note %} - {{ extra.note }}{% endif %}
+{% else %}\
+    ## [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+    ### {{ group | upper_first }}
+    {% for commit in commits %}
+        - {{ commit.message | upper_first }}{% if commit.extra.note %} ({{ commit.extra.note }}){% endif %}\
+    {% endfor %}
+{% endfor %}\n
+"""
+# template for the changelog footer
+footer = """
+<!-- generated by git-cliff -->
+"""
+# remove the leading and trailing whitespace from the templates
+trim = true
+
+[git]
+# regex for parsing and grouping commits
+commit_parsers = [
+    { message = "^feat", group = "Features", default_scope = "app" },
+    { message = "^fix", group = "Bug Fixes", scope = "cli" },
+]
diff --git a/.github/fixtures/test-from-context/commit.sh b/.github/fixtures/test-from-context/commit.sh
new file mode 100755
index 0000000000..86dc2c57eb
--- /dev/null
+++ b/.github/fixtures/test-from-context/commit.sh
@@ -0,0 +1,11 @@
+#!/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: add feature 1"
+GIT_COMMITTER_DATE="2022-04-06 01:25:10" git commit --allow-empty -m "fix: fix feature 1"
+git tag v0.1.0
+GIT_COMMITTER_DATE="2022-04-06 01:25:11" git commit --allow-empty -m "feat(gui): add feature 2"
+GIT_COMMITTER_DATE="2022-04-06 01:25:12" git commit --allow-empty -m "fix(gui): fix feature 2 SHOULD NOT BE IN THE CHANGELOG"
+git tag v0.2.0
+GIT_COMMITTER_DATE="2022-04-06 01:25:13" git commit --allow-empty -m "test: add tests"
diff --git a/.github/fixtures/test-from-context/context.json b/.github/fixtures/test-from-context/context.json
new file mode 100644
index 0000000000..5dee19a9f8
--- /dev/null
+++ b/.github/fixtures/test-from-context/context.json
@@ -0,0 +1,626 @@
+[
+    {
+        "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": []
+            }
+        },
+        "repository": "/home/johndoe/repo/",
+        "github": {
+            "contributors": []
+        },
+        "gitlab": {
+            "contributors": []
+        },
+        "gitea": {
+            "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": "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": []
+            }
+        },
+        "extra": {
+            "note": "This is so awesome!"
+        },
+        "repository": "/home/johndoe/repo/",
+        "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": []
+            }
+        },
+        "repository": "/home/johndoe/repo/",
+        "github": {
+            "contributors": []
+        },
+        "gitlab": {
+            "contributors": []
+        },
+        "gitea": {
+            "contributors": []
+        },
+        "bitbucket": {
+            "contributors": []
+        }
+    }
+]
diff --git a/.github/fixtures/test-from-context/expected.md b/.github/fixtures/test-from-context/expected.md
new file mode 100644
index 0000000000..5e0ff03ca1
--- /dev/null
+++ b/.github/fixtures/test-from-context/expected.md
@@ -0,0 +1,31 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [unreleased]
+
+### Test
+
+- Add tests
+
+## [0.2.0] - 2022-04-05 - This is so awesome!
+
+### Bug Fixes
+
+- Fix feature 2 (that was a tough one)
+
+### Features
+
+- Add feature 2
+
+## [0.1.0] - 2022-04-05
+
+### Bug Fixes
+
+- Fix feature 1
+
+### Features
+
+- Add feature 1
+
+<!-- generated by git-cliff -->
diff --git a/.github/workflows/test-fixtures.yml b/.github/workflows/test-fixtures.yml
index ba9daa2f4a..69b74e2f9a 100644
--- a/.github/workflows/test-fixtures.yml
+++ b/.github/workflows/test-fixtures.yml
@@ -91,6 +91,8 @@ jobs:
           - fixtures-name: test-tag-message
           - fixtures-name: test-bump-unreleased-with-tag-message-arg
             command: --bump --unreleased --with-tag-message "Some text"
+          - fixtures-name: test-from-context
+            command: --from-context context.json
 
     steps:
       - name: Checkout
diff --git a/git-cliff-core/tests/integration_test.rs b/git-cliff-core/tests/integration_test.rs
index 355dd11526..58141a93b3 100644
--- a/git-cliff-core/tests/integration_test.rs
+++ b/git-cliff-core/tests/integration_test.rs
@@ -151,6 +151,7 @@ fn generate_changelog() -> Result<()> {
 		Release {
 			version:   Some(String::from("v2.0.0")),
 			message: None,
+            extra: None,
 			commits:   vec![
 
 				Commit::new(
@@ -216,6 +217,7 @@ fn generate_changelog() -> Result<()> {
 		Release {
 			version:   Some(String::from("v1.0.0")),
 			message: None,
+            extra: None,
 			commits:   vec![
 				Commit::new(
 					String::from("0bc123"),