use super::{
downloader::{http_get, write_bytes},
includes::{parse_includes, resolve_include},
provider::{FetchResult, Provider, RepoRef},
};
use std::{
collections::{HashSet, VecDeque},
fs, path::{Path, PathBuf},
};
use url::Url;
pub struct SourceHut;
impl SourceHut {
fn raw_url(r: &RepoRef, repo_rel: &str) -> String {
format!("https://git.sr.ht/{}/{}/blob/{}/{}?raw=1",
r.owner, r.repo, r.branch, repo_rel)
}
fn fetch_file(r: &RepoRef, out_root: &Path, repo_rel: &str) -> Result<PathBuf, Box<dyn std::error::Error>> {
let url = Self::raw_url(r, repo_rel);
let bytes = http_get(&url)?;
let local = out_root.join(repo_rel);
if let Some(p) = local.parent() { fs::create_dir_all(p)?; }
write_bytes(&local, &bytes)?;
Ok(local)
}
}
impl Provider for SourceHut {
fn parse_url(&self, url: &str) -> Option<RepoRef> {
let u = Url::parse(url).ok()?;
if u.domain()? != "git.sr.ht" { return None; }
let segs = u.path_segments()?.collect::<Vec<_>>();
if segs.len() < 2 || !segs[0].starts_with('~') { return None; }
let owner = segs[0].to_string();
let repo = segs[1].to_string();
if segs.len() == 2 {
return Some(RepoRef { host: "git.sr.ht".into(), owner, repo, branch: "HEAD".into(), path: None });
}
match segs[2] {
"blob" | "tree" | "blame" | "log" if segs.len() >= 4 => {
let branch = segs[3].to_string();
let mut path_start = 4;
if (segs[2] == "tree" || segs[2]=="log") && segs.get(4) == Some(&"item") {
path_start = 5;
}
let path = if segs.len() > path_start {
Some(segs[path_start..].join("/"))
} else {
None
};
Some(RepoRef { host: "git.sr.ht".into(), owner, repo, branch, path })
}
_ => {
let path = if segs.len() > 2 { Some(segs[2..].join("/")) } else { None };
Some(RepoRef { host: "git.sr.ht".into(), owner, repo, branch: "HEAD".into(), path })
}
}
}
fn fetch_tal_tree(&self, r: &RepoRef, out_root: &Path) -> Result<FetchResult, Box<dyn std::error::Error>> {
let entry_rel = match &r.path {
Some(p) if p.to_ascii_lowercase().ends_with(".tal") || p.to_ascii_lowercase().ends_with(".rom") => p.replace('\\', "/"),
_ => return Err("sr.ht: URL must point to a .tal or .rom file; not guessing entries".into()),
};
let entry_local = Self::fetch_file(r, out_root, &entry_rel)?;
let mut all = vec![entry_local.clone()];
if entry_rel.to_ascii_lowercase().ends_with(".tal") {
let mut visited: HashSet<String> = [entry_rel.clone()].into_iter().collect();
let mut q: VecDeque<(String, PathBuf)> = VecDeque::from([(entry_rel.clone(), entry_local.clone())]);
while let Some((curr_rel, curr_local)) = q.pop_front() {
let src = fs::read_to_string(&curr_local).unwrap_or_default();
for inc in parse_includes(&src) {
let target = resolve_include(&curr_rel, &inc);
if !visited.insert(target.clone()) { continue; }
match Self::fetch_file(r, out_root, &target) {
Ok(loc) => { all.push(loc.clone()); q.push_back((target, loc)); }
Err(e) => eprintln!("[srht] warn: missing include {} ({})", target, e),
}
}
}
}
Ok(FetchResult { entry_local: all[0].clone(), all_files: all })
}
}