1
1
mirror of https://github.com/orhun/git-cliff.git synced 2024-10-03 18:27:14 +03:00

feat(bitbucket): add Bitbucket support (#663)

* Draft: feat(bitbucket): Closes  #566

Signed-off-by: dark0dave <dark0dave@mykolab.com>

* refactor: improve bitbucket integration

---------

Signed-off-by: dark0dave <dark0dave@mykolab.com>
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
This commit is contained in:
dark0dave 2024-06-02 18:49:37 +01:00 committed by GitHub
parent 2e1ed71876
commit 8ffc0548fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 809 additions and 85 deletions

View File

@ -1,5 +1,5 @@
name: Integration ⚙️
description: Report a bug or request a feature about GitHub/GitLab integration
description: Report a bug or request a feature about an integration (e.g GitHub/GitLab/Bitbucket)
labels: ["integration"]
assignees:
- orhun
@ -8,7 +8,7 @@ body:
attributes:
value: |
Thanks for taking the time to fill out this bug report!
Please see https://git-cliff.org/docs/category/integration for more information about GitHub/GitLab integration.
Please see https://git-cliff.org/docs/category/integration for more information about integrations.
- type: checkboxes
id: new-bug
attributes:

View File

@ -0,0 +1,46 @@
[remote.bitbucket]
owner = "orhunp"
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.bitbucket.username %} by @{{ commit.bitbucket.username }}{%- endif -%}
{% if commit.bitbucket.pr_number %} in #{{ commit.bitbucket.pr_number }}{%- endif %}
{%- endfor -%}
{% if bitbucket.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
{% raw %}\n{% endraw -%}
## New Contributors
{%- endif %}\
{% for contributor in bitbucket.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://bitbucket.org/orhunp/git-cliff-readme-example
git pull origin master
git fetch --tags

View File

@ -0,0 +1,16 @@
## What's Changed
* feat(config): support multiple file formats by @orhun <orhun@archlinux.org>
* feat(cache): use cache while fetching pages by @orhun <orhun@archlinux.org>
## What's Changed in v1.0.1
* refactor(parser): expose string functions by @orhun <orhun@archlinux.org>
* chore(release): add release script by @orhun <orhun@archlinux.org>
## What's Changed in v1.0.0
* Initial commit by @orhun <orhun@archlinux.org>
* docs(project): add README.md by @orhun <orhun@archlinux.org>
* feat(parser): add ability to parse arrays by @orhun <orhun@archlinux.org>
* fix(args): rename help argument due to conflict by @orhun <orhun@archlinux.org>
* docs(example)!: add tested usage example by @orhun <orhun@archlinux.org>
<!-- generated by -cliff -->

View File

@ -11,7 +11,7 @@ COPY --from=planner /app/recipe.json recipe.json
ENV CARGO_NET_GIT_FETCH_WITH_CLI=true
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
RUN cargo build --release --locked --no-default-features --features github --features gitlab
RUN cargo build --release --locked --no-default-features --features github --features gitlab --features bitbucket
RUN rm -f target/release/deps/git_cliff*
FROM debian:buster-slim as runner

View File

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

View File

@ -8,6 +8,8 @@ use crate::release::{
Release,
Releases,
};
#[cfg(feature = "bitbucket")]
use crate::remote::bitbucket::BitbucketClient;
#[cfg(feature = "github")]
use crate::remote::github::GitHubClient;
#[cfg(feature = "gitlab")]
@ -297,6 +299,62 @@ impl<'a> Changelog<'a> {
}
}
/// Returns the Bitbucket metadata needed for the changelog.
///
/// This function creates a multithread async runtime for handling the
///
/// requests. The following are fetched from the bitbucket REST API:
///
/// - Commits
/// - Pull requests
///
/// Each of these are paginated requests so they are being run in parallel
/// for speedup.
///
///
/// If no bitbucket related variable is used in the template then this
/// function returns empty vectors.
#[cfg(feature = "bitbucket")]
fn get_bitbucket_metadata(&self) -> Result<crate::remote::RemoteMetadata> {
use crate::remote::bitbucket;
if self
.body_template
.contains_variable(bitbucket::TEMPLATE_VARIABLES) ||
self.footer_template
.as_ref()
.map(|v| v.contains_variable(bitbucket::TEMPLATE_VARIABLES))
.unwrap_or(false)
{
warn!("You are using an experimental feature! Please report bugs at <https://git-cliff.org/issues>");
let bitbucket_client =
BitbucketClient::try_from(self.config.remote.bitbucket.clone())?;
info!(
"{} ({})",
bitbucket::START_FETCHING_MSG,
self.config.remote.bitbucket
);
let data = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(async {
let (commits, pull_requests) = tokio::try_join!(
bitbucket_client.get_commits(),
bitbucket_client.get_pull_requests()
)?;
debug!("Number of Bitbucket commits: {}", commits.len());
debug!(
"Number of Bitbucket pull requests: {}",
pull_requests.len()
);
Ok((commits, pull_requests))
});
info!("{}", bitbucket::FINISHED_FETCHING_MSG);
data
} else {
Ok((vec![], vec![]))
}
}
/// Increments the version for the unreleased changes based on semver.
pub fn bump_version(&mut self) -> Result<Option<String>> {
if let Some(ref mut last_release) = self.releases.iter_mut().next() {
@ -337,6 +395,14 @@ impl<'a> Changelog<'a> {
} else {
(vec![], vec![])
};
#[cfg(feature = "bitbucket")]
let (bitbucket_commits, bitbucket_pull_request) =
if self.config.remote.bitbucket.is_set() {
self.get_bitbucket_metadata()
.expect("Could not get bitbucket metadata")
} else {
(vec![], vec![])
};
let postprocessors = self
.config
.changelog
@ -363,6 +429,11 @@ impl<'a> Changelog<'a> {
gitlab_commits.clone(),
gitlab_merge_request.clone(),
)?;
#[cfg(feature = "bitbucket")]
release.update_bitbucket_metadata(
bitbucket_commits.clone(),
bitbucket_pull_request.clone(),
)?;
let write_result = write!(
out,
"{}",
@ -601,12 +672,17 @@ mod test {
limit_commits: None,
},
remote: RemoteConfig {
github: Remote {
github: Remote {
owner: String::from("coolguy"),
repo: String::from("awesome"),
token: None,
},
gitlab: Remote {
gitlab: Remote {
owner: String::from("coolguy"),
repo: String::from("awesome"),
token: None,
},
bitbucket: Remote {
owner: String::from("coolguy"),
repo: String::from("awesome"),
token: None,
@ -685,6 +761,10 @@ mod test {
gitlab: crate::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "bitbucket")]
bitbucket: crate::remote::RemoteReleaseMetadata {
contributors: vec![],
},
};
let releases = vec![
test_release.clone(),
@ -736,6 +816,10 @@ mod test {
gitlab: crate::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "bitbucket")]
bitbucket: crate::remote::RemoteReleaseMetadata {
contributors: vec![],
},
},
];
(config, releases)

View File

@ -128,6 +128,9 @@ pub struct Commit<'a> {
/// GitLab metadata of the commit.
#[cfg(feature = "gitlab")]
pub gitlab: crate::remote::RemoteContributor,
/// Bitbucket metadata of the commit.
#[cfg(feature = "bitbucket")]
pub bitbucket: crate::remote::RemoteContributor,
}
impl<'a> From<String> for Commit<'a> {
@ -443,6 +446,8 @@ impl Serialize for Commit<'_> {
commit.serialize_field("github", &self.github)?;
#[cfg(feature = "gitlab")]
commit.serialize_field("gitlab", &self.gitlab)?;
#[cfg(feature = "bitbucket")]
commit.serialize_field("bitbucket", &self.bitbucket)?;
commit.end()
}
}

View File

@ -122,10 +122,13 @@ pub struct GitConfig {
pub struct RemoteConfig {
/// GitHub remote.
#[serde(default)]
pub github: Remote,
pub github: Remote,
/// GitLab remote.
#[serde(default)]
pub gitlab: Remote,
pub gitlab: Remote,
/// Bitbucket remote.
#[serde(default)]
pub bitbucket: Remote,
}
/// A single 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"))]
#[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}`")]

View File

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

View File

@ -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,
@ -36,6 +36,9 @@ pub struct Release<'a> {
/// Contributors.
#[cfg(feature = "gitlab")]
pub gitlab: RemoteReleaseMetadata,
/// Contributors.
#[cfg(feature = "bitbucket")]
pub bitbucket: RemoteReleaseMetadata,
}
#[cfg(feature = "github")]
@ -44,6 +47,9 @@ crate::update_release_metadata!(github, update_github_metadata);
#[cfg(feature = "gitlab")]
crate::update_release_metadata!(gitlab, update_gitlab_metadata);
#[cfg(feature = "bitbucket")]
crate::update_release_metadata!(bitbucket, update_bitbucket_metadata);
impl<'a> Release<'a> {
/// Calculates the next version based on the commits.
///
@ -156,6 +162,10 @@ mod test {
gitlab: crate::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "bitbucket")]
bitbucket: crate::remote::RemoteReleaseMetadata {
contributors: vec![],
},
}
}
@ -341,6 +351,9 @@ mod test {
gitlab: RemoteReleaseMetadata {
contributors: vec![],
},
bitbucket: RemoteReleaseMetadata {
contributors: vec![],
},
};
release.update_github_metadata(
vec![
@ -617,6 +630,9 @@ mod test {
gitlab: RemoteReleaseMetadata {
contributors: vec![],
},
bitbucket: RemoteReleaseMetadata {
contributors: vec![],
},
};
release.update_gitlab_metadata(
vec![

View File

@ -0,0 +1,222 @@
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"];
/// Maximum number of entries to fetch for bitbucket pull requests.
pub(crate) const BITBUCKET_MAX_PAGE_PRS: usize = 50;
/// 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 BitbucketPagination<BitbucketCommit> {
fn url(_id: i64, api_url: &str, remote: &Remote, page: i32) -> String {
let commit_page = page + 1;
format!(
"{}/{}/{}/commits?pagelen={MAX_PAGE_SIZE}&page={commit_page}",
api_url, remote.owner, remote.repo
)
}
fn buffer_size() -> usize {
10
}
fn early_exit(&self) -> bool {
self.values.is_empty()
}
}
/// Bitbucket Pagination Header
///
/// <https://developer.atlassian.com/cloud/bitbucket/rest/intro/#pagination>
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BitbucketPagination<T> {
/// Total number of objects in the response.
pub size: Option<i64>,
/// Page number of the current results.
pub page: Option<i64>,
/// Current number of objects on the existing page. Globally, the minimum
/// length is 10 and the maximum is 100.
pub pagelen: Option<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 = "raw")]
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,
/// Author of Pull Request
pub author: BitbucketCommitAuthor,
}
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 BitbucketPagination<BitbucketPullRequest> {
fn url(_id: i64, api_url: &str, remote: &Remote, page: i32) -> String {
let pr_page = page + 1;
format!(
"{}/{}/{}/pullrequests?&pagelen={BITBUCKET_MAX_PAGE_PRS}&\
page={pr_page}&state=MERGED",
api_url, remote.owner, remote.repo
)
}
fn buffer_size() -> usize {
5
}
fn early_exit(&self) -> bool {
self.values.is_empty()
}
}
/// HTTP client for handling Bitbucket REST API requests.
#[derive(Debug, Clone)]
pub struct BitbucketClient {
/// Remote.
remote: Remote,
/// HTTP client.
client: ClientWithMiddleware,
}
/// Constructs a Bitbucket 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/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_with_early_exit::<BitbucketPagination<BitbucketCommit>>(0)
.await?
.into_iter()
.flat_map(|v| v.values)
.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
.collect())
}
/// Fetches the Bitbucket API and returns the pull requests.
pub async fn get_pull_requests(
&self,
) -> Result<Vec<Box<dyn RemotePullRequest>>> {
Ok(self
.fetch_with_early_exit::<BitbucketPagination<BitbucketPullRequest>>(0)
.await?
.into_iter()
.flat_map(|v| v.values)
.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
.collect())
}
}

