git_cliff_core/remote/
mod.rs#[cfg(feature = "github")]
pub mod github;
#[cfg(feature = "gitlab")]
pub mod gitlab;
#[cfg(feature = "bitbucket")]
pub mod bitbucket;
#[cfg(feature = "gitea")]
pub mod gitea;
use crate::config::Remote;
use crate::contributor::RemoteContributor;
use crate::error::{
Error,
Result,
};
use dyn_clone::DynClone;
use futures::{
future,
stream,
StreamExt,
};
use http_cache_reqwest::{
CACacheManager,
Cache,
CacheMode,
HttpCache,
HttpCacheOptions,
};
use reqwest::header::{
HeaderMap,
HeaderValue,
};
use reqwest::Client;
use reqwest_middleware::{
ClientBuilder,
ClientWithMiddleware,
};
use secrecy::ExposeSecret;
use serde::de::DeserializeOwned;
use serde::{
Deserialize,
Serialize,
};
use std::fmt::Debug;
use std::hash::Hash;
use std::time::Duration;
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 RemoteEntry {
fn url(project_id: i64, api_url: &str, remote: &Remote, page: i32) -> String;
fn buffer_size() -> usize;
fn early_exit(&self) -> bool;
}
pub trait RemoteCommit: DynClone {
fn id(&self) -> String;
fn username(&self) -> Option<String>;
}
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>,
}
fn create_remote_client(
remote: &Remote,
accept_header: &str,
) -> Result<ClientWithMiddleware> {
if !remote.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) = &remote.token {
headers.insert(
reqwest::header::AUTHORIZATION,
format!("Bearer {}", token.expose_secret()).parse()?,
);
}
headers.insert(reqwest::header::USER_AGENT, USER_AGENT.parse()?);
let client = Client::builder()
.timeout(Duration::from_secs(REQUEST_TIMEOUT))
.tcp_keepalive(Duration::from_secs(REQUEST_KEEP_ALIVE))
.default_headers(headers)
.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 {
fn api_url() -> String;
fn remote(&self) -> Remote;
fn client(&self) -> ClientWithMiddleware;
fn early_exit<T: DeserializeOwned + RemoteEntry>(&self, page: &T) -> bool {
page.early_exit()
}
async fn get_entry<T: DeserializeOwned + RemoteEntry>(
&self,
project_id: i64,
page: i32,
) -> Result<T> {
let url = T::url(project_id, &Self::api_url(), &self.remote(), page);
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?;
trace!("Response: {:?}", text);
text
} else {
let text = response.text().await?;
error!("Request error: {}", text);
text
};
Ok(serde_json::from_str::<T>(&response_text)?)
}
async fn get_entries_with_page<T: DeserializeOwned + RemoteEntry>(
&self,
project_id: i64,
page: i32,
) -> Result<Vec<T>> {
let url = T::url(project_id, &Self::api_url(), &self.remote(), page);
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?;
trace!("Response: {:?}", text);
text
} else {
let text = response.text().await?;
error!("Request error: {}", text);
text
};
let response = serde_json::from_str::<Vec<T>>(&response_text)?;
if response.is_empty() {
Err(Error::PaginationError(String::from("end of entries")))
} else {
Ok(response)
}
}
async fn fetch<T: DeserializeOwned + RemoteEntry>(
&self,
project_id: i64,
) -> Result<Vec<T>> {
let entries: Vec<Vec<T>> = stream::iter(0..)
.map(|i| self.get_entries_with_page(project_id, i))
.buffered(T::buffer_size())
.take_while(|page| {
if let Err(e) = page {
debug!("Error while fetching page: {:?}", e);
}
future::ready(page.is_ok())
})
.map(|page| match page {
Ok(v) => v,
Err(ref e) => {
log::error!("{:#?}", e);
page.expect("failed to fetch page: {}")
}
})
.collect()
.await;
Ok(entries.into_iter().flatten().collect())
}
async fn fetch_with_early_exit<T: DeserializeOwned + RemoteEntry>(
&self,
project_id: i64,
) -> Result<Vec<T>> {
let entries: Vec<T> = stream::iter(0..)
.map(|i| self.get_entry::<T>(project_id, i))
.buffered(T::buffer_size())
.take_while(|page| {
let status = match page {
Ok(v) => !self.early_exit(v),
Err(e) => {
debug!("Error while fetching page: {:?}", e);
true
}
};
future::ready(status && page.is_ok())
})
.map(|page| match page {
Ok(v) => v,
Err(ref e) => {
log::error!("{:#?}", e);
page.expect("failed to fetch page: {}")
}
})
.collect()
.await;
Ok(entries)
}
}
#[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();
commits.retain(|v| {
if let Some(commit) =
self.commits.iter_mut().find(|commit| commit.id == v.id())
{
let pull_request = pull_requests
.iter()
.find(|pr| pr.merge_commit() == Some(v.id().clone()));
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());
false
} else {
true
}
});
self.$remote.contributors = contributors
.into_iter()
.map(|mut v| {
v.is_first_time = !commits
.iter()
.map(|v| v.username())
.any(|login| login == v.username);
v
})
.collect();
Ok(())
}
}
};
}