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
17#[cfg(feature = "azure_devops")]
19pub mod azure_devops;
20
21use std::env;
22use std::fmt::Debug;
23use std::time::Duration;
24
25use dyn_clone::DynClone;
26use etcetera::{BaseStrategy, choose_base_strategy};
27use http_cache_reqwest::{CACacheManager, Cache, CacheMode, HttpCache, HttpCacheOptions};
28use reqwest::Client;
29use reqwest::header::{HeaderMap, HeaderValue};
30use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
31use secrecy::ExposeSecret;
32use serde::de::DeserializeOwned;
33use serde::{Deserialize, Serialize};
34use time::OffsetDateTime;
35use time::format_description::well_known::Rfc3339;
36
37use crate::config::Remote;
38use crate::contributor::RemoteContributor;
39use crate::error::{Error, Result};
40
41pub(crate) const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
45
46pub(crate) const REQUEST_TIMEOUT: u64 = 30;
48
49pub(crate) const REQUEST_KEEP_ALIVE: u64 = 60;
51
52pub(crate) const MAX_PAGE_SIZE: i32 = 100;
54
55pub trait RemoteCommit: DynClone {
57 fn id(&self) -> String;
59 fn username(&self) -> Option<String>;
61 fn timestamp(&self) -> Option<i64>;
63 fn convert_to_unix_timestamp(&self, date: &str) -> i64 {
65 OffsetDateTime::parse(date, &Rfc3339)
66 .expect("failed to parse date")
67 .unix_timestamp()
68 }
69}
70
71dyn_clone::clone_trait_object!(RemoteCommit);
72
73pub trait RemotePullRequest: DynClone {
75 fn number(&self) -> i64;
77 fn title(&self) -> Option<String>;
79 fn labels(&self) -> Vec<String>;
81 fn merge_commit(&self) -> Option<String>;
83}
84
85dyn_clone::clone_trait_object!(RemotePullRequest);
86
87pub type RemoteMetadata = (Vec<Box<dyn RemoteCommit>>, Vec<Box<dyn RemotePullRequest>>);
89
90#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
92pub struct RemoteReleaseMetadata {
93 pub contributors: Vec<RemoteContributor>,
95}
96
97impl Remote {
98 fn create_client(&self, accept_header: &str) -> Result<ClientWithMiddleware> {
100 if !self.is_set() {
101 return Err(Error::RemoteNotSetError);
102 }
103 let strategy = choose_base_strategy()
105 .expect("cannot determine current OS's default strategy (layout)");
106 let mut headers = HeaderMap::new();
107 headers.insert(
108 reqwest::header::ACCEPT,
109 HeaderValue::from_str(accept_header)?,
110 );
111 if let Some(token) = &self.token {
112 headers.insert(
113 reqwest::header::AUTHORIZATION,
114 format!("Bearer {}", token.expose_secret()).parse()?,
115 );
116 }
117 headers.insert(reqwest::header::USER_AGENT, USER_AGENT.parse()?);
118 let client_builder = Client::builder()
119 .timeout(Duration::from_secs(REQUEST_TIMEOUT))
120 .tcp_keepalive(Duration::from_secs(REQUEST_KEEP_ALIVE))
121 .default_headers(headers)
122 .tls_built_in_root_certs(false);
123 let client_builder = if self.native_tls.unwrap_or(false) {
124 client_builder.tls_built_in_native_certs(true)
125 } else {
126 client_builder.tls_built_in_webpki_certs(true)
127 };
128 let client = client_builder.build()?;
129 let client = ClientBuilder::new(client)
130 .with(Cache(HttpCache {
131 mode: CacheMode::Default,
132 manager: CACacheManager {
133 path: strategy.cache_dir().join(env!("CARGO_PKG_NAME")),
134 },
135 options: HttpCacheOptions::default(),
136 }))
137 .build();
138 Ok(client)
139 }
140}
141
142pub trait RemoteClient {
144 const API_URL: &'static str;
146
147 const API_URL_ENV: &'static str;
150
151 fn api_url(&self) -> String {
153 env::var(Self::API_URL_ENV)
154 .ok()
155 .or(self.remote().api_url)
156 .unwrap_or_else(|| Self::API_URL.to_string())
157 }
158
159 fn remote(&self) -> Remote;
161
162 fn client(&self) -> ClientWithMiddleware;
164
165 async fn get_json<T: DeserializeOwned>(&self, url: &str) -> Result<T> {
169 tracing::debug!("Sending request to: {url}");
170 let response = self.client().get(url).send().await?.error_for_status()?;
171 let response_text = if response.status().is_success() {
172 let text = response.text().await?;
173 tracing::trace!("Response: {text:?}");
174 text
175 } else {
176 let text = response.text().await?;
177 tracing::error!("Request error: {text}");
178 text
179 };
180 Ok(serde_json::from_str::<T>(&response_text)?)
181 }
182}
183
184#[doc(hidden)]
186#[macro_export]
187macro_rules! update_release_metadata {
188 ($remote: ident, $fn: ident) => {
189 impl<'a> Release<'a> {
190 #[allow(deprecated)]
197 pub fn $fn(
198 &mut self,
199 mut commits: Vec<Box<dyn RemoteCommit>>,
200 pull_requests: Vec<Box<dyn RemotePullRequest>>,
201 ) -> Result<()> {
202 let mut contributors: Vec<RemoteContributor> = Vec::new();
203 let mut release_commit_timestamp: Option<i64> = None;
204 commits.retain(|v| {
207 if let Some(commit) = self.commits.iter_mut().find(|commit| commit.id == v.id())
208 {
209 let sha_short = Some(v.id().clone().chars().take(12).collect());
210 let pull_request = pull_requests.iter().find(|pr| {
211 pr.merge_commit() == Some(v.id().clone()) ||
212 pr.merge_commit() == sha_short
213 });
214 commit.$remote.username = v.username();
215 commit.$remote.pr_number = pull_request.map(|v| v.number());
216 commit.$remote.pr_title = pull_request.and_then(|v| v.title().clone());
217 commit.$remote.pr_labels =
218 pull_request.map(|v| v.labels().clone()).unwrap_or_default();
219 if !contributors
220 .iter()
221 .any(|v| commit.$remote.username == v.username)
222 {
223 contributors.push(RemoteContributor {
224 username: commit.$remote.username.clone(),
225 pr_title: commit.$remote.pr_title.clone(),
226 pr_number: commit.$remote.pr_number,
227 pr_labels: commit.$remote.pr_labels.clone(),
228 is_first_time: false,
229 });
230 }
231 commit.remote = Some(commit.$remote.clone());
232 if Some(v.id().clone()) == self.commit_id {
235 release_commit_timestamp = v.timestamp().clone();
236 }
237 false
238 } else {
239 true
240 }
241 });
242 self.$remote.contributors = contributors
244 .into_iter()
245 .map(|mut v| {
246 v.is_first_time = !commits
247 .iter()
248 .filter(|commit| {
249 self.timestamp.is_none() ||
253 release_commit_timestamp.is_none() ||
254 commit.timestamp() < release_commit_timestamp
255 })
256 .map(|v| v.username())
257 .any(|login| login == v.username);
258 v
259 })
260 .collect();
261 Ok(())
262 }
263 }
264 };
265}