git_cliff_core/remote/
github.rs1use async_stream::stream as async_stream;
2use futures::{Stream, StreamExt, stream};
3use reqwest_middleware::ClientWithMiddleware;
4use serde::{Deserialize, Serialize};
5
6use super::{Debug, MAX_PAGE_SIZE, RemoteClient, RemoteCommit, RemotePullRequest};
7use crate::config::Remote;
8use crate::error::{Error, Result};
9
10pub(crate) const TEMPLATE_VARIABLES: &[&str] = &["github", "commit.github", "commit.remote"];
12
13#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct GitHubCommit {
16 pub sha: String,
18 pub author: Option<GitHubCommitAuthor>,
20 pub commit: Option<GitHubCommitDetails>,
22}
23
24#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct GitHubCommitDetails {
27 pub author: GitHubCommitDetailsAuthor,
29}
30
31#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct GitHubCommitDetailsAuthor {
34 pub date: String,
36}
37
38impl RemoteCommit for GitHubCommit {
39 fn id(&self) -> String {
40 self.sha.clone()
41 }
42
43 fn username(&self) -> Option<String> {
44 self.author.clone().and_then(|v| v.login)
45 }
46
47 fn timestamp(&self) -> Option<i64> {
48 self.commit
49 .clone()
50 .map(|f| self.convert_to_unix_timestamp(f.author.date.clone().as_str()))
51 }
52}
53
54#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub struct GitHubCommitAuthor {
57 pub login: Option<String>,
59}
60
61#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct PullRequestLabel {
65 pub name: String,
67}
68
69#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
71pub struct GitHubPullRequest {
72 pub number: i64,
74 pub title: Option<String>,
76 pub merge_commit_sha: Option<String>,
78 pub labels: Vec<PullRequestLabel>,
80}
81
82impl RemotePullRequest for GitHubPullRequest {
83 fn number(&self) -> i64 {
84 self.number
85 }
86
87 fn title(&self) -> Option<String> {
88 self.title.clone()
89 }
90
91 fn labels(&self) -> Vec<String> {
92 self.labels.iter().map(|v| v.name.clone()).collect()
93 }
94
95 fn merge_commit(&self) -> Option<String> {
96 self.merge_commit_sha.clone()
97 }
98}
99
100#[derive(Debug, Clone)]
102pub struct GitHubClient {
103 remote: Remote,
105 client: ClientWithMiddleware,
107}
108
109impl TryFrom<Remote> for GitHubClient {
111 type Error = Error;
112 fn try_from(remote: Remote) -> Result<Self> {
113 Ok(Self {
114 client: remote.create_client("application/vnd.github+json")?,
115 remote,
116 })
117 }
118}
119
120impl RemoteClient for GitHubClient {
121 const API_URL: &'static str = "https://api.github.com";
122 const API_URL_ENV: &'static str = "GITHUB_API_URL";
123
124 fn remote(&self) -> Remote {
125 self.remote.clone()
126 }
127
128 fn client(&self) -> ClientWithMiddleware {
129 self.client.clone()
130 }
131}
132
133impl GitHubClient {
134 fn commits_url(api_url: &str, remote: &Remote, ref_name: Option<&str>, page: i32) -> String {
136 let mut url = format!(
137 "{}/repos/{}/{}/commits?per_page={MAX_PAGE_SIZE}&page={page}",
138 api_url, remote.owner, remote.repo
139 );
140
141 if let Some(ref_name) = ref_name {
142 url.push_str(&format!("&sha={ref_name}"));
143 }
144
145 url
146 }
147
148 fn pull_requests_url(api_url: &str, remote: &Remote, page: i32) -> String {
150 format!(
151 "{}/repos/{}/{}/pulls?per_page={MAX_PAGE_SIZE}&page={page}&state=closed",
152 api_url, remote.owner, remote.repo
153 )
154 }
155
156 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
160 pub async fn get_commits(&self, ref_name: Option<&str>) -> Result<Vec<Box<dyn RemoteCommit>>> {
161 use futures::TryStreamExt;
162 crate::set_progress_message!("Fetching all commits from GitHub");
163 self.get_commit_stream(ref_name).try_collect().await
164 }
165
166 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
170 pub async fn get_pull_requests(&self) -> Result<Vec<Box<dyn RemotePullRequest>>> {
171 use futures::TryStreamExt;
172 crate::set_progress_message!("Fetching all pull requests from GitHub");
173 self.get_pull_request_stream().try_collect().await
174 }
175
176 fn get_commit_stream(
177 &self,
178 ref_name: Option<&str>,
179 ) -> impl Stream<Item = Result<Box<dyn RemoteCommit>>> + '_ {
180 let ref_name = ref_name.map(ToString::to_string);
181 async_stream! {
182 let page_stream = stream::iter(0..)
183 .map(|page|
184 {
185 let ref_name = ref_name.clone();
186 async move {
187 let url = Self::commits_url(&self.api_url(), &self.remote(), ref_name.as_deref(), page);
188 self.get_json::<Vec<GitHubCommit>>(&url).await
189 }})
190 .buffered(10);
191
192 let mut page_stream = Box::pin(page_stream);
193
194 while let Some(page_result) = page_stream.next().await {
195 match page_result {
196 Ok(commits) => {
197 if commits.is_empty() {
198 break;
199 }
200
201 for commit in commits {
202 yield Ok(Box::new(commit) as Box<dyn RemoteCommit>);
203 }
204 }
205 Err(e) => {
206 yield Err(e);
207 break;
208 }
209 }
210 }
211 }
212 }
213
214 fn get_pull_request_stream(
215 &self,
216 ) -> impl Stream<Item = Result<Box<dyn RemotePullRequest>>> + '_ {
217 async_stream! {
218 let page_stream = stream::iter(0..)
219 .map(|page| async move {
220 let url = Self::pull_requests_url(&self.api_url(), &self.remote(), page);
221 self.get_json::<Vec<GitHubPullRequest>>(&url).await
222 })
223 .buffered(5);
224
225 let mut page_stream = Box::pin(page_stream);
226
227 while let Some(page_result) = page_stream.next().await {
228 match page_result {
229 Ok(prs) => {
230 if prs.is_empty() {
231 break;
232 }
233
234 for pr in prs {
235 yield Ok(Box::new(pr) as Box<dyn RemotePullRequest>);
236 }
237 }
238 Err(e) => {
239 yield Err(e);
240 break;
241 }
242 }
243 }
244 }
245 }
246}
247
248#[cfg(test)]
249mod test {
250 use pretty_assertions::assert_eq;
251
252 use super::*;
253 use crate::remote::RemoteCommit;
254
255 #[test]
256 fn timestamp() {
257 let remote_commit = GitHubCommit {
258 sha: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
259 author: Some(GitHubCommitAuthor {
260 login: Some(String::from("orhun")),
261 }),
262 commit: Some(GitHubCommitDetails {
263 author: GitHubCommitDetailsAuthor {
264 date: String::from("2021-07-18T15:14:39+03:00"),
265 },
266 }),
267 };
268
269 assert_eq!(Some(1_626_610_479), remote_commit.timestamp());
270 }
271}