1#[cfg(feature = "github")]
3pub mod github;
4
5#[cfg(feature = "gitlab")]
7pub mod gitlab;
8
9#[cfg(feature = "bitbucket")]
11pub mod bitbucket;
12
13#[cfg(feature = "gitea")]
15pub mod gitea;
16
17use crate::config::Remote;
18use crate::contributor::RemoteContributor;
19use crate::error::{
20 Error,
21 Result,
22};
23use dyn_clone::DynClone;
24use futures::{
25 StreamExt,
26 future,
27 stream,
28};
29use http_cache_reqwest::{
30 CACacheManager,
31 Cache,
32 CacheMode,
33 HttpCache,
34 HttpCacheOptions,
35};
36use reqwest::Client;
37use reqwest::header::{
38 HeaderMap,
39 HeaderValue,
40};
41use reqwest_middleware::{
42 ClientBuilder,
43 ClientWithMiddleware,
44};
45use secrecy::ExposeSecret;
46use serde::de::DeserializeOwned;
47use serde::{
48 Deserialize,
49 Serialize,
50};
51use std::env;
52use std::fmt::Debug;
53use std::time::Duration;
54use time::{
55 OffsetDateTime,
56 format_description::well_known::Rfc3339,
57};
58
59pub(crate) const USER_AGENT: &str =
63 concat!(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
64
65pub(crate) const REQUEST_TIMEOUT: u64 = 30;
67
68pub(crate) const REQUEST_KEEP_ALIVE: u64 = 60;
70
71pub(crate) const MAX_PAGE_SIZE: usize = 100;
73
74pub trait RemoteEntry {
76 fn url(
78 project_id: i64,
79 api_url: &str,
80 remote: &Remote,
81 ref_name: Option<&str>,
82 page: i32,
83 ) -> String;
84 fn buffer_size() -> usize;
86 fn early_exit(&self) -> bool;
88}
89
90pub trait RemoteCommit: DynClone {
92 fn id(&self) -> String;
94 fn username(&self) -> Option<String>;
96 fn timestamp(&self) -> Option<i64>;
98 fn convert_to_unix_timestamp(&self, date: &str) -> i64 {
100 OffsetDateTime::parse(date, &Rfc3339)
101 .expect("failed to parse date")
102 .unix_timestamp()
103 }
104}
105
106dyn_clone::clone_trait_object!(RemoteCommit);
107
108pub trait RemotePullRequest: DynClone {
110 fn number(&self) -> i64;
112 fn title(&self) -> Option<String>;
114 fn labels(&self) -> Vec<String>;
116 fn merge_commit(&self) -> Option<String>;
118}
119
120dyn_clone::clone_trait_object!(RemotePullRequest);
121
122pub type RemoteMetadata =
124 (Vec<Box<dyn RemoteCommit>>, Vec<Box<dyn RemotePullRequest>>);
125
126#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
128pub struct RemoteReleaseMetadata {
129 pub contributors: Vec<RemoteContributor>,
131}
132
133impl Remote {
134 fn create_client(&self, accept_header: &str) -> Result<ClientWithMiddleware> {
136 if !self.is_set() {
137 return Err(Error::RemoteNotSetError);
138 }
139 let mut headers = HeaderMap::new();
140 headers.insert(
141 reqwest::header::ACCEPT,
142 HeaderValue::from_str(accept_header)?,
143 );
144 if let Some(token) = &self.token {
145 headers.insert(
146 reqwest::header::AUTHORIZATION,
147 format!("Bearer {}", token.expose_secret()).parse()?,
148 );
149 }
150 headers.insert(reqwest::header::USER_AGENT, USER_AGENT.parse()?);
151 let client_builder = Client::builder()
152 .timeout(Duration::from_secs(REQUEST_TIMEOUT))
153 .tcp_keepalive(Duration::from_secs(REQUEST_KEEP_ALIVE))
154 .default_headers(headers)
155 .tls_built_in_root_certs(false);
156 let client_builder = if self.native_tls.unwrap_or(false) {
157 client_builder.tls_built_in_native_certs(true)
158 } else {
159 client_builder.tls_built_in_webpki_certs(true)
160 };
161 let client = client_builder.build()?;
162 let client = ClientBuilder::new(client)
163 .with(Cache(HttpCache {
164 mode: CacheMode::Default,
165 manager: CACacheManager {
166 path: dirs::cache_dir()
167 .ok_or_else(|| {
168 Error::DirsError(String::from(
169 "failed to find the user's cache directory",
170 ))
171 })?
172 .join(env!("CARGO_PKG_NAME")),
173 },
174 options: HttpCacheOptions::default(),
175 }))
176 .build();
177 Ok(client)
178 }
179}
180
181pub trait RemoteClient {
183 const API_URL: &'static str;
185
186 const API_URL_ENV: &'static str;
189
190 fn api_url(&self) -> String {
192 env::var(Self::API_URL_ENV)
193 .ok()
194 .or(self.remote().api_url)
195 .unwrap_or_else(|| Self::API_URL.to_string())
196 }
197
198 fn remote(&self) -> Remote;
200
201 fn client(&self) -> ClientWithMiddleware;
203
204 fn early_exit<T: DeserializeOwned + RemoteEntry>(&self, page: &T) -> bool {
206 page.early_exit()
207 }
208
209 async fn get_entry<T: DeserializeOwned + RemoteEntry>(
211 &self,
212 project_id: i64,
213 ref_name: Option<&str>,
214 page: i32,
215 ) -> Result<T> {
216 let url =
217 T::url(project_id, &self.api_url(), &self.remote(), ref_name, page);
218 debug!("Sending request to: {url}");
219 let response = self.client().get(&url).send().await?;
220 let response_text = if response.status().is_success() {
221 let text = response.text().await?;
222 trace!("Response: {:?}", text);
223 text
224 } else {
225 let text = response.text().await?;
226 error!("Request error: {}", text);
227 text
228 };
229 Ok(serde_json::from_str::<T>(&response_text)?)
230 }
231
232 async fn get_entries_with_page<T: DeserializeOwned + RemoteEntry>(
234 &self,
235 project_id: i64,
236 ref_name: Option<&str>,
237 page: i32,
238 ) -> Result<Vec<T>> {
239 let url =
240 T::url(project_id, &self.api_url(), &self.remote(), ref_name, page);
241 debug!("Sending request to: {url}");
242 let response = self.client().get(&url).send().await?;
243 let response_text = if response.status().is_success() {
244 let text = response.text().await?;
245 trace!("Response: {:?}", text);
246 text
247 } else {
248 let text = response.text().await?;
249 error!("Request error: {}", text);
250 text
251 };
252 let response = serde_json::from_str::<Vec<T>>(&response_text)?;
253 if response.is_empty() {
254 Err(Error::PaginationError(String::from("end of entries")))
255 } else {
256 Ok(response)
257 }
258 }
259
260 async fn fetch<T: DeserializeOwned + RemoteEntry>(
264 &self,
265 project_id: i64,
266 ref_name: Option<&str>,
267 ) -> Result<Vec<T>> {
268 let entries: Vec<Vec<T>> = stream::iter(0..)
269 .map(|i| self.get_entries_with_page(project_id, ref_name, i))
270 .buffered(T::buffer_size())
271 .take_while(|page| {
272 if let Err(e) = page {
273 debug!("Error while fetching page: {:?}", e);
274 }
275 future::ready(page.is_ok())
276 })
277 .map(|page| match page {
278 Ok(v) => v,
279 Err(ref e) => {
280 log::error!("{:#?}", e);
281 page.expect("failed to fetch page: {}")
282 }
283 })
284 .collect()
285 .await;
286 Ok(entries.into_iter().flatten().collect())
287 }
288
289 async fn fetch_with_early_exit<T: DeserializeOwned + RemoteEntry>(
293 &self,
294 project_id: i64,
295 ref_name: Option<&str>,
296 ) -> Result<Vec<T>> {
297 let entries: Vec<T> = stream::iter(0..)
298 .map(|i| self.get_entry::<T>(project_id, ref_name, i))
299 .buffered(T::buffer_size())
300 .take_while(|page| {
301 let status = match page {
302 Ok(v) => !self.early_exit(v),
303 Err(e) => {
304 debug!("Error while fetching page: {:?}", e);
305 true
306 }
307 };
308 future::ready(status && page.is_ok())
309 })
310 .map(|page| match page {
311 Ok(v) => v,
312 Err(ref e) => {
313 log::error!("{:#?}", e);
314 page.expect("failed to fetch page: {}")
315 }
316 })
317 .collect()
318 .await;
319 Ok(entries)
320 }
321}
322
323#[doc(hidden)]
325#[macro_export]
326macro_rules! update_release_metadata {
327 ($remote: ident, $fn: ident) => {
328 impl<'a> Release<'a> {
329 #[allow(deprecated)]
338 pub fn $fn(
339 &mut self,
340 mut commits: Vec<Box<dyn RemoteCommit>>,
341 pull_requests: Vec<Box<dyn RemotePullRequest>>,
342 ) -> Result<()> {
343 let mut contributors: Vec<RemoteContributor> = Vec::new();
344 let mut release_commit_timestamp: Option<i64> = None;
345 commits.retain(|v| {
348 if let Some(commit) =
349 self.commits.iter_mut().find(|commit| commit.id == v.id())
350 {
351 let sha_short =
352 Some(v.id().clone().chars().take(12).collect());
353 let pull_request = pull_requests.iter().find(|pr| {
354 pr.merge_commit() == Some(v.id().clone()) ||
355 pr.merge_commit() == sha_short
356 });
357 commit.$remote.username = v.username();
358 commit.$remote.pr_number = pull_request.map(|v| v.number());
359 commit.$remote.pr_title =
360 pull_request.and_then(|v| v.title().clone());
361 commit.$remote.pr_labels = pull_request
362 .map(|v| v.labels().clone())
363 .unwrap_or_default();
364 if !contributors
365 .iter()
366 .any(|v| commit.$remote.username == v.username)
367 {
368 contributors.push(RemoteContributor {
369 username: commit.$remote.username.clone(),
370 pr_title: commit.$remote.pr_title.clone(),
371 pr_number: commit.$remote.pr_number,
372 pr_labels: commit.$remote.pr_labels.clone(),
373 is_first_time: false,
374 });
375 }
376 commit.remote = Some(commit.$remote.clone());
377 if Some(v.id().clone()) == self.commit_id {
380 release_commit_timestamp = v.timestamp().clone();
381 }
382 false
383 } else {
384 true
385 }
386 });
387 self.$remote.contributors = contributors
389 .into_iter()
390 .map(|mut v| {
391 v.is_first_time = !commits
392 .iter()
393 .filter(|commit| {
394 self.timestamp == 0 ||
398 commit.timestamp() < release_commit_timestamp
399 })
400 .map(|v| v.username())
401 .any(|login| login == v.username);
402 v
403 })
404 .collect();
405 Ok(())
406 }
407 }
408 };
409}