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

Issue:
Support a body template specifically for when there are no contributing commits
### Is there an existing issue or pull request for this?

- [X] I have searched the existing issues and pull requests

### Feature description

In the case where no commits contribute to the new version, which is fairly likely with `--include-path` and monorepos, the `body` template is apparently not evaluated at all. Then when the changelog is modified (for example with `--prepend …`) no changes occur, and then there is nothing to stage, which `git commit` doesn't like and will exit with a non-zero status.

### Desired solution

A template specifically for the case where there are in fact no commits contributing, then it would be possible to still output a changelog for this, which would a) record the new version's existence and b) be explicit about the fact that there are no changes.

### Alternatives considered

#### Alternative 1: `git commit --allow-empty`

This is the workaround we opted for, but unfortunately it means the version isn't recorded at all in the changelog.

#### Alternative 2: Reuse the existing `body` template

Requiring everyone to cater for branching on zero commits is breaking the current contract for the `body` template, and seems slightly annoying to write the template for.

### Additional context

I'm basing my assumption that `body` isn't run at all by placing some literal text at the very top of `body` and noting that it does not appear in the changelog until I remove filters/include paths to eventually include at least one commit.


Discussion / hints:
Thanks for opening your first issue at **git-cliff**! Be sure to follow the issue template! ⛰️


Fix patch:
diff --git a/config/cliff.toml b/config/cliff.toml
index fc5def181c..3d562041ba 100644
--- a/config/cliff.toml
+++ b/config/cliff.toml
@@ -38,6 +38,8 @@ trim = true
 postprocessors = [
   # { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
 ]
+# render body even when there are no releases to process
+# render_always = true
 # output file path
 # output = "test.md"
 
