Skip to content

Commit

Permalink
Draft: feat(bitbucket): Closes #566
Browse files Browse the repository at this point in the history
Signed-off-by: dark0dave <[email protected]>
  • Loading branch information
dark0dave committed May 29, 2024
1 parent 7cd96dd commit 4913d88
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 9 deletions.
10 changes: 10 additions & 0 deletions git-cliff-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
6 changes: 3 additions & 3 deletions git-cliff-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}`")]
Expand Down
2 changes: 1 addition & 1 deletion git-cliff-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion git-cliff-core/src/release.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
202 changes: 202 additions & 0 deletions git-cliff-core/src/remote/bitbucket.rs
Original file line number Diff line number Diff line change
@@ -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<BitbucketCommitAuthor>,
}

impl RemoteCommit for BitbucketCommit {
fn id(&self) -> String {
self.hash.clone()
}

fn username(&self) -> Option<String> {
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<T> {
/// 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<String>,
/// Link to the previous page if it exists.
pub previous: Option<String>,
/// List of Objects.
pub values: Vec<T>,
}

/// Author of the commit.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BitbucketCommitAuthor {
/// Username.
#[serde(rename = "type")]
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'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<String>,
/// Bitbucket Pull Request Merge Commit
pub merge_commit_sha: BitbucketPullRequestMergeCommit,
}

impl RemotePullRequest for BitbucketPullRequest {
fn number(&self) -> i64 {
self.id
}

fn title(&self) -> Option<String> {
self.title.clone()
}

fn labels(&self) -> Vec<String> {
vec![]
}

fn merge_commit(&self) -> Option<String> {
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<Remote> for BitbucketClient {
type Error = Error;
fn try_from(remote: Remote) -> Result<Self> {
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<Vec<Box<dyn RemoteCommit>>> {
Ok(self
.fetch::<BitbucketCommit>(0)
.await?
.into_iter()
.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
.collect())
}

/// Fetches the GitHub API and returns the pull requests.
pub async fn get_pull_requests(
&self,
) -> Result<Vec<Box<dyn RemotePullRequest>>> {
Ok(self
.fetch::<BitbucketPullRequest>(0)
.await?
.into_iter()
.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
.collect())
}
}
4 changes: 4 additions & 0 deletions git-cliff-core/src/remote/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion git-cliff-core/src/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 3 additions & 1 deletion git-cliff/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions git-cliff/src/logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = {
Expand Down

0 comments on commit 4913d88

Please sign in to comment.