1use async_stream::stream as async_stream;
2use futures::{Stream, StreamExt, stream};
3use reqwest_middleware::ClientWithMiddleware;
4use serde::{Deserialize, Serialize};
5
6use super::{Debug, MAX_PAGE_SIZE, RemoteClient, RemoteCommit, RemotePullRequest};
7use crate::config::Remote;
8use crate::error::{Error, Result};
9
10pub(crate) const TEMPLATE_VARIABLES: &[&str] = &["bitbucket", "commit.bitbucket", "commit.remote"];
12
13pub(crate) const BITBUCKET_MAX_PAGE_PRS: usize = 50;
15
16#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct BitbucketCommit {
19 pub hash: String,
21 pub date: String,
23 pub author: Option<BitbucketCommitAuthor>,
25}
26
27impl RemoteCommit for BitbucketCommit {
28 fn id(&self) -> String {
29 self.hash.clone()
30 }
31
32 fn username(&self) -> Option<String> {
33 self.author.clone().and_then(|v| v.login)
34 }
35
36 fn timestamp(&self) -> Option<i64> {
37 Some(self.convert_to_unix_timestamp(self.date.clone().as_str()))
38 }
39}
40
41#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
45pub struct BitbucketPagination<T> {
46 pub size: Option<i64>,
48 pub page: Option<i64>,
50 pub pagelen: Option<i64>,
53 pub next: Option<String>,
55 pub previous: Option<String>,
57 pub values: Vec<T>,
59}
60
61#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
63pub struct BitbucketCommitAuthor {
64 #[serde(rename = "raw")]
66 pub login: Option<String>,
67}
68
69#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
71#[serde(rename_all = "camelCase")]
72pub struct PullRequestLabel {
73 pub name: String,
75}
76
77#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
79pub struct BitbucketPullRequestMergeCommit {
80 pub hash: String,
82}
83
84#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
86pub struct BitbucketPullRequest {
87 pub id: i64,
89 pub title: Option<String>,
91 pub merge_commit: BitbucketPullRequestMergeCommit,
93 pub author: BitbucketCommitAuthor,
95}
96
97impl RemotePullRequest for BitbucketPullRequest {
98 fn number(&self) -> i64 {
99 self.id
100 }
101
102 fn title(&self) -> Option<String> {
103 self.title.clone()
104 }
105
106 fn labels(&self) -> Vec<String> {
107 vec![]
108 }
109
110 fn merge_commit(&self) -> Option<String> {
111 Some(self.merge_commit.hash.clone())
112 }
113}
114
115#[derive(Debug, Clone)]
117pub struct BitbucketClient {
118 remote: Remote,
120 client: ClientWithMiddleware,
122}
123
124impl TryFrom<Remote> for BitbucketClient {
126 type Error = Error;
127 fn try_from(remote: Remote) -> Result<Self> {
128 Ok(Self {
129 client: remote.create_client("application/json")?,
130 remote,
131 })
132 }
133}
134
135impl RemoteClient for BitbucketClient {
136 const API_URL: &'static str = "https://api.bitbucket.org/2.0/repositories";
137 const API_URL_ENV: &'static str = "BITBUCKET_API_URL";
138
139 fn remote(&self) -> Remote {
140 self.remote.clone()
141 }
142
143 fn client(&self) -> ClientWithMiddleware {
144 self.client.clone()
145 }
146}
147
148impl BitbucketClient {
149 fn commits_url(api_url: &str, remote: &Remote, ref_name: Option<&str>, page: i32) -> String {
151 let mut url = format!(
152 "{}/{}/{}/commits?pagelen={MAX_PAGE_SIZE}&page={page}",
153 api_url, remote.owner, remote.repo
154 );
155
156 if let Some(ref_name) = ref_name {
157 url.push_str(&format!("&include={ref_name}"));
158 }
159
160 url
161 }
162
163 fn pull_requests_url(api_url: &str, remote: &Remote, page: i32) -> String {
165 format!(
166 "{}/{}/{}/pullrequests?&pagelen={BITBUCKET_MAX_PAGE_PRS}&page={page}&state=MERGED",
167 api_url, remote.owner, remote.repo
168 )
169 }
170
171 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
175 pub async fn get_commits(&self, ref_name: Option<&str>) -> Result<Vec<Box<dyn RemoteCommit>>> {
176 use futures::TryStreamExt;
177 crate::set_progress_message!("Fetching all commits from Bitbucket");
178 self.get_commit_stream(ref_name).try_collect().await
179 }
180
181 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
185 pub async fn get_pull_requests(&self) -> Result<Vec<Box<dyn RemotePullRequest>>> {
186 use futures::TryStreamExt;
187 crate::set_progress_message!("Fetching all pull requests from Bitbucket");
188 self.get_pull_request_stream().try_collect().await
189 }
190
191 fn get_commit_stream(
192 &self,
193 ref_name: Option<&str>,
194 ) -> impl Stream<Item = Result<Box<dyn RemoteCommit>>> + '_ {
195 let ref_name = ref_name.map(ToString::to_string);
196 async_stream! {
197 let page_stream = stream::iter(1..)
199 .map(|page| {
200 let ref_name = ref_name.clone();
201 async move {
202 let url = Self::commits_url(&self.api_url(), &self.remote(), ref_name.as_deref(), page);
203 self.get_json::<BitbucketPagination<BitbucketCommit>>(&url).await
204 }
205 })
206 .buffered(10);
207
208 let mut page_stream = Box::pin(page_stream);
209
210 while let Some(page_result) = page_stream.next().await {
211 match page_result {
212 Ok(page) => {
213 if page.values.is_empty() {
214 break;
215 }
216
217 for commit in page.values {
218 yield Ok(Box::new(commit) as Box<dyn RemoteCommit>);
219 }
220 }
221 Err(e) => {
222 yield Err(e);
223 break;
224 }
225 }
226 }
227 }
228 }
229
230 fn get_pull_request_stream(
231 &self,
232 ) -> impl Stream<Item = Result<Box<dyn RemotePullRequest>>> + '_ {
233 async_stream! {
234 let page_stream = stream::iter(1..)
236 .map(|page| async move {
237 let url = Self::pull_requests_url(&self.api_url(), &self.remote(), page);
238 self.get_json::<BitbucketPagination<BitbucketPullRequest>>(&url).await
239 })
240 .buffered(5);
241
242 let mut page_stream = Box::pin(page_stream);
243
244 while let Some(page_result) = page_stream.next().await {
245 match page_result {
246 Ok(page) => {
247 if page.values.is_empty() {
248 break;
249 }
250
251 for pr in page.values {
252 yield Ok(Box::new(pr) as Box<dyn RemotePullRequest>);
253 }
254 }
255 Err(e) => {
256 yield Err(e);
257 break;
258 }
259 }
260 }
261 }
262 }
263}
264
265#[cfg(test)]
266mod test {
267 use pretty_assertions::assert_eq;
268
269 use super::*;
270 use crate::remote::RemoteCommit;
271
272 #[test]
273 fn timestamp() {
274 let remote_commit = BitbucketCommit {
275 hash: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
276 author: Some(BitbucketCommitAuthor {
277 login: Some(String::from("orhun")),
278 }),
279 date: String::from("2021-07-18T15:14:39+03:00"),
280 };
281
282 assert_eq!(Some(1_626_610_479), remote_commit.timestamp());
283 }
284}