1use reqwest_middleware::ClientWithMiddleware;
2use serde::{Deserialize, Serialize};
3
4use super::*;
5use crate::config::Remote;
6use crate::error::*;
7
8pub const START_FETCHING_MSG: &str = "Retrieving data from GitLab...";
10
11pub const FINISHED_FETCHING_MSG: &str = "Done fetching GitLab data.";
13
14pub(crate) const TEMPLATE_VARIABLES: &[&str] = &["gitlab", "commit.gitlab", "commit.remote"];
16
17#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct GitLabProject {
22 pub id: i64,
24 pub description: Option<String>,
26 pub name: String,
28 pub name_with_namespace: String,
30 pub path_with_namespace: String,
32 pub created_at: String,
34 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#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
67pub struct GitLabCommit {
68 pub id: String,
70 pub short_id: String,
72 pub title: String,
74 pub author_name: String,
76 pub author_email: String,
78 pub authored_date: String,
80 pub committer_name: String,
82 pub committer_email: String,
84 pub committed_date: String,
86 pub created_at: String,
88 pub message: String,
90 pub parent_ids: Vec<String>,
92 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#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
138pub struct GitLabMergeRequest {
139 pub id: i64,
141 pub iid: i64,
143 pub project_id: i64,
145 pub title: String,
147 pub description: String,
149 pub state: String,
151 pub created_at: String,
153 pub author: GitLabUser,
155 pub sha: String,
157 pub merge_commit_sha: Option<String>,
159 pub squash_commit_sha: Option<String>,
161 pub web_url: String,
163 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#[derive(Debug, Default, Clone, Hash, Eq, PartialEq, Deserialize, Serialize)]
207pub struct GitLabUser {
208 pub id: i64,
210 pub name: String,
212 pub username: String,
214 pub state: String,
216 pub avatar_url: Option<String>,
218 pub web_url: String,
220}
221
222#[derive(Debug, Default, Clone, Hash, Eq, PartialEq, Deserialize, Serialize)]
224pub struct GitLabReference {
225 pub short: String,
227 pub relative: String,
229 pub full: String,
231}
232
233#[derive(Debug, Clone)]
235pub struct GitLabClient {
236 remote: Remote,
238 client: ClientWithMiddleware,
240}
241
242impl 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 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 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 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}