use crate::ensure;
use crate::error::Result;
use crate::fs::{FsScanOutput, FsScanner, FsScannerOpts};
use crate::git::{RemoteInfo, Repo};
use std::path::{Path, PathBuf};
#[derive(Debug, Default, Clone)]
pub struct SubjectOpts {
pub input: String,
pub reporter: Option<indicatif::ProgressBar>,
}
#[derive(Debug, Default)]
pub struct Subject {
pub root: PathBuf,
pub remote: Option<RemoteInfo>,
pub file: Option<String>,
pub repo: Option<Repo>,
reporter: Option<indicatif::ProgressBar>,
}
impl Subject {
pub fn new(opts: SubjectOpts) -> Result<Self> {
ensure!(
!opts.input.is_empty(),
InvalidArgument,
"Input cannot be empty string!"
);
match RemoteInfo::is_remote_input(&opts.input) {
true => Self::new_remote(opts),
false => Self::new_local(opts),
}
}
pub fn scan(&self, mut opts: FsScannerOpts) -> Result<FsScanOutput> {
opts.ignore_patterns
.extend_from_slice(crate::fs::misc::common_extra_ignore());
let scanner = FsScanner::new(&self.root, opts, self.reporter.clone())?;
let output = scanner.scan()?;
Ok(output)
}
fn new_local(opts: SubjectOpts) -> Result<Self> {
let path = PathBuf::from(&opts.input);
ensure!(path.exists(), PathNotFound, "{}", path.display());
Ok(Subject {
root: get_root(&path),
file: get_file(path),
repo: None,
..Default::default()
})
}
fn new_remote(opts: SubjectOpts) -> Result<Self> {
let remote_info = RemoteInfo::new(opts.input)?;
let mut repo = Repo::new(remote_info.clone());
repo.ensure_cloned(opts.reporter.clone())?;
if let Some(ref tree) = remote_info.tree {
repo.root = repo.root.join(tree)
}
ensure!(repo.root.exists(), PathNotFound, "{}", repo.root.display());
Ok(Subject {
root: repo.root.clone(),
file: remote_info.blob.clone(),
remote: Some(remote_info),
repo: Some(repo),
reporter: opts.reporter,
})
}
}
impl std::fmt::Display for Subject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Subject info:")?;
writeln!(f, " Root: {:?}", self.root)?;
writeln!(f, " File: {:?}", self.file)?;
if let Some(remote) = self.remote.as_ref() {
writeln!(f, " Remote URL: {}", remote.url)?;
writeln!(f, " Owner: {}", remote.owner)?;
writeln!(f, " Name: {}", remote.name)?;
writeln!(f, " Branch: {:?}", remote.branch)?;
if let Some(tree) = remote.tree.as_ref() {
writeln!(f, " Tree: {}", tree)?;
}
if let Some(blob) = remote.blob.as_ref() {
writeln!(f, " Blob: {}", blob)?;
}
}
Ok(())
}
}
fn get_file(path: PathBuf) -> Option<String> {
path.is_file()
.then(|| path.file_name().map(|f| f.to_string_lossy().to_string()))
.flatten()
}
fn get_root(path: &Path) -> PathBuf {
path.is_dir()
.then_some(path)
.or_else(|| path.parent())
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cache;
use std::fs;
#[test]
fn test_subject_with_remote_url() {
let temp_dir = tempfile::tempdir().unwrap();
let cache_path = temp_dir.path();
unsafe { std::env::set_var("XDG_CACHE_HOME", cache_path) };
let subject = Subject::new(SubjectOpts {
input: "https://github.com/dtolnay/anyhow".into(),
..Default::default()
})
.expect("Subject creation should succeed");
assert!(subject.remote.is_some());
let remote = subject.remote.unwrap();
assert_eq!(remote.owner.as_str(), "dtolnay");
assert_eq!(remote.name.as_str(), "anyhow");
let expected_path = cache_path.join(format!("{}/dtolnay/anyhow", cache::prefix()));
assert_eq!(subject.root, expected_path);
temp_dir.close().unwrap();
}
#[test]
fn test_subject_with_local_path() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
fs::write(&file_path, "test content").unwrap();
let subject = Subject::new(SubjectOpts {
input: file_path.to_string_lossy().into(),
..Default::default()
})
.expect("Subject creation should succeed");
assert!(subject.remote.is_none());
assert_eq!(subject.root, temp_dir.path());
assert_eq!(subject.file, Some("test_file.txt".to_string()));
temp_dir.close().unwrap();
}
#[test]
fn test_subject_with_nonexistent_path() {
let result = Subject::new(SubjectOpts {
input: "/path/that/does/not/exist".into(),
..Default::default()
});
assert!(result.is_err());
match result {
Err(crate::error::Error::PathNotFound(_)) => {}
_ => panic!("Expected PathNotFound error"),
}
}
#[test]
fn test_subject_with_malformed_url() {
let temp_dir = tempfile::tempdir().unwrap();
let cache_path = temp_dir.path();
unsafe { std::env::set_var("XDG_CACHE_HOME", cache_path) };
let result = Subject::new(SubjectOpts {
input: "https://github.com/malformed".into(),
..Default::default()
});
assert!(result.is_err());
match result {
Err(crate::error::Error::InvalidArgument(_)) => {}
_ => panic!("Expected InvalidArgument error"),
}
}
}