use std::path::PathBuf;
use regex::Regex;
use tedi::{
IssueIndex, IssueSelector, RepoInfo, github,
local::{FsReader, Local, LocalError, LocalIssueSource, LocalPath, LocalPathError, LocalPathErrorKind, ReaderError},
};
use v_utils::{macros::wrap_err, utils::exit_on_error};
use super::command::ProjectType;
#[wrap_err]
#[derive(Debug, thiserror::Error)]
pub enum TouchError {
#[own]
Path(LocalPathError),
#[own]
Local(LocalError),
#[leaf]
#[error("Ambiguous {kind}: pattern '{pattern}' matches multiple entries\nMatches: {matches}")]
Ambiguous { kind: &'static str, pattern: String, matches: String },
#[leaf]
#[error("Repository '{owner}/{repo}' doesn't exist locally and is not accessible on GitHub\nCheck that the owner/repo is correct, or use --parent=virtual for local-only tracking.")]
RepoNotAccessible { owner: String, repo: String },
}
pub async fn parse_touch_path(user_input: &str, parent: Option<ProjectType>, offline: bool) -> Result<LocalIssueSource<FsReader>, TouchError> {
let segments: Vec<&str> = user_input.split('/').collect();
if segments.len() < 3 {
return Err(LocalPathError::not_found(IssueSelector::regex(user_input), Local::issues_dir()).into());
}
let owner_rgx = segments[0];
let repo_rgx = segments[1];
let issue_rgxs = &segments[2..];
let local_result: Result<LocalIssueSource<FsReader>, TouchError> = async {
let issues_dir = Local::issues_dir();
let owner = regex_match_unique(&issues_dir, owner_rgx, "owner")?;
let repo = regex_match_unique(&issues_dir.join(&owner), repo_rgx, "repo")?;
let repo_info = RepoInfo::new(&owner, &repo);
let selectors: Vec<IssueSelector> = issue_rgxs.iter().map(|s| IssueSelector::regex(strip_md_extension(s))).collect();
let index = IssueIndex::with_index(repo_info, selectors);
let local_path = LocalPath::new(index);
let resolved = local_path.resolve_parent(FsReader)?;
match resolved.clone().search() {
Ok(found) => Ok(LocalIssueSource::<FsReader>::build_from_path(&found.path()).await?),
Err(e) => match e.kind {
LocalPathErrorKind::NotFound | LocalPathErrorKind::MissingParent | LocalPathErrorKind::ParentIsFlat => {
let title = strip_md_extension(issue_rgxs.last().unwrap());
let create_path = resolved.deterministic(title, false, false).path();
Ok(LocalIssueSource::<FsReader>::build_from_path(&create_path).await?)
}
LocalPathErrorKind::NotUnique => Err(TouchError::new_ambiguous("issue", issue_rgxs.last().unwrap().to_string(), format!("{e}"))),
LocalPathErrorKind::Reader => Err(e.into()),
},
}
}
.await;
if let Ok(source) = local_result {
return Ok(source);
}
let owner = owner_rgx.to_string();
let repo = repo_rgx.to_string();
let repo_info = RepoInfo::new(&owner, &repo);
let repo_accessible = if offline {
false
} else {
let client = exit_on_error(github::client::get());
exit_on_error(client.repo_exists(repo_info).await)
};
if repo_accessible {
let selectors: Vec<IssueSelector> = issue_rgxs.iter().map(|s| IssueSelector::title(strip_md_extension(s))).collect();
let index = IssueIndex::with_index(repo_info, selectors);
let local_path = LocalPath::new(index);
let resolved = local_path.resolve_parent(FsReader)?;
let title = strip_md_extension(issue_rgxs.last().unwrap());
let create_path = resolved.deterministic(title, false, false).path();
return Ok(LocalIssueSource::<FsReader>::build_from_path(&create_path).await?);
}
match parent {
Some(ProjectType::Virtual) => {
Local::ensure_virtual_project(repo_info).expect("failed to create virtual project");
let selectors: Vec<IssueSelector> = issue_rgxs.iter().map(|s| IssueSelector::title(strip_md_extension(s))).collect();
let index = IssueIndex::with_index(repo_info, selectors);
let local_path = LocalPath::new(index);
let resolved = local_path.resolve_parent(FsReader)?;
let title = strip_md_extension(issue_rgxs.last().unwrap());
let create_path = resolved.deterministic(title, false, false).path();
Ok(LocalIssueSource::<FsReader>::build_from_path(&create_path).await?)
}
Some(ProjectType::Default) | None => Err(TouchError::new_repo_not_accessible(owner, repo)),
}
}
fn strip_md_extension(s: &str) -> &str {
s.strip_suffix(".md").unwrap_or(s)
}
fn regex_match_unique(dir: &PathBuf, pattern: &str, kind: &'static str) -> Result<String, TouchError> {
if !dir.exists() || !dir.is_dir() {
return Err(LocalPathError::not_found(IssueSelector::regex(pattern), dir.clone()).into());
}
let entries: Vec<String> = std::fs::read_dir(dir)
.map_err(|e| LocalPathError::reader(IssueSelector::regex(pattern), ReaderError::other(e)))?
.flatten()
.filter_map(|e| e.file_name().to_str().map(|s| s.to_string()))
.filter(|name| !name.starts_with('.'))
.collect();
let regex = Regex::new(pattern).expect("invalid regex pattern");
let matches: Vec<&String> = entries.iter().filter(|name| regex.is_match(name)).collect();
match matches.len() {
0 => Err(LocalPathError::not_found(IssueSelector::regex(pattern), dir.clone()).into()),
1 => Ok(matches[0].clone()),
_ => Err(TouchError::new_ambiguous(
kind,
pattern.to_string(),
matches.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", "),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_parse_touch_path_errors() {
let err1 = parse_touch_path("owner/issue.md", None, true).await.unwrap_err();
let err2 = parse_touch_path("issue.md", None, true).await.unwrap_err();
eprintln!("{err1:?}\n\n&{err2:?}"); }
}