use std::{collections::HashMap, sync::Arc, time::Duration};
#[cfg(test)]
use global_counter::primitive::exact::CounterUsize;
use octorust::{
auth::Credentials,
http_cache::HttpCache,
types::{FullRepository, PublicUser},
Client,
};
use once_cell::sync::Lazy;
use crate::RUNTIME;
#[cfg(test)]
pub(crate) static GH_API_CALL_COUNTER: CounterUsize = CounterUsize::new(0);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GitHubRepositoryId {
owner: String,
repo: String,
}
impl GitHubRepositoryId {
#[must_use]
pub fn new(owner: String, repo: String) -> Self {
Self { owner, repo }
}
}
impl From<(String, String)> for GitHubRepositoryId {
fn from(value: (String, String)) -> Self {
Self {
owner: value.0,
repo: value.1,
}
}
}
static GITHUB_CLIENT: Lazy<octorust::Client> = Lazy::new(|| {
let http_cache = <dyn HttpCache>::in_home_dir();
let user_agent = std::env::var("USER_AGENT")
.expect("USER_AGENT environment variable not set");
let credentials = Credentials::Token(
std::env::var("GITHUB_API_TOKEN")
.expect("GITHUB_API_TOKEN environment variable not set"),
);
Client::custom(
user_agent,
credentials,
reqwest::Client::builder()
.build()
.expect("could not create GitHub reqwest client")
.into(),
http_cache,
)
});
static GITHUB_REPOS_CLIENT: Lazy<octorust::repos::Repos> =
Lazy::new(|| octorust::repos::Repos::new(GITHUB_CLIENT.clone()));
static GITHUB_USERS_CLIENT: Lazy<octorust::users::Users> =
Lazy::new(|| octorust::users::Users::new(GITHUB_CLIENT.clone()));
static GITHUB_RATE_LIMIT_CLIENT: Lazy<octorust::rate_limit::RateLimit> =
Lazy::new(|| octorust::rate_limit::RateLimit::new(GITHUB_CLIENT.clone()));
#[derive(Debug, Clone)]
pub struct GitHubClient {
repo_cache: HashMap<GitHubRepositoryId, Arc<FullRepository>>,
user_cache: HashMap<Arc<str>, Arc<PublicUser>>,
await_quota: bool,
}
enum AwaitQuotaResult {
QuotaAwaited { success: bool },
QuotaNotReached,
CouldNotCheck,
}
impl GitHubClient {
#[must_use]
pub fn new(await_quota: bool) -> Self {
Self {
repo_cache: HashMap::new(),
user_cache: HashMap::new(),
await_quota,
}
}
fn await_new_quota(&self) -> AwaitQuotaResult {
if self.await_quota {
let future = GITHUB_RATE_LIMIT_CLIENT.get();
match RUNTIME.block_on(future) {
Ok(r) => {
let rate = r.resources.core;
if rate.remaining == 0 {
let current_time = chrono::Utc::now();
let time_until_new_quota = Duration::from_millis(
(rate.reset - current_time.timestamp_millis())
as u64,
);
println!("No GitHub rate remaining, will wait for {} seconds ({} minutes)", time_until_new_quota.as_secs(), time_until_new_quota.as_secs() / 60);
std::thread::sleep(time_until_new_quota);
return match self.await_new_quota() {
AwaitQuotaResult::QuotaNotReached => {
AwaitQuotaResult::QuotaAwaited { success: true }
}
AwaitQuotaResult::QuotaAwaited { success } => {
AwaitQuotaResult::QuotaAwaited { success }
}
AwaitQuotaResult::CouldNotCheck => {
AwaitQuotaResult::CouldNotCheck
}
};
}
AwaitQuotaResult::QuotaNotReached
}
Err(e) => {
eprintln!(
"Failed to check GitHub rate limit due to error {e}"
);
AwaitQuotaResult::CouldNotCheck
}
}
} else {
panic!("client tried awaiating a new GitHub quota, but was marked to not do so");
}
}
pub fn get_repository(
&mut self,
id: &GitHubRepositoryId,
) -> Option<Arc<FullRepository>> {
if let Some(r) = self.repo_cache.get(id) {
Some(Arc::clone(r))
} else {
let future = GITHUB_REPOS_CLIENT.get(&id.owner, &id.repo);
#[cfg(test)]
{
GH_API_CALL_COUNTER.inc();
}
match RUNTIME.block_on(future) {
Ok(r) => {
let arcr = Arc::new(r);
self.repo_cache.insert(id.clone(), Arc::clone(&arcr));
Some(arcr)
}
Err(e) => {
if self.await_quota {
match self.await_new_quota() {
AwaitQuotaResult::QuotaAwaited {
success: true,
} => {
return self.get_repository(id);
}
AwaitQuotaResult::QuotaAwaited {
success: false,
} => {
eprintln!("GitHub quota reached, but new could not be awaited");
}
_ => {}
}
}
eprintln!("Failed to resolve GitHub repository {}/{} due to error: {e}", id.owner, id.repo);
None
}
}
}
}
pub fn get_public_user(
&mut self,
username: &str,
) -> Option<Arc<PublicUser>> {
if let Some(r) = self.user_cache.get(username) {
Some(Arc::clone(r))
} else {
let future = GITHUB_USERS_CLIENT.get_by_username(username);
#[cfg(test)]
{
GH_API_CALL_COUNTER.inc();
}
match RUNTIME.block_on(future) {
Ok(u) => {
let u = u
.public_user()
.expect(
"could not convert user response to public user",
)
.clone();
let arc_pubu = Arc::new(u);
self.user_cache
.insert(username.into(), Arc::clone(&arc_pubu));
Some(arc_pubu)
}
Err(e) => {
if self.await_quota {
match self.await_new_quota() {
AwaitQuotaResult::QuotaAwaited {
success: true,
} => {
return self.get_public_user(username);
}
AwaitQuotaResult::QuotaAwaited {
success: false,
} => {
eprintln!("GitHub quota reached, but new could not be awaited");
}
_ => {}
}
}
eprintln!("Failed to resolve GitHub user {username} due to error: {e}");
None
}
}
}
}
}
impl Default for GitHubClient {
fn default() -> Self {
Self::new(false)
}
}