git_cliff_core/remote/
gitlab.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 GitLab.
9pub const START_FETCHING_MSG: &str = "Retrieving data from GitLab...";
10
11/// Log message to show when done fetching from GitLab.
12pub const FINISHED_FETCHING_MSG: &str = "Done fetching GitLab data.";
13
14/// Template variables related to this remote.
15pub(crate) const TEMPLATE_VARIABLES: &[&str] = &["gitlab", "commit.gitlab", "commit.remote"];
16
17/// Representation of a single GitLab Project.
18///
19/// <https://docs.gitlab.com/ee/api/projects.html#get-single-project>
20#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct GitLabProject {
22    /// GitLab id for project
23    pub id: i64,
24    /// Optional Description of project
25    pub description: Option<String>,
26    /// Name of project
27    pub name: String,
28    /// Name of project with namespace owner / repo
29    pub name_with_namespace: String,
30    /// Name of project with namespace owner/repo
31    pub path_with_namespace: String,
32    /// Project created at
33    pub created_at: String,
34    /// Default branch eg (main/master)
35    pub default_branch: String,
36}
37
38impl RemoteEntry for GitLabProject {
39    fn url(
40        _id: i64,
41        api_url: &str,
42        remote: &Remote,
43        _ref_name: Option<&str>,
44        _page: i32,
45    ) -> String {
46        format!(
47            "{}/projects/{}%2F{}",
48            api_url,
49            urlencoding::encode(remote.owner.as_str()),
50            remote.repo
51        )
52    }
53
54    fn buffer_size() -> usize {
55        1
56    }
57
58    fn early_exit(&self) -> bool {
59        false
60    }
61}
62
63/// Representation of a single commit.
64///
65/// <https://docs.gitlab.com/ee/api/commits.html>
66#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
67pub struct GitLabCommit {
68    /// Sha
69    pub id: String,
70    /// Short Sha
71    pub short_id: String,
72    /// Git message
73    pub title: String,
74    /// Author
75    pub author_name: String,
76    /// Author Email
77    pub author_email: String,
78    /// Authored Date
79    pub authored_date: String,
80    /// Committer Name
81    pub committer_name: String,
82    /// Committer Email
83    pub committer_email: String,
84    /// Committed Date
85    pub committed_date: String,
86    /// Created At
87    pub created_at: String,
88    /// Git Message
89    pub message: String,
90    /// Parent Ids
91    pub parent_ids: Vec<String>,
92    /// Web Url
93    pub web_url: String,
94}
95
96impl RemoteCommit for GitLabCommit {
97    fn id(&self) -> String {
98        self.id.clone()
99    }
100
101    fn username(&self) -> Option<String> {
102        Some(self.author_name.clone())
103    }
104
105    fn timestamp(&self) -> Option<i64> {
106        Some(self.convert_to_unix_timestamp(self.committed_date.clone().as_str()))
107    }
108}
109
110impl RemoteEntry for GitLabCommit {
111    fn url(id: i64, api_url: &str, _remote: &Remote, ref_name: Option<&str>, page: i32) -> String {
112        let commit_page = page + 1;
113        let mut url = format!(
114            "{}/projects/{}/repository/commits?per_page={MAX_PAGE_SIZE}&page={commit_page}",
115            api_url, id
116        );
117
118        if let Some(ref_name) = ref_name {
119            url.push_str(&format!("&ref_name={}", ref_name));
120        }
121
122        url
123    }
124
125    fn buffer_size() -> usize {
126        10
127    }
128
129    fn early_exit(&self) -> bool {
130        false
131    }
132}
133
134/// Representation of a single pull request.
135///
136/// <https://docs.gitlab.com/ee/api/merge_requests.html>
137#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
138pub struct GitLabMergeRequest {
139    /// Id
140    pub id: i64,
141    /// Iid
142    pub iid: i64,
143    /// Project Id
144    pub project_id: i64,
145    /// Title
146    pub title: String,
147    /// Description
148    pub description: String,
149    /// State
150    pub state: String,
151    /// Created At
152    pub created_at: String,
153    /// Author
154    pub author: GitLabUser,
155    /// Commit Sha
156    pub sha: String,
157    /// Merge Commit Sha
158    pub merge_commit_sha: Option<String>,
159    /// Squash Commit Sha
160    pub squash_commit_sha: Option<String>,
161    /// Web Url
162    pub web_url: String,
163    /// Labels
164    pub labels: Vec<String>,
165}
166
167impl RemotePullRequest for GitLabMergeRequest {
168    fn number(&self) -> i64 {
169        self.iid
170    }
171
172    fn title(&self) -> Option<String> {
173        Some(self.title.clone())
174    }
175
176    fn labels(&self) -> Vec<String> {
177        self.labels.clone()
178    }
179
180    fn merge_commit(&self) -> Option<String> {
181        self.merge_commit_sha
182            .clone()
183            .or(self.squash_commit_sha.clone())
184            .or(Some(self.sha.clone()))
185    }
186}
187
188impl RemoteEntry for GitLabMergeRequest {
189    fn url(id: i64, api_url: &str, _remote: &Remote, _ref_name: Option<&str>, page: i32) -> String {
190        format!(
191            "{}/projects/{}/merge_requests?per_page={MAX_PAGE_SIZE}&page={page}&state=merged",
192            api_url, id
193        )
194    }
195
196    fn buffer_size() -> usize {
197        5
198    }
199
200    fn early_exit(&self) -> bool {
201        false
202    }
203}
204
205/// Representation of a GitLab User.
206#[derive(Debug, Default, Clone, Hash, Eq, PartialEq, Deserialize, Serialize)]
207pub struct GitLabUser {
208    /// Id
209    pub id: i64,
210    /// Name
211    pub name: String,
212    /// Username
213    pub username: String,
214    /// State of the User
215    pub state: String,
216    /// Url for avatar
217    pub avatar_url: Option<String>,
218    /// Web Url
219    pub web_url: String,
220}
221
222/// Representation of a GitLab Reference.
223#[derive(Debug, Default, Clone, Hash, Eq, PartialEq, Deserialize, Serialize)]
224pub struct GitLabReference {
225    /// Short id
226    pub short: String,
227    /// Relative Link
228    pub relative: String,
229    /// Full Link
230    pub full: String,
231}
232
233/// HTTP client for handling GitLab REST API requests.
234#[derive(Debug, Clone)]
235pub struct GitLabClient {
236    /// Remote.
237    remote: Remote,
238    /// HTTP client.
239    client: ClientWithMiddleware,
240}
241
242/// Constructs a GitLab client from the remote configuration.
243impl TryFrom<Remote> for GitLabClient {
244    type Error = Error;
245    fn try_from(remote: Remote) -> Result<Self> {
246        Ok(Self {
247            client: remote.create_client("application/json")?,
248            remote,
249        })
250    }
251}
252
253impl RemoteClient for GitLabClient {
254    const API_URL: &'static str = "https://gitlab.com/api/v4";
255    const API_URL_ENV: &'static str = "GITLAB_API_URL";
256
257    fn remote(&self) -> Remote {
258        self.remote.clone()
259    }
260
261    fn client(&self) -> ClientWithMiddleware {
262        self.client.clone()
263    }
264}
265
266impl GitLabClient {
267    /// Fetches the GitLab API and returns the pull requests.
268    pub async fn get_project(&self, ref_name: Option<&str>) -> Result<GitLabProject> {
269        self.get_entry::<GitLabProject>(0, ref_name, 1).await
270    }
271
272    /// Fetches the GitLab API and returns the commits.
273    pub async fn get_commits(
274        &self,
275        project_id: i64,
276        ref_name: Option<&str>,
277    ) -> Result<Vec<Box<dyn RemoteCommit>>> {
278        Ok(self
279            .fetch::<GitLabCommit>(project_id, ref_name)
280            .await?
281            .into_iter()
282            .map(|v| Box::new(v) as Box<dyn RemoteCommit>)
283            .collect())
284    }
285
286    /// Fetches the GitLab API and returns the pull requests.
287    pub async fn get_merge_requests(
288        &self,
289        project_id: i64,
290        ref_name: Option<&str>,
291    ) -> Result<Vec<Box<dyn RemotePullRequest>>> {
292        Ok(self
293            .fetch::<GitLabMergeRequest>(project_id, ref_name)
294            .await?
295            .into_iter()
296            .map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
297            .collect())
298    }
299}
300#[cfg(test)]
301mod test {
302    use pretty_assertions::assert_eq;
303
304    use super::*;
305
306    #[test]
307    fn gitlab_remote_encodes_owner() {
308        let remote = Remote::new("abc/def", "xyz1");
309        assert_eq!(
310            "https://gitlab.test.com/api/v4/projects/abc%2Fdef%2Fxyz1",
311            GitLabProject::url(1, "https://gitlab.test.com/api/v4", &remote, None, 0)
312        );
313    }
314
315    #[test]
316    fn timestamp() {
317        let remote_commit = GitLabCommit {
318            id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
319            author_name: String::from("orhun"),
320            committed_date: String::from("2021-07-18T15:14:39+03:00"),
321            ..Default::default()
322        };
323
324        assert_eq!(Some(1626610479), remote_commit.timestamp());
325    }
326
327    #[test]
328    fn merge_request_no_merge_commit() {
329        let mr = GitLabMergeRequest {
330            sha: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
331            ..Default::default()
332        };
333        assert!(mr.merge_commit().is_some());
334    }
335
336    #[test]
337    fn merge_request_squash_commit() {
338        let mr = GitLabMergeRequest {
339            squash_commit_sha: Some(String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071")),
340            ..Default::default()
341        };
342        assert!(mr.merge_commit().is_some());
343    }
344}