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