Skip to main content

git_cliff_core/remote/
gitea.rs

1use 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
10/// Template variables related to this remote.
11pub(crate) const TEMPLATE_VARIABLES: &[&str] = &["gitea", "commit.gitea", "commit.remote"];
12
13/// Representation of a single commit.
14#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct GiteaCommit {
16    /// SHA.
17    pub sha: String,
18    /// Author of the commit.
19    pub author: Option<GiteaCommitAuthor>,
20    /// Timestamp of the commit.
21    pub created: String,
22}
23
24impl RemoteCommit for GiteaCommit {
25    fn id(&self) -> String {
26        self.sha.clone()
27    }
28
29    fn username(&self) -> Option<String> {
30        self.author.clone().and_then(|v| v.login)
31    }
32
33    fn timestamp(&self) -> Option<i64> {
34        Some(self.convert_to_unix_timestamp(self.created.clone().as_str()))
35    }
36}
37
38/// Author of the commit.
39#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub struct GiteaCommitAuthor {
41    /// Username.
42    pub login: Option<String>,
43}
44
45/// Label of the pull request.
46#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct PullRequestLabel {
49    /// Name of the label.
50    pub name: String,
51}
52
53/// Representation of a single pull request.
54#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub struct GiteaPullRequest {
56    /// Pull request number.
57    pub number: i64,
58    /// Pull request title.
59    pub title: Option<String>,
60    /// SHA of the merge commit.
61    pub merge_commit_sha: Option<String>,
62    /// Labels of the pull request.
63    pub labels: Vec<PullRequestLabel>,
64}
65
66impl RemotePullRequest for GiteaPullRequest {
67    fn number(&self) -> i64 {
68        self.number
69    }
70
71    fn title(&self) -> Option<String> {
72        self.title.clone()
73    }
74
75    fn labels(&self) -> Vec<String> {
76        self.labels.iter().map(|v| v.name.clone()).collect()
77    }
78
79    fn merge_commit(&self) -> Option<String> {
80        self.merge_commit_sha.clone()
81    }
82}
83
84/// HTTP client for handling Gitea REST API requests.
85#[derive(Debug, Clone)]
86pub struct GiteaClient {
87    /// Remote.
88    remote: Remote,
89    /// HTTP client.
90    client: ClientWithMiddleware,
91}
92
93/// Constructs a Gitea client from the remote configuration.
94impl TryFrom<Remote> for GiteaClient {
95    type Error = Error;
96    fn try_from(remote: Remote) -> Result<Self> {
97        Ok(Self {
98            client: remote.create_client("application/json")?,
99            remote,
100        })
101    }
102}
103
104impl RemoteClient for GiteaClient {
105    const API_URL: &'static str = "https://codeberg.org";
106    const API_URL_ENV: &'static str = "GITEA_API_URL";
107
108    fn remote(&self) -> Remote {
109        self.remote.clone()
110    }
111
112    fn client(&self) -> ClientWithMiddleware {
113        self.client.clone()
114    }
115}
116
117impl GiteaClient {
118    /// Constructs the URL for Gitea commits API.
119    fn commits_url(api_url: &str, remote: &Remote, ref_name: Option<&str>, page: i32) -> String {
120        let mut url = format!(
121            "{}/api/v1/repos/{}/{}/commits?limit={MAX_PAGE_SIZE}&page={page}",
122            api_url, remote.owner, remote.repo
123        );
124
125        if let Some(ref_name) = ref_name {
126            url.push_str(&format!("&sha={ref_name}"));
127        }
128
129        url
130    }
131
132    /// Constructs the URL for Gitea pull requests API.
133    fn pull_requests_url(api_url: &str, remote: &Remote, page: i32) -> String {
134        format!(
135            "{}/api/v1/repos/{}/{}/pulls?limit={MAX_PAGE_SIZE}&page={page}&state=closed",
136            api_url, remote.owner, remote.repo
137        )
138    }
139
140    /// Fetches the complete list of commits.
141    /// This is inefficient for large repositories; consider using
142    /// `get_commit_stream` instead.
143    #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
144    pub async fn get_commits(&self, ref_name: Option<&str>) -> Result<Vec<Box<dyn RemoteCommit>>> {
145        use futures::TryStreamExt;
146        crate::set_progress_message!("Fetching all commits from Gitea");
147        self.get_commit_stream(ref_name).try_collect().await
148    }
149
150    /// Fetches the complete list of pull requests.
151    /// This is inefficient for large repositories; consider using
152    /// `get_pull_request_stream` instead.
153    #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
154    pub async fn get_pull_requests(&self) -> Result<Vec<Box<dyn RemotePullRequest>>> {
155        use futures::TryStreamExt;
156        crate::set_progress_message!("Fetching all pull requests from Gitea");
157        self.get_pull_request_stream().try_collect().await
158    }
159
160    fn get_commit_stream(
161        &self,
162        ref_name: Option<&str>,
163    ) -> impl Stream<Item = Result<Box<dyn RemoteCommit>>> + '_ {
164        let ref_name = ref_name.map(ToString::to_string);
165        async_stream! {
166            let page_stream = stream::iter(0..)
167                .map(|page| {
168                    let ref_name = ref_name.clone();
169                    async move {
170                        let url = Self::commits_url(&self.api_url(), &self.remote(), ref_name.as_deref(), page);
171                        self.get_json::<Vec<GiteaCommit>>(&url).await
172                    }
173                })
174                .buffered(10);
175
176            let mut page_stream = Box::pin(page_stream);
177
178            while let Some(page_result) = page_stream.next().await {
179                match page_result {
180                    Ok(commits) => {
181                        if commits.is_empty() {
182                            break;
183                        }
184
185                        for commit in commits {
186                            yield Ok(Box::new(commit) as Box<dyn RemoteCommit>);
187                        }
188                    }
189                    Err(e) => {
190                        yield Err(e);
191                        break;
192                    }
193                }
194            }
195        }
196    }
197
198    fn get_pull_request_stream(
199        &self,
200    ) -> impl Stream<Item = Result<Box<dyn RemotePullRequest>>> + '_ {
201        async_stream! {
202        let page_stream = stream::iter(0..)
203            .map(|page| async move {
204                let url = Self::pull_requests_url(&self.api_url(), &self.remote(), page);
205                self.get_json::<Vec<GiteaPullRequest>>(&url).await
206            })
207            .buffered(5);
208
209        let mut page_stream = Box::pin(page_stream);
210
211        while let Some(page_result) = page_stream.next().await {
212            match page_result {
213                Ok(prs) => {
214                    if prs.is_empty() {
215                        break;
216                    }
217
218                    for pr in prs {
219                        yield Ok(Box::new(pr) as Box<dyn RemotePullRequest>);
220                    }
221                }
222                Err(e) => {
223                    yield Err(e);
224                    break;
225                }
226            }
227        }
228            }
229    }
230}
231
232#[cfg(test)]
233mod test {
234    use pretty_assertions::assert_eq;
235
236    use super::*;
237    use crate::remote::RemoteCommit;
238
239    #[test]
240    fn timestamp() {
241        let remote_commit = GiteaCommit {
242            sha: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
243            author: Some(GiteaCommitAuthor {
244                login: Some(String::from("orhun")),
245            }),
246            created: String::from("2021-07-18T15:14:39+03:00"),
247        };
248
249        assert_eq!(Some(1_626_610_479), remote_commit.timestamp());
250    }
251}