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 is_html = bytes.starts_with(b"<!doctype html") || bytes.starts_with(b"<html");
if is_html {
return Err(format!("SourceHut: Got HTML/404 for {}", url).into());
}
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,
});
}
let (branch, path, is_tree_or_blob) = match segs.get(2) {
Some(&("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
};
(branch, path, true)
}
_ => (
"HEAD".to_string(),
if segs.len() > 2 {
Some(segs[2..].join("/"))
} else {
None
},
false,
),
};
eprintln!(
"[SourceHut::parse_url] url: {} branch: {} path: {:?} is_tree_or_blob: {}",
url, branch, path, is_tree_or_blob
);
Some(RepoRef {
host: "git.sr.ht".into(),
owner,
repo,
branch,
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.to_ascii_lowercase().ends_with(".rom.txt") =>
{
p.replace('\\', "/")
}
_ => return Err(
"sr.ht: URL must point to a .tal, .rom, or .rom.txt 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;
}
let mut attempts: Vec<(String, Option<std::path::PathBuf>)> =
vec![(target.clone(), None)];
let mut success: Option<(String, PathBuf)> = None;
let mut errors: Vec<(String, String)> = vec![];
match Self::fetch_file(r, out_root, &target) {
Ok(loc) => {
success = Some((target.clone(), loc));
}
Err(e) => {
let local = out_root.join(&target);
let _ = std::fs::remove_file(&local);
errors.push((target.clone(), format!("{}", e)));
}
}
if success.is_none() {
let parts: Vec<&str> = target.split('/').collect();
if parts.len() >= 3 && parts[0] == parts[1] {
let deduped = parts[1..].join("/");
attempts.push((deduped.clone(), None));
match Self::fetch_file(r, out_root, &deduped) {
Ok(loc) => {
success = Some((deduped.clone(), loc));
}
Err(e) => {
let local = out_root.join(&deduped);
let _ = std::fs::remove_file(&local);
errors.push((deduped.clone(), format!("{}", e)));
}
}
}
}
if success.is_none() {
if let Some(fname) = Path::new(&target).file_name() {
let mut ancestor = Path::new(&target);
while let Some(parent) = ancestor.parent() {
let try_path = parent.join(fname);
let try_str = try_path.to_string_lossy().replace('\\', "/");
if try_str == target {
break;
}
attempts.push((try_str.clone(), None));
match Self::fetch_file(r, out_root, &try_str) {
Ok(loc) => {
success = Some((try_str.clone(), loc));
break;
}
Err(e) => {
let local = out_root.join(&try_str);
let _ = std::fs::remove_file(&local);
errors.push((try_str.clone(), format!("{}", e)));
}
}
ancestor = parent;
}
}
}
if success.is_none() {
if let Some(fname) = Path::new(&target).file_name() {
if let Some(entry_dir) = Path::new(&entry_rel).parent() {
let entry_dir_path = entry_dir.join(fname);
let entry_dir_str =
entry_dir_path.to_string_lossy().replace('\\', "/");
attempts.push((entry_dir_str.clone(), None));
match Self::fetch_file(r, out_root, &entry_dir_str) {
Ok(loc) => {
success = Some((entry_dir_str.clone(), loc));
}
Err(e) => {
let local = out_root.join(&entry_dir_str);
let _ = std::fs::remove_file(&local);
errors.push((entry_dir_str.clone(), format!("{}", e)));
}
}
}
}
}
if let Some((path, loc)) = success {
all.push(loc.clone());
q.push_back((path, loc));
continue;
}
let error_msg = format!(
"Failed to fetch include '{}'. Attempts: {}. Errors: {}",
inc,
attempts
.iter()
.map(|(p, _)| format!("'{}'", p))
.collect::<Vec<_>>()
.join(", "),
errors
.iter()
.map(|(p, e)| format!("{}: {}", p, e))
.collect::<Vec<_>>()
.join("; ")
);
return Err(error_msg.into());
}
}
}
Ok(FetchResult {
entry_local: all[0].clone(),
all_files: all,
})
}
}