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(_id: i64, api_url: &str, remote: &Remote, page: i32) -> String {
64		format!(
65			"{}/repos/{}/{}/commits?per_page={MAX_PAGE_SIZE}&page={page}",
66			api_url, remote.owner, remote.repo
67		)
68	}
69	fn buffer_size() -> usize {
70		10
71	}
72
73	fn early_exit(&self) -> bool {
74		false
75	}
76}
77
78/// Author of the commit.
79#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub struct GitHubCommitAuthor {
81	/// Username.
82	pub login: Option<String>,
83}
84
85/// Label of the pull request.
86#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
87#[serde(rename_all = "camelCase")]
88pub struct PullRequestLabel {
89	/// Name of the label.
90	pub name: String,
91}
92
93/// Representation of a single pull request.
94#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
95pub struct GitHubPullRequest {
96	/// Pull request number.
97	pub number:           i64,
98	/// Pull request title.
99	pub title:            Option<String>,
100	/// SHA of the merge commit.
101	pub merge_commit_sha: Option<String>,
102	/// Labels of the pull request.
103	pub labels:           Vec<PullRequestLabel>,
104}
105
106impl RemotePullRequest for GitHubPullRequest {
107	fn number(&self) -> i64 {
108		self.number
109	}
110
111	fn title(&self) -> Option<String> {
112		self.title.clone()
113	}
114
115	fn labels(&self) -> Vec<String> {
116		self.labels.iter().map(|v| v.name.clone()).collect()
117	}
118
119	fn merge_commit(&self) -> Option<String> {
120		self.merge_commit_sha.clone()
121	}
122}
123
124impl RemoteEntry for GitHubPullRequest {
125	fn url(_id: i64, api_url: &str, remote: &Remote, page: i32) -> String {
126		format!(
127			"{}/repos/{}/{}/pulls?per_page={MAX_PAGE_SIZE}&page={page}&state=closed",
128			api_url, remote.owner, remote.repo
129		)
130	}
131
132	fn buffer_size() -> usize {
133		5
134	}
135
136	fn early_exit(&self) -> bool {
137		false
138	}
139}
140
141/// HTTP client for handling GitHub REST API requests.
142#[derive(Debug, Clone)]
143pub struct GitHubClient {
144	/// Remote.
145	remote: Remote,
146	/// HTTP client.
147	client: ClientWithMiddleware,
148}
149
150/// Constructs a GitHub client from the remote configuration.
151impl TryFrom<Remote> for GitHubClient {
152	type Error = Error;
153	fn try_from(remote: Remote) -> Result<Self> {
154		Ok(Self {
155			client: remote.create_client("application/vnd.github+json")?,
156			remote,
157		})
158	}
159}
160
161impl RemoteClient for GitHubClient {
162	const API_URL: &'static str = "https://api.github.com";
163	const API_URL_ENV: &'static str = "GITHUB_API_URL";
164
165	fn remote(&self) -> Remote {
166		self.remote.clone()
167	}
168
169	fn client(&self) -> ClientWithMiddleware {
170		self.client.clone()
171	}
172}
173
174impl GitHubClient {
175	/// Fetches the GitHub API and returns the commits.
176	pub async fn get_commits(&self) -> Result<Vec<Box<dyn RemoteCommit>>> {
177		Ok(self
178			.fetch::<GitHubCommit>(0)
179			.await?
180			.into_iter()
181			.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
182			.collect())
183	}
184
185	/// Fetches the GitHub API and returns the pull requests.
186	pub async fn get_pull_requests(
187		&self,
188	) -> Result<Vec<Box<dyn RemotePullRequest>>> {
189		Ok(self
190			.fetch::<GitHubPullRequest>(0)
191			.await?
192			.into_iter()
193			.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
194			.collect())
195	}
196}
197
198#[cfg(test)]
199mod test {
200	use super::*;
201	use crate::remote::RemoteCommit;
202	use pretty_assertions::assert_eq;
203
204	#[test]
205	fn timestamp() {
206		let remote_commit = GitHubCommit {
207			sha:    String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
208			author: Some(GitHubCommitAuthor {
209				login: Some(String::from("orhun")),
210			}),
211			commit: Some(GitHubCommitDetails {
212				author: GitHubCommitDetailsAuthor {
213					date: String::from("2021-07-18T15:14:39+03:00"),
214				},
215			}),
216		};
217
218		assert_eq!(Some(1626610479), remote_commit.timestamp());
219	}
220}