Repository: orhun/git-cliff (rust)

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_

Pull request description:
fix(remote): preserve first time contributors
## Description

Preserve first time contributors in older change logs

Fixes #891 

## Motivation and Context

When a new release is created and all change logs regenerated the first time contributor section is removed.

## How Has This Been Tested?

Tested using existing fixtures.

## Types of Changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation (no code change)
- [ ] Refactor (refactoring production code)
- [ ] Other <!--- (provide information) -->

## Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project.
- [x] I have updated the documentation accordingly.
- [x] I have formatted the code with [rustfmt](https://github.com/rust-lang/rustfmt).
- [x] I checked the lints with [clippy](https://github.com/rust-lang/rust-clippy).
- [x] I have added tests to cover my changes.
- [x] All new and existing tests passed.

<!--- Thank you for contributing to git-cliff! ⛰️  -->


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78152b8..4fa64ad 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/CONTRIBUTING.md b/CONTRIBUTING.md
index 3812f70..a2024c9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -15,6 +15,8 @@ Note that we have a [Code of Conduct](./CODE_OF_CONDUCT.md), please follow it in
 
 ```sh
 git clone https://github.com/{username}/git-cliff && cd git-cliff
+# OR
+git clone git@github.com:{username}/git-cliff && cd git-cliff
 ```
 
 To ensure the successful execution of the tests, it is essential to fetch the tags as follows:
diff --git a/Cargo.lock b/Cargo.lock
index 8afeb24..c72aefd 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 71cd0fc..b11afa4 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 d303ac8..9930851 100644
--- a/git-cliff-core/src/release.rs
+++ b/git-cliff-core/src/release.rs
@@ -379,6 +379,8 @@ mod test {
 		use crate::remote::github::{
 			GitHubCommit,
 			GitHubCommitAuthor,
+			GitHubCommitDetails,
+			GitHubCommitDetailsAuthor,
 			GitHubPullRequest,
 			PullRequestLabel,
 		};
@@ -439,50 +441,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()
@@ -1157,54 +1196,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()
@@ -1494,48 +1558,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 a3e3603..5fe0a76 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 f6df6b2..cc9cf34 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 8a06a70..80fc212 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 52b2d59..84c2553 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 a79137b..5ae8d11 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
diff --git a/git-cliff-core/src/repo.rs b/git-cliff-core/src/repo.rs
index 170ab1b..81a930b 100644
--- a/git-cliff-core/src/repo.rs
+++ b/git-cliff-core/src/repo.rs
@@ -399,6 +399,8 @@ impl Repository {
 	///
 	/// Find the branch that HEAD points to, and read the remote configured for
 	/// that branch returns the remote and the name of the local branch.
+	///
+	/// Note: HEAD must not be detached.
 	pub fn upstream_remote(&self) -> Result<Remote> {
 		for branch in self.inner.branches(Some(BranchType::Local))? {
 			let branch = branch?.0;
@@ -424,30 +426,79 @@ impl Repository {
 					})?
 					.to_string();
 				trace!("Upstream URL: {url}");
-				let url = Url::parse(&url)?;
-				let segments: Vec<&str> = url
-					.path_segments()
-					.ok_or_else(|| {
-						Error::RepoError(String::from("failed to get URL segments"))
-					})?
-					.rev()
-					.collect();
-				if let (Some(owner), Some(repo)) =
-					(segments.get(1), segments.first())
-				{
-					return Ok(Remote {
-						owner:     owner.to_string(),
-						repo:      repo.trim_end_matches(".git").to_string(),
-						token:     None,
-						is_custom: false,
-					});
-				}
+				return find_remote(&url);
 			}
 		}
-		Err(Error::RepoError(String::from("no remotes configured")))
+		Err(Error::RepoError(String::from(
+			"no remotes configured or HEAD is detached",
+		)))
 	}
 }
 
+fn find_remote(url: &str) -> Result<Remote> {
+	url_path_segments(url).or_else(|err| {
+		if url.contains("@") && url.contains(":") && url.contains("/") {
+			ssh_path_segments(url)
+		} else {
+			Err(err)
+		}
+	})
+}
+
+/// Returns the Remote from parsing the HTTPS format URL.
+///
+/// This function expects the URL to be in the following format:
+///
+/// > https://hostname/query/path.git
+fn url_path_segments(url: &str) -> Result<Remote> {
+	let parsed_url = Url::parse(url.strip_suffix(".git").unwrap_or(url))?;
+	let segments: Vec<&str> = parsed_url
+		.path_segments()
+		.ok_or_else(|| Error::RepoError(String::from("failed to get URL segments")))?
+		.rev()
+		.collect();
+	let [repo, owner, ..] = &segments[..] else {
+		return Err(Error::RepoError(String::from(
+			"failed to get the owner and repo",
+		)));
+	};
+	Ok(Remote {
+		owner:     owner.to_string(),
+		repo:      repo.to_string(),
+		token:     None,
+		is_custom: false,
+	})
+}
+
+/// Returns the Remote from parsing the SSH format URL.
+///
+/// This function expects the URL to be in the following format:
+///
+/// > git@hostname:owner/repo.git
+fn ssh_path_segments(url: &str) -> Result<Remote> {
+	let [_, owner_repo, ..] = url
+		.strip_suffix(".git")
+		.unwrap_or(url)
+		.split(":")
+		.collect::<Vec<_>>()[..]
+	else {
+		return Err(Error::RepoError(String::from(
+			"failed to get the owner and repo from ssh remote (:)",
+		)));
+	};
+	let [owner, repo] = owner_repo.split("/").collect::<Vec<_>>()[..] else {
+		return Err(Error::RepoError(String::from(
+			"failed to get the owner and repo from ssh remote (/)",
+		)));
+	};
+	Ok(Remote {
+		owner:     owner.to_string(),
+		repo:      repo.to_string(),
+		token:     None,
+		is_custom: false,
+	})
+}
+
 #[cfg(test)]
 mod test {
 	use super::*;
@@ -502,6 +553,24 @@ mod test {
 		)
 	}
 
+	#[test]
+	fn http_url_repo_owner() -> Result<()> {
+		let url = "https://hostname.com/bob/magic.git";
+		let remote = find_remote(url)?;
+		assert_eq!(remote.owner, "bob", "match owner");
+		assert_eq!(remote.repo, "magic", "match repo");
+		Ok(())
+	}
+
+	#[test]
+	fn ssh_url_repo_owner() -> Result<()> {
+		let url = "git@hostname.com:bob/magic.git";
+		let remote = find_remote(url)?;
+		assert_eq!(remote.owner, "bob", "match owner");
+		assert_eq!(remote.repo, "magic", "match repo");
+		Ok(())
+	}
+
 	#[test]
 	fn get_latest_commit() -> Result<()> {
 		let repository = get_repository()?;


Test patch:
diff --git a/.github/fixtures/test-bitbucket-integration/cliff.toml b/.github/fixtures/test-bitbucket-integration/cliff.toml
index 10a7ec2..5d1a85f 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 84fbdee..2d32ecf 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 05a1c1c..45e2a9f 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 00c548f..d869d2e 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 ff728a5..7c68ba4 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 0bb109c..b1d07e7 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 82c8ed1..d8f27aa 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 2490a37..dc72ccd 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