1use crate::config::Remote;
2use crate::error::*;
3use reqwest_middleware::ClientWithMiddleware;
4use serde::{
5 Deserialize,
6 Serialize,
7};
8
9use super::*;
10
11pub const START_FETCHING_MSG: &str = "Retrieving data from GitLab...";
13
14pub const FINISHED_FETCHING_MSG: &str = "Done fetching GitLab data.";
16
17pub(crate) const TEMPLATE_VARIABLES: &[&str] =
19 &["gitlab", "commit.gitlab", "commit.remote"];
20
21#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct GitLabProject {
26 pub id: i64,
28 pub description: Option<String>,
30 pub name: String,
32 pub name_with_namespace: String,
34 pub path_with_namespace: String,
36 pub created_at: String,
38 pub default_branch: String,
40}
41
42impl RemoteEntry for GitLabProject {
43 fn url(
44 _id: i64,
45 api_url: &str,
46 remote: &Remote,
47 _ref_name: Option<&str>,
48 _page: i32,
49 ) -> String {
50 format!(
51 "{}/projects/{}%2F{}",
52 api_url,
53 urlencoding::encode(remote.owner.as_str()),
54 remote.repo
55 )
56 }
57
58 fn buffer_size() -> usize {
59 1
60 }
61
62 fn early_exit(&self) -> bool {
63 false
64 }
65}
66
67#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
71pub struct GitLabCommit {
72 pub id: String,
74 pub short_id: String,
76 pub title: String,
78 pub author_name: String,
80 pub author_email: String,
82 pub authored_date: String,
84 pub committer_name: String,
86 pub committer_email: String,
88 pub committed_date: String,
90 pub created_at: String,
92 pub message: String,
94 pub parent_ids: Vec<String>,
96 pub web_url: String,
98}
99
100impl RemoteCommit for GitLabCommit {
101 fn id(&self) -> String {
102 self.id.clone()
103 }
104
105 fn username(&self) -> Option<String> {
106 Some(self.author_name.clone())
107 }
108
109 fn timestamp(&self) -> Option<i64> {
110 Some(self.convert_to_unix_timestamp(self.committed_date.clone().as_str()))
111 }
112}
113
114impl RemoteEntry for GitLabCommit {
115 fn url(
116 id: i64,
117 api_url: &str,
118 _remote: &Remote,
119 ref_name: Option<&str>,
120 page: i32,
121 ) -> String {
122 let commit_page = page + 1;
123 let mut url = format!(
124 "{}/projects/{}/repository/commits?per_page={MAX_PAGE_SIZE}&\
125 page={commit_page}",
126 api_url, id
127 );
128
129 if let Some(ref_name) = ref_name {
130 url.push_str(&format!("&ref_name={}", ref_name));
131 }
132
133 url
134 }
135
136 fn buffer_size() -> usize {
137 10
138 }
139
140 fn early_exit(&self) -> bool {
141 false
142 }
143}
144
145#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
149pub struct GitLabMergeRequest {
150 pub id: i64,
152 pub iid: i64,
154 pub project_id: i64,
156 pub title: String,
158 pub description: String,
160 pub state: String,
162 pub created_at: String,
164 pub author: GitLabUser,
166 pub sha: String,
168 pub merge_commit_sha: Option<String>,
170 pub squash_commit_sha: Option<String>,
172 pub web_url: String,
174 pub labels: Vec<String>,
176}
177
178impl RemotePullRequest for GitLabMergeRequest {
179 fn number(&self) -> i64 {
180 self.iid
181 }
182
183 fn title(&self) -> Option<String> {
184 Some(self.title.clone())
185 }
186
187 fn labels(&self) -> Vec<String> {
188 self.labels.clone()
189 }
190
191 fn merge_commit(&self) -> Option<String> {
192 self.merge_commit_sha
193 .clone()
194 .or(self.squash_commit_sha.clone())
195 .or(Some(self.sha.clone()))
196 }
197}
198
199impl RemoteEntry for GitLabMergeRequest {
200 fn url(
201 id: i64,
202 api_url: &str,
203 _remote: &Remote,
204 _ref_name: Option<&str>,
205 page: i32,
206 ) -> String {
207 format!(
208 "{}/projects/{}/merge_requests?per_page={MAX_PAGE_SIZE}&page={page}&\
209 state=merged",
210 api_url, id
211 )
212 }
213
214 fn buffer_size() -> usize {
215 5
216 }
217
218 fn early_exit(&self) -> bool {
219 false
220 }
221}
222
223#[derive(Debug, Default, Clone, Hash, Eq, PartialEq, Deserialize, Serialize)]
225pub struct GitLabUser {
226 pub id: i64,
228 pub name: String,
230 pub username: String,
232 pub state: String,
234 pub avatar_url: Option<String>,
236 pub web_url: String,
238}
239
240#[derive(Debug, Default, Clone, Hash, Eq, PartialEq, Deserialize, Serialize)]
242pub struct GitLabReference {
243 pub short: String,
245 pub relative: String,
247 pub full: String,
249}
250
251#[derive(Debug, Clone)]
253pub struct GitLabClient {
254 remote: Remote,
256 client: ClientWithMiddleware,
258}
259
260impl TryFrom<Remote> for GitLabClient {
262 type Error = Error;
263 fn try_from(remote: Remote) -> Result<Self> {
264 Ok(Self {
265 client: remote.create_client("application/json")?,
266 remote,
267 })
268 }
269}
270
271impl RemoteClient for GitLabClient {
272 const API_URL: &'static str = "https://gitlab.com/api/v4";
273 const API_URL_ENV: &'static str = "GITLAB_API_URL";
274
275 fn remote(&self) -> Remote {
276 self.remote.clone()
277 }
278
279 fn client(&self) -> ClientWithMiddleware {
280 self.client.clone()
281 }
282}
283
284impl GitLabClient {
285 pub async fn get_project(
287 &self,
288 ref_name: Option<&str>,
289 ) -> Result<GitLabProject> {
290 self.get_entry::<GitLabProject>(0, ref_name, 1).await
291 }
292
293 pub async fn get_commits(
295 &self,
296 project_id: i64,
297 ref_name: Option<&str>,
298 ) -> Result<Vec<Box<dyn RemoteCommit>>> {
299 Ok(self
300 .fetch::<GitLabCommit>(project_id, ref_name)
301 .await?
302 .into_iter()
303 .map(|v| Box::new(v) as Box<dyn RemoteCommit>)
304 .collect())
305 }
306
307 pub async fn get_merge_requests(
309 &self,
310 project_id: i64,
311 ref_name: Option<&str>,
312 ) -> Result<Vec<Box<dyn RemotePullRequest>>> {
313 Ok(self
314 .fetch::<GitLabMergeRequest>(project_id, ref_name)
315 .await?
316 .into_iter()
317 .map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
318 .collect())
319 }
320}
321#[cfg(test)]
322mod test {
323 use super::*;
324 use pretty_assertions::assert_eq;
325
326 #[test]
327 fn gitlab_remote_encodes_owner() {
328 let remote = Remote::new("abc/def", "xyz1");
329 assert_eq!(
330 "https://gitlab.test.com/api/v4/projects/abc%2Fdef%2Fxyz1",
331 GitLabProject::url(
332 1,
333 "https://gitlab.test.com/api/v4",
334 &remote,
335 None,
336 0
337 )
338 );
339 }
340
341 #[test]
342 fn timestamp() {
343 let remote_commit = GitLabCommit {
344 id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
345 author_name: String::from("orhun"),
346 committed_date: String::from("2021-07-18T15:14:39+03:00"),
347 ..Default::default()
348 };
349
350 assert_eq!(Some(1626610479), remote_commit.timestamp());
351 }
352
353 #[test]
354 fn merge_request_no_merge_commit() {
355 let mr = GitLabMergeRequest {
356 sha: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
357 ..Default::default()
358 };
359 assert!(mr.merge_commit().is_some());
360 }
361
362 #[test]
363 fn merge_request_squash_commit() {
364 let mr = GitLabMergeRequest {
365 squash_commit_sha: Some(String::from(
366 "1d244937ee6ceb8e0314a4a201ba93a7a61f2071",
367 )),
368 ..Default::default()
369 };
370 assert!(mr.merge_commit().is_some());
371 }
372}