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 user: Option<GiteaCommitAuthor>,
62 pub merge_commit_sha: Option<String>,
64 pub labels: Vec<PullRequestLabel>,
66}
67
68impl RemotePullRequest for GiteaPullRequest {
69 fn number(&self) -> i64 {
70 self.number
71 }
72
73 fn title(&self) -> Option<String> {
74 self.title.clone()
75 }
76
77 fn author(&self) -> Option<String> {
78 self.user.clone().and_then(|v| v.login)
79 }
80
81 fn labels(&self) -> Vec<String> {
82 self.labels.iter().map(|v| v.name.clone()).collect()
83 }
84
85 fn merge_commit(&self) -> Option<String> {
86 self.merge_commit_sha.clone()
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct GiteaClient {
93 remote: Remote,
95 client: ClientWithMiddleware,
97}
98
99impl TryFrom<Remote> for GiteaClient {
101 type Error = Error;
102 fn try_from(remote: Remote) -> Result<Self> {
103 Ok(Self {
104 client: remote.create_client("application/json")?,
105 remote,
106 })
107 }
108}
109
110impl RemoteClient for GiteaClient {
111 const API_URL: &'static str = "https://codeberg.org";
112 const API_URL_ENV: &'static str = "GITEA_API_URL";
113
114 fn remote(&self) -> Remote {
115 self.remote.clone()
116 }
117
118 fn client(&self) -> ClientWithMiddleware {
119 self.client.clone()
120 }
121}
122
123impl GiteaClient {
124 fn commits_url(api_url: &str, remote: &Remote, ref_name: Option<&str>, page: i32) -> String {
126 let mut url = format!(
127 "{}/api/v1/repos/{}/{}/commits?limit={MAX_PAGE_SIZE}&page={page}",
128 api_url, remote.owner, remote.repo
129 );
130
131 if let Some(ref_name) = ref_name {
132 url.push_str(&format!("&sha={ref_name}"));
133 }
134
135 url
136 }
137
138 fn pull_requests_url(api_url: &str, remote: &Remote, page: i32) -> String {
140 format!(
141 "{}/api/v1/repos/{}/{}/pulls?limit={MAX_PAGE_SIZE}&page={page}&state=closed",
142 api_url, remote.owner, remote.repo
143 )
144 }
145
146 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
150 pub async fn get_commits(&self, ref_name: Option<&str>) -> Result<Vec<Box<dyn RemoteCommit>>> {
151 use futures::TryStreamExt;
152 crate::set_progress_message!("Fetching all commits from Gitea");
153 self.get_commit_stream(ref_name).try_collect().await
154 }
155
156 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
160 pub async fn get_pull_requests(&self) -> Result<Vec<Box<dyn RemotePullRequest>>> {
161 use futures::TryStreamExt;
162 crate::set_progress_message!("Fetching all pull requests from Gitea");
163 self.get_pull_request_stream().try_collect().await
164 }
165
166 fn get_commit_stream(
167 &self,
168 ref_name: Option<&str>,
169 ) -> impl Stream<Item = Result<Box<dyn RemoteCommit>>> + '_ {
170 let ref_name = ref_name.map(ToString::to_string);
171 async_stream! {
172 let page_stream = stream::iter(0..)
173 .map(|page| {
174 let ref_name = ref_name.clone();
175 async move {
176 let url = Self::commits_url(&self.api_url(), &self.remote(), ref_name.as_deref(), page);
177 self.get_json::<Vec<GiteaCommit>>(&url).await
178 }
179 })
180 .buffered(10);
181
182 let mut page_stream = Box::pin(page_stream);
183
184 while let Some(page_result) = page_stream.next().await {
185 match page_result {
186 Ok(commits) => {
187 if commits.is_empty() {
188 break;
189 }
190
191 for commit in commits {
192 yield Ok(Box::new(commit) as Box<dyn RemoteCommit>);
193 }
194 }
195 Err(e) => {
196 yield Err(e);
197 break;
198 }
199 }
200 }
201 }
202 }
203
204 fn get_pull_request_stream(
205 &self,
206 ) -> impl Stream<Item = Result<Box<dyn RemotePullRequest>>> + '_ {
207 async_stream! {
208 let page_stream = stream::iter(0..)
209 .map(|page| async move {
210 let url = Self::pull_requests_url(&self.api_url(), &self.remote(), page);
211 self.get_json::<Vec<GiteaPullRequest>>(&url).await
212 })
213 .buffered(5);
214
215 let mut page_stream = Box::pin(page_stream);
216
217 while let Some(page_result) = page_stream.next().await {
218 match page_result {
219 Ok(prs) => {
220 if prs.is_empty() {
221 break;
222 }
223
224 for pr in prs {
225 yield Ok(Box::new(pr) as Box<dyn RemotePullRequest>);
226 }
227 }
228 Err(e) => {
229 yield Err(e);
230 break;
231 }
232 }
233 }
234 }
235 }
236}
237
238#[cfg(test)]
239mod test {
240 use pretty_assertions::assert_eq;
241
242 use super::*;
243 use crate::remote::{RemoteCommit, RemotePullRequest};
244
245 #[test]
246 fn timestamp() {
247 let remote_commit = GiteaCommit {
248 sha: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
249 author: Some(GiteaCommitAuthor {
250 login: Some(String::from("orhun")),
251 }),
252 created: String::from("2021-07-18T15:14:39+03:00"),
253 };
254
255 assert_eq!(Some(1_626_610_479), remote_commit.timestamp());
256 }
257
258 #[test]
259 fn pull_request_author() {
260 let pull_request: GiteaPullRequest = serde_json::from_str(
261 r#"{
262 "number": 42,
263 "title": "feat: add pr_author",
264 "merge_commit_sha": "1d244937ee6ceb8e0314a4a201ba93a7a61f2071",
265 "labels": [],
266 "user": { "login": "contributor" }
267 }"#,
268 )
269 .expect("failed to deserialize pull request");
270
271 assert_eq!(Some(String::from("contributor")), pull_request.author());
272 }
273
274 #[test]
275 fn pull_request_author_missing() {
276 let pull_request: GiteaPullRequest = serde_json::from_str(
277 r#"{
278 "number": 42,
279 "title": "feat: add pr_author",
280 "merge_commit_sha": "1d244937ee6ceb8e0314a4a201ba93a7a61f2071",
281 "labels": []
282 }"#,
283 )
284 .expect("failed to deserialize pull request");
285
286 assert_eq!(None, pull_request.author());
287 }
288}