git_cliff_core/remote/
mod.rs1#[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 std::env;
18use std::fmt::Debug;
19use std::time::Duration;
20
21use dyn_clone::DynClone;
22use futures::{StreamExt, future, stream};
23use http_cache_reqwest::{CACacheManager, Cache, CacheMode, HttpCache, HttpCacheOptions};
24use reqwest::Client;
25use reqwest::header::{HeaderMap, HeaderValue};
26use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
27use secrecy::ExposeSecret;
28use serde::de::DeserializeOwned;
29use serde::{Deserialize, Serialize};
30use time::OffsetDateTime;
31use time::format_description::well_known::Rfc3339;
32
33use crate::config::Remote;
34use crate::contributor::RemoteContributor;
35use crate::error::{Error, Result};
36
37pub(crate) const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
41
42pub(crate) const REQUEST_TIMEOUT: u64 = 30;
44
45pub(crate) const REQUEST_KEEP_ALIVE: u64 = 60;
47
48pub(crate) const MAX_PAGE_SIZE: usize = 100;
50
51pub trait RemoteEntry {
53 fn url(
55 project_id: i64,
56 api_url: &str,
57 remote: &Remote,
58 ref_name: Option<&str>,
59 page: i32,
60 ) -> String;
61 fn buffer_size() -> usize;
63 fn early_exit(&self) -> bool;
65}
66
67pub trait RemoteCommit: DynClone {
69 fn id(&self) -> String;
71 fn username(&self) -> Option<String>;
73 fn timestamp(&self) -> Option<i64>;
75 fn convert_to_unix_timestamp(&self, date: &str) -> i64 {
77 OffsetDateTime::parse(date, &Rfc3339)
78 .expect("failed to parse date")
79 .unix_timestamp()
80 }
81}
82
83dyn_clone::clone_trait_object!(RemoteCommit);
84
85pub trait RemotePullRequest: DynClone {
87 fn number(&self) -> i64;
89 fn title(&self) -> Option<String>;
91 fn labels(&self) -> Vec<String>;
93 fn merge_commit(&self) -> Option<String>;
95}
96
97dyn_clone::clone_trait_object!(RemotePullRequest);
98
99pub type RemoteMetadata = (Vec<Box<dyn RemoteCommit>>, Vec<Box<dyn RemotePullRequest>>);
101
102#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
104pub struct RemoteReleaseMetadata {
105 pub contributors: Vec<RemoteContributor>,
107}
108
109impl Remote {
110 fn create_client(&self, accept_header: &str) -> Result<ClientWithMiddleware> {
112 if !self.is_set() {
113 return Err(Error::RemoteNotSetError);
114 }
115 let mut headers = HeaderMap::new();
116 headers.insert(
117 reqwest::header::ACCEPT,
118 HeaderValue::from_str(accept_header)?,
119 );
120 if let Some(token) = &self.token {
121 headers.insert(
122 reqwest::header::AUTHORIZATION,
123 format!("Bearer {}", token.expose_secret()).parse()?,
124 );
125 }
126 headers.insert(reqwest::header::USER_AGENT, USER_AGENT.parse()?);
127 let client_builder = Client::builder()
128 .timeout(Duration::from_secs(REQUEST_TIMEOUT))
129 .tcp_keepalive(Duration::from_secs(REQUEST_KEEP_ALIVE))
130 .default_headers(headers)
131 .tls_built_in_root_certs(false);
132 let client_builder = if self.native_tls.unwrap_or(false) {
133 client_builder.tls_built_in_native_certs(true)
134 } else {
135 client_builder.tls_built_in_webpki_certs(true)
136 };
137 let client = client_builder.build()?;
138 let client = ClientBuilder::new(client)
139 .with(Cache(HttpCache {
140 mode: CacheMode::Default,
141 manager: CACacheManager {
142 path: dirs::cache_dir()
143 .ok_or_else(|| {
144 Error::DirsError(String::from(
145 "failed to find the user's cache directory",
146 ))
147 })?
148 .join(env!("CARGO_PKG_NAME")),
149 },
150 options: HttpCacheOptions::default(),
151 }))
152 .build();
153 Ok(client)
154 }
155}
156
157pub trait RemoteClient {
159 const API_URL: &'static str;
161
162 const API_URL_ENV: &'static str;
165
166 fn api_url(&self) -> String {
168 env::var(Self::API_URL_ENV)
169 .ok()
170 .or(self.remote().api_url)
171 .unwrap_or_else(|| Self::API_URL.to_string())
172 }
173
174 fn remote(&self) -> Remote;
176
177 fn client(&self) -> ClientWithMiddleware;
179
180 fn early_exit<T: DeserializeOwned + RemoteEntry>(&self, page: &T) -> bool {
182 page.early_exit()
183 }
184
185 async fn get_entry<T: DeserializeOwned + RemoteEntry>(
187 &self,
188 project_id: i64,
189 ref_name: Option<&str>,
190 page: i32,
191 ) -> Result<T> {
192 let url = T::url(project_id, &self.api_url(), &self.remote(), ref_name, page);
193 debug!("Sending request to: {url}");
194 let response = self.client().get(&url).send().await?;
195 let response_text = if response.status().is_success() {
196 let text = response.text().await?;
197 trace!("Response: {:?}", text);
198 text
199 } else {
200 let text = response.text().await?;
201 error!("Request error: {}", text);
202 text
203 };
204 Ok(serde_json::from_str::<T>(&response_text)?)
205 }
206
207 async fn get_entries_with_page<T: DeserializeOwned + RemoteEntry>(
209 &self,
210 project_id: i64,
211 ref_name: Option<&str>,
212 page: i32,
213 ) -> Result<Vec<T>> {
214 let url = T::url(project_id, &self.api_url(), &self.remote(), ref_name, page);
215 debug!("Sending request to: {url}");
216 let response = self.client().get(&url).send().await?;
217 let response_text = if response.status().is_success() {
218 let text = response.text().await?;
219 trace!("Response: {:?}", text);
220 text
221 } else {
222 let text = response.text().await?;
223 error!("Request error: {}", text);
224 text
225 };
226 let response = serde_json::from_str::<Vec<T>>(&response_text)?;
227 if response.is_empty() {
228 Err(Error::PaginationError(String::from("end of entries")))
229 } else {
230 Ok(response)
231 }
232 }
233
234 async fn fetch<T: DeserializeOwned + RemoteEntry>(
238 &self,
239 project_id: i64,
240 ref_name: Option<&str>,
241 ) -> Result<Vec<T>> {
242 let entries: Vec<Vec<T>> = stream::iter(0..)
243 .map(|i| self.get_entries_with_page(project_id, ref_name, i))
244 .buffered(T::buffer_size())
245 .take_while(|page| {
246 if let Err(e) = page {
247 debug!("Error while fetching page: {:?}", e);
248 }
249 future::ready(page.is_ok())
250 })
251 .map(|page| match page {
252 Ok(v) => v,
253 Err(ref e) => {
254 log::error!("{:#?}", e);
255 page.expect("failed to fetch page: {}")
256 }
257 })
258 .collect()
259 .await;
260 Ok(entries.into_iter().flatten().collect())
261 }
262
263 async fn fetch_with_early_exit<T: DeserializeOwned + RemoteEntry>(
267 &self,
268 project_id: i64,
269 ref_name: Option<&str>,
270 ) -> Result<Vec<T>> {
271 let entries: Vec<T> = stream::iter(0..)
272 .map(|i| self.get_entry::<T>(project_id, ref_name, i))
273 .buffered(T::buffer_size())
274 .take_while(|page| {
275 let status = match page {
276 Ok(v) => !self.early_exit(v),
277 Err(e) => {
278 debug!("Error while fetching page: {:?}", e);
279 true
280 }
281 };
282 future::ready(status && page.is_ok())
283 })
284 .map(|page| match page {
285 Ok(v) => v,
286 Err(ref e) => {
287 log::error!("{:#?}", e);
288 page.expect("failed to fetch page: {}")
289 }
290 })
291 .collect()
292 .await;
293 Ok(entries)
294 }
295}
296
297#[doc(hidden)]
299#[macro_export]
300macro_rules! update_release_metadata {
301 ($remote: ident, $fn: ident) => {
302 impl<'a> Release<'a> {
303 #[allow(deprecated)]
310 pub fn $fn(
311 &mut self,
312 mut commits: Vec<Box<dyn RemoteCommit>>,
313 pull_requests: Vec<Box<dyn RemotePullRequest>>,
314 ) -> Result<()> {
315 let mut contributors: Vec<RemoteContributor> = Vec::new();
316 let mut release_commit_timestamp: Option<i64> = None;
317 commits.retain(|v| {
320 if let Some(commit) = self.commits.iter_mut().find(|commit| commit.id == v.id())
321 {
322 let sha_short = Some(v.id().clone().chars().take(12).collect());
323 let pull_request = pull_requests.iter().find(|pr| {
324 pr.merge_commit() == Some(v.id().clone()) ||
325 pr.merge_commit() == sha_short
326 });
327 commit.$remote.username = v.username();
328 commit.$remote.pr_number = pull_request.map(|v| v.number());
329 commit.$remote.pr_title = pull_request.and_then(|v| v.title().clone());
330 commit.$remote.pr_labels =
331 pull_request.map(|v| v.labels().clone()).unwrap_or_default();
332 if !contributors
333 .iter()
334 .any(|v| commit.$remote.username == v.username)
335 {
336 contributors.push(RemoteContributor {
337 username: commit.$remote.username.clone(),
338 pr_title: commit.$remote.pr_title.clone(),
339 pr_number: commit.$remote.pr_number,
340 pr_labels: commit.$remote.pr_labels.clone(),
341 is_first_time: false,
342 });
343 }
344 commit.remote = Some(commit.$remote.clone());
345 if Some(v.id().clone()) == self.commit_id {
348 release_commit_timestamp = v.timestamp().clone();
349 }
350 false
351 } else {
352 true
353 }
354 });
355 self.$remote.contributors = contributors
357 .into_iter()
358 .map(|mut v| {
359 v.is_first_time = !commits
360 .iter()
361 .filter(|commit| {
362 self.timestamp == None ||
366 commit.timestamp() < release_commit_timestamp
367 })
368 .map(|v| v.username())
369 .any(|login| login == v.username);
370 v
371 })
372 .collect();
373 Ok(())
374 }
375 }
376 };
377}