git_cliff_core/remote/
github.rs

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