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

Issue:
Fix the first contributor filter
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Description of the bug

In the following scenario:

1. User foo contributes for the first time.
2. New release is created and changelog is generated.
3. User foo contributes after the release.
4. New release is created and changelog is generated BUT user foo is removed from the first contributors section from the previous release.

Might be related to #825 

### Steps To Reproduce

See above.

### Expected behavior

This happens due to **not checking** the dates of the new commits while updating the release metadata.

https://github.com/orhun/git-cliff/blob/8d10edb7450aaf189fbce5f78a72274739f73ba9/git-cliff-core/src/remote/mod.rs#L337-L347

Here, we check all the commits, but you see, in the new release user foo already contributed, so his contribution is not marked as first time anymore.

To fix it, we should simply:

```patch
diff --git a/git-cliff-core/src/remote/mod.rs b/git-cliff-core/src/remote/mod.rs
index 77d14e7..32e61a9 100644
--- a/git-cliff-core/src/remote/mod.rs
+++ b/git-cliff-core/src/remote/mod.rs
@@ -83,6 +83,8 @@ pub trait RemoteCommit: DynClone {
 	fn id(&self) -> String;
 	/// Commit author.
 	fn username(&self) -> Option<String>;
+	/// Timestamp.
+	fn timestamp(&self) -> Option<i64>;
 }
 
 dyn_clone::clone_trait_object!(RemoteCommit);
@@ -340,6 +342,9 @@ macro_rules! update_release_metadata {
 					.map(|mut v| {
 						v.is_first_time = !commits
 							.iter()
+							.filter(|commit| {
+								commit.timestamp() < Some(self.timestamp)
+							})
 							.map(|v| v.username())
 							.any(|login| login == v.username);
 						v
```

Apply the patch above and you will see that we are missing the implementation for GitHub, GitLab, etc. We need to check each remote and add the appropriate field for serializing the timestamp from their API response.

### Screenshots / Logs

nA

### Software information

nA

### Additional context

_No response_


Discussion / hints:
Hi @orhun can i work on this?
please do! @jaiakash 
any progress/ETA about this @jaiakash? I think this is affecting more users than I was expecting #906 
I tested the suggested fix on [`mcwarman-playground/git-cliff-readme-example`](https://bitbucket.org/mcwarman-playground/git-cliff-readme-example/).

And something isn't quite correct in the logic, it repots `orhun` as new contributor again:

```markdown
## What's Changed
* feat(config): support multiple file formats by @orhun <orhun@archlinux.org>
* feat(cache): use cache while fetching pages by @orhun <orhun@archlinux.org>
* feat(ci): add pipeline config by @mcwarman <7236500+mcwarman@users.noreply.github.com> in #1

### New Contributors
* @mcwarman <7236500+mcwarman@users.noreply.github.com> made their first contribution in [#1](https://bitbucket.org/mcwarman-playground/git-cliff-readme-example/pull-requests/1)
* @orhun <orhun@archlinux.org> made their first contribution

## 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>

## What's Changed in v1.0.0
* Initial commit by @orhun <orhun@archlinux.org>
* docs(project): add README.md by @orhun <orhun@archlinux.org>
* feat(parser): add ability to parse arrays by @orhun <orhun@archlinux.org>
* fix(args): rename help argument due to conflict by @orhun <orhun@archlinux.org>
* docs(example)!: add tested usage example by @orhun <orhun@archlinux.org>

### New Contributors
* @orhun <orhun@archlinux.org> made their first contribution

<!-- generated by -cliff -->
```

```diff
diff --git a/git-cliff-core/src/remote/bitbucket.rs b/git-cliff-core/src/remote/bitbucket.rs
index a3e3603..26ef85a 100644
--- a/git-cliff-core/src/remote/bitbucket.rs
+++ b/git-cliff-core/src/remote/bitbucket.rs
@@ -1,5 +1,6 @@
 use crate::config::Remote;
 use crate::error::*;
+use chrono::DateTime;
 use reqwest_middleware::ClientWithMiddleware;
 use serde::{
        Deserialize,
@@ -33,6 +34,8 @@ pub(crate) const BITBUCKET_MAX_PAGE_PRS: usize = 50;
 pub struct BitbucketCommit {
        /// SHA.
        pub hash:   String,
+       /// SHA.
+       pub date:   String,
        /// Author of the commit.
        pub author: Option<BitbucketCommitAuthor>,
 }
@@ -45,6 +48,15 @@ impl RemoteCommit for BitbucketCommit {
        fn username(&self) -> Option<String> {
                self.author.clone().and_then(|v| v.login)
        }
+
+       fn timestamp(&self) -> Option<i64> {
+               debug!("{} {}", self.hash.clone(), self.date.clone());
+               Some(
+                       DateTime::parse_from_rfc3339(&self.date)
+                               .unwrap()
+                               .timestamp(),
+               )
+       }
 }
```

```diff
diff --git a/git-cliff-core/src/remote/mod.rs b/git-cliff-core/src/remote/mod.rs
index a79137b..d4a9681 100644
--- a/git-cliff-core/src/remote/mod.rs
+++ b/git-cliff-core/src/remote/mod.rs
@@ -82,6 +82,8 @@ pub trait RemoteCommit: DynClone {
        fn id(&self) -> String;
        /// Commit author.
        fn username(&self) -> Option<String>;
+       /// Timestamp.
+       fn timestamp(&self) -> Option<i64>;
 }

 dyn_clone::clone_trait_object!(RemoteCommit);
@@ -342,6 +344,9 @@ macro_rules! update_release_metadata {
                                        .map(|mut v| {
                                                v.is_first_time = !commits
                                                        .iter()
+                                                       .filter(|commit| {
+                                                               commit.timestamp() < Some(self.timestamp)
+                                                       })
                                                        .map(|v| v.username())
                                                        .any(|login| login == v.username);
                                                v
```




I guess we still need to check the past releases... the logic here boggles my mind a bit.

- We are filtering commits by timestamp to exclude those that occurred before the current release's timestamp.
- However, this doesn't fully account for the fact that is_first_time is being evaluated globally, not per release.

🤔 
~~I wasn't sure if it was some how not filtering the username correctly, and `mcwarman` first time contribution was marking both contributors as true.~~

~~The only reason i thought it might be this is because `v1.0.1` doesn't have a new contributors section in again.~~

I now understand this a little bit more after some debugging.

Not sure if something like this works:

```rust
.filter(|commit| {
	//unreleased
	self.timestamp == 0 ||
		commit.timestamp() < Some(self.timestamp)
})
```
Yes, I think something like that might work. Are you getting the correct results with it?
For the Bitbucket it was, but possibly more by the luck

The approach isn't flawless, because I realised `self.timestamp` is the tag timestamp, and that differs to the commit related to that tag timestamp so the filter doesn't do the 100% the correct thing but gets you close.
Hmm I see.. Either way, can you create a PR with your changes? I think anything that gets us to a more accurate result is welcome :)
Also I just updated the issue labels. This turned out to be more difficult than I expected. I'm still not sure about the logic above.
I think this works:

