git_cliff_core/remote/
gitea.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] = &["gitea", "commit.gitea", "commit.remote"];
12
13#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub struct GiteaCommit {
16 pub sha: String,
18 pub author: Option<GiteaCommitAuthor>,
20 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#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub struct GiteaCommitAuthor {
41 pub login: Option<String>,
43}
44
45#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct PullRequestLabel {
49 pub name: String,
51}
52
53#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub struct GiteaPullRequest {
56 pub number: i64,
58 pub title: Option<String>,
60 pub merge_commit_sha: Option<String>,
62 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#[derive(Debug, Clone)]
86pub struct GiteaClient {
87 remote: Remote,
89 client: ClientWithMiddleware,
91}
92
93impl 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 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 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 #[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 #[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}