use anyhow::{Context, Result}; use std::{ ffi::{OsStr, OsString}, fs::create_dir_all, path::{Path, PathBuf}, process::Command, }; use url::Url; use walkdir::WalkDir; const MAX_DEPTH: usize = 5; pub struct SrcRoot { root_path: PathBuf, } impl SrcRoot { fn clone_repo(url: &Url, destination: &Path) -> Result<()> { let url = OsString::from(url.as_ref()); Command::new("git") .args([ OsString::from("clone").as_os_str(), url.as_os_str(), destination.as_os_str(), ]) .status()?; Ok(()) } fn as_repo_path(&self, url: &Url) -> PathBuf { let mut dir = PathBuf::new(); dir.push::<&Path>(self.root_path.as_ref()); dir.push(url.host_str().unwrap_or("misc")); url.path_segments() .into_iter() .flatten() .filter(|s| !s.is_empty()) .for_each(|s| dir.push(s.trim_end_matches(".git"))); dir } fn ensure_repo_checkout(&self, url: &Url) -> Result { let repo_path = self.as_repo_path(url); if !repo_path.is_dir() { create_dir_all(&repo_path)?; } Self::clone_repo(url, &repo_path)?; Ok(repo_path) } fn search_repo>(&self, slug: S) -> Result> { let walker = WalkDir::new(&self.root_path) .follow_links(false) .max_depth(MAX_DEPTH) .follow_root_links(false) .max_open(1) .sort_by_file_name() .contents_first(false) .same_file_system(true); for path in walker { let p = path?; let git = { let mut g = p.clone().into_path(); g.push(".git"); g }; if let Some(file_name) = p.path().file_name() { if file_name == slug.as_ref() && git.is_dir() { return Ok(Some(p.into_path())); } } } Ok(None) } /// # Errors /// File system errors pub fn resolve_repo(&self, arg: &str) -> Result> { if let Some(found) = self.search_repo(arg)? { Ok(found) } else { let url = normalize_url(arg)?; self.ensure_repo_checkout(&url) } } #[must_use] pub fn new(path: PathBuf) -> Self { Self { root_path: path } } } fn normalize_url(url: &str) -> Result { match Url::parse(url) { Ok(url) => Ok(url), Err(e @ url::ParseError::RelativeUrlWithoutBase) => { let normalized_url = &format!("ssh://{url}"); normalize_ssh(e, normalized_url) } Err(e @ url::ParseError::InvalidPort) => normalize_ssh(e, url), Err(e) => Err(e.into()), } } fn normalize_ssh(e: url::ParseError, url: &str) -> Result { match Url::parse(url) { Ok(o) => Ok(o), Err(url::ParseError::InvalidPort) => { let mut split = url.splitn(3, ':'); let mut normalized_url = String::with_capacity(url.len()); normalized_url.push_str(split.next().context(e)?); normalized_url.push(':'); normalized_url.push_str(split.next().context(e)?); normalized_url.push('/'); let tail = split.next().context(e)?; normalized_url.push_str(tail); let normalized_url = Url::parse(&normalized_url)?; Ok(normalized_url) } Err(e) => Err(e.into()), } }