use anyhow::{Context, Result};
use async_trait::async_trait;
use serde::Deserialize;
use std::sync::OnceLock;
use crate::constants::{
CHANNEL_STABLE, GITHUB_MAX_PAGES, GITHUB_PER_PAGE, GITHUB_RELEASES_API, RELEASES_MD_URL,
};
use crate::domain::RustRelease;
use crate::options::NetworkConfig;
use super::http::build_client;
use super::ReleaseFetcher;
static RELEASES_MD_RE: OnceLock<regex_lite::Regex> = OnceLock::new();
fn releases_md_regex() -> &'static regex_lite::Regex {
RELEASES_MD_RE.get_or_init(|| {
regex_lite::Regex::new(r"Version\s+(\d+\.\d+\.\d+)\s+\((\d{4}-\d{2}-\d{2})\)")
.expect("RELEASES_MD_RE regex pattern is statically valid")
})
}
#[derive(Debug, Deserialize)]
struct GhRelease {
tag_name: String,
published_at: String,
prerelease: bool,
draft: bool,
}
pub(super) struct StableFetcher {
full: bool,
}
impl StableFetcher {
pub fn new(full: bool) -> Self {
Self { full }
}
}
#[async_trait]
impl ReleaseFetcher for StableFetcher {
fn channel_name(&self) -> &'static str {
CHANNEL_STABLE
}
fn source_description(&self) -> &str {
if self.full {
"RELEASES.md (full history)"
} else {
"GitHub Releases API"
}
}
async fn fetch(&self, network: &NetworkConfig) -> Result<Vec<RustRelease>> {
if self.full {
fetch_from_releases_md(network).await
} else {
fetch_from_github(network).await
}
}
}
async fn fetch_from_github(network: &NetworkConfig) -> Result<Vec<RustRelease>> {
let client = build_client(network)?;
let mut all = Vec::new();
let mut page = 1u32;
loop {
let url = format!("{GITHUB_RELEASES_API}?per_page={GITHUB_PER_PAGE}&page={page}");
let resp = client
.get(&url)
.send()
.await
.with_context(|| format!("Failed to request GitHub Releases API (page {page})"))?;
if !resp.status().is_success() {
anyhow::bail!(
"GitHub API returned error: {} (page {})",
resp.status(),
page
);
}
let releases: Vec<GhRelease> = resp.json().await?;
if releases.is_empty() {
break;
}
for r in &releases {
if r.draft || r.prerelease {
continue;
}
let version = r
.tag_name
.strip_prefix('v')
.unwrap_or(&r.tag_name)
.to_string();
let date = r
.published_at
.get(..10)
.unwrap_or(&r.published_at)
.to_string();
all.push(RustRelease {
version,
date,
channel: CHANNEL_STABLE.to_string(),
});
}
page += 1;
if page > GITHUB_MAX_PAGES {
break;
}
}
Ok(all)
}
async fn fetch_from_releases_md(network: &NetworkConfig) -> Result<Vec<RustRelease>> {
let text = build_client(network)?
.get(RELEASES_MD_URL)
.send()
.await
.context("Failed to request RELEASES.md. This may be due to slow network connectivity or firewall restrictions.\nHint: Try increasing the timeout with --timeout <seconds> or use the default GitHub API mode without --full")?
.text()
.await
.context("Failed to read RELEASES.md content")?;
let mut releases = Vec::new();
for cap in releases_md_regex().captures_iter(&text) {
releases.push(RustRelease {
version: cap[1].to_string(),
date: cap[2].to_string(),
channel: CHANNEL_STABLE.to_string(),
});
}
Ok(releases)
}