git_cliff_core/remote/
bitbucket.rs

1use crate::config::Remote;
2use crate::error::*;
3use reqwest_middleware::ClientWithMiddleware;
4use serde::{
5	Deserialize,
6	Serialize,
7};
8
9use super::*;
10
11/// Log message to show while fetching data from Bitbucket.
12pub const START_FETCHING_MSG: &str = "Retrieving data from Bitbucket...";
13
14/// Log message to show when done fetching from Bitbucket.
15pub const FINISHED_FETCHING_MSG: &str = "Done fetching Bitbucket data.";
16
17/// Template variables related to this remote.
18pub(crate) const TEMPLATE_VARIABLES: &[&str] =
19	&["bitbucket", "commit.bitbucket", "commit.remote"];
20
21/// Maximum number of entries to fetch for bitbucket pull requests.
22pub(crate) const BITBUCKET_MAX_PAGE_PRS: usize = 50;
23
24/// Representation of a single commit.
25#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct BitbucketCommit {
27	/// SHA.
28	pub hash:   String,
29	/// Date of the commit
30	pub date:   String,
31	/// Author of the commit.
32	pub author: Option<BitbucketCommitAuthor>,
33}
34
35impl RemoteCommit for BitbucketCommit {
36	fn id(&self) -> String {
37		self.hash.clone()
38	}
39
40	fn username(&self) -> Option<String> {
41		self.author.clone().and_then(|v| v.login)
42	}
43
44	fn timestamp(&self) -> Option<i64> {
45		Some(self.convert_to_unix_timestamp(self.date.clone().as_str()))
46	}
47}
48
49/// <https://developer.atlassian.com/cloud/bitbucket/rest/api-group-commits/#api-repositories-workspace-repo-slug-commits-get>
50impl RemoteEntry for BitbucketPagination<BitbucketCommit> {
51	fn url(
52		_id: i64,
53		api_url: &str,
54		remote: &Remote,
55		ref_name: Option<&str>,
56		page: i32,
57	) -> String {
58		let commit_page = page + 1;
59		let mut url = format!(
60			"{}/{}/{}/commits?pagelen={MAX_PAGE_SIZE}&page={commit_page}",
61			api_url, remote.owner, remote.repo
62		);
63
64		if let Some(ref_name) = ref_name {
65			url.push_str(&format!("&include={}", ref_name));
66		}
67
68		url
69	}
70
71	fn buffer_size() -> usize {
72		10
73	}
74
75	fn early_exit(&self) -> bool {
76		self.values.is_empty()
77	}
78}
79
80/// Bitbucket Pagination Header
81///
82/// <https://developer.atlassian.com/cloud/bitbucket/rest/intro/#pagination>
83#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
84pub struct BitbucketPagination<T> {
85	/// Total number of objects in the response.
86	pub size:     Option<i64>,
87	/// Page number of the current results.
88	pub page:     Option<i64>,
89	/// Current number of objects on the existing page.  Globally, the minimum
90	/// length is 10 and the maximum is 100.
91	pub pagelen:  Option<i64>,
92	/// Link to the next page if it exists.
93	pub next:     Option<String>,
94	/// Link to the previous page if it exists.
95	pub previous: Option<String>,
96	/// List of Objects.
97	pub values:   Vec<T>,
98}
99
100/// Author of the commit.
101#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
102pub struct BitbucketCommitAuthor {
103	/// Username.
104	#[serde(rename = "raw")]
105	pub login: Option<String>,
106}
107
108/// Label of the pull request.
109#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct PullRequestLabel {
112	/// Name of the label.
113	pub name: String,
114}
115
116/// Representation of a single pull request's merge commit
117#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
118pub struct BitbucketPullRequestMergeCommit {
119	/// SHA of the merge commit.
120	pub hash: String,
121}
122
123/// Representation of a single pull request.
124#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
125pub struct BitbucketPullRequest {
126	/// Pull request number.
127	pub id:           i64,
128	/// Pull request title.
129	pub title:        Option<String>,
130	/// Bitbucket Pull Request Merge Commit
131	pub merge_commit: BitbucketPullRequestMergeCommit,
132	/// Author of Pull Request
133	pub author:       BitbucketCommitAuthor,
134}
135
136impl RemotePullRequest for BitbucketPullRequest {
137	fn number(&self) -> i64 {
138		self.id
139	}
140
141	fn title(&self) -> Option<String> {
142		self.title.clone()
143	}
144
145	fn labels(&self) -> Vec<String> {
146		vec![]
147	}
148
149	fn merge_commit(&self) -> Option<String> {
150		Some(self.merge_commit.hash.clone())
151	}
152}
153
154/// <https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/#api-repositories-workspace-repo-slug-pullrequests-get>
155impl RemoteEntry for BitbucketPagination<BitbucketPullRequest> {
156	fn url(
157		_id: i64,
158		api_url: &str,
159		remote: &Remote,
160		_ref_name: Option<&str>,
161		page: i32,
162	) -> String {
163		let pr_page = page + 1;
164		format!(
165			"{}/{}/{}/pullrequests?&pagelen={BITBUCKET_MAX_PAGE_PRS}&\
166			 page={pr_page}&state=MERGED",
167			api_url, remote.owner, remote.repo
168		)
169	}
170
171	fn buffer_size() -> usize {
172		5
173	}
174
175	fn early_exit(&self) -> bool {
176		self.values.is_empty()
177	}
178}
179
180/// HTTP client for handling Bitbucket REST API requests.
181#[derive(Debug, Clone)]
182pub struct BitbucketClient {
183	/// Remote.
184	remote: Remote,
185	/// HTTP client.
186	client: ClientWithMiddleware,
187}
188
189/// Constructs a Bitbucket client from the remote configuration.
190impl TryFrom<Remote> for BitbucketClient {
191	type Error = Error;
192	fn try_from(remote: Remote) -> Result<Self> {
193		Ok(Self {
194			client: remote.create_client("application/json")?,
195			remote,
196		})
197	}
198}
199
200impl RemoteClient for BitbucketClient {
201	const API_URL: &'static str = "https://api.bitbucket.org/2.0/repositories";
202	const API_URL_ENV: &'static str = "BITBUCKET_API_URL";
203
204	fn remote(&self) -> Remote {
205		self.remote.clone()
206	}
207
208	fn client(&self) -> ClientWithMiddleware {
209		self.client.clone()
210	}
211}
212
213impl BitbucketClient {
214	/// Fetches the Bitbucket API and returns the commits.
215	pub async fn get_commits(
216		&self,
217		ref_name: Option<&str>,
218	) -> Result<Vec<Box<dyn RemoteCommit>>> {
219		Ok(self
220			.fetch_with_early_exit::<BitbucketPagination<BitbucketCommit>>(
221				0, ref_name,
222			)
223			.await?
224			.into_iter()
225			.flat_map(|v| v.values)
226			.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
227			.collect())
228	}
229
230	/// Fetches the Bitbucket API and returns the pull requests.
231	pub async fn get_pull_requests(
232		&self,
233		ref_name: Option<&str>,
234	) -> Result<Vec<Box<dyn RemotePullRequest>>> {
235		Ok(self
236			.fetch_with_early_exit::<BitbucketPagination<BitbucketPullRequest>>(
237				0, ref_name,
238			)
239			.await?
240			.into_iter()
241			.flat_map(|v| v.values)
242			.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
243			.collect())
244	}
245}
246
247#[cfg(test)]
248mod test {
249	use super::*;
250	use crate::remote::RemoteCommit;
251	use pretty_assertions::assert_eq;
252
253	#[test]
254	fn timestamp() {
255		let remote_commit = BitbucketCommit {
256			hash:   String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
257			author: Some(BitbucketCommitAuthor {
258				login: Some(String::from("orhun")),
259			}),
260			date:   String::from("2021-07-18T15:14:39+03:00"),
261		};
262
263		assert_eq!(Some(1626610479), remote_commit.timestamp());
264	}
265}