git_cliff_core/remote/
github.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 GitHub.
12pub const START_FETCHING_MSG: &str = "Retrieving data from GitHub...";
13
14/// Log message to show when done fetching from GitHub.
15pub const FINISHED_FETCHING_MSG: &str = "Done fetching GitHub data.";
16
17/// Template variables related to this remote.
18pub(crate) const TEMPLATE_VARIABLES: &[&str] =
19	&["github", "commit.github", "commit.remote"];
20
21/// Representation of a single commit.
22#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct GitHubCommit {
24	/// SHA.
25	pub sha:    String,
26	/// Author of the commit.
27	pub author: Option<GitHubCommitAuthor>,
28	/// Details of the commit
29	pub commit: Option<GitHubCommitDetails>,
30}
31
32/// Representation of subset of commit details
33#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub struct GitHubCommitDetails {
35	/// Author of the commit
36	pub author: GitHubCommitDetailsAuthor,
37}
38
39/// Representation of subset of commit author details
40#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
41pub struct GitHubCommitDetailsAuthor {
42	/// Date of the commit
43	pub date: String,
44}
45
46impl RemoteCommit for GitHubCommit {
47	fn id(&self) -> String {
48		self.sha.clone()
49	}
50
51	fn username(&self) -> Option<String> {
52		self.author.clone().and_then(|v| v.login)
53	}
54
55	fn timestamp(&self) -> Option<i64> {
56		self.commit
57			.clone()
58			.map(|f| self.convert_to_unix_timestamp(f.author.date.clone().as_str()))
59	}
60}
61
62impl RemoteEntry for GitHubCommit {
63	fn url(
64		_id: i64,
65		api_url: &str,
66		remote: &Remote,
67		ref_name: Option<&str>,
68		page: i32,
69	) -> String {
70		let mut url = format!(
71			"{}/repos/{}/{}/commits?per_page={MAX_PAGE_SIZE}&page={page}",
72			api_url, remote.owner, remote.repo
73		);
74
75		if let Some(ref_name) = ref_name {
76			url.push_str(&format!("&sha={}", ref_name));
77		}
78
79		url
80	}
81
82	fn buffer_size() -> usize {
83		10
84	}
85
86	fn early_exit(&self) -> bool {
87		false
88	}
89}
90
91/// Author of the commit.
92#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
93pub struct GitHubCommitAuthor {
94	/// Username.
95	pub login: Option<String>,
96}
97
98/// Label of the pull request.
99#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct PullRequestLabel {
102	/// Name of the label.
103	pub name: String,
104}
105
106/// Representation of a single pull request.
107#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
108pub struct GitHubPullRequest {
109	/// Pull request number.
110	pub number:           i64,
111	/// Pull request title.
112	pub title:            Option<String>,
113	/// SHA of the merge commit.
114	pub merge_commit_sha: Option<String>,
115	/// Labels of the pull request.
116	pub labels:           Vec<PullRequestLabel>,
117}
118
119impl RemotePullRequest for GitHubPullRequest {
120	fn number(&self) -> i64 {
121		self.number
122	}
123
124	fn title(&self) -> Option<String> {
125		self.title.clone()
126	}
127
128	fn labels(&self) -> Vec<String> {
129		self.labels.iter().map(|v| v.name.clone()).collect()
130	}
131
132	fn merge_commit(&self) -> Option<String> {
133		self.merge_commit_sha.clone()
134	}
135}
136
137impl RemoteEntry for GitHubPullRequest {
138	fn url(
139		_id: i64,
140		api_url: &str,
141		remote: &Remote,
142		_ref_name: Option<&str>,
143		page: i32,
144	) -> String {
145		format!(
146			"{}/repos/{}/{}/pulls?per_page={MAX_PAGE_SIZE}&page={page}&state=closed",
147			api_url, remote.owner, remote.repo
148		)
149	}
150
151	fn buffer_size() -> usize {
152		5
153	}
154
155	fn early_exit(&self) -> bool {
156		false
157	}
158}
159
160/// HTTP client for handling GitHub REST API requests.
161#[derive(Debug, Clone)]
162pub struct GitHubClient {
163	/// Remote.
164	remote: Remote,
165	/// HTTP client.
166	client: ClientWithMiddleware,
167}
168
169/// Constructs a GitHub client from the remote configuration.
170impl TryFrom<Remote> for GitHubClient {
171	type Error = Error;
172	fn try_from(remote: Remote) -> Result<Self> {
173		Ok(Self {
174			client: remote.create_client("application/vnd.github+json")?,
175			remote,
176		})
177	}
178}
179
180impl RemoteClient for GitHubClient {
181	const API_URL: &'static str = "https://api.github.com";
182	const API_URL_ENV: &'static str = "GITHUB_API_URL";
183
184	fn remote(&self) -> Remote {
185		self.remote.clone()
186	}
187
188	fn client(&self) -> ClientWithMiddleware {
189		self.client.clone()
190	}
191}
192
193impl GitHubClient {
194	/// Fetches the GitHub API and returns the commits.
195	pub async fn get_commits(
196		&self,
197		ref_name: Option<&str>,
198	) -> Result<Vec<Box<dyn RemoteCommit>>> {
199		Ok(self
200			.fetch::<GitHubCommit>(0, ref_name)
201			.await?
202			.into_iter()
203			.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
204			.collect())
205	}
206
207	/// Fetches the GitHub API and returns the pull requests.
208	pub async fn get_pull_requests(
209		&self,
210		ref_name: Option<&str>,
211	) -> Result<Vec<Box<dyn RemotePullRequest>>> {
212		Ok(self
213			.fetch::<GitHubPullRequest>(0, ref_name)
214			.await?
215			.into_iter()
216			.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
217			.collect())
218	}
219}
220
221#[cfg(test)]
222mod test {
223	use super::*;
224	use crate::remote::RemoteCommit;
225	use pretty_assertions::assert_eq;
226
227	#[test]
228	fn timestamp() {
229		let remote_commit = GitHubCommit {
230			sha:    String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
231			author: Some(GitHubCommitAuthor {
232				login: Some(String::from("orhun")),
233			}),
234			commit: Some(GitHubCommitDetails {
235				author: GitHubCommitDetailsAuthor {
236					date: String::from("2021-07-18T15:14:39+03:00"),
237				},
238			}),
239		};
240
241		assert_eq!(Some(1626610479), remote_commit.timestamp());
242	}
243}