1
1
mirror of https://github.com/orhun/git-cliff.git synced 2024-09-11 15:05:30 +03:00

feat(codeberg): add Gitea support (#680)

* feat: add gitea integration

* fix: several fixes to get gitea working, add test fixture

* docs: update docs

* fix: cargo fmt

* fix: remove dbg trait

* fix: tests

* chore: remove vscode settings

* refactor: cleanup implementation

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
This commit is contained in:
ThetaDev 2024-06-15 19:42:15 +02:00 committed by GitHub
parent 5b58a3771d
commit 403d3dcd32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 947 additions and 52 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 -->

View File

@ -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

View File

@ -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 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 = [
"dep:reqwest",
"dep:http-cache-reqwest",
"dep:reqwest-middleware",
"dep:tokio",
"dep:futures",
]
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 }

View File

@ -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![],

View File

@ -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()

View File

@ -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,

View File

@ -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}`")]

View File

@ -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.

View File

@ -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![],
},
gitlab: RemoteReleaseMetadata {
#[cfg(feature = "gitlab")]
gitlab: RemoteReleaseMetadata {
contributors: vec![],
},
#[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(())
}
}

View File

@ -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())
}
}

View File

@ -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,

View File

@ -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![],

View File

@ -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![],

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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();

View File

@ -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.

View File

@ -1,5 +1,5 @@
---
sidebar_position: 3
sidebar_position: 4
---
# Bitbucket Integration 📘

View File

@ -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
```

View File

@ -1,5 +1,5 @@
---
sidebar_position: 5
sidebar_position: 6
---
# Python 🐍

View File

@ -1,5 +1,5 @@
---
sidebar_position: 4
sidebar_position: 5
---
# Rust/Cargo 🦀