- Store the remote commit timestamp of the current release (while iterating)
- Use that timestamp to perform the filter

```diff
diff --git a/git-cliff-core/src/remote/mod.rs b/git-cliff-core/src/remote/mod.rs
index a79137b..d83fe28 100644
--- a/git-cliff-core/src/remote/mod.rs
+++ b/git-cliff-core/src/remote/mod.rs
@@ -82,6 +82,8 @@ pub trait RemoteCommit: DynClone {
 	fn id(&self) -> String;
 	/// Commit author.
 	fn username(&self) -> Option<String>;
+	/// Timestamp.
+	fn timestamp(&self) -> Option<i64>;
 }
 
 dyn_clone::clone_trait_object!(RemoteCommit);
@@ -299,6 +301,7 @@ macro_rules! update_release_metadata {
 				pull_requests: Vec<Box<dyn RemotePullRequest>>,
 			) -> Result<()> {
 				let mut contributors: Vec<RemoteContributor> = Vec::new();
+				let mut release_commit_timestamp: Option<i64> = None;
 				// retain the commits that are not a part of this release for later
 				// on checking the first contributors.
 				commits.retain(|v| {
@@ -331,6 +334,11 @@ macro_rules! update_release_metadata {
 							});
 						}
 						commit.remote = Some(commit.$remote.clone());
+						// if remote commit is the release commit store timestamp for
+						// use in calculation of first time
+						if Some(v.id().clone()) == self.commit_id {
+							release_commit_timestamp = v.timestamp().clone();
+						}
 						false
 					} else {
 						true
@@ -342,6 +350,12 @@ macro_rules! update_release_metadata {
 					.map(|mut v| {
 						v.is_first_time = !commits
 							.iter()
+							.filter(|commit| {
+								// if current release is unreleased no need to filter
+								// commits or filter commits that are from
+								// newer releases
+								self.timestamp == 0 ||
+									commit.timestamp() < release_commit_timestamp
+							})
 							.map(|v| v.username())
 							.any(|login| login == v.username);
 						v
```

Although to make it work bitbucket I had to override the endpoint because of #906

```markdown
## What's Changed
* feat(config): support multiple file formats by @orhun <orhun@archlinux.org>
* feat(cache): use cache while fetching pages by @orhun <orhun@archlinux.org>
* feat(ci): add pipeline config by @mcwarman <7236500+mcwarman@users.noreply.github.com> in #1

### New Contributors
* @mcwarman <7236500+mcwarman@users.noreply.github.com> made their first contribution in [#1](https://bitbucket.org/mcwarman-playground/git-cliff-readme-example/pull-requests/1)

## 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>

## What's Changed in v1.0.0
* Initial commit by @orhun <orhun@archlinux.org>
* docs(project): add README.md by @orhun <orhun@archlinux.org>
* feat(parser): add ability to parse arrays by @orhun <orhun@archlinux.org>
* fix(args): rename help argument due to conflict by @orhun <orhun@archlinux.org>
* docs(example)!: add tested usage example by @orhun <orhun@archlinux.org>

### New Contributors
* @orhun <orhun@archlinux.org> made their first contribution

<!-- generated by -cliff -->
```

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78152b805d..4fa64adf0f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -64,6 +64,7 @@
 
 ## New Contributors ❤️
 
