From 4913d88ebc56da8796caad3fc7aed99e5ec90807 Mon Sep 17 00:00:00 2001 From: dark0dave Date: Wed, 29 May 2024 15:55:10 +0100 Subject: [PATCH] Draft: feat(bitbucket): Closes #566 Signed-off-by: dark0dave --- git-cliff-core/Cargo.toml | 10 ++ git-cliff-core/src/error.rs | 6 +- git-cliff-core/src/lib.rs | 2 +- git-cliff-core/src/release.rs | 2 +- git-cliff-core/src/remote/bitbucket.rs | 202 +++++++++++++++++++++++++ git-cliff-core/src/remote/mod.rs | 4 + git-cliff-core/src/template.rs | 2 +- git-cliff/Cargo.toml | 4 +- git-cliff/src/logger.rs | 4 +- 9 files changed, 227 insertions(+), 9 deletions(-) create mode 100644 git-cliff-core/src/remote/bitbucket.rs diff --git a/git-cliff-core/Cargo.toml b/git-cliff-core/Cargo.toml index 6cd527a5ee..7309b7eca7 100644 --- a/git-cliff-core/Cargo.toml +++ b/git-cliff-core/Cargo.toml @@ -37,6 +37,16 @@ gitlab = [ "dep:tokio", "dep:futures", ] +## 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", +] [dependencies] glob = { workspace = true, optional = true } diff --git a/git-cliff-core/src/error.rs b/git-cliff-core/src/error.rs index 1435c05c4a..90d6105e4a 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"))] + #[cfg(any(feature = "github", feature = "gitlab", feature="bitbucket"))] 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"))] + #[cfg(any(feature = "github", feature = "gitlab", feature="bitbucket"))] 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"))] + #[cfg(any(feature = "github", feature = "gitlab", feature="bitbucket"))] 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 50878ba404..58671ca367 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"))] +#[cfg(any(feature = "github", feature = "gitlab", feature="bitbucket"))] #[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 6f58e3462a..b566e55e0d 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"))] +#[cfg(any(feature = "github", feature = "gitlab", feature="bitbucket"))] use crate::remote::{ RemoteCommit, RemoteContributor, diff --git a/git-cliff-core/src/remote/bitbucket.rs b/git-cliff-core/src/remote/bitbucket.rs new file mode 100644 index 0000000000..2c83f72995 --- /dev/null +++ b/git-cliff-core/src/remote/bitbucket.rs @@ -0,0 +1,202 @@ +use crate::config::Remote; +use crate::error::*; +use reqwest_middleware::ClientWithMiddleware; +use serde::{ + Deserialize, + Serialize, +}; +use std::env; + +use super::*; + +/// Bitbucket REST API url. +const BITBUCKET_API_URL: &str = "https://api.bitbucket.org/2.0/repositories"; + +/// Environment variable for overriding the Bitbucket REST API url. +const BITBUCKET_API_URL_ENV: &str = "BITBUCKET_API_URL"; + +/// Log message to show while fetching data from Bitbucket. +pub const START_FETCHING_MSG: &str = "Retrieving data from BITBUCKET..."; + +/// Log message to show when done fetching from Bitbucket. +pub const FINISHED_FETCHING_MSG: &str = "Done fetching Bitbucket data."; + +/// Template variables related to this remote. +pub(crate) const TEMPLATE_VARIABLES: &[&str] = &["bitbucket", "commit.bitbucket"]; + +/// Representation of a single commit. +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BitbucketCommit { + /// SHA. + pub hash: String, + /// Author of the commit. + pub author: Option, +} + +impl RemoteCommit for BitbucketCommit { + fn id(&self) -> String { + self.hash.clone() + } + + fn username(&self) -> Option { + self.author.clone().and_then(|v| v.login) + } +} + +/// https://developer.atlassian.com/cloud/bitbucket/rest/api-group-commits/#api-repositories-workspace-repo-slug-commits-get +impl RemoteEntry for BitbucketCommit { + fn url(_id: i64, api_url: &str, remote: &Remote, page: i32) -> String { + format!( + "{}/{}/{}/commits?size={MAX_PAGE_SIZE}&page={page}", + api_url, remote.owner, remote.repo + ) + } + fn buffer_size() -> usize { + 10 + } +} + +/// Bitbucket Pagination Header +/// https://developer.atlassian.com/cloud/bitbucket/rest/intro/#pagination +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BitbucketPaginatin { + /// Total number of objects in the response. + pub size: i64, + /// Page number of the current results. + pub page: i64, + /// Current number of objects on the existing page. Globally, the minimum length is 10 and the maximum is 100. + pub pagelen: i64, + /// Link to the next page if it exists. + pub next: Option, + /// Link to the previous page if it exists. + pub previous: Option, + /// List of Objects. + pub values: Vec, +} + +/// Author of the commit. +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BitbucketCommitAuthor { + /// Username. + #[serde(rename = "type")] + pub login: Option, +} + +/// 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's merge commit +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BitbucketPullRequestMergeCommit { + /// SHA of the merge commit. + pub hash: String, +} + +/// Representation of a single pull request. +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BitbucketPullRequest { + /// Pull request number. + pub id: i64, + /// Pull request title. + pub title: Option, + /// Bitbucket Pull Request Merge Commit + pub merge_commit_sha: BitbucketPullRequestMergeCommit, +} + +impl RemotePullRequest for BitbucketPullRequest { + fn number(&self) -> i64 { + self.id + } + + fn title(&self) -> Option { + self.title.clone() + } + + fn labels(&self) -> Vec { + vec![] + } + + fn merge_commit(&self) -> Option { + Some(self.merge_commit_sha.hash.clone()) + } +} + +/// https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/#api-repositories-workspace-repo-slug-pullrequests-get +impl RemoteEntry for BitbucketPullRequest { + fn url(_id: i64, api_url: &str, remote: &Remote, page: i32) -> String { + format!( + "{}/{}/{}/pullrequests?per_page={MAX_PAGE_SIZE}&page={page}&state=MERGED", + api_url, remote.owner, remote.repo + ) + } + + fn buffer_size() -> usize { + 5 + } +} + +/// HTTP client for handling GitHub REST API requests. +#[derive(Debug, Clone)] +pub struct BitbucketClient { + /// Remote. + remote: Remote, + /// HTTP client. + client: ClientWithMiddleware, +} + +/// Constructs a GitHub client from the remote configuration. +impl TryFrom for BitbucketClient { + type Error = Error; + fn try_from(remote: Remote) -> Result { + Ok(Self { + client: create_remote_client(&remote, "application/vnd.github+json")?, + remote, + }) + } +} + +impl RemoteClient for BitbucketClient { + fn api_url() -> String { + env::var(BITBUCKET_API_URL_ENV) + .ok() + .unwrap_or_else(|| BITBUCKET_API_URL.to_string()) + } + + fn remote(&self) -> Remote { + self.remote.clone() + } + + fn client(&self) -> ClientWithMiddleware { + self.client.clone() + } +} + +impl BitbucketClient { + /// Fetches the Bitbucket API and returns the commits. + pub async fn get_commits(&self) -> Result>> { + Ok(self + .fetch::(0) + .await? + .into_iter() + .map(|v| Box::new(v) as Box) + .collect()) + } + + /// Fetches the GitHub API and returns the pull requests. + pub async fn get_pull_requests( + &self, + ) -> Result>> { + Ok(self + .fetch::(0) + .await? + .into_iter() + .map(|v| Box::new(v) as Box) + .collect()) + } +} diff --git a/git-cliff-core/src/remote/mod.rs b/git-cliff-core/src/remote/mod.rs index 8e8e40503e..152317050b 100644 --- a/git-cliff-core/src/remote/mod.rs +++ b/git-cliff-core/src/remote/mod.rs @@ -6,6 +6,10 @@ pub mod github; #[cfg(feature = "gitlab")] pub mod gitlab; +/// Bitbucket client. +#[cfg(feature = "bitbucket")] +pub mod bitbucket; + use crate::config::Remote; use crate::error::{ Error, diff --git a/git-cliff-core/src/template.rs b/git-cliff-core/src/template.rs index 2d3fc25274..9bde303d61 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"))] + #[cfg(any(feature = "github", feature = "gitlab", feature="bitbucket"))] pub(crate) fn contains_variable(&self, variables: &[&str]) -> bool { variables .iter() diff --git a/git-cliff/Cargo.toml b/git-cliff/Cargo.toml index 885ec22070..6d361169ee 100644 --- a/git-cliff/Cargo.toml +++ b/git-cliff/Cargo.toml @@ -23,13 +23,15 @@ path = "src/bin/mangen.rs" [features] # check for new versions -default = ["update-informer", "github", "gitlab"] +default = ["update-informer", "github", "gitlab", "bitbucket"] # inform about new releases update-informer = ["dep:update-informer"] # enable GitHub integration github = ["git-cliff-core/github", "dep:indicatif"] # enable GitLab integration gitlab = ["git-cliff-core/gitlab", "dep:indicatif"] +# enable Bitbucket integration +bitbucket = ["git-cliff-core/bitbucket", "dep:indicatif"] [dependencies] glob.workspace = true diff --git a/git-cliff/src/logger.rs b/git-cliff/src/logger.rs index 4f7cc60fa7..4c3f9030b1 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"))] +#[cfg(any(feature = "github", feature = "gitlab", feature="bitbucket"))] 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"))] +#[cfg(any(feature = "github", feature = "gitlab", feature="bitbucket"))] lazy_static::lazy_static! { /// Lazily initialized progress bar. pub static ref PROGRESS_BAR: ProgressBar = {