View File

@ -53,6 +53,10 @@ impl RemoteEntry for GitHubCommit {
fn buffer_size() -> usize {
10
}
fn early_exit(&self) -> bool {
false
}
}
/// Author of the commit.
@ -112,6 +116,10 @@ impl RemoteEntry for GitHubPullRequest {
fn buffer_size() -> usize {
5
}
fn early_exit(&self) -> bool {
false
}
}
/// HTTP client for handling GitHub REST API requests.

View File

@ -49,9 +49,14 @@ impl RemoteEntry for GitLabProject {
fn url(_id: i64, api_url: &str, remote: &Remote, _page: i32) -> String {
format!("{}/projects/{}%2F{}", api_url, remote.owner, remote.repo)
}
fn buffer_size() -> usize {
1
}
fn early_exit(&self) -> bool {
false
}
}
/// Representation of a single commit.
@ -109,6 +114,10 @@ impl RemoteEntry for GitLabCommit {
fn buffer_size() -> usize {
10
}
fn early_exit(&self) -> bool {
false
}
}
/// Representation of a single pull request.
@ -174,6 +183,10 @@ impl RemoteEntry for GitLabMergeRequest {
fn buffer_size() -> usize {
5
}
fn early_exit(&self) -> bool {
false
}
}
/// Representation of a GitLab User.
@ -213,7 +226,7 @@ pub struct GitLabClient {
client: ClientWithMiddleware,
}
/// Constructs a GitHub client from the remote configuration.
/// Constructs a GitLab client from the remote configuration.
impl TryFrom<Remote> for GitLabClient {
type Error = Error;
fn try_from(remote: Remote) -> Result<Self> {
@ -243,7 +256,7 @@ impl RemoteClient for GitLabClient {
impl GitLabClient {
/// Fetches the GitLab API and returns the pull requests.
pub async fn get_project(&self) -> Result<GitLabProject> {
self.get_entry::<GitLabProject>().await
self.get_entry::<GitLabProject>(0, 1).await
}
/// Fetches the GitLab API and returns the commits.

View File

@ -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,
@ -66,6 +70,8 @@ pub trait RemoteEntry {
fn url(project_id: i64, api_url: &str, remote: &Remote, page: i32) -> String;
/// Returns the request buffer size.
fn buffer_size() -> usize;
/// Whether if exit early.
fn early_exit(&self) -> bool;
}
/// Trait for handling remote commits.
@ -178,6 +184,32 @@ pub trait RemoteClient {
/// Returns the HTTP client for making requests.
fn client(&self) -> ClientWithMiddleware;
/// Returns true if the client should early exit.
fn early_exit<T: DeserializeOwned + RemoteEntry>(&self, page: &T) -> bool {
page.early_exit()
}
/// Retrieves a single object.
async fn get_entry<T: DeserializeOwned + RemoteEntry>(
&self,
project_id: i64,
page: i32,
) -> Result<T> {
let url = T::url(project_id, &Self::api_url(), &self.remote(), page);
debug!("Sending request to: {url}");
let response = self.client().get(&url).send().await?;
let response_text = if response.status().is_success() {
let text = response.text().await?;
trace!("Response: {:?}", text);
text
} else {
let text = response.text().await?;
error!("Request error: {}", text);
text
};
Ok(serde_json::from_str::<T>(&response_text)?)
}
/// Retrieves a single page of entries.
async fn get_entries_with_page<T: DeserializeOwned + RemoteEntry>(
&self,
@ -205,6 +237,8 @@ pub trait RemoteClient {
}
/// Fetches the remote API and returns the given entry.
///
/// See `fetch_with_early_exit` for the early exit version of this method.
async fn fetch<T: DeserializeOwned + RemoteEntry>(
&self,
project_id: i64,
@ -230,21 +264,36 @@ pub trait RemoteClient {
Ok(entries.into_iter().flatten().collect())
}
/// Retrieves a single object.
async fn get_entry<T: DeserializeOwned + RemoteEntry>(&self) -> Result<T> {
let url = T::url(0, &Self::api_url(), &self.remote(), 1);
debug!("Sending request to: {url}");
let response = self.client().get(&url).send().await?;
let response_text = if response.status().is_success() {
let text = response.text().await?;
trace!("Response: {:?}", text);
text
} else {
let text = response.text().await?;
error!("Request error: {}", text);
text
};
Ok(serde_json::from_str::<T>(&response_text)?)
/// Fetches the remote API and returns the given entry.
///
/// Early exits based on the response.
async fn fetch_with_early_exit<T: DeserializeOwned + RemoteEntry>(
&self,
project_id: i64,
) -> Result<Vec<T>> {
let entries: Vec<T> = stream::iter(0..)
.map(|i| self.get_entry::<T>(project_id, i))
.buffered(T::buffer_size())
.take_while(|page| {
let status = match page {
Ok(v) => !self.early_exit(v),
Err(e) => {
debug!("Error while fetching page: {:?}", e);
true
}
};
future::ready(status && page.is_ok())
})
.map(|page| match page {
Ok(v) => v,
Err(ref e) => {
log::error!("{:#?}", e);
page.expect("failed to fetch page: {}")
}
})
.collect()
.await;
Ok(entries)
}
}

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"))]
#[cfg(any(feature = "github", feature = "gitlab", feature = "bitbucket"))]
pub(crate) fn contains_variable(&self, variables: &[&str]) -> bool {
variables
.iter()
@ -232,6 +232,10 @@ mod test {
gitlab: crate::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "bitbucket")]
bitbucket: crate::remote::RemoteReleaseMetadata {
contributors: vec![],
},
},
Option::<HashMap<&str, String>>::None.as_ref(),
&[TextProcessor {

View File

@ -196,6 +196,10 @@ fn generate_changelog() -> Result<()> {
gitlab: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "bitbucket")]
bitbucket: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
},
Release {
version: Some(String::from("v1.0.0")),
@ -228,6 +232,10 @@ fn generate_changelog() -> Result<()> {
gitlab: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
#[cfg(feature = "bitbucket")]
bitbucket: git_cliff_core::remote::RemoteReleaseMetadata {
contributors: vec![],
},
},
];

View File

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

View File

@ -64,7 +64,7 @@ pub struct Opt {
help = "Prints help information",
help_heading = "FLAGS"
)]
pub help: Option<bool>,
pub help: Option<bool>,
#[arg(
short = 'V',
long,
@ -73,10 +73,10 @@ pub struct Opt {
help = "Prints version information",
help_heading = "FLAGS"
)]
pub version: Option<bool>,
pub version: Option<bool>,
/// Increases the logging verbosity.
#[arg(short, long, action = ArgAction::Count, alias = "debug", help_heading = Some("FLAGS"))]
pub verbose: u8,
pub verbose: u8,
/// Writes the default configuration file to cliff.toml
#[arg(
short,
@ -85,7 +85,7 @@ pub struct Opt {
num_args = 0..=1,
required = false
)]
pub init: Option<Option<String>>,
pub init: Option<Option<String>>,
/// Sets the configuration file.
#[arg(
short,
@ -95,7 +95,7 @@ pub struct Opt {
default_value = DEFAULT_CONFIG,
value_parser = Opt::parse_dir
)]
pub config: PathBuf,
pub config: PathBuf,
/// Sets the working directory.
#[arg(
short,
@ -104,7 +104,7 @@ pub struct Opt {
value_name = "PATH",
value_parser = Opt::parse_dir
)]
pub workdir: Option<PathBuf>,
pub workdir: Option<PathBuf>,
/// Sets the git repository.
#[arg(
short,
@ -114,7 +114,7 @@ pub struct Opt {
num_args(1..),
value_parser = Opt::parse_dir
)]
pub repository: Option<Vec<PathBuf>>,
pub repository: Option<Vec<PathBuf>>,
/// Sets the path to include related commits.
#[arg(
long,
@ -122,7 +122,7 @@ pub struct Opt {
value_name = "PATTERN",
num_args(1..)
)]
pub include_path: Option<Vec<Pattern>>,
pub include_path: Option<Vec<Pattern>>,
/// Sets the path to exclude related commits.
#[arg(
long,
@ -130,10 +130,10 @@ pub struct Opt {
value_name = "PATTERN",
num_args(1..)
)]
pub exclude_path: Option<Vec<Pattern>>,
pub exclude_path: Option<Vec<Pattern>>,
/// Sets the regex for matching git tags.
#[arg(long, env = "GIT_CLIFF_TAG_PATTERN", value_name = "PATTERN")]
pub tag_pattern: Option<Regex>,
pub tag_pattern: Option<Regex>,
/// Sets custom commit messages to include in the changelog.
#[arg(
long,
@ -141,7 +141,7 @@ pub struct Opt {
value_name = "MSG",
num_args(1..)
)]
pub with_commit: Option<Vec<String>>,
pub with_commit: Option<Vec<String>>,
/// Sets commits that will be skipped in the changelog.
#[arg(
long,
@ -149,7 +149,7 @@ pub struct Opt {
value_name = "SHA1",
num_args(1..)
)]
pub skip_commit: Option<Vec<String>>,
pub skip_commit: Option<Vec<String>>,
/// Prepends entries to the given changelog file.
#[arg(
short,
@ -158,7 +158,7 @@ pub struct Opt {
value_name = "PATH",
value_parser = Opt::parse_dir
)]
pub prepend: Option<PathBuf>,
pub prepend: Option<PathBuf>,
/// Writes output to the given file.
#[arg(
short,
@ -169,7 +169,7 @@ pub struct Opt {
num_args = 0..=1,
default_missing_value = DEFAULT_OUTPUT
)]
pub output: Option<PathBuf>,
pub output: Option<PathBuf>,
/// Sets the tag for the latest version.
#[arg(
short,
@ -178,13 +178,13 @@ pub struct Opt {
value_name = "TAG",
allow_hyphen_values = true
)]
pub tag: Option<String>,
pub tag: Option<String>,
/// Bumps the version for unreleased changes.
#[arg(long, help_heading = Some("FLAGS"))]
pub bump: bool,
pub bump: bool,
/// Prints bumped version for unreleased changes.
#[arg(long, help_heading = Some("FLAGS"))]
pub bumped_version: bool,
pub bumped_version: bool,
/// Sets the template for the changelog body.
#[arg(
short,
@ -193,38 +193,38 @@ pub struct Opt {
value_name = "TEMPLATE",
allow_hyphen_values = true
)]
pub body: Option<String>,
pub body: Option<String>,
/// Processes the commits starting from the latest tag.
#[arg(short, long, help_heading = Some("FLAGS"))]
pub latest: bool,
pub latest: bool,
/// Processes the commits that belong to the current tag.
#[arg(long, help_heading = Some("FLAGS"))]
pub current: bool,
pub current: bool,
/// Processes the commits that do not belong to a tag.
#[arg(short, long, help_heading = Some("FLAGS"))]
pub unreleased: bool,
pub unreleased: bool,
/// Sorts the tags topologically.
#[arg(long, help_heading = Some("FLAGS"))]
pub topo_order: bool,
pub topo_order: bool,
/// Disables the external command execution.
#[arg(long, help_heading = Some("FLAGS"))]
pub no_exec: bool,
pub no_exec: bool,
/// Prints changelog context as JSON.
#[arg(short = 'x', long, help_heading = Some("FLAGS"))]
pub context: bool,
pub context: bool,
/// Strips the given parts from the changelog.
#[arg(short, long, value_name = "PART", value_enum)]
pub strip: Option<Strip>,
pub strip: Option<Strip>,
/// Sets sorting of the commits inside sections.
#[arg(
long,
value_enum,
default_value_t = Sort::Oldest
)]
pub sort: Sort,
pub sort: Sort,
/// Sets the commit range to process.
#[arg(value_name = "RANGE", help_heading = Some("ARGS"))]
pub range: Option<String>,
pub range: Option<String>,
/// Sets the GitHub API token.
#[arg(
long,
@ -233,7 +233,7 @@ pub struct Opt {
hide_env_values = true,
hide = !cfg!(feature = "github"),
)]
pub github_token: Option<SecretString>,
pub github_token: Option<SecretString>,
/// Sets the GitHub repository.
#[arg(
long,
@ -242,7 +242,7 @@ pub struct Opt {
value_name = "OWNER/REPO",
hide = !cfg!(feature = "github"),
)]
pub github_repo: Option<RemoteValue>,
pub github_repo: Option<RemoteValue>,
/// Sets the GitLab API token.
#[arg(
long,
@ -251,7 +251,7 @@ pub struct Opt {
hide_env_values = true,
hide = !cfg!(feature = "gitlab"),
)]
pub gitlab_token: Option<SecretString>,
pub gitlab_token: Option<SecretString>,
/// Sets the GitLab repository.
#[arg(
long,
@ -260,7 +260,25 @@ pub struct Opt {
value_name = "OWNER/REPO",
hide = !cfg!(feature = "gitlab"),
)]
pub gitlab_repo: Option<RemoteValue>,
pub gitlab_repo: Option<RemoteValue>,
/// Sets the Bitbucket API token.
#[arg(
long,
env = "BITBUCKET_TOKEN",
value_name = "TOKEN",
hide_env_values = true,
hide = !cfg!(feature = "bitbucket"),
)]
pub bitbucket_token: Option<SecretString>,
/// Sets the Bitbucket repository.
#[arg(
long,
env = "BITBUCKET_REPO",
value_parser = clap::value_parser!(RemoteValue),
value_name = "OWNER/REPO",
hide = !cfg!(feature = "bitbucket"),
)]
pub bitbucket_repo: Option<RemoteValue>,
}
/// Custom type for the remote value.

