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(_id: i64, api_url: &str, remote: &Remote, page: i32) -> String {
52		let commit_page = page + 1;
53		format!(
54			"{}/{}/{}/commits?pagelen={MAX_PAGE_SIZE}&page={commit_page}",
55			api_url, remote.owner, remote.repo
56		)
57	}
58
59	fn buffer_size() -> usize {
60		10
61	}
62
63	fn early_exit(&self) -> bool {
64		self.values.is_empty()
65	}
66}
67
68/// Bitbucket Pagination Header
69///
70/// <https://developer.atlassian.com/cloud/bitbucket/rest/intro/#pagination>
71#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
72pub struct BitbucketPagination<T> {
73	/// Total number of objects in the response.
74	pub size:     Option<i64>,
75	/// Page number of the current results.
76	pub page:     Option<i64>,
77	/// Current number of objects on the existing page.  Globally, the minimum
78	/// length is 10 and the maximum is 100.
79	pub pagelen:  Option<i64>,
80	/// Link to the next page if it exists.
81	pub next:     Option<String>,
82	/// Link to the previous page if it exists.
83	pub previous: Option<String>,
84	/// List of Objects.
85	pub values:   Vec<T>,
86}
87
88/// Author of the commit.
89#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
90pub struct BitbucketCommitAuthor {
91	/// Username.
92	#[serde(rename = "raw")]
93	pub login: Option<String>,
94}
95
96/// Label of the pull request.
97#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct PullRequestLabel {
100	/// Name of the label.
101	pub name: String,
102}
103
104/// Representation of a single pull request's merge commit
105#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
106pub struct BitbucketPullRequestMergeCommit {
107	/// SHA of the merge commit.
108	pub hash: String,
109}
110
111/// Representation of a single pull request.
112#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct BitbucketPullRequest {
114	/// Pull request number.
115	pub id:           i64,
116	/// Pull request title.
117	pub title:        Option<String>,
118	/// Bitbucket Pull Request Merge Commit
119	pub merge_commit: BitbucketPullRequestMergeCommit,
120	/// Author of Pull Request
121	pub author:       BitbucketCommitAuthor,
122}
123
124impl RemotePullRequest for BitbucketPullRequest {
125	fn number(&self) -> i64 {
126		self.id
127	}
128
129	fn title(&self) -> Option<String> {
130		self.title.clone()
131	}
132
133	fn labels(&self) -> Vec<String> {
134		vec![]
135	}
136
137	fn merge_commit(&self) -> Option<String> {
138		Some(self.merge_commit.hash.clone())
139	}
140}
141
142/// <https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/#api-repositories-workspace-repo-slug-pullrequests-get>
143impl RemoteEntry for BitbucketPagination<BitbucketPullRequest> {
144	fn url(_id: i64, api_url: &str, remote: &Remote, page: i32) -> String {
145		let pr_page = page + 1;
146		format!(
147			"{}/{}/{}/pullrequests?&pagelen={BITBUCKET_MAX_PAGE_PRS}&\
148			 page={pr_page}&state=MERGED",
149			api_url, remote.owner, remote.repo
150		)
151	}
152
153	fn buffer_size() -> usize {
154		5
155	}
156
157	fn early_exit(&self) -> bool {
158		self.values.is_empty()
159	}
160}
161
162/// HTTP client for handling Bitbucket REST API requests.
163#[derive(Debug, Clone)]
164pub struct BitbucketClient {
165	/// Remote.
166	remote: Remote,
167	/// HTTP client.
168	client: ClientWithMiddleware,
169}
170
171/// Constructs a Bitbucket client from the remote configuration.
172impl TryFrom<Remote> for BitbucketClient {
173	type Error = Error;
174	fn try_from(remote: Remote) -> Result<Self> {
175		Ok(Self {
176			client: remote.create_client("application/json")?,
177			remote,
178		})
179	}
180}
181
182impl RemoteClient for BitbucketClient {
183	const API_URL: &'static str = "https://api.bitbucket.org/2.0/repositories";
184	const API_URL_ENV: &'static str = "BITBUCKET_API_URL";
185
186	fn remote(&self) -> Remote {
187		self.remote.clone()
188	}
189
190	fn client(&self) -> ClientWithMiddleware {
191		self.client.clone()
192	}
193}
194
195impl BitbucketClient {
196	/// Fetches the Bitbucket API and returns the commits.
197	pub async fn get_commits(&self) -> Result<Vec<Box<dyn RemoteCommit>>> {
198		Ok(self
199			.fetch_with_early_exit::<BitbucketPagination<BitbucketCommit>>(0)
200			.await?
201			.into_iter()
202			.flat_map(|v| v.values)
203			.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
204			.collect())
205	}
206
207	/// Fetches the Bitbucket API and returns the pull requests.
208	pub async fn get_pull_requests(
209		&self,
210	) -> Result<Vec<Box<dyn RemotePullRequest>>> {
211		Ok(self
212			.fetch_with_early_exit::<BitbucketPagination<BitbucketPullRequest>>(0)
213			.await?
214			.into_iter()
215			.flat_map(|v| v.values)
216			.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
217			.collect())
218	}
219}
220
221#[cfg(test)]
222mod test {
223	use super::*;
224	use crate::remote::RemoteCommit;
225	use pretty_assertions::assert_eq;
226
227	#[test]
228	fn timestamp() {
229		let remote_commit = BitbucketCommit {
230			hash:   String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
231			author: Some(BitbucketCommitAuthor {
232				login: Some(String::from("orhun")),
233			}),
234			date:   String::from("2021-07-18T15:14:39+03:00"),
235		};
236
237		assert_eq!(Some(1626610479), remote_commit.timestamp());
238	}
239}