git_cliff_core/remote/
bitbucket.rs

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