122 lines
3.5 KiB
Rust
122 lines
3.5 KiB
Rust
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<PathBuf> {
|
|
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<S: AsRef<OsStr>>(&self, slug: S) -> Result<Option<PathBuf>> {
|
|
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<impl AsRef<Path>> {
|
|
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<Url> {
|
|
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<Url> {
|
|
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()),
|
|
}
|
|
}
|