diff --git a/git-cliff-core/src/changelog.rs b/git-cliff-core/src/changelog.rs
index 6ff289242e..9649bf8767 100644
--- a/git-cliff-core/src/changelog.rs
+++ b/git-cliff-core/src/changelog.rs
@@ -148,7 +148,7 @@ impl<'a> Changelog<'a> {
 
 	/// Processes the releases and filters them out based on the configuration.
 	fn process_releases(&mut self) {
-		debug!("Processing the releases...");
+		debug!("Processing {} release(s)...", self.releases.len());
 		let skip_regex = self.config.git.skip_tags.as_ref();
 		let mut skipped_tags = Vec::new();
 		self.releases = self
@@ -161,7 +161,12 @@ impl<'a> Changelog<'a> {
 					if let Some(version) = release.version.clone() {
 						trace!("Release doesn't have any commits: {}", version);
 					}
-					false
+					match &release.previous {
+						Some(prev_release) if prev_release.commits.is_empty() => {
+							self.config.changelog.render_always.unwrap_or(false)
+						}
+						_ => false,
+					}
 				} else if let Some(version) = &release.version {
 					!skip_regex.is_some_and(|r| {
 						let skip_tag = r.is_match(version);
@@ -670,6 +675,7 @@ mod test {
 					replace:         Some(String::from("exciting")),
 					replace_command: None,
 				}]),
+				render_always:  None,
 				output:         None,
 			},
 			git:       GitConfig {
diff --git a/git-cliff-core/src/config.rs b/git-cliff-core/src/config.rs
index 3afcc2b99e..fd0000d9c2 100644
--- a/git-cliff-core/src/config.rs
+++ b/git-cliff-core/src/config.rs
@@ -77,6 +77,8 @@ pub struct ChangelogConfig {
 	pub footer:         Option<String>,
 	/// Trim the template.
 	pub trim:           Option<bool>,
+	/// Always render the body template.
+	pub render_always:  Option<bool>,
 	/// Changelog postprocessors.
 	pub postprocessors: Option<Vec<TextProcessor>>,
 	/// Output file path.
diff --git a/git-cliff/src/lib.rs b/git-cliff/src/lib.rs
index c71007dfa2..73715c55ac 100644
--- a/git-cliff/src/lib.rs
+++ b/git-cliff/src/lib.rs
@@ -235,6 +235,7 @@ fn process_repository<'a>(
 	}
 
 	// Update tags.
+	let mut releases = vec![Release::default()];
 	let mut tag_timestamp = None;
 	if let Some(ref tag) = args.tag {
 		if let Some(commit_id) = commits.first().map(|c| c.id().to_string()) {
@@ -247,11 +248,16 @@ fn process_repository<'a>(
 					tags.insert(commit_id, repository.resolve_tag(tag));
 				}
 			}
+		} else {
+			releases[0].version = Some(tag.to_string());
+			releases[0].timestamp = SystemTime::now()
+				.duration_since(UNIX_EPOCH)?
+				.as_secs()
+				.try_into()?;
 		}
 	}
 
 	// Process releases.
-	let mut releases = vec![Release::default()];
 	let mut previous_release = Release::default();
 	let mut first_processed_tag = None;
 	for git_commit in commits.iter().rev() {
diff --git a/website/docs/configuration/changelog.md b/website/docs/configuration/changelog.md
index 8636eafbfd..45f9cb7e8b 100644
--- a/website/docs/configuration/changelog.md
+++ b/website/docs/configuration/changelog.md
@@ -50,6 +50,10 @@ If set to `true`, leading and trailing whitespace are removed from the [`body`](
 
 It is useful for adding indentation to the template for readability, as shown [in the example](#changelog).
 
+### render_always
+
+If set to `true`, the changelog [body](#body) will be rendered even if there are no releases to process.
+
 ### postprocessors
 
 An array of commit postprocessors for manipulating the changelog before outputting.


Test patch:
diff --git a/.github/fixtures/test-always-render-unreleased/cliff.toml b/.github/fixtures/test-always-render-unreleased/cliff.toml
new file mode 100644
index 0000000000..abcdd994ec
--- /dev/null
+++ b/.github/fixtures/test-always-render-unreleased/cliff.toml
@@ -0,0 +1,33 @@
+[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") }}
+{% else %}
+    ## [unreleased]
+{% endif %}
+
+{% if commits | length == 0 -%}
+No significant changes.
+
+{% else -%}
+{% for group, commits in commits | group_by(attribute="group") -%}
+    ### {{ group | upper_first }}
+    etc.
+{% endfor %}
+{%- endif %}
+"""
+# template for the changelog footer
+footer = """
+<!-- generated by git-cliff -->
+"""
+# remove the leading and trailing whitespace from the templates
+trim = true
+# render body even when there are no releases to process
+render_always = true
diff --git a/.github/fixtures/test-always-render-unreleased/commit.sh b/.github/fixtures/test-always-render-unreleased/commit.sh
new file mode 100755
index 0000000000..3dc1a2fc3f
--- /dev/null
+++ b/.github/fixtures/test-always-render-unreleased/commit.sh
@@ -0,0 +1,7 @@
+#!/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
diff --git a/.github/fixtures/test-always-render-unreleased/expected.md b/.github/fixtures/test-always-render-unreleased/expected.md
new file mode 100644
index 0000000000..e482232a72
--- /dev/null
+++ b/.github/fixtures/test-always-render-unreleased/expected.md
@@ -0,0 +1,11 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+
+## [unreleased]
+
+
+No significant changes.
+
+<!-- generated by git-cliff -->
diff --git a/.github/fixtures/test-always-render/cliff.toml b/.github/fixtures/test-always-render/cliff.toml
new file mode 100644
index 0000000000..abcdd994ec
--- /dev/null
+++ b/.github/fixtures/test-always-render/cliff.toml
@@ -0,0 +1,33 @@
+[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") }}
+{% else %}
+    ## [unreleased]
+{% endif %}
+
+{% if commits | length == 0 -%}
+No significant changes.
+
+{% else -%}
+{% for group, commits in commits | group_by(attribute="group") -%}
+    ### {{ group | upper_first }}
+    etc.
+{% endfor %}
+{%- endif %}
+"""
+# template for the changelog footer
+footer = """
+<!-- generated by git-cliff -->
+"""
+# remove the leading and trailing whitespace from the templates
+trim = true
+# render body even when there are no releases to process
+render_always = true
diff --git a/.github/fixtures/test-always-render/commit.sh b/.github/fixtures/test-always-render/commit.sh
new file mode 100755
index 0000000000..3dc1a2fc3f
--- /dev/null
+++ b/.github/fixtures/test-always-render/commit.sh
@@ -0,0 +1,7 @@
+#!/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
diff --git a/.github/fixtures/test-always-render/expected.md b/.github/fixtures/test-always-render/expected.md
new file mode 100644
index 0000000000..6ba57d2824
--- /dev/null
+++ b/.github/fixtures/test-always-render/expected.md
@@ -0,0 +1,11 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+
+## [0.2.0] - 2024-09-21
+
+
+No significant changes.
+
+<!-- generated by git-cliff -->
diff --git a/.github/workflows/test-fixtures.yml b/.github/workflows/test-fixtures.yml
index 18f1163a9d..6512af6280 100644
--- a/.github/workflows/test-fixtures.yml
+++ b/.github/workflows/test-fixtures.yml
@@ -93,6 +93,10 @@ jobs:
             command: --bump --unreleased --with-tag-message "Some text"
           - fixtures-name: test-from-context
             command: --from-context context.json
+          - fixtures-name: test-always-render-unreleased
+            command: --unreleased
+          - fixtures-name: test-always-render
+            command: --unreleased --tag v0.2.0
           - fixtures-name: test-unchanged-tag-date
             command: --tag v0.2.0
 
diff --git a/git-cliff-core/tests/integration_test.rs b/git-cliff-core/tests/integration_test.rs
index fc166c3bd5..2676d21c77 100644
--- a/git-cliff-core/tests/integration_test.rs
+++ b/git-cliff-core/tests/integration_test.rs
@@ -40,6 +40,7 @@ fn generate_changelog() -> Result<()> {
 		)),
 		footer:         Some(String::from("eoc - end of changelog")),
 		trim:           None,
+		render_always:  None,
 		postprocessors: None,
 		output:         None,
 	};