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 cacache::RemoveOpts;
26use dyn_clone::DynClone;
27use etcetera::{BaseStrategy, choose_base_strategy};
28use http_cache_reqwest::{CACacheManager, Cache, CacheMode, HttpCache, HttpCacheOptions};
29use reqwest::Client;
30use reqwest::header::{HeaderMap, HeaderValue};
31use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
32use secrecy::ExposeSecret;
33use serde::de::DeserializeOwned;
34use serde::{Deserialize, Serialize};
35use time::OffsetDateTime;
36use time::format_description::well_known::Rfc3339;
37
38use crate::config::Remote;
39use crate::contributor::RemoteContributor;
40use crate::error::{Error, Result};
41
42pub(crate) const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
46
47pub(crate) const REQUEST_KEEP_ALIVE: u64 = 60;
49
50pub(crate) const MAX_PAGE_SIZE: i32 = 100;
52
53pub trait RemoteCommit: DynClone {
55 fn id(&self) -> String;
57 fn username(&self) -> Option<String>;
59 fn timestamp(&self) -> Option<i64>;
61 fn convert_to_unix_timestamp(&self, date: &str) -> i64 {
63 OffsetDateTime::parse(date, &Rfc3339)
64 .expect("failed to parse date")
65 .unix_timestamp()
66 }
67}
68
69dyn_clone::clone_trait_object!(RemoteCommit);
70
71pub trait RemotePullRequest: DynClone {
73 fn number(&self) -> i64;
75 fn title(&self) -> Option<String>;
77 fn author(&self) -> Option<String> {
81 None
82 }
83 fn labels(&self) -> Vec<String>;
85 fn merge_commit(&self) -> Option<String>;
87}
88
89dyn_clone::clone_trait_object!(RemotePullRequest);
90
91pub type RemoteMetadata = (Vec<Box<dyn RemoteCommit>>, Vec<Box<dyn RemotePullRequest>>);
93
94#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
96pub struct RemoteReleaseMetadata {
97 pub contributors: Vec<RemoteContributor>,
99}
100
101impl Remote {
102 fn create_client(&self, accept_header: &str) -> Result<ClientWithMiddleware> {
104 if !self.is_set() {
105 return Err(Error::RemoteNotSetError);
106 }
107 let strategy = choose_base_strategy()
109 .expect("cannot determine current OS's default strategy (layout)");
110 let mut headers = HeaderMap::new();
111 headers.insert(
112 reqwest::header::ACCEPT,
113 HeaderValue::from_str(accept_header)?,
114 );
115 if let Some(token) = &self.token {
116 headers.insert(
117 reqwest::header::AUTHORIZATION,
118 format!("Bearer {}", token.expose_secret()).parse()?,
119 );
120 }
121 headers.insert(reqwest::header::USER_AGENT, USER_AGENT.parse()?);
122 let client_builder = Client::builder()
123 .timeout(self.http_timeout)
124 .tcp_keepalive(Duration::from_secs(REQUEST_KEEP_ALIVE))
125 .default_headers(headers)
126 .tls_built_in_root_certs(false);
127 let client_builder = if self.native_tls.unwrap_or(false) {
128 client_builder.tls_built_in_native_certs(true)
129 } else {
130 client_builder.tls_built_in_webpki_certs(true)
131 };
132 let client = client_builder.build()?;
133 let client = ClientBuilder::new(client)
134 .with(Cache(HttpCache {
135 mode: CacheMode::Default,
136 manager: CACacheManager {
137 path: strategy.cache_dir().join(env!("CARGO_PKG_NAME")),
138 remove_opts: RemoveOpts::default(),
139 },
140 options: HttpCacheOptions::default(),
141 }))
142 .build();
143 Ok(client)
144 }
145}
146
147pub trait RemoteClient {
149 const API_URL: &'static str;
151
152 const API_URL_ENV: &'static str;
155
156 fn api_url(&self) -> String {
158 env::var(Self::API_URL_ENV)
159 .ok()
160 .or(self.remote().api_url)
161 .unwrap_or_else(|| Self::API_URL.to_string())
162 }
163
164 fn remote(&self) -> Remote;
166
167 fn client(&self) -> ClientWithMiddleware;
169
170 async fn get_json<T: DeserializeOwned>(&self, url: &str) -> Result<T> {
174 tracing::debug!("Sending request to: {url}");
175 let response = self.client().get(url).send().await?.error_for_status()?;
176 let response_text = if response.status().is_success() {
177 let text = response.text().await?;
178 tracing::trace!("Response: {text:?}");
179 text
180 } else {
181 let text = response.text().await?;
182 tracing::error!("Request error: {text}");
183 text
184 };
185 Ok(serde_json::from_str::<T>(&response_text)?)
186 }
187}
188
189#[doc(hidden)]
191#[macro_export]
192macro_rules! update_release_metadata {
193 ($remote: ident, $fn: ident) => {
194 impl<'a> Release<'a> {
195 #[allow(deprecated)]
202 pub fn $fn(
203 &mut self,
204 mut commits: Vec<Box<dyn RemoteCommit>>,
205 pull_requests: Vec<Box<dyn RemotePullRequest>>,
206 ) -> Result<()> {
207 let mut contributors: Vec<RemoteContributor> = Vec::new();
208 let mut release_commit_timestamp: Option<i64> = None;
209 commits.retain(|v| {
212 if let Some(commit) = self.commits.iter_mut().find(|commit| commit.id == v.id())
213 {
214 let sha_short = Some(v.id().clone().chars().take(12).collect());
215 let pull_request = pull_requests.iter().find(|pr| {
216 pr.merge_commit() == Some(v.id().clone()) ||
217 pr.merge_commit() == sha_short
218 });
219 commit.$remote.username = v.username();
220 commit.$remote.pr_author = pull_request.and_then(|v| v.author());
221 commit.$remote.pr_number = pull_request.map(|v| v.number());
222 commit.$remote.pr_title = pull_request.and_then(|v| v.title().clone());
223 commit.$remote.pr_labels =
224 pull_request.map(|v| v.labels().clone()).unwrap_or_default();
225 if let Some(existing) = contributors
226 .iter_mut()
227 .find(|v| commit.$remote.username == v.username)
228 {
229 if let Some(pr_num) = commit.$remote.pr_number {
230 if !existing.pr_numbers.contains(&pr_num) {
231 existing.pr_numbers.push(pr_num);
232 }
233 }
234 } else {
235 contributors.push(RemoteContributor {
236 username: commit.$remote.username.clone(),
237 pr_author: None,
242 pr_title: commit.$remote.pr_title.clone(),
243 pr_number: commit.$remote.pr_number,
244 pr_numbers: commit.$remote.pr_number.into_iter().collect(),
245 pr_labels: commit.$remote.pr_labels.clone(),
246 is_first_time: false,
247 });
248 }
249 commit.remote = Some(commit.$remote.clone());
250 if Some(v.id().clone()) == self.commit_id {
253 release_commit_timestamp = v.timestamp().clone();
254 }
255 false
256 } else {
257 true
258 }
259 });
260 self.$remote.contributors = contributors
262 .into_iter()
263 .map(|mut v| {
264 v.pr_numbers.sort_unstable();
265 v.is_first_time = !commits
266 .iter()
267 .filter(|commit| {
268 self.timestamp.is_none() ||
272 release_commit_timestamp.is_none() ||
273 commit.timestamp() < release_commit_timestamp
274 })
275 .map(|v| v.username())
276 .any(|login| login == v.username);
277 v
278 })
279 .collect();
280 Ok(())
281 }
282 }
283 };
284}