View File

@ -134,6 +134,17 @@ fn process_repository<'a>(
debug!("Failed to get remote from GitLab repository: {:?}", e);
}
}
} else if !config.remote.bitbucket.is_set() {
match repository.upstream_remote() {
Ok(remote) => {
debug!("No Bitbucket remote is set, using remote: {}", remote);
config.remote.bitbucket.owner = remote.owner;
config.remote.bitbucket.repo = remote.repo;
}
Err(e) => {
debug!("Failed to get remote from Bitbucket repository: {:?}", e);
}
}
}
// Print debug information about configuration and arguments.
@ -426,6 +437,13 @@ pub fn run(mut args: Opt) -> Result<()> {
if args.gitlab_token.is_some() {
config.remote.gitlab.token.clone_from(&args.gitlab_token);
}
if args.bitbucket_token.is_some() {
config
.remote
.bitbucket
.token
.clone_from(&args.bitbucket_token);
}
if let Some(ref remote) = args.github_repo {
config.remote.github.owner = remote.0.owner.to_string();
config.remote.github.repo = remote.0.repo.to_string();
@ -434,6 +452,10 @@ pub fn run(mut args: Opt) -> Result<()> {
config.remote.gitlab.owner = remote.0.owner.to_string();
config.remote.gitlab.repo = remote.0.repo.to_string();
}
if let Some(ref remote) = args.bitbucket_repo {
config.remote.bitbucket.owner = remote.0.owner.to_string();
config.remote.bitbucket.repo = remote.0.repo.to_string();
}
if args.no_exec {
if let Some(ref mut preprocessors) = config.git.commit_preprocessors {
preprocessors

View File

@ -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 = {
@ -144,6 +144,24 @@ pub fn init() -> Result<()> {
}
}
#[cfg(feature = "bitbucket")]
{
let message = record.args().to_string();
if message
.starts_with(git_cliff_core::remote::bitbucket::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::bitbucket::FINISHED_FETCHING_MSG,
) {
PROGRESS_BAR.finish_and_clear();
return Ok(());
}
}
writeln!(f, " {} {} > {}", level, target, record.args())
});

View File

@ -2,6 +2,8 @@
This section contains the Git remote related configuration options.
You can configure a remote for GitHub, GitLab or Bitbucket as follows:
```toml
[remote.github]
owner = "orhun"
@ -9,24 +11,13 @@ repo = "git-cliff"
token = ""
```
:::tip
See the [GitHub integration](/docs/integration/github).
:::
Or if you are using GitLab:
```toml
[remote.gitlab]
owner = "orhun"
repo = "git-cliff"
token = ""
```
Change this to `remote.gitlab` or `remote.bitbucket` accordingly to your project.
:::tip
See the [GitLab integration](/docs/integration/gitlab).
- See the [GitHub integration](/docs/integration/github).
- See the [GitLab integration](/docs/integration/gitlab).
- See the [Bitbucket integration](/docs/integration/bitbucket).
:::
@ -46,7 +37,7 @@ e.g.
git cliff --github-repo orhun/git-cliff
```
Same applies for GitLab with `--gitlab-repo` and `GITLAB_REPO` environment variables.
Same applies for GitLab/Bitbucket with `--gitlab-repo`/`--bitbucket-repo` and `GITLAB_REPO`/`BITBUCKET_REPO` environment variables.
### token
@ -58,4 +49,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 with `--gitlab-token` and `GITLAB_TOKEN` environment variables.
Same applies for GitLab/Bitbucket with `--gitlab-token`/`--bitbucket-token` and `GITLAB_TOKEN`/`BITBUCKET_TOKEN` environment variables.

View File

@ -20,8 +20,12 @@ The minimum supported Rust version is `1.70.0`.
Also, **git-cliff** has the following feature flags which can be enabled via `--features` argument:
- `update-informer`: inform about the new releases of **git-cliff** (enabled as default)
- `github`: enables the [GitHub integration](/docs/integration/github) (enabled as default)
- `update-informer`: inform about the new releases of **git-cliff**
- `github`: enables the [GitHub integration](/docs/integration/github)
- `gitlab`: enables the [GitLab integration](/docs/integration/gitlab)
- `bitbucket`: enables the [Bitbucket integration](/docs/integration/bitbucket)
All these features are enabled as default.
To install without these features:

View File

@ -0,0 +1,179 @@
---
sidebar_position: 3
---
# Bitbucket 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 `bitbucket` feature flag for the integration to work.
:::
For projects hosted on Bitbucket, you can use **git-cliff** to add the following to your changelog:
- Bitbucket 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.bitbucket]
owner = "orhun"
repo = "git-cliff"
token = "***"
```
- Use the `--bitbucket-repo` argument (takes values in `OWNER/REPO` format, e.g. "orhun/git-cliff")
- Use the `BITBUCKET_REPO` environment variable (same format as `--bitbucket-repo`)
## Authentication
:::tip
[Bitbucket REST API](https://developer.atlassian.com/cloud/bitbucket/rest/) is being used to retrieve data from Bitbucket and it has [rate limiting](https://support.atlassian.com/bitbucket-cloud/docs/api-request-limits/) rules.
You can follow [this guide](https://developer.atlassian.com/cloud/bitbucket/rest/intro/#authentication) for creating an access token.
:::
To set an access token, you can use the [configuration file](/docs/configuration/remote) (not recommended), `--bitbucket-token` argument or `BITBUCKET_TOKEN` environment variable.
For example:
```bash
BITBUCKET_TOKEN="***" git cliff --bitbucket-repo "orhun/git-cliff"
```
:::tip
You can use the `BITBUCKET_API_URL` environment variable want to override the API URL. This is useful if you are using your own Bitbucket 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
{
"bitbucket": {
"owner": "orhun",
"repo": "git-cliff"
}
}
```
For example:
```jinja2
https://bitbucket.org/{{ remote.bitbucket.owner }}/{{ remote.bitbucket.repo }}/commits/tag/{{ version }}
```
### Commit authors
For each commit, Bitbucket related values are added as a nested object (named `bitbucket`) to the [template context](/docs/templating/context):
```json
{
"id": "8edec7fd50f703811d55f14a3c5f0fd02b43d9e7",
"message": "refactor(config): remove unnecessary newline from configs\n",
"group": "🚜 Refactor",
"...": "<strip>",
"bitbucket": {
"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.bitbucket.username %} by @{{ commit.bitbucket.username }}{%- endif %}\
{% if commit.bitbucket.pr_number %} in #{{ commit.bitbucket.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,
"bitbucket": {
"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 bitbucket.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: 4
sidebar_position: 5
---
# Python

View File

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