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

Issue:
feat(bitbucket): add Bitbucket support
## Description

Add bitbucket integration.

Closes #566 

## Motivation and Context

Add bitbucket integration.

## How Has This Been Tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->

## Screenshots / Logs (if applicable)

## Types of Changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] 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.
- [ ] 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! ⛰️  -->



Discussion / hints:
## [Codecov](https://app.codecov.io/gh/orhun/git-cliff/pull/663?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z) Report
Attention: Patch coverage is `7.74648%` with `131 lines` in your changes are missing coverage. Please review.
> Project coverage is 36.97%. Comparing base [(`2e1ed71`)](https://app.codecov.io/gh/orhun/git-cliff/commit/2e1ed718769690f07959bf771dc3be1ff6eb7a2c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z) to head [(`674ae20`)](https://app.codecov.io/gh/orhun/git-cliff/commit/674ae2053c1e9628ea19a05dd7718a6563753bc7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z).

| [Files](https://app.codecov.io/gh/orhun/git-cliff/pull/663?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z) | Patch % | Lines |
|---|---|---|
| [git-cliff-core/src/remote/bitbucket.rs](https://app.codecov.io/gh/orhun/git-cliff/pull/663?src=pr&el=tree&filepath=git-cliff-core%2Fsrc%2Fremote%2Fbitbucket.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z#diff-Z2l0LWNsaWZmLWNvcmUvc3JjL3JlbW90ZS9iaXRidWNrZXQucnM=) | 0.00% | [43 Missing :warning: ](https://app.codecov.io/gh/orhun/git-cliff/pull/663?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z) |
| [git-cliff-core/src/remote/mod.rs](https://app.codecov.io/gh/orhun/git-cliff/pull/663?src=pr&el=tree&filepath=git-cliff-core%2Fsrc%2Fremote%2Fmod.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z#diff-Z2l0LWNsaWZmLWNvcmUvc3JjL3JlbW90ZS9tb2QucnM=) | 0.00% | [32 Missing :warning: ](https://app.codecov.io/gh/orhun/git-cliff/pull/663?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z) |
| [git-cliff-core/src/changelog.rs](https://app.codecov.io/gh/orhun/git-cliff/pull/663?src=pr&el=tree&filepath=git-cliff-core%2Fsrc%2Fchangelog.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z#diff-Z2l0LWNsaWZmLWNvcmUvc3JjL2NoYW5nZWxvZy5ycw==) | 27.78% | [26 Missing :warning: ](https://app.codecov.io/gh/orhun/git-cliff/pull/663?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z) |
| [git-cliff/src/lib.rs](https://app.codecov.io/gh/orhun/git-cliff/pull/663?src=pr&el=tree&filepath=git-cliff%2Fsrc%2Flib.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z#diff-Z2l0LWNsaWZmL3NyYy9saWIucnM=) | 0.00% | [14 Missing :warning: ](https://app.codecov.io/gh/orhun/git-cliff/pull/663?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z) |
| [git-cliff/src/logger.rs](https://app.codecov.io/gh/orhun/git-cliff/pull/663?src=pr&el=tree&filepath=git-cliff%2Fsrc%2Flogger.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z#diff-Z2l0LWNsaWZmL3NyYy9sb2dnZXIucnM=) | 0.00% | [9 Missing :warning: ](https://app.codecov.io/gh/orhun/git-cliff/pull/663?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z) |
| [git-cliff-core/src/remote/gitlab.rs](https://app.codecov.io/gh/orhun/git-cliff/pull/663?src=pr&el=tree&filepath=git-cliff-core%2Fsrc%2Fremote%2Fgitlab.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z#diff-Z2l0LWNsaWZmLWNvcmUvc3JjL3JlbW90ZS9naXRsYWIucnM=) | 0.00% | [4 Missing :warning: ](https://app.codecov.io/gh/orhun/git-cliff/pull/663?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z) |
| [git-cliff-core/src/remote/github.rs](https://app.codecov.io/gh/orhun/git-cliff/pull/663?src=pr&el=tree&filepath=git-cliff-core%2Fsrc%2Fremote%2Fgithub.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z#diff-Z2l0LWNsaWZmLWNvcmUvc3JjL3JlbW90ZS9naXRodWIucnM=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/orhun/git-cliff/pull/663?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z) |
| [git-cliff-core/src/commit.rs](https://app.codecov.io/gh/orhun/git-cliff/pull/663?src=pr&el=tree&filepath=git-cliff-core%2Fsrc%2Fcommit.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z#diff-Z2l0LWNsaWZmLWNvcmUvc3JjL2NvbW1pdC5ycw==) | 50.00% | [1 Missing :warning: ](https://app.codecov.io/gh/orhun/git-cliff/pull/663?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z) |

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #663      +/-   ##
==========================================
- Coverage   39.97%   36.97%   -3.00%     
==========================================
  Files          17       18       +1     
  Lines        1221     1350     +129     
==========================================
+ Hits          488      499      +11     
- Misses        733      851     +118     
```

| [Flag](https://app.codecov.io/gh/orhun/git-cliff/pull/663/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z) | Coverage Δ | |
|---|---|---|
| [unit-tests](https://app.codecov.io/gh/orhun/git-cliff/pull/663/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z) | `36.97% <7.75%> (-3.00%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/orhun/git-cliff/pull/663?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Orhun+Parmaks%C4%B1z).

I created a repo for test fixtures: <https://bitbucket.org/orhunp/git-cliff-readme-example>
> I created a repo for test fixtures: https://bitbucket.org/orhunp/git-cliff-readme-example

Big thanks, I was going to have to do that!
How is this looking? I think I'm gonna release a new version on Monday so it would be nice to get this in :D

@orhun still fixing some bugs, bitbucket has weird author structure.
Congrats on merging your first pull request! ⛰️


Fix patch:
diff --git a/.github/ISSUE_TEMPLATE/integration.yml b/.github/ISSUE_TEMPLATE/integration.yml
index 1dca65a7aa..c93f63ebf8 100644
--- a/.github/ISSUE_TEMPLATE/integration.yml
+++ b/.github/ISSUE_TEMPLATE/integration.yml
@@ -1,5 +1,5 @@
 name: Integration ⚙️
-description: Report a bug or request a feature about an integration (e.g GitHub/GitLab/Bitbucket)
+description: Report a bug or request a feature about an integration (e.g GitHub/GitLab/Gitea/Bitbucket)
 labels: ["integration"]
 assignees:
   - orhun
diff --git a/git-cliff-core/Cargo.toml b/git-cliff-core/Cargo.toml
index 207bfab93d..0dc7530e62 100644
--- a/git-cliff-core/Cargo.toml
+++ b/git-cliff-core/Cargo.toml
@@ -17,36 +17,30 @@ default = ["repo"]
 ## You can turn this off if you already have the commits to put in the
 ## changelog and you don't need `git-cliff` to parse them.
 repo = ["dep:git2", "dep:glob", "dep:indexmap"]
-## Enable integration with GitHub.
-## You can turn this off if you don't use GitHub and don't want
-## to make network requests to the GitHub API.
-github = [
+# Enable integration with remote repositories.
+remote = [
   "dep:reqwest",
   "dep:http-cache-reqwest",
   "dep:reqwest-middleware",
   "dep:tokio",
   "dep:futures",
 ]
+## Enable integration with GitHub.
+## You can turn this off if you don't use GitHub and don't want
+## to make network requests to the GitHub API.
+github = ["remote"]
 ## Enable integration with GitLab.
 ## You can turn this off if you don't use GitLab and don't want
 ## to make network requests to the GitLab API.
-gitlab = [
-  "dep:reqwest",
-  "dep:http-cache-reqwest",
-  "dep:reqwest-middleware",
-  "dep:tokio",
-  "dep:futures",
-]
+gitlab = ["remote"]
 ## Enable integration with Bitbucket.
 ## You can turn this off if you don't use Bitbucket and don't want
 ## to make network requests to the Bitbucket API.
-bitbucket = [
-  "dep:reqwest",
-  "dep:http-cache-reqwest",
-  "dep:reqwest-middleware",
-  "dep:tokio",
-  "dep:futures",
-]
+bitbucket = ["remote"]
+## Enable integration with Gitea.
+## You can turn this off if you don't use Gitea and don't want
+## to make network requests to the Gitea API.
+gitea = ["remote"]
 
 [dependencies]
 glob = { workspace = true, optional = true }
diff --git a/git-cliff-core/src/changelog.rs b/git-cliff-core/src/changelog.rs
index 777b75ff0b..7707f37596 100644
--- a/git-cliff-core/src/changelog.rs
+++ b/git-cliff-core/src/changelog.rs
@@ -10,6 +10,8 @@ use crate::release::{
 };
 #[cfg(feature = "bitbucket")]
 use crate::remote::bitbucket::BitbucketClient;
+#[cfg(feature = "gitea")]
+use crate::remote::gitea::GiteaClient;
 #[cfg(feature = "github")]
 use crate::remote::github::GitHubClient;
 #[cfg(feature = "gitlab")]
@@ -224,7 +226,10 @@ impl<'a> Changelog<'a> {
 						github_client.get_pull_requests(),
 					)?;
 					debug!("Number of GitHub commits: {}", commits.len());
-					debug!("Number of GitHub pull requests: {}", commits.len());
+					debug!(
+						"Number of GitHub pull requests: {}",
+						pull_requests.len()
+					);
 					Ok((commits, pull_requests))
 				});
 			info!("{}", github::FINISHED_FETCHING_MSG);
@@ -300,6 +305,57 @@ impl<'a> Changelog<'a> {
 		}
 	}
 
+	/// Returns the Gitea metadata needed for the changelog.
+	///
+	/// This function creates a multithread async runtime for handling the
+	/// requests. The following are fetched from the GitHub REST API:
+	///
+	/// - Commits
+	/// - Pull requests
+	///
+	/// Each of these are paginated requests so they are being run in parallel
+	/// for speedup.
+	///
+	/// If no Gitea related variable is used in the template then this function
+	/// returns empty vectors.
+	#[cfg(feature = "gitea")]
+	fn get_gitea_metadata(&self) -> Result<crate::remote::RemoteMetadata> {
+		use crate::remote::gitea;
+		if self
+			.body_template
+			.contains_variable(gitea::TEMPLATE_VARIABLES) ||
+			self.footer_template
+				.as_ref()
+				.map(|v| v.contains_variable(gitea::TEMPLATE_VARIABLES))
+				.unwrap_or(false)
+		{
+			warn!("You are using an experimental feature! Please report bugs at <https://git-cliff.org/issues>");
+			let gitea_client =
+				GiteaClient::try_from(self.config.remote.gitea.clone())?;
+			info!(
+				"{} ({})",
+				gitea::START_FETCHING_MSG,
+				self.config.remote.gitea
+			);
+			let data = tokio::runtime::Builder::new_multi_thread()
+				.enable_all()
+				.build()?
+				.block_on(async {
+					let (commits, pull_requests) = tokio::try_join!(
+						gitea_client.get_commits(),
+						gitea_client.get_pull_requests(),
+					)?;
+					debug!("Number of Gitea commits: {}", commits.len());
+					debug!("Number of Gitea pull requests: {}", pull_requests.len());
+					Ok((commits, pull_requests))
+				});
+			info!("{}", gitea::FINISHED_FETCHING_MSG);
+			data
+		} else {
+			Ok((vec![], vec![]))
+		}
+	}
+
 	/// Returns the Bitbucket metadata needed for the changelog.
 	///
 	/// This function creates a multithread async runtime for handling the
@@ -379,6 +435,13 @@ impl<'a> Changelog<'a> {
 		} else {
 			(vec![], vec![])
 		};
+		#[cfg(feature = "gitea")]
+		let (gitea_commits, gitea_merge_request) = if self.config.remote.gitea.is_set() {
+			self.get_gitea_metadata()
+				.expect("Could not get gitea metadata")
+		} else {
+			(vec![], vec![])
+		};
 		#[cfg(feature = "bitbucket")]
 		let (bitbucket_commits, bitbucket_pull_request) =
 			if self.config.remote.bitbucket.is_set() {
@@ -398,6 +461,11 @@ impl<'a> Changelog<'a> {
 				gitlab_commits.clone(),
 				gitlab_merge_request.clone(),
 			)?;
+			#[cfg(feature = "gitea")]
+			release.update_gitea_metadata(
+				gitea_commits.clone(),
+				gitea_merge_request.clone(),
+			)?;
 			#[cfg(feature = "bitbucket")]
 			release.update_bitbucket_metadata(
 				bitbucket_commits.clone(),
@@ -692,6 +760,11 @@ mod test {
 					repo:  String::from("awesome"),
 					token: None,
 				},
+				gitea:     Remote {
+					owner: String::from("coolguy"),
+					repo:  String::from("awesome"),
+					token: None,
+				},
 				bitbucket: Remote {
 					owner: String::from("coolguy"),
 					repo:  String::from("awesome"),
@@ -771,6 +844,10 @@ mod test {
 			gitlab: crate::remote::RemoteReleaseMetadata {
 				contributors: vec![],
 			},
+			#[cfg(feature = "gitea")]
+			gitea: crate::remote::RemoteReleaseMetadata {
+				contributors: vec![],
+			},
 			#[cfg(feature = "bitbucket")]
 			bitbucket: crate::remote::RemoteReleaseMetadata {
 				contributors: vec![],
@@ -826,6 +903,10 @@ mod test {
 				gitlab: crate::remote::RemoteReleaseMetadata {
 					contributors: vec![],
 				},
+				#[cfg(feature = "gitea")]
+				gitea: crate::remote::RemoteReleaseMetadata {
+					contributors: vec![],
+				},
 				#[cfg(feature = "bitbucket")]
 				bitbucket: crate::remote::RemoteReleaseMetadata {
 					contributors: vec![],
diff --git a/git-cliff-core/src/commit.rs b/git-cliff-core/src/commit.rs
index e34612877a..1a74d1e0d4 100644
--- a/git-cliff-core/src/commit.rs
+++ b/git-cliff-core/src/commit.rs
@@ -128,6 +128,9 @@ pub struct Commit<'a> {
 	/// GitLab metadata of the commit.
 	#[cfg(feature = "gitlab")]
 	pub gitlab:        crate::remote::RemoteContributor,
+	/// Gitea metadata of the commit.
+	#[cfg(feature = "gitea")]
+	pub gitea:         crate::remote::RemoteContributor,
 	/// Bitbucket metadata of the commit.
 	#[cfg(feature = "bitbucket")]
 	pub bitbucket:     crate::remote::RemoteContributor,
@@ -446,6 +449,8 @@ impl Serialize for Commit<'_> {
 		commit.serialize_field("github", &self.github)?;
 		#[cfg(feature = "gitlab")]
 		commit.serialize_field("gitlab", &self.gitlab)?;
+		#[cfg(feature = "gitea")]
+		commit.serialize_field("gitea", &self.gitea)?;
 		#[cfg(feature = "bitbucket")]
 		commit.serialize_field("bitbucket", &self.bitbucket)?;
 		commit.end()
diff --git a/git-cliff-core/src/config.rs b/git-cliff-core/src/config.rs
index bd20264faf..a2bbc56e47 100644
--- a/git-cliff-core/src/config.rs
+++ b/git-cliff-core/src/config.rs
@@ -126,6 +126,9 @@ pub struct RemoteConfig {
 	/// GitLab remote.
 	#[serde(default)]
 	pub gitlab:    Remote,
+	/// Gitea remote.
+	#[serde(default)]
+	pub gitea:     Remote,
 	/// Bitbucket remote.
 	#[serde(default)]
 	pub bitbucket: Remote,
diff --git a/git-cliff-core/src/error.rs b/git-cliff-core/src/error.rs
index dc31658d37..5f96b589d3 100644
--- a/git-cliff-core/src/error.rs
+++ b/git-cliff-core/src/error.rs
@@ -77,17 +77,17 @@ pub enum Error {
 	SemverError(#[from] semver::Error),
 	/// The errors that may occur when processing a HTTP request.
 	#[error("HTTP client error: `{0}`")]
-	#[cfg(any(feature = "github", feature = "gitlab", feature = "bitbucket"))]
+	#[cfg(feature = "remote")]
 	HttpClientError(#[from] reqwest::Error),
 	/// The errors that may occur while constructing the HTTP client with
 	/// middleware.
 	#[error("HTTP client with middleware error: `{0}`")]
-	#[cfg(any(feature = "github", feature = "gitlab", feature = "bitbucket"))]
+	#[cfg(feature = "remote")]
 	HttpClientMiddlewareError(#[from] reqwest_middleware::Error),
 	/// A possible error when converting a HeaderValue from a string or byte
 	/// slice.
 	#[error("HTTP header error: `{0}`")]
-	#[cfg(any(feature = "github", feature = "gitlab", feature = "bitbucket"))]
+	#[cfg(feature = "remote")]
 	HttpHeaderError(#[from] reqwest::header::InvalidHeaderValue),
 	/// Error that may occur during handling pages.
 	#[error("Pagination error: `{0}`")]
diff --git a/git-cliff-core/src/lib.rs b/git-cliff-core/src/lib.rs
index 49a2639fdd..6f78c91ea4 100644
--- a/git-cliff-core/src/lib.rs
+++ b/git-cliff-core/src/lib.rs
@@ -27,7 +27,7 @@ pub mod error;
 /// Common release type.
 pub mod release;
 /// Remote handler.
-#[cfg(any(feature = "github", feature = "gitlab", feature = "bitbucket"))]
+#[cfg(feature = "remote")]
 #[allow(async_fn_in_trait)]
 pub mod remote;
 /// Git repository.
diff --git a/git-cliff-core/src/release.rs b/git-cliff-core/src/release.rs
index b256813e16..1cea7580b6 100644
--- a/git-cliff-core/src/release.rs
+++ b/git-cliff-core/src/release.rs
@@ -1,7 +1,7 @@
 use crate::commit::Commit;
 use crate::config::Bump;
 use crate::error::Result;
-#[cfg(any(feature = "github", feature = "gitlab", feature = "bitbucket"))]
+#[cfg(feature = "remote")]
 use crate::remote::{
 	RemoteCommit,
 	RemoteContributor,
@@ -37,6 +37,9 @@ pub struct Release<'a> {
 	#[cfg(feature = "gitlab")]
 	pub gitlab:    RemoteReleaseMetadata,
 	/// Contributors.
+	#[cfg(feature = "gitea")]
+	pub gitea:     RemoteReleaseMetadata,
+	/// Contributors.
 	#[cfg(feature = "bitbucket")]
 	pub bitbucket: RemoteReleaseMetadata,
 }
@@ -47,6 +50,9 @@ crate::update_release_metadata!(github, update_github_metadata);
 #[cfg(feature = "gitlab")]
 crate::update_release_metadata!(gitlab, update_gitlab_metadata);
 
+#[cfg(feature = "gitea")]
+crate::update_release_metadata!(gitea, update_gitea_metadata);
+
 #[cfg(feature = "bitbucket")]
 crate::update_release_metadata!(bitbucket, update_bitbucket_metadata);
 
@@ -162,6 +168,10 @@ mod test {
 				gitlab: crate::remote::RemoteReleaseMetadata {
 					contributors: vec![],
 				},
+				#[cfg(feature = "gitea")]
+				gitea: crate::remote::RemoteReleaseMetadata {
+					contributors: vec![],
+				},
 				#[cfg(feature = "bitbucket")]
 				bitbucket: crate::remote::RemoteReleaseMetadata {
 					contributors: vec![],
@@ -316,8 +326,8 @@ mod test {
 		};
 
 		let mut release = Release {
-			version:   None,
-			commits:   vec![
+			version: None,
+			commits: vec![
 				Commit::from(String::from(
 					"1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github \
 					 integration",
@@ -341,16 +351,22 @@ mod test {
 			],
 			commit_id: None,
 			timestamp: 0,
-			previous:  Some(Box::new(Release {
+			previous: Some(Box::new(Release {
 				version: Some(String::from("1.0.0")),
 				..Default::default()
 			})),
-			github:    RemoteReleaseMetadata {
+			github: RemoteReleaseMetadata {
+				contributors: vec![],
+			},
+			#[cfg(feature = "gitlab")]
+			gitlab: RemoteReleaseMetadata {
 				contributors: vec![],
 			},
-			gitlab:    RemoteReleaseMetadata {
+			#[cfg(feature = "gitea")]
+			gitea: RemoteReleaseMetadata {
 				contributors: vec![],
 			},
+			#[cfg(feature = "bitbucket")]
 			bitbucket: RemoteReleaseMetadata {
 				contributors: vec![],
 			},
@@ -595,8 +611,8 @@ mod test {
 		};
 
 		let mut release = Release {
-			version:   None,
-			commits:   vec![
+			version: None,
+			commits: vec![
 				Commit::from(String::from(
 					"1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github \
 					 integration",
@@ -620,16 +636,22 @@ mod test {
 			],
 			commit_id: None,
 			timestamp: 0,
-			previous:  Some(Box::new(Release {
+			previous: Some(Box::new(Release {
 				version: Some(String::from("1.0.0")),
 				..Default::default()
 			})),
-			github:    RemoteReleaseMetadata {
+			#[cfg(feature = "github")]
+			github: RemoteReleaseMetadata {
 				contributors: vec![],
 			},
-			gitlab:    RemoteReleaseMetadata {
+			gitlab: RemoteReleaseMetadata {
 				contributors: vec![],
 			},
+			#[cfg(feature = "gitea")]
+			gitea: RemoteReleaseMetadata {
+				contributors: vec![],
+			},
+			#[cfg(feature = "bitbucket")]
 			bitbucket: RemoteReleaseMetadata {
 				contributors: vec![],
 			},
@@ -920,4 +942,290 @@ mod test {
 
 		Ok(())
 	}
+
+	#[cfg(feature = "gitea")]
+	#[test]
+	fn update_gitea_metadata() -> Result<()> {
+		use crate::remote::gitea::{
+			GiteaCommit,
+			GiteaCommitAuthor,
+			GiteaPullRequest,
+			PullRequestLabel,
+		};
+
+		let mut release = Release {
+			version: None,
+			commits: vec![
+				Commit::from(String::from(
+					"1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github \
+					 integration",
+				)),
+				Commit::from(String::from(
+					"21f6aa587fcb772de13f2fde0e92697c51f84162 fix github \
+					 integration",
+				)),
+				Commit::from(String::from(
+					"35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973 update metadata",
+				)),
+				Commit::from(String::from(
+					"4d3ffe4753b923f4d7807c490e650e6624a12074 do some stuff",
+				)),
+				Commit::from(String::from(
+					"5a55e92e5a62dc5bf9872ffb2566959fad98bd05 alright",
+				)),
+				Commit::from(String::from(
+					"6c34967147560ea09658776d4901709139b4ad66 should be fine",
+				)),
+			],
+			commit_id: None,
+			timestamp: 0,
+			previous: Some(Box::new(Release {
+				version: Some(String::from("1.0.0")),
+				..Default::default()
+			})),
+			#[cfg(feature = "github")]
+			github: RemoteReleaseMetadata {
+				contributors: vec![],
+			},
+			#[cfg(feature = "gitlab")]
+			gitlab: RemoteReleaseMetadata {
+				contributors: vec![],
+			},
+			gitea: RemoteReleaseMetadata {
+				contributors: vec![],
+			},
+			#[cfg(feature = "bitbucket")]
+			bitbucket: RemoteReleaseMetadata {
+				contributors: vec![],
+			},
+		};
+		release.update_gitea_metadata(
+			vec![
+				GiteaCommit {
+					sha:    String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
+					author: Some(GiteaCommitAuthor {
+						login: Some(String::from("orhun")),
+					}),
+				},
+				GiteaCommit {
+					sha:    String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
+					author: Some(GiteaCommitAuthor {
+						login: Some(String::from("orhun")),
+					}),
+				},
+				GiteaCommit {
+					sha:    String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
+					author: Some(GiteaCommitAuthor {
+						login: Some(String::from("nuhro")),
+					}),
+				},
+				GiteaCommit {
+					sha:    String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
+					author: Some(GiteaCommitAuthor {
+						login: Some(String::from("awesome_contributor")),
+					}),
+				},
+				GiteaCommit {
+					sha:    String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
+					author: Some(GiteaCommitAuthor {
+						login: Some(String::from("orhun")),
+					}),
+				},
+				GiteaCommit {
+					sha:    String::from("6c34967147560ea09658776d4901709139b4ad66"),
+					author: Some(GiteaCommitAuthor {
+						login: Some(String::from("someone")),
+					}),
+				},
+				GiteaCommit {
+					sha:    String::from("0c34967147560e809658776d4901709139b4ad68"),
+					author: Some(GiteaCommitAuthor {
+						login: Some(String::from("idk")),
+					}),
+				},
+				GiteaCommit {
+					sha:    String::from("kk34967147560e809658776d4901709139b4ad68"),
+					author: None,
+				},
+				GiteaCommit {
+					sha:    String::new(),
+					author: None,
+				},
+			]
+			.into_iter()
+			.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
+			.collect(),
+			vec![
+				GiteaPullRequest {
+					title:            Some(String::from("1")),
+					number:           42,
+					merge_commit_sha: Some(String::from(
+						"1d244937ee6ceb8e0314a4a201ba93a7a61f2071",
+					)),
+					labels:           vec![PullRequestLabel {
+						name: String::from("rust"),
+					}],
+				},
+				GiteaPullRequest {
+					title:            Some(String::from("2")),
+					number:           66,
+					merge_commit_sha: Some(String::from(
+						"21f6aa587fcb772de13f2fde0e92697c51f84162",
+					)),
+					labels:           vec![PullRequestLabel {
+						name: String::from("rust"),
+					}],
+				},
+				GiteaPullRequest {
+					title:            Some(String::from("3")),
+					number:           53,
+					merge_commit_sha: Some(String::from(
+						"35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973",
+					)),
+					labels:           vec![PullRequestLabel {
+						name: String::from("deps"),
+					}],
+				},
+				GiteaPullRequest {
+					title:            Some(String::from("4")),
+					number:           1000,
+					merge_commit_sha: Some(String::from(
+						"4d3ffe4753b923f4d7807c490e650e6624a12074",
+					)),
+					labels:           vec![PullRequestLabel {
+						name: String::from("deps"),
+					}],
+				},
+				GiteaPullRequest {
+					title:            Some(String::from("5")),
+					number:           999999,
+					merge_commit_sha: Some(String::from(
+						"5a55e92e5a62dc5bf9872ffb2566959fad98bd05",
+					)),
+					labels:           vec![PullRequestLabel {
+						name: String::from("github"),
+					}],
+				},
+			]
+			.into_iter()
+			.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
+			.collect(),
+		)?;
+		let expected_commits = vec![
+			Commit {
+				id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
+				message: String::from("add github integration"),
+				gitea: RemoteContributor {
+					username:      Some(String::from("orhun")),
+					pr_title:      Some(String::from("1")),
+					pr_number:     Some(42),
+					pr_labels:     vec![String::from("rust")],
+					is_first_time: false,
+				},
+				..Default::default()
+			},
+			Commit {
+				id: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
+				message: String::from("fix github integration"),
+				gitea: RemoteContributor {
+					username:      Some(String::from("orhun")),
+					pr_title:      Some(String::from("2")),
+					pr_number:     Some(66),
+					pr_labels:     vec![String::from("rust")],
+					is_first_time: false,
+				},
+				..Default::default()
+			},
+			Commit {
+				id: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
+				message: String::from("update metadata"),
+				gitea: RemoteContributor {
+					username:      Some(String::from("nuhro")),
+					pr_title:      Some(String::from("3")),
+					pr_number:     Some(53),
+					pr_labels:     vec![String::from("deps")],
+					is_first_time: false,
+				},
+				..Default::default()
+			},
+			Commit {
+				id: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
+				message: String::from("do some stuff"),
+				gitea: RemoteContributor {
+					username:      Some(String::from("awesome_contributor")),
+					pr_title:      Some(String::from("4")),
+					pr_number:     Some(1000),
+					pr_labels:     vec![String::from("deps")],
+					is_first_time: false,
+				},
+				..Default::default()
+			},
+			Commit {
+				id: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
+				message: String::from("alright"),
+				gitea: RemoteContributor {
+					username:      Some(String::from("orhun")),
+					pr_title:      Some(String::from("5")),
+					pr_number:     Some(999999),
+					pr_labels:     vec![String::from("github")],
+					is_first_time: false,
+				},
+				..Default::default()
+			},
+			Commit {
+				id: String::from("6c34967147560ea09658776d4901709139b4ad66"),
+				message: String::from("should be fine"),
+				gitea: RemoteContributor {
+					username:      Some(String::from("someone")),
+					pr_title:      None,
+					pr_number:     None,
+					pr_labels:     vec![],
+					is_first_time: false,
+				},
+				..Default::default()
+			},
+		];
+		assert_eq!(expected_commits, release.commits);
+
+		release
+			.gitea
+			.contributors
+			.sort_by(|a, b| a.pr_number.cmp(&b.pr_number));
+
+		let expected_metadata = RemoteReleaseMetadata {
+			contributors: vec![
+				RemoteContributor {
+					username:      Some(String::from("someone")),
+					pr_title:      None,
+					pr_number:     None,
+					pr_labels:     vec![],
+					is_first_time: true,
+				},
+				RemoteContributor {
+					username:      Some(String::from("orhun")),
+					pr_title:      Some(String::from("1")),
+					pr_number:     Some(42),
+					pr_labels:     vec![String::from("rust")],
+					is_first_time: true,
+				},
+				RemoteContributor {
+					username:      Some(String::from("nuhro")),
+					pr_title:      Some(String::from("3")),
+					pr_number:     Some(53),
+					pr_labels:     vec![String::from("deps")],
+					is_first_time: true,
+				},
+				RemoteContributor {
+					username:      Some(String::from("awesome_contributor")),
+					pr_title:      Some(String::from("4")),
+					pr_number:     Some(1000),
+					pr_labels:     vec![String::from("deps")],
+					is_first_time: true,
+				},
+			],
+		};
+		assert_eq!(expected_metadata, release.gitea);
+
+		Ok(())
+	}
 }
diff --git a/git-cliff-core/src/remote/gitea.rs b/git-cliff-core/src/remote/gitea.rs
new file mode 100644
index 0000000000..251776c300
--- /dev/null
+++ b/git-cliff-core/src/remote/gitea.rs
@@ -0,0 +1,184 @@
+use crate::config::Remote;
+use crate::error::*;
+use reqwest_middleware::ClientWithMiddleware;
+use serde::{
+	Deserialize,
+	Serialize,
+};
+use std::env;
+
+use super::*;
+
+/// Gitea API url.
+const GITEA_API_URL: &str = "https://codeberg.org";
+
+/// Environment variable for overriding the Gitea REST API url.
+const GITEA_API_URL_ENV: &str = "GITEA_API_URL";
+
+/// Log message to show while fetching data from Gitea.
+pub const START_FETCHING_MSG: &str = "Retrieving data from Gitea...";
+
+/// Log message to show when done fetching from Gitea.
+pub const FINISHED_FETCHING_MSG: &str = "Done fetching Gitea data.";
+
+/// Template variables related to this remote.
+pub(crate) const TEMPLATE_VARIABLES: &[&str] = &["gitea", "commit.gitea"];
+
+/// Representation of a single commit.
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct GiteaCommit {
+	/// SHA.
+	pub sha:    String,
+	/// Author of the commit.
+	pub author: Option<GiteaCommitAuthor>,
+}
+
+impl RemoteCommit for GiteaCommit {
+	fn id(&self) -> String {
+		self.sha.clone()
+	}
+
+	fn username(&self) -> Option<String> {
+		self.author.clone().and_then(|v| v.login)
+	}
+}
+
+impl RemoteEntry for GiteaCommit {
+	fn url(_id: i64, api_url: &str, remote: &Remote, page: i32) -> String {
+		format!(
+			"{}/api/v1/repos/{}/{}/commits?limit={MAX_PAGE_SIZE}&page={page}",
+			api_url, remote.owner, remote.repo
+		)
+	}
+	fn buffer_size() -> usize {
+		10
+	}
+
+	fn early_exit(&self) -> bool {
+		false
+	}
+}
+
+/// Author of the commit.
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct GiteaCommitAuthor {
+	/// Username.
+	pub login: Option<String>,
+}
+
+/// Label of the pull request.
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PullRequestLabel {
+	/// Name of the label.
+	pub name: String,
+}
+
+/// Representation of a single pull request.
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct GiteaPullRequest {
+	/// Pull request number.
+	pub number:           i64,
+	/// Pull request title.
+	pub title:            Option<String>,
+	/// SHA of the merge commit.
+	pub merge_commit_sha: Option<String>,
+	/// Labels of the pull request.
+	pub labels:           Vec<PullRequestLabel>,
+}
+
+impl RemotePullRequest for GiteaPullRequest {
+	fn number(&self) -> i64 {
+		self.number
+	}
+
+	fn title(&self) -> Option<String> {
+		self.title.clone()
+	}
+
+	fn labels(&self) -> Vec<String> {
+		self.labels.iter().map(|v| v.name.clone()).collect()
+	}
+
+	fn merge_commit(&self) -> Option<String> {
+		self.merge_commit_sha.clone()
+	}
+}
+
+impl RemoteEntry for GiteaPullRequest {
+	fn url(_id: i64, api_url: &str, remote: &Remote, page: i32) -> String {
+		format!(
+			"{}/api/v1/repos/{}/{}/pulls?limit={MAX_PAGE_SIZE}&page={page}&\
+			 state=closed",
+			api_url, remote.owner, remote.repo
+		)
+	}
+
+	fn buffer_size() -> usize {
+		5
+	}
+
+	fn early_exit(&self) -> bool {
+		false
+	}
+}
+
+/// HTTP client for handling Gitea REST API requests.
+#[derive(Debug, Clone)]
+pub struct GiteaClient {
+	/// Remote.
+	remote: Remote,
+	/// HTTP client.
+	client: ClientWithMiddleware,
+}
+
+/// Constructs a Gitea client from the remote configuration.
+impl TryFrom<Remote> for GiteaClient {
+	type Error = Error;
+	fn try_from(remote: Remote) -> Result<Self> {
+		Ok(Self {
+			client: create_remote_client(&remote, "application/json")?,
+			remote,
+		})
+	}
+}
+
+impl RemoteClient for GiteaClient {
+	fn api_url() -> String {
+		env::var(GITEA_API_URL_ENV)
+			.ok()
+			.unwrap_or_else(|| GITEA_API_URL.to_string())
+	}
+
+	fn remote(&self) -> Remote {
+		self.remote.clone()
+	}
+
+	fn client(&self) -> ClientWithMiddleware {
+		self.client.clone()
+	}
+}
+
+impl GiteaClient {
+	/// Fetches the Gitea API and returns the commits.
+	pub async fn get_commits(&self) -> Result<Vec<Box<dyn RemoteCommit>>> {
+		Ok(self
+			.fetch::<GiteaCommit>(0)
+			.await?
+			.into_iter()
+			.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
+			.collect())
+	}
+
+	/// Fetches the Gitea API and returns the pull requests.
+	pub async fn get_pull_requests(
+		&self,
+	) -> Result<Vec<Box<dyn RemotePullRequest>>> {
+		Ok(self
+			.fetch::<GiteaPullRequest>(0)
+			.await?
+			.into_iter()
+			.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
+			.collect())
+	}
+}
diff --git a/git-cliff-core/src/remote/mod.rs b/git-cliff-core/src/remote/mod.rs
index 3349df0c2d..f4bf29c982 100644
--- a/git-cliff-core/src/remote/mod.rs
+++ b/git-cliff-core/src/remote/mod.rs
@@ -10,6 +10,10 @@ pub mod gitlab;
 #[cfg(feature = "bitbucket")]
 pub mod bitbucket;
 
+/// Gitea client.
+#[cfg(feature = "gitea")]
+pub mod gitea;
+
 use crate::config::Remote;
 use crate::error::{
 	Error,
@@ -43,6 +47,7 @@ use serde::{
 	Deserialize,
 	Serialize,
 };
+use std::fmt::Debug;
 use std::hash::{
 	Hash,
 	Hasher,
diff --git a/git-cliff-core/src/template.rs b/git-cliff-core/src/template.rs
index 93506b0103..d5dbf93012 100644
--- a/git-cliff-core/src/template.rs
+++ b/git-cliff-core/src/template.rs
@@ -132,7 +132,7 @@ impl Template {
 	}
 
 	/// Returns `true` if the template contains one of the given variables.
-	#[cfg(any(feature = "github", feature = "gitlab", feature = "bitbucket"))]
+	#[cfg(feature = "remote")]
 	pub(crate) fn contains_variable(&self, variables: &[&str]) -> bool {
 		variables
 			.iter()
@@ -213,6 +213,10 @@ mod test {
 			gitlab: crate::remote::RemoteReleaseMetadata {
 				contributors: vec![],
 			},
+			#[cfg(feature = "gitea")]
+			gitea: crate::remote::RemoteReleaseMetadata {
+				contributors: vec![],
+			},
 			#[cfg(feature = "bitbucket")]
 			bitbucket: crate::remote::RemoteReleaseMetadata {
 				contributors: vec![],
diff --git a/git-cliff/Cargo.toml b/git-cliff/Cargo.toml
index af9ae0e3be..96d86a3157 100644
--- a/git-cliff/Cargo.toml
+++ b/git-cliff/Cargo.toml
@@ -23,15 +23,19 @@ path = "src/bin/mangen.rs"
 
 [features]
 # check for new versions
-default = ["update-informer", "github", "gitlab", "bitbucket"]
+default = ["update-informer", "github", "gitlab", "gitea", "bitbucket"]
 # inform about new releases
 update-informer = ["dep:update-informer"]
+# enable remote repository integration
+remote = ["dep:indicatif"]
 # enable GitHub integration
-github = ["git-cliff-core/github", "dep:indicatif"]
+github = ["remote", "git-cliff-core/github"]
 # enable GitLab integration
-gitlab = ["git-cliff-core/gitlab", "dep:indicatif"]
+gitlab = ["remote", "git-cliff-core/gitlab"]
+# enable Gitea integration
+gitea = ["remote", "git-cliff-core/gitea"]
 # enable Bitbucket integration
-bitbucket = ["git-cliff-core/bitbucket", "dep:indicatif"]
+bitbucket = ["remote", "git-cliff-core/bitbucket"]
 
 [dependencies]
 glob.workspace = true
diff --git a/git-cliff/src/args.rs b/git-cliff/src/args.rs
index 175df1d998..3d52a7b697 100644
--- a/git-cliff/src/args.rs
+++ b/git-cliff/src/args.rs
@@ -261,6 +261,24 @@ pub struct Opt {
 		hide = !cfg!(feature = "gitlab"),
 	)]
 	pub gitlab_repo:     Option<RemoteValue>,
+	/// Sets the Gitea API token.
+	#[arg(
+		long,
+		env = "GITEA_TOKEN",
+		value_name = "TOKEN",
+		hide_env_values = true,
+		hide = !cfg!(feature = "gitea"),
+	)]
+	pub gitea_token:     Option<SecretString>,
+	/// Sets the GitLab repository.
+	#[arg(
+		long,
+		env = "GITEA_REPO",
+		value_parser = clap::value_parser!(RemoteValue),
+		value_name = "OWNER/REPO",
+		hide = !cfg!(feature = "gitea"),
+	)]
+	pub gitea_repo:      Option<RemoteValue>,
 	/// Sets the Bitbucket API token.
 	#[arg(
 		long,
diff --git a/git-cliff/src/lib.rs b/git-cliff/src/lib.rs
index f8cc86c026..3939919a41 100644
--- a/git-cliff/src/lib.rs
+++ b/git-cliff/src/lib.rs
@@ -134,6 +134,17 @@ fn process_repository<'a>(
 				debug!("Failed to get remote from GitLab repository: {:?}", e);
 			}
 		}
+	} else if !config.remote.gitea.is_set() {
+		match repository.upstream_remote() {
+			Ok(remote) => {
+				debug!("No Gitea remote is set, using remote: {}", remote);
+				config.remote.gitea.owner = remote.owner;
+				config.remote.gitea.repo = remote.repo;
+			}
+			Err(e) => {
+				debug!("Failed to get remote from Gitea repository: {:?}", e);
+			}
+		}
 	} else if !config.remote.bitbucket.is_set() {
 		match repository.upstream_remote() {
 			Ok(remote) => {
@@ -437,6 +448,9 @@ pub fn run(mut args: Opt) -> Result<()> {
 	if args.gitlab_token.is_some() {
 		config.remote.gitlab.token.clone_from(&args.gitlab_token);
 	}
+	if args.gitea_token.is_some() {
+		config.remote.gitea.token.clone_from(&args.gitea_token);
+	}
 	if args.bitbucket_token.is_some() {
 		config
 			.remote
diff --git a/git-cliff/src/logger.rs b/git-cliff/src/logger.rs
index 76869b5093..17e80d399f 100644
--- a/git-cliff/src/logger.rs
+++ b/git-cliff/src/logger.rs
@@ -10,7 +10,7 @@ use git_cliff_core::error::{
 	Error,
 	Result,
 };
-#[cfg(any(feature = "github", feature = "gitlab", feature = "bitbucket"))]
+#[cfg(feature = "remote")]
 use indicatif::{
 	ProgressBar,
 	ProgressStyle,
@@ -66,7 +66,7 @@ fn colored_level(style: &mut Style, level: Level) -> StyledValue<'_, &'static st
 	}
 }
 
-#[cfg(any(feature = "github", feature = "gitlab", feature = "bitbucket"))]
+#[cfg(feature = "remote")]
 lazy_static::lazy_static! {
 	/// Lazily initialized progress bar.
 	pub static ref PROGRESS_BAR: ProgressBar = {
@@ -144,6 +144,23 @@ pub fn init() -> Result<()> {
 			}
 		}
 
+		#[cfg(feature = "gitea")]
+		{
+			let message = record.args().to_string();
+			if message.starts_with(git_cliff_core::remote::gitea::START_FETCHING_MSG)
+			{
+				PROGRESS_BAR
+					.enable_steady_tick(std::time::Duration::from_millis(80));
+				PROGRESS_BAR.set_message(message);
+				return Ok(());
+			} else if message
+				.starts_with(git_cliff_core::remote::gitea::FINISHED_FETCHING_MSG)
+			{
+				PROGRESS_BAR.finish_and_clear();
+				return Ok(());
+			}
+		}
+
 		#[cfg(feature = "bitbucket")]
 		{
 			let message = record.args().to_string();
diff --git a/website/docs/configuration/remote.md b/website/docs/configuration/remote.md
index fa085c8362..7d5706917c 100644
--- a/website/docs/configuration/remote.md
+++ b/website/docs/configuration/remote.md
@@ -2,7 +2,7 @@
 
 This section contains the Git remote related configuration options.
 
-You can configure a remote for GitHub, GitLab or Bitbucket as follows:
+You can configure a remote for GitHub, GitLab, Gitea/Forgejo or Bitbucket as follows:
 
 ```toml
 [remote.github]
@@ -11,12 +11,13 @@ repo = "git-cliff"
 token = ""
 ```
 
-Change this to `remote.gitlab` or `remote.bitbucket` accordingly to your project.
+Change this to `remote.gitlab`, `remote.gitea` or `remote.bitbucket` accordingly to your project.
 
 :::tip
 
 - See the [GitHub integration](/docs/integration/github).
 - See the [GitLab integration](/docs/integration/gitlab).
+- See the [Gitea integration](/docs/integration/gitea).
 - See the [Bitbucket integration](/docs/integration/bitbucket).
 
 :::
@@ -37,7 +38,7 @@ e.g.
 git cliff --github-repo orhun/git-cliff
 ```
 
-Same applies for GitLab/Bitbucket with `--gitlab-repo`/`--bitbucket-repo` and `GITLAB_REPO`/`BITBUCKET_REPO` environment variables.
+Same applies for GitLab/Bitbucket with `--gitlab-repo`/`--gitea-repo`/`--bitbucket-repo` and `GITLAB_REPO`/`GITEA_REPO`/`BITBUCKET_REPO` environment variables.
 
 ### token
 
@@ -49,4 +50,4 @@ If you are using GitHub, then you can also pass this value via `--github-token`
 git cliff --github-token <TOKEN>
 ```
 
-Same applies for GitLab/Bitbucket with `--gitlab-token`/`--bitbucket-token` and `GITLAB_TOKEN`/`BITBUCKET_TOKEN` environment variables.
+Same applies for GitLab/Bitbucket with `--gitlab-token`/`--gitea-token`/`--bitbucket-token` and `GITLAB_TOKEN`/`GITEA_TOKEN`/`BITBUCKET_TOKEN` environment variables.
diff --git a/website/docs/integration/bitbucket.md b/website/docs/integration/bitbucket.md
index 459dd5d5e3..7295bb9ce3 100644
--- a/website/docs/integration/bitbucket.md
+++ b/website/docs/integration/bitbucket.md
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 3
+sidebar_position: 4
 ---
 
 # Bitbucket Integration 📘
diff --git a/website/docs/integration/gitea.md b/website/docs/integration/gitea.md
new file mode 100644
index 0000000000..8020bd243f
--- /dev/null
+++ b/website/docs/integration/gitea.md
@@ -0,0 +1,179 @@
+---
+sidebar_position: 3
+---
+
+# Gitea Integration 🍵
+
+:::warning
+
+This is still an experimental feature, please [report bugs](https://github.com/orhun/git-cliff/issues/new/choose).
+
+:::
+
+:::note
+
+If you have built from source, enable the `gitea` feature flag for the integration to work.
+
+:::
+
+For projects hosted on Gitea/Forgejo, you can use **git-cliff** to add the following to your changelog:
+
+- Gitea usernames
+- Contributors list (all contributors / first time)
+- Pull request links (associated with the commits)
+
+## Setting up the remote
+
+As default, remote upstream URL is automatically retrieved from the Git repository.
+
+If that doesn't work or if you want to set a custom remote, there are a couple of ways of doing it:
+
+- Use the [remote option](/docs/configuration/remote) in the configuration file:
+
+```toml
+[remote.gitea]
+owner = "orhun"
+repo = "git-cliff"
+token = "***"
+```
+
+- Use the `--gitea-repo` argument (takes values in `OWNER/REPO` format, e.g. "orhun/git-cliff")
+
+- Use the `GITEA_REPO` environment variable (same format as `--gitea-repo`)
+
+## Authentication
+
+:::tip
+
+[Gitea REST API](https://gitea.com/api/swagger) is being used to retrieve data from Gitea.
+It does not require authentication for public repositories. If your project uses a private
+repository, you need to create an access token under *Settings* > *Applications* > *Access tokens*.
+
+:::
+
+To set an access token, you can use the [configuration file](/docs/configuration/remote) (not recommended), `--gitea-token` argument or `GITEA_TOKEN` environment variable.
+
+For example:
+
+```bash
+GITEA_TOKEN="***" git cliff --gitea-repo "orhun/git-cliff"
+```
+
+:::tip
+
+You can use the `GITEA_API_URL` environment variable want to override the API URL. This is useful if you are using your own Gitea instance.
+
+:::
+
+## Templating
+
+:::tip
+
+See the [templating documentation](/docs/category/templating) for general information about how the template engine works.
+
+:::
+
+### Remote
+
+You can use the following [context](/docs/templating/context) for adding the remote to the changelog:
+
+```json
+{
+  "gitea": {
+    "owner": "orhun",
+    "repo": "git-cliff"
+  }
+}
+```
+
+For example:
+
+```jinja2
+https://codeberg.org/{{ remote.gitea.owner }}/{{ remote.gitea.repo }}/commits/tag/{{ version }}
+```
+
+### Commit authors
+
+For each commit, Gitea related values are added as a nested object (named `gitea`) to the [template context](/docs/templating/context):
+
+```json
+{
+  "id": "8edec7fd50f703811d55f14a3c5f0fd02b43d9e7",
+  "message": "refactor(config): remove unnecessary newline from configs\n",
+  "group": "🚜 Refactor",
+
+  "...": "<strip>",
+
+  "gitea": {
+    "username": "orhun",
+    "pr_title": "some things have changed",
+    "pr_number": 420,
+    "pr_labels": ["rust"],
+    "is_first_time": false
+  }
+}
+```
+
+This can be used in the template as follows:
+
+```
+{% for commit in commits %}
+  * {{ commit.message | split(pat="\n") | first | trim }}\
+    {% if commit.gitea.username %} by @{{ commit.gitea.username }}{%- endif %}\
+    {% if commit.gitea.pr_number %} in #{{ commit.gitea.pr_number }}{%- endif %}
+{%- endfor -%}
+```
+
+The will result in:
+
+```md
+- feat(commit): add merge_commit flag to the context by @orhun in #389
+- feat(args): set `CHANGELOG.md` as default missing value for output option by @sh-cho in #354
+```
+
+### Contributors
+
+For each release, following contributors data is added to the [template context](/docs/templating/context) as a nested object:
+
+```json
+{
+  "version": "v1.4.0",
+  "commits": [],
+  "commit_id": "0af9eb24888d1a8c9b2887fbe5427985582a0f26",
+  "timestamp": 0,
+  "previous": null,
+  "gitea": {
+    "contributors": [
+      {
+        "username": "orhun",
+        "pr_title": "some things have changed",
+        "pr_number": 420,
+        "pr_labels": ["rust"],
+        "is_first_time": true
+      },
+      {
+        "username": "cliffjumper",
+        "pr_title": "I love jumping",
+        "pr_number": 999,
+        "pr_labels": ["rust"],
+        "is_first_time": true
+      }
+    ]
+  }
+}
+```
+
+This can be used in the template as follows:
+
+```
+{% for contributor in gitea.contributors | filter(attribute="is_first_time", value=true) %}
+  * @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }}
+{%- endfor -%}
+```
+
+The will result in:
+
+```md
+- @orhun made their first contribution in #420
+- @cliffjumper made their first contribution in #999
+```
diff --git a/website/docs/integration/python.md b/website/docs/integration/python.md
index 623ec8500a..6615e1cdfc 100644
--- a/website/docs/integration/python.md
+++ b/website/docs/integration/python.md
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 5
+sidebar_position: 6
 ---
 
 # Python 🐍
diff --git a/website/docs/integration/rust.md b/website/docs/integration/rust.md
index be3c150181..75a1b68690 100644
--- a/website/docs/integration/rust.md
+++ b/website/docs/integration/rust.md
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 4
+sidebar_position: 5
 ---
 
 # Rust/Cargo 🦀


Test patch:
diff --git a/.github/fixtures/test-gitea-integration/cliff.toml b/.github/fixtures/test-gitea-integration/cliff.toml
new file mode 100644
index 0000000000..20c9b4e34f
--- /dev/null
+++ b/.github/fixtures/test-gitea-integration/cliff.toml
@@ -0,0 +1,46 @@
+[remote.gitea]
+owner = "ThetaDev"
+repo = "git-cliff-readme-example"
+
+[changelog]
+# template for the changelog body
+# https://keats.github.io/tera/docs/#introduction
+body = """
+## What's Changed
+{%- if version %} in {{ version }}{%- endif -%}
+{% for commit in commits %}
+  * {{ commit.message | split(pat="\n") | first | trim }}\
+    {% if commit.gitea.username %} by @{{ commit.gitea.username }}{%- endif -%}
+    {% if commit.gitea.pr_number %} in #{{ commit.gitea.pr_number }}{%- endif %}
+{%- endfor -%}
+
+{% if gitea.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
+  {% raw %}\n{% endraw -%}
+  ## New Contributors
+{%- endif %}\
+{% for contributor in gitea.contributors | filter(attribute="is_first_time", value=true) %}
+  * @{{ contributor.username }} made their first contribution
+    {%- if contributor.pr_number %} in \
+      [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
+    {%- endif %}
+{%- endfor -%}
+{% raw %}\n\n{% endraw -%}
+"""
+# remove the leading and trailing whitespace from the template
+trim = true
+# changelog footer
+footer = """
+<!-- generated by -cliff -->
+"""
+
+[git]
+# parse the commits based on https://www.conventionalcommits.org
+conventional_commits = false
+# filter out the commits that are not conventional
+filter_unconventional = true
+# process each line of a commit as an individual commit
+split_commits = false
+# regex for preprocessing the commit messages
+commit_preprocessors = [{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }]
+# protect breaking changes from being skipped due to matching a skipping commit_parser
+protect_breaking_commits = false
diff --git a/.github/fixtures/test-gitea-integration/commit.sh b/.github/fixtures/test-gitea-integration/commit.sh
new file mode 100755
index 0000000000..d15ef150ec
--- /dev/null
+++ b/.github/fixtures/test-gitea-integration/commit.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -e
+
+git remote add origin https://codeberg.org/ThetaDev/git-cliff-readme-example.git
+git pull origin master
+git fetch --tags
diff --git a/.github/fixtures/test-gitea-integration/expected.md b/.github/fixtures/test-gitea-integration/expected.md
new file mode 100644
index 0000000000..ff728a5e48
--- /dev/null
+++ b/.github/fixtures/test-gitea-integration/expected.md
@@ -0,0 +1,15 @@
+## What's Changed
+* Initial commit by @ThetaDev
+* docs(project): add README.md by @ThetaDev
+* feat(parser): add ability to parse arrays by @ThetaDev
+* fix(args): rename help argument due to conflict by @ThetaDev
+* docs(example)!: add tested usage example by @ThetaDev
+* refactor(parser): expose string functions by @ThetaDev
+* chore(release): add release script by @ThetaDev
+* feat(config): support multiple file formats by @ThetaDev
+* feat(cache): use cache while fetching pages by @ThetaDev
+
+## New Contributors
+* @ThetaDev made their first contribution
+
+<!-- generated by -cliff -->
diff --git a/.github/workflows/test-fixtures.yml b/.github/workflows/test-fixtures.yml
index 2e35cf7010..3bb433671b 100644
--- a/.github/workflows/test-fixtures.yml
+++ b/.github/workflows/test-fixtures.yml
@@ -18,6 +18,9 @@ jobs:
         include:
           - fixtures-name: new-fixture-template
           - fixtures-name: test-github-integration
+          - fixtures-name: test-gitlab-integration
+          - fixtures-name: test-gitea-integration
+          - fixtures-name: test-bitbucket-integration
           - fixtures-name: test-ignore-tags
           - fixtures-name: test-topo-order
             command: --latest
diff --git a/git-cliff-core/tests/integration_test.rs b/git-cliff-core/tests/integration_test.rs
index e93aeba64b..99d790dc2f 100644
--- a/git-cliff-core/tests/integration_test.rs
+++ b/git-cliff-core/tests/integration_test.rs
@@ -196,6 +196,10 @@ fn generate_changelog() -> Result<()> {
 			gitlab: git_cliff_core::remote::RemoteReleaseMetadata {
 				contributors: vec![],
 			},
+			#[cfg(feature = "gitea")]
+			gitea: git_cliff_core::remote::RemoteReleaseMetadata {
+				contributors: vec![],
+			},
 			#[cfg(feature = "bitbucket")]
 			bitbucket: git_cliff_core::remote::RemoteReleaseMetadata {
 				contributors: vec![],
@@ -232,6 +236,10 @@ fn generate_changelog() -> Result<()> {
 			gitlab: git_cliff_core::remote::RemoteReleaseMetadata {
 				contributors: vec![],
 			},
+			#[cfg(feature = "gitea")]
+			gitea: git_cliff_core::remote::RemoteReleaseMetadata {
+				contributors: vec![],
+			},
 			#[cfg(feature = "bitbucket")]
 			bitbucket: git_cliff_core::remote::RemoteReleaseMetadata {
 				contributors: vec![],