git_cliff_core/remote/
gitlab.rs

1use crate::config::Remote;
2use crate::error::*;
3use reqwest_middleware::ClientWithMiddleware;
4use serde::{
5	Deserialize,
6	Serialize,
7};
8
9use super::*;
10
11/// Log message to show while fetching data from GitLab.
12pub const START_FETCHING_MSG: &str = "Retrieving data from GitLab...";
13
14/// Log message to show when done fetching from GitLab.
15pub const FINISHED_FETCHING_MSG: &str = "Done fetching GitLab data.";
16
17/// Template variables related to this remote.
18pub(crate) const TEMPLATE_VARIABLES: &[&str] =
19	&["gitlab", "commit.gitlab", "commit.remote"];
20
21/// Representation of a single GitLab Project.
22///
23/// <https://docs.gitlab.com/ee/api/projects.html#get-single-project>
24#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct GitLabProject {
26	/// GitLab id for project
27	pub id:                  i64,
28	/// Optional Description of project
29	pub description:         Option<String>,
30	/// Name of project
31	pub name:                String,
32	/// Name of project with namespace owner / repo
33	pub name_with_namespace: String,
34	/// Name of project with namespace owner/repo
35	pub path_with_namespace: String,
36	/// Project created at
37	pub created_at:          String,
38	/// Default branch eg (main/master)
39	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/// Representation of a single commit.
68///
69/// <https://docs.gitlab.com/ee/api/commits.html>
70#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
71pub struct GitLabCommit {
72	/// Sha
73	pub id:              String,
74	/// Short Sha
75	pub short_id:        String,
76	/// Git message
77	pub title:           String,
78	/// Author
79	pub author_name:     String,
80	/// Author Email
81	pub author_email:    String,
82	/// Authored Date
83	pub authored_date:   String,
84	/// Committer Name
85	pub committer_name:  String,
86	/// Committer Email
87	pub committer_email: String,
88	/// Committed Date
89	pub committed_date:  String,
90	/// Created At
91	pub created_at:      String,
92	/// Git Message
93	pub message:         String,
94	/// Parent Ids
95	pub parent_ids:      Vec<String>,
96	/// Web Url
97	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/// Representation of a single pull request.
146///
147/// <https://docs.gitlab.com/ee/api/merge_requests.html>
148#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
149pub struct GitLabMergeRequest {
150	/// Id
151	pub id:                i64,
152	/// Iid
153	pub iid:               i64,
154	/// Project Id
155	pub project_id:        i64,
156	/// Title
157	pub title:             String,
158	/// Description
159	pub description:       String,
160	/// State
161	pub state:             String,
162	/// Created At
163	pub created_at:        String,
164	/// Author
165	pub author:            GitLabUser,
166	/// Commit Sha
167	pub sha:               String,
168	/// Merge Commit Sha
169	pub merge_commit_sha:  Option<String>,
170	/// Squash Commit Sha
171	pub squash_commit_sha: Option<String>,
172	/// Web Url
173	pub web_url:           String,
174	/// Labels
175	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/// Representation of a GitLab User.
224#[derive(Debug, Default, Clone, Hash, Eq, PartialEq, Deserialize, Serialize)]
225pub struct GitLabUser {
226	/// Id
227	pub id:         i64,
228	/// Name
229	pub name:       String,
230	/// Username
231	pub username:   String,
232	/// State of the User
233	pub state:      String,
234	/// Url for avatar
235	pub avatar_url: Option<String>,
236	/// Web Url
237	pub web_url:    String,
238}
239
240/// Representation of a GitLab Reference.
241#[derive(Debug, Default, Clone, Hash, Eq, PartialEq, Deserialize, Serialize)]
242pub struct GitLabReference {
243	/// Short id
244	pub short:    String,
245	/// Relative Link
246	pub relative: String,
247	/// Full Link
248	pub full:     String,
249}
250
251/// HTTP client for handling GitLab REST API requests.
252#[derive(Debug, Clone)]
253pub struct GitLabClient {
254	/// Remote.
255	remote: Remote,
256	/// HTTP client.
257	client: ClientWithMiddleware,
258}
259
260/// Constructs a GitLab client from the remote configuration.
261impl 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	/// Fetches the GitLab API and returns the pull requests.
286	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	/// Fetches the GitLab API and returns the commits.
294	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	/// Fetches the GitLab API and returns the pull requests.
308	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}