#[cfg(feature = "github")]
pub mod github;
#[cfg(feature = "gitlab")]
pub mod gitlab;
#[cfg(feature = "bitbucket")]
pub mod bitbucket;
#[cfg(feature = "gitea")]
pub mod gitea;
#[cfg(feature = "azure_devops")]
pub mod azure_devops;
use std::env;
use std::fmt::Debug;
use std::time::Duration;
use dyn_clone::DynClone;
use http_cache_reqwest::{CACacheManager, Cache, CacheMode, HttpCache, HttpCacheOptions};
use reqwest::Client;
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use secrecy::ExposeSecret;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use time::format_description::well_known::Rfc3339;
use crate::config::Remote;
use crate::contributor::RemoteContributor;
use crate::error::{Error, Result};
pub(crate) const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
pub(crate) const REQUEST_TIMEOUT: u64 = 30;
pub(crate) const REQUEST_KEEP_ALIVE: u64 = 60;
pub(crate) const MAX_PAGE_SIZE: usize = 100;
pub trait RemoteCommit: DynClone {
fn id(&self) -> String;
fn username(&self) -> Option<String>;
fn timestamp(&self) -> Option<i64>;
fn convert_to_unix_timestamp(&self, date: &str) -> i64 {
OffsetDateTime::parse(date, &Rfc3339)
.expect("failed to parse date")
.unix_timestamp()
}
}
dyn_clone::clone_trait_object!(RemoteCommit);
pub trait RemotePullRequest: DynClone {
fn number(&self) -> i64;
fn title(&self) -> Option<String>;
fn labels(&self) -> Vec<String>;
fn merge_commit(&self) -> Option<String>;
}
dyn_clone::clone_trait_object!(RemotePullRequest);
pub type RemoteMetadata = (Vec<Box<dyn RemoteCommit>>, Vec<Box<dyn RemotePullRequest>>);
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
pub struct RemoteReleaseMetadata {
pub contributors: Vec<RemoteContributor>,
}
impl Remote {
fn create_client(&self, accept_header: &str) -> Result<ClientWithMiddleware> {
if !self.is_set() {
return Err(Error::RemoteNotSetError);
}
let mut headers = HeaderMap::new();
headers.insert(
reqwest::header::ACCEPT,
HeaderValue::from_str(accept_header)?,
);
if let Some(token) = &self.token {
headers.insert(
reqwest::header::AUTHORIZATION,
format!("Bearer {}", token.expose_secret()).parse()?,
);
}
headers.insert(reqwest::header::USER_AGENT, USER_AGENT.parse()?);
let client_builder = Client::builder()
.timeout(Duration::from_secs(REQUEST_TIMEOUT))
.tcp_keepalive(Duration::from_secs(REQUEST_KEEP_ALIVE))
.default_headers(headers)
.tls_built_in_root_certs(false);
let client_builder = if self.native_tls.unwrap_or(false) {
client_builder.tls_built_in_native_certs(true)
} else {
client_builder.tls_built_in_webpki_certs(true)
};
let client = client_builder.build()?;
let client = ClientBuilder::new(client)
.with(Cache(HttpCache {
mode: CacheMode::Default,
manager: CACacheManager {
path: dirs::cache_dir()
.ok_or_else(|| {
Error::DirsError(String::from(
"failed to find the user's cache directory",
))
})?
.join(env!("CARGO_PKG_NAME")),
},
options: HttpCacheOptions::default(),
}))
.build();
Ok(client)
}
}
pub trait RemoteClient {
const API_URL: &'static str;
const API_URL_ENV: &'static str;
fn api_url(&self) -> String {
env::var(Self::API_URL_ENV)
.ok()
.or(self.remote().api_url)
.unwrap_or_else(|| Self::API_URL.to_string())
}
fn remote(&self) -> Remote;
fn client(&self) -> ClientWithMiddleware;
async fn get_json<T: DeserializeOwned>(&self, url: &str) -> Result<T> {
log::debug!("Sending request to: {url}");
let response = self.client().get(url).send().await?;
let response_text = if response.status().is_success() {
let text = response.text().await?;
log::trace!("Response: {text:?}");
text
} else {
let text = response.text().await?;
log::error!("Request error: {text}");
text
};
Ok(serde_json::from_str::<T>(&response_text)?)
}
}
#[doc(hidden)]
#[macro_export]
macro_rules! update_release_metadata {
($remote: ident, $fn: ident) => {
impl<'a> Release<'a> {
#[allow(deprecated)]
pub fn $fn(
&mut self,
mut commits: Vec<Box<dyn RemoteCommit>>,
pull_requests: Vec<Box<dyn RemotePullRequest>>,
) -> Result<()> {
let mut contributors: Vec<RemoteContributor> = Vec::new();
let mut release_commit_timestamp: Option<i64> = None;
commits.retain(|v| {
if let Some(commit) = self.commits.iter_mut().find(|commit| commit.id == v.id())
{
let sha_short = Some(v.id().clone().chars().take(12).collect());
let pull_request = pull_requests.iter().find(|pr| {
pr.merge_commit() == Some(v.id().clone()) ||
pr.merge_commit() == sha_short
});
commit.$remote.username = v.username();
commit.$remote.pr_number = pull_request.map(|v| v.number());
commit.$remote.pr_title = pull_request.and_then(|v| v.title().clone());
commit.$remote.pr_labels =
pull_request.map(|v| v.labels().clone()).unwrap_or_default();
if !contributors
.iter()
.any(|v| commit.$remote.username == v.username)
{
contributors.push(RemoteContributor {
username: commit.$remote.username.clone(),
pr_title: commit.$remote.pr_title.clone(),
pr_number: commit.$remote.pr_number,
pr_labels: commit.$remote.pr_labels.clone(),
is_first_time: false,
});
}
commit.remote = Some(commit.$remote.clone());
if Some(v.id().clone()) == self.commit_id {
release_commit_timestamp = v.timestamp().clone();
}
false
} else {
true
}
});
self.$remote.contributors = contributors
.into_iter()
.map(|mut v| {
v.is_first_time = !commits
.iter()
.filter(|commit| {
self.timestamp.is_none() ||
release_commit_timestamp.is_none() ||
commit.timestamp() < release_commit_timestamp
})
.map(|v| v.username())
.any(|login| login == v.username);
v
})
.collect();
Ok(())
}
}
};
}