+* @nejcgalof made their first contribution in [#853](https://github.com/orhun/git-cliff/pull/853)
 * @pplmx made their first contribution in [#824](https://github.com/orhun/git-cliff/pull/824)
 
 ## [2.5.0](https://github.com/orhun/git-cliff/compare/v2.4.0..v2.5.0) - 2024-08-24
@@ -120,11 +121,14 @@
 
 ## New Contributors ❤️
 
+* @weichweich made their first contribution in [#807](https://github.com/orhun/git-cliff/pull/807)
 * @janbuchar made their first contribution in [#784](https://github.com/orhun/git-cliff/pull/784)
 * @oberrich made their first contribution in [#809](https://github.com/orhun/git-cliff/pull/809)
 * @tisonkun made their first contribution in [#599](https://github.com/orhun/git-cliff/pull/599)
 * @DerTiedemann made their first contribution in [#758](https://github.com/orhun/git-cliff/pull/758)
+* @DaniPopes made their first contribution in [#709](https://github.com/orhun/git-cliff/pull/709)
 * @artrz made their first contribution in [#779](https://github.com/orhun/git-cliff/pull/779)
+* @braineo made their first contribution in [#744](https://github.com/orhun/git-cliff/pull/744)
 * @myl7 made their first contribution in [#776](https://github.com/orhun/git-cliff/pull/776)
 * @pawamoy made their first contribution in [#774](https://github.com/orhun/git-cliff/pull/774)
 * @tonybutt made their first contribution in [#742](https://github.com/orhun/git-cliff/pull/742)
@@ -205,6 +209,7 @@
 
 ## New Contributors ❤️
 
+* @R11baka made their first contribution in [#672](https://github.com/orhun/git-cliff/pull/672)
 * @0x61nas made their first contribution in [#669](https://github.com/orhun/git-cliff/pull/669)
 * @dark0dave made their first contribution in [#663](https://github.com/orhun/git-cliff/pull/663)
 * @antonengelhardt made their first contribution in [#653](https://github.com/orhun/git-cliff/pull/653)
@@ -299,6 +304,7 @@
 
 ## New Contributors ❤️
 
+* @aminya made their first contribution in [#567](https://github.com/orhun/git-cliff/pull/567)
 * @jsurkont made their first contribution in [#530](https://github.com/orhun/git-cliff/pull/530)
 * @dupuy made their first contribution in [#535](https://github.com/orhun/git-cliff/pull/535)
 * @daniel-carr-3000 made their first contribution in [#532](https://github.com/orhun/git-cliff/pull/532)
@@ -626,8 +632,10 @@
 ## New Contributors ❤️
 
 * @alexfertel made their first contribution in [#253](https://github.com/orhun/git-cliff/pull/253)
+* @tvcsantos made their first contribution
 * @beeb made their first contribution in [#167](https://github.com/orhun/git-cliff/pull/167)
 * @PSeitz made their first contribution in [#155](https://github.com/orhun/git-cliff/pull/155)
+* @alerque made their first contribution in [#142](https://github.com/orhun/git-cliff/pull/142)
 
 ## [1.2.0](https://github.com/orhun/git-cliff/compare/v1.1.2..v1.2.0) - 2023-04-28
 
@@ -716,8 +724,11 @@
 
 ## New Contributors ❤️
 
+* @bors[bot] made their first contribution in [#161](https://github.com/orhun/git-cliff/pull/161)
+* @dependabot[bot] made their first contribution
 * @mackness made their first contribution in [#157](https://github.com/orhun/git-cliff/pull/157)
 * @jackton1 made their first contribution in [#152](https://github.com/orhun/git-cliff/pull/152)
+* @radusuciu made their first contribution in [#147](https://github.com/orhun/git-cliff/pull/147)
 * @lbowenwest made their first contribution in [#137](https://github.com/orhun/git-cliff/pull/137)
 * @jankatins made their first contribution in [#141](https://github.com/orhun/git-cliff/pull/141)
 * @saidsay-so made their first contribution in [#140](https://github.com/orhun/git-cliff/pull/140)
@@ -993,6 +1004,7 @@
 
 ## New Contributors ❤️
 
+* @favna made their first contribution in [#83](https://github.com/orhun/git-cliff/pull/83)
 * @kaushalmodi made their first contribution in [#80](https://github.com/orhun/git-cliff/pull/80)
 * @uyha made their first contribution in [#76](https://github.com/orhun/git-cliff/pull/76)
 * @mgrachev made their first contribution in [#73](https://github.com/orhun/git-cliff/pull/73)
@@ -1011,6 +1023,9 @@
 - *(core)* Document timestamp format of `Release` struct ([#67](https://github.com/orhun/git-cliff/issues/67)) - ([d68eb12](https://github.com/orhun/git-cliff/commit/d68eb120c0a0a98bc1e7264a3aede17b5f5c54be))
 - *(readme)* Add another option of GitHub Actions ([#64](https://github.com/orhun/git-cliff/issues/64)) - ([db7edf5](https://github.com/orhun/git-cliff/commit/db7edf5707f2bfdf49c749026969fd1833530ed7))
 
+## New Contributors ❤️
+
+* @MarcoIeni made their first contribution in [#67](https://github.com/orhun/git-cliff/pull/67)
 
 ## [0.6.0](https://github.com/orhun/git-cliff/compare/v0.5.0..v0.6.0) - 2022-02-12
 
@@ -1059,6 +1074,7 @@
 
 ## New Contributors ❤️
 
+* @tranzystorekk made their first contribution in [#57](https://github.com/orhun/git-cliff/pull/57)
 * @bachp made their first contribution in [#42](https://github.com/orhun/git-cliff/pull/42)
 
 ## [0.5.0](https://github.com/orhun/git-cliff/compare/v0.4.2..v0.5.0) - 2021-12-15
@@ -1104,6 +1120,9 @@
 - *(fixtures)* Run test fixtures on ubuntu-latest - ([dea65f2](https://github.com/orhun/git-cliff/commit/dea65f235e2091001d8de41794bf3c98a7223917))
 - *(fixtures)* Improve the workflow for test fixtures - ([92a54d6](https://github.com/orhun/git-cliff/commit/92a54d67b825b53b6993a769ea9d5cf37ea2e43e))
 
+## New Contributors ❤️
+
+* @kenji-miyake made their first contribution in [#40](https://github.com/orhun/git-cliff/pull/40)
 
 ## [0.4.2](https://github.com/orhun/git-cliff/compare/v0.4.1..v0.4.2) - 2021-10-22
 
diff --git a/Cargo.lock b/Cargo.lock
index 8afeb249ed..c72aefd92d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -916,6 +916,7 @@ dependencies = [
  "temp-dir",
  "tera",
  "thiserror",
+ "time",
  "tokio",
  "toml",
  "url",
diff --git a/git-cliff-core/Cargo.toml b/git-cliff-core/Cargo.toml
index 71cd0fc8d6..b11afa4757 100644
--- a/git-cliff-core/Cargo.toml
+++ b/git-cliff-core/Cargo.toml
@@ -77,6 +77,7 @@ url.workspace = true
 dyn-clone = "1.0.17"
 urlencoding = "2.1.3"
 cacache = { version = "13.0.0", features = ["mmap"], default-features = false }
+time = "0.3.36"
 
 [dependencies.git2]
 version = "0.19.0"
diff --git a/git-cliff-core/src/release.rs b/git-cliff-core/src/release.rs
index fec293f51f..42577cf11c 100644
--- a/git-cliff-core/src/release.rs
+++ b/git-cliff-core/src/release.rs
@@ -377,6 +377,8 @@ mod test {
 		use crate::remote::github::{
 			GitHubCommit,
 			GitHubCommitAuthor,
+			GitHubCommitDetails,
+			GitHubCommitDetailsAuthor,
 			GitHubPullRequest,
 			PullRequestLabel,
 		};
@@ -437,50 +439,87 @@ mod test {
 					author: Some(GitHubCommitAuthor {
 						login: Some(String::from("orhun")),
 					}),
+					commit: Some(GitHubCommitDetails {
+						author: GitHubCommitDetailsAuthor {
+							date: String::from("2021-07-18T15:14:39+03:00"),
+						},
+					}),
 				},
 				GitHubCommit {
 					sha:    String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
 					author: Some(GitHubCommitAuthor {
 						login: Some(String::from("orhun")),
 					}),
+					commit: Some(GitHubCommitDetails {
+						author: GitHubCommitDetailsAuthor {
+							date: String::from("2021-07-18T15:12:19+03:00"),
+						},
+					}),
 				},
 				GitHubCommit {
 					sha:    String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
 					author: Some(GitHubCommitAuthor {
 						login: Some(String::from("nuhro")),
 					}),
+					commit: Some(GitHubCommitDetails {
+						author: GitHubCommitDetailsAuthor {
+							date: String::from("2021-07-18T15:07:23+03:00"),
+						},
+					}),
 				},
 				GitHubCommit {
 					sha:    String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
 					author: Some(GitHubCommitAuthor {
 						login: Some(String::from("awesome_contributor")),
 					}),
+					commit: Some(GitHubCommitDetails {
+						author: GitHubCommitDetailsAuthor {
+							date: String::from("2021-07-18T15:05:10+03:00"),
+						},
+					}),
 				},
 				GitHubCommit {
 					sha:    String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
 					author: Some(GitHubCommitAuthor {
 						login: Some(String::from("orhun")),
 					}),
+					commit: Some(GitHubCommitDetails {
+						author: GitHubCommitDetailsAuthor {
+							date: String::from("2021-07-18T15:03:30+03:00"),
+						},
+					}),
 				},
 				GitHubCommit {
 					sha:    String::from("6c34967147560ea09658776d4901709139b4ad66"),
 					author: Some(GitHubCommitAuthor {
 						login: Some(String::from("someone")),
 					}),
+					commit: Some(GitHubCommitDetails {
+						author: GitHubCommitDetailsAuthor {
+							date: String::from("2021-07-18T15:00:38+03:00"),
+						},
+					}),
 				},
 				GitHubCommit {
 					sha:    String::from("0c34967147560e809658776d4901709139b4ad68"),
 					author: Some(GitHubCommitAuthor {
 						login: Some(String::from("idk")),
 					}),
+					commit: Some(GitHubCommitDetails {
+						author: GitHubCommitDetailsAuthor {
+							date: String::from("2021-07-18T15:00:38+03:00"),
+						},
+					}),
 				},
 				GitHubCommit {
 					sha:    String::from("kk34967147560e809658776d4901709139b4ad68"),
 					author: None,
+					commit: None,
 				},
 				GitHubCommit {
 					sha:    String::new(),
 					author: None,
+					commit: None,
 				},
 			]
 			.into_iter()
@@ -1155,54 +1194,79 @@ mod test {
 		release.update_gitea_metadata(
 			vec![
 				GiteaCommit {
-					sha:    String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
-					author: Some(GiteaCommitAuthor {
+					sha:     String::from(
+						"1d244937ee6ceb8e0314a4a201ba93a7a61f2071",
+					),
+					author:  Some(GiteaCommitAuthor {
 						login: Some(String::from("orhun")),
 					}),
+					created: String::from("2021-07-18T15:14:39+03:00"),
 				},
 				GiteaCommit {
-					sha:    String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
-					author: Some(GiteaCommitAuthor {
+					sha:     String::from(
+						"21f6aa587fcb772de13f2fde0e92697c51f84162",
+					),
+					author:  Some(GiteaCommitAuthor {
 						login: Some(String::from("orhun")),
 					}),
+					created: String::from("2021-07-18T15:12:19+03:00"),
 				},
 				GiteaCommit {
-					sha:    String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
-					author: Some(GiteaCommitAuthor {
+					sha:     String::from(
+						"35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973",
+					),
+					author:  Some(GiteaCommitAuthor {
 						login: Some(String::from("nuhro")),
 					}),
+					created: String::from("2021-07-18T15:07:23+03:00"),
 				},
 				GiteaCommit {
-					sha:    String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
-					author: Some(GiteaCommitAuthor {
+					sha:     String::from(
+						"4d3ffe4753b923f4d7807c490e650e6624a12074",
+					),
+					author:  Some(GiteaCommitAuthor {
 						login: Some(String::from("awesome_contributor")),
 					}),
+					created: String::from("2021-07-18T15:05:10+03:00"),
 				},
 				GiteaCommit {
-					sha:    String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
-					author: Some(GiteaCommitAuthor {
+					sha:     String::from(
+						"5a55e92e5a62dc5bf9872ffb2566959fad98bd05",
+					),
+					author:  Some(GiteaCommitAuthor {
 						login: Some(String::from("orhun")),
 					}),
+					created: String::from("2021-07-18T15:03:30+03:00"),
 				},
 				GiteaCommit {
-					sha:    String::from("6c34967147560ea09658776d4901709139b4ad66"),
-					author: Some(GiteaCommitAuthor {
+					sha:     String::from(
+						"6c34967147560ea09658776d4901709139b4ad66",
+					),
+					author:  Some(GiteaCommitAuthor {
 						login: Some(String::from("someone")),
 					}),
+					created: String::from("2021-07-18T15:00:38+03:00"),
 				},
 				GiteaCommit {
-					sha:    String::from("0c34967147560e809658776d4901709139b4ad68"),
-					author: Some(GiteaCommitAuthor {
+					sha:     String::from(
+						"0c34967147560e809658776d4901709139b4ad68",
+					),
+					author:  Some(GiteaCommitAuthor {
 						login: Some(String::from("idk")),
 					}),
+					created: String::from("2021-07-18T15:00:38+03:00"),
 				},
 				GiteaCommit {
-					sha:    String::from("kk34967147560e809658776d4901709139b4ad68"),
-					author: None,
+					sha:     String::from(
+						"kk34967147560e809658776d4901709139b4ad68",
+					),
+					author:  None,
+					created: String::new(),
 				},
 				GiteaCommit {
-					sha:    String::new(),
-					author: None,
+					sha:     String::new(),
+					author:  None,
+					created: String::new(),
 				},
 			]
 			.into_iter()
@@ -1492,48 +1556,56 @@ mod test {
 					author: Some(BitbucketCommitAuthor {
 						login: Some(String::from("orhun")),
 					}),
+					date:   String::from("2021-07-18T15:14:39+03:00"),
 				},
 				BitbucketCommit {
 					hash:   String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
 					author: Some(BitbucketCommitAuthor {
 						login: Some(String::from("orhun")),
 					}),
+					date:   String::from("2021-07-18T15:12:19+03:00"),
 				},
 				BitbucketCommit {
 					hash:   String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
 					author: Some(BitbucketCommitAuthor {
 						login: Some(String::from("nuhro")),
 					}),
+					date:   String::from("2021-07-18T15:07:23+03:00"),
 				},
 				BitbucketCommit {
 					hash:   String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
 					author: Some(BitbucketCommitAuthor {
 						login: Some(String::from("awesome_contributor")),
 					}),
+					date:   String::from("2021-07-18T15:05:10+03:00"),
 				},
 				BitbucketCommit {
 					hash:   String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
 					author: Some(BitbucketCommitAuthor {
 						login: Some(String::from("orhun")),
 					}),
+					date:   String::from("2021-07-18T15:03:30+03:00"),
 				},
 				BitbucketCommit {
 					hash:   String::from("6c34967147560ea09658776d4901709139b4ad66"),
 					author: Some(BitbucketCommitAuthor {
 						login: Some(String::from("someone")),
 					}),
+					date:   String::from("2021-07-18T15:00:38+03:00"),
 				},
 				BitbucketCommit {
 					hash:   String::from("0c34967147560e809658776d4901709139b4ad68"),
 					author: Some(BitbucketCommitAuthor {
 						login: Some(String::from("idk")),
 					}),
+					date:   String::from("2021-07-18T15:00:01+03:00"),
 				},
 				BitbucketCommit {
 					hash:   String::from("kk34967147560e809658776d4901709139b4ad68"),
 					author: Some(BitbucketCommitAuthor {
 						login: Some(String::from("orhun")),
 					}),
+					date:   String::from("2021-07-14T21:25:24+03:00"),
 				},
 			]
 			.into_iter()
diff --git a/git-cliff-core/src/remote/bitbucket.rs b/git-cliff-core/src/remote/bitbucket.rs
index a3e3603d73..5fe0a763ae 100644
--- a/git-cliff-core/src/remote/bitbucket.rs
+++ b/git-cliff-core/src/remote/bitbucket.rs
@@ -33,6 +33,8 @@ pub(crate) const BITBUCKET_MAX_PAGE_PRS: usize = 50;
 pub struct BitbucketCommit {
 	/// SHA.
 	pub hash:   String,
+	/// Date of the commit
+	pub date:   String,
 	/// Author of the commit.
 	pub author: Option<BitbucketCommitAuthor>,
 }
@@ -45,6 +47,10 @@ impl RemoteCommit for BitbucketCommit {
 	fn username(&self) -> Option<String> {
 		self.author.clone().and_then(|v| v.login)
 	}
+
+	fn timestamp(&self) -> Option<i64> {
+		Some(self.convert_to_unix_timestamp(self.date.clone().as_str()))
+	}
 }
 
 /// <https://developer.atlassian.com/cloud/bitbucket/rest/api-group-commits/#api-repositories-workspace-repo-slug-commits-get>
@@ -221,3 +227,23 @@ impl BitbucketClient {
 			.collect())
 	}
 }
+
+#[cfg(test)]
+mod test {
+	use super::*;
+	use crate::remote::RemoteCommit;
+	use pretty_assertions::assert_eq;
+
+	#[test]
+	fn timestamp() {
+		let remote_commit = BitbucketCommit {
+			hash:   String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
+			author: Some(BitbucketCommitAuthor {
+				login: Some(String::from("orhun")),
+			}),
+			date:   String::from("2021-07-18T15:14:39+03:00"),
+		};
+
+		assert_eq!(Some(1626610479), remote_commit.timestamp());
+	}
+}
diff --git a/git-cliff-core/src/remote/gitea.rs b/git-cliff-core/src/remote/gitea.rs
index f6df6b2e30..cc9cf349cc 100644
--- a/git-cliff-core/src/remote/gitea.rs
+++ b/git-cliff-core/src/remote/gitea.rs
@@ -29,9 +29,11 @@ pub(crate) const TEMPLATE_VARIABLES: &[&str] =
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
 pub struct GiteaCommit {
 	/// SHA.
-	pub sha:    String,
+	pub sha:     String,
 	/// Author of the commit.
-	pub author: Option<GiteaCommitAuthor>,
+	pub author:  Option<GiteaCommitAuthor>,
+	/// Timestamp of the commit.
+	pub created: String,
 }
 
 impl RemoteCommit for GiteaCommit {
@@ -42,6 +44,10 @@ impl RemoteCommit for GiteaCommit {
 	fn username(&self) -> Option<String> {
 		self.author.clone().and_then(|v| v.login)
 	}
+
+	fn timestamp(&self) -> Option<i64> {
+		Some(self.convert_to_unix_timestamp(self.created.clone().as_str()))
+	}
 }
 
 impl RemoteEntry for GiteaCommit {
@@ -183,3 +189,23 @@ impl GiteaClient {
 			.collect())
 	}
 }
+
+#[cfg(test)]
+mod test {
+	use super::*;
+	use crate::remote::RemoteCommit;
+	use pretty_assertions::assert_eq;
+
+	#[test]
+	fn timestamp() {
+		let remote_commit = GiteaCommit {
+			sha:     String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
+			author:  Some(GiteaCommitAuthor {
+				login: Some(String::from("orhun")),
+			}),
+			created: String::from("2021-07-18T15:14:39+03:00"),
+		};
+
+		assert_eq!(Some(1626610479), remote_commit.timestamp());
+	}
+}
diff --git a/git-cliff-core/src/remote/github.rs b/git-cliff-core/src/remote/github.rs
index 8a06a706c9..80fc2125d7 100644
--- a/git-cliff-core/src/remote/github.rs
+++ b/git-cliff-core/src/remote/github.rs
@@ -32,6 +32,22 @@ pub struct GitHubCommit {
 	pub sha:    String,
 	/// Author of the commit.
 	pub author: Option<GitHubCommitAuthor>,
+	/// Details of the commit
+	pub commit: Option<GitHubCommitDetails>,
+}
+
+/// Representation of subset of commit details
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct GitHubCommitDetails {
+	/// Author of the commit
+	pub author: GitHubCommitDetailsAuthor,
+}
+
+/// Representation of subset of commit author details
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct GitHubCommitDetailsAuthor {
+	/// Date of the commit
+	pub date: String,
 }
 
 impl RemoteCommit for GitHubCommit {
@@ -42,6 +58,12 @@ impl RemoteCommit for GitHubCommit {
 	fn username(&self) -> Option<String> {
 		self.author.clone().and_then(|v| v.login)
 	}
+
+	fn timestamp(&self) -> Option<i64> {
+		self.commit
+			.clone()
+			.map(|f| self.convert_to_unix_timestamp(f.author.date.clone().as_str()))
+	}
 }
 
 impl RemoteEntry for GitHubCommit {
@@ -182,3 +204,27 @@ impl GitHubClient {
 			.collect())
 	}
 }
+
+#[cfg(test)]
+mod test {
+	use super::*;
+	use crate::remote::RemoteCommit;
+	use pretty_assertions::assert_eq;
+
+	#[test]
+	fn timestamp() {
+		let remote_commit = GitHubCommit {
+			sha:    String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
+			author: Some(GitHubCommitAuthor {
+				login: Some(String::from("orhun")),
+			}),
+			commit: Some(GitHubCommitDetails {
+				author: GitHubCommitDetailsAuthor {
+					date: String::from("2021-07-18T15:14:39+03:00"),
+				},
+			}),
+		};
+
+		assert_eq!(Some(1626610479), remote_commit.timestamp());
+	}
+}
diff --git a/git-cliff-core/src/remote/gitlab.rs b/git-cliff-core/src/remote/gitlab.rs
index 52b2d590c9..84c2553966 100644
--- a/git-cliff-core/src/remote/gitlab.rs
+++ b/git-cliff-core/src/remote/gitlab.rs
@@ -106,6 +106,10 @@ impl RemoteCommit for GitLabCommit {
 	fn username(&self) -> Option<String> {
 		Some(self.author_name.clone())
 	}
+
+	fn timestamp(&self) -> Option<i64> {
+		Some(self.convert_to_unix_timestamp(self.committed_date.clone().as_str()))
+	}
 }
 
 impl RemoteEntry for GitLabCommit {
@@ -304,4 +308,16 @@ mod test {
 			GitLabProject::url(1, "https://gitlab.test.com/api/v4", &remote, 0)
 		);
 	}
+
+	#[test]
+	fn timestamp() {
+		let remote_commit = GitLabCommit {
+			id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
+			author_name: String::from("orhun"),
+			committed_date: String::from("2021-07-18T15:14:39+03:00"),
+			..Default::default()
+		};
+
+		assert_eq!(Some(1626610479), remote_commit.timestamp());
+	}
 }
diff --git a/git-cliff-core/src/remote/mod.rs b/git-cliff-core/src/remote/mod.rs
index a79137b291..5ae8d114c5 100644
--- a/git-cliff-core/src/remote/mod.rs
+++ b/git-cliff-core/src/remote/mod.rs
@@ -50,6 +50,10 @@ use serde::{
 };
 use std::fmt::Debug;
 use std::time::Duration;
+use time::{
+	format_description::well_known::Rfc3339,
+	OffsetDateTime,
+};
 
 /// User agent for interacting with the GitHub API.
 ///
@@ -82,6 +86,14 @@ pub trait RemoteCommit: DynClone {
 	fn id(&self) -> String;
 	/// Commit author.
 	fn username(&self) -> Option<String>;
+	/// Timestamp.
+	fn timestamp(&self) -> Option<i64>;
+	/// Convert date in RFC3339 format to unix timestamp
+	fn convert_to_unix_timestamp(&self, date: &str) -> i64 {
+		OffsetDateTime::parse(date, &Rfc3339)
+			.expect("failed to parse date")
+			.unix_timestamp()
+	}
 }
 
 dyn_clone::clone_trait_object!(RemoteCommit);
@@ -299,6 +311,7 @@ macro_rules! update_release_metadata {
 				pull_requests: Vec<Box<dyn RemotePullRequest>>,
 			) -> Result<()> {
 				let mut contributors: Vec<RemoteContributor> = Vec::new();
+				let mut release_commit_timestamp: Option<i64> = None;
 				// retain the commits that are not a part of this release for later
 				// on checking the first contributors.
 				commits.retain(|v| {
@@ -331,6 +344,11 @@ macro_rules! update_release_metadata {
 							});
 						}
 						commit.remote = Some(commit.$remote.clone());
+						// if remote commit is the release commit store timestamp for
+						// use in calculation of first time
+						if Some(v.id().clone()) == self.commit_id {
+							release_commit_timestamp = v.timestamp().clone();
+						}
 						false
 					} else {
 						true
@@ -342,6 +360,13 @@ macro_rules! update_release_metadata {
 					.map(|mut v| {
 						v.is_first_time = !commits
 							.iter()
+							.filter(|commit| {
+								// if current release is unreleased no need to filter
+								// commits or filter commits that are from
+								// newer releases
+								self.timestamp == 0 ||
+									commit.timestamp() < release_commit_timestamp
+							})
 							.map(|v| v.username())
 							.any(|login| login == v.username);
 						v


Test patch:
diff --git a/.github/fixtures/test-bitbucket-integration/cliff.toml b/.github/fixtures/test-bitbucket-integration/cliff.toml
index 10a7ec2ad6..5d1a85f171 100644
--- a/.github/fixtures/test-bitbucket-integration/cliff.toml
+++ b/.github/fixtures/test-bitbucket-integration/cliff.toml
@@ -22,7 +22,7 @@ body = """
 
 {% if bitbucket.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
   {% raw %}\n{% endraw -%}
-  ## New Contributors
+  ### New Contributors
 {%- endif %}\
 {% for contributor in bitbucket.contributors | filter(attribute="is_first_time", value=true) %}
   * @{{ contributor.username }} made their first contribution
diff --git a/.github/fixtures/test-bitbucket-integration/expected.md b/.github/fixtures/test-bitbucket-integration/expected.md
index 84fbdee247..2d32ecfe31 100644
--- a/.github/fixtures/test-bitbucket-integration/expected.md
+++ b/.github/fixtures/test-bitbucket-integration/expected.md
@@ -14,4 +14,7 @@
 * fix(args): rename help argument due to conflict by @orhun <orhun@archlinux.org>
 * docs(example)!: add tested usage example by @orhun <orhun@archlinux.org>
 
+### New Contributors
+* @orhun <orhun@archlinux.org> made their first contribution
+
 <!-- generated by -cliff -->
diff --git a/.github/fixtures/test-fixtures-locally.sh b/.github/fixtures/test-fixtures-locally.sh
index 05a1c1c23c..45e2a9f023 100755
--- a/.github/fixtures/test-fixtures-locally.sh
+++ b/.github/fixtures/test-fixtures-locally.sh
@@ -12,7 +12,7 @@ export FIXTURES_DIR="$SCRIPT_DIR/$1"
 
 # Set up a temporary repository
 cd "$(mktemp -d)"
-git init
+git init >&2
 
 # Commit
 "$FIXTURES_DIR/commit.sh"
@@ -31,5 +31,5 @@ if [ -n "$MSYSTEM" ] && [ "$MSYSTEM" = "MINGW64" ]; then
 fi
 
 # Show results
-echo -e "\n---Run git-cliff---"
+echo -e "\n---Run git-cliff---" >&2
 cargo run --manifest-path "$SCRIPT_DIR/../../Cargo.toml" -- -vv --config "$FIXTURES_DIR/cliff.toml" "${@:2}"
diff --git a/.github/fixtures/test-gitea-integration/cliff.toml b/.github/fixtures/test-gitea-integration/cliff.toml
index 00c548fc44..d869d2e4e6 100644
--- a/.github/fixtures/test-gitea-integration/cliff.toml
+++ b/.github/fixtures/test-gitea-integration/cliff.toml
@@ -16,7 +16,7 @@ body = """
 
 {% if gitea.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
   {% raw %}\n{% endraw -%}
-  ## New Contributors
+  ### New Contributors
 {%- endif %}\
 {% for contributor in gitea.contributors | filter(attribute="is_first_time", value=true) %}
   * @{{ contributor.username }} made their first contribution
diff --git a/.github/fixtures/test-gitea-integration/expected.md b/.github/fixtures/test-gitea-integration/expected.md
index ff728a5e48..7c68ba4d14 100644
--- a/.github/fixtures/test-gitea-integration/expected.md
+++ b/.github/fixtures/test-gitea-integration/expected.md
@@ -9,7 +9,7 @@
 * feat(config): support multiple file formats by @ThetaDev
 * feat(cache): use cache while fetching pages by @ThetaDev
 
-## New Contributors
+### New Contributors
 * @ThetaDev made their first contribution
 
 <!-- generated by -cliff -->
diff --git a/.github/fixtures/test-github-integration/cliff.toml b/.github/fixtures/test-github-integration/cliff.toml
index 0bb109c19b..b1d07e7490 100644
--- a/.github/fixtures/test-github-integration/cliff.toml
+++ b/.github/fixtures/test-github-integration/cliff.toml
@@ -17,7 +17,7 @@ body = """
 
 {% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
   {% raw %}\n{% endraw -%}
-  ## New Contributors
+  ### New Contributors
 {%- endif %}\
 {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
   * @{{ contributor.username }} made their first contribution
diff --git a/.github/fixtures/test-github-integration/expected.md b/.github/fixtures/test-github-integration/expected.md
index 82c8ed1243..d8f27aa0d5 100644
--- a/.github/fixtures/test-github-integration/expected.md
+++ b/.github/fixtures/test-github-integration/expected.md
@@ -15,4 +15,7 @@
 * fix(args): rename help argument due to conflict by @orhun
 * docs(example)!: add tested usage example by @orhun
 
+### New Contributors
+* @orhun made their first contribution
+
 <!-- generated by git-cliff -->
diff --git a/.github/fixtures/test-gitlab-integration/cliff.toml b/.github/fixtures/test-gitlab-integration/cliff.toml
index 2490a37abc..dc72ccdf19 100644
--- a/.github/fixtures/test-gitlab-integration/cliff.toml
+++ b/.github/fixtures/test-gitlab-integration/cliff.toml
@@ -17,7 +17,7 @@ body = """
 
 {% if gitlab.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
   {% raw %}\n{% endraw -%}
-  ## New Contributors
+  ### New Contributors
 {%- endif %}\
 {% for contributor in gitlab.contributors | filter(attribute="is_first_time", value=true) %}
   * @{{ contributor.username }} made their first contribution