use std::path::Path;
use tempfile::TempDir;
pub fn is_git_url(input: &str) -> bool {
input.starts_with("http://") || input.starts_with("https://") || input.starts_with("git@")
}
pub fn clone_repo(url: &str) -> Result<TempDir, String> {
let temp_dir = TempDir::new().map_err(|e| format!("Failed to create temp directory: {e}"))?;
let mut fetch_opts = git2::FetchOptions::new();
fetch_opts.depth(1);
let mut builder = git2::build::RepoBuilder::new();
builder.fetch_options(fetch_opts);
builder
.clone(url, temp_dir.path())
.map_err(|e| classify_clone_error(url, &e))?;
Ok(temp_dir)
}
pub fn resolve_input(input: &str) -> Result<(std::path::PathBuf, Option<TempDir>), String> {
if is_git_url(input) {
let temp_dir = clone_repo(input)?;
let path = temp_dir.path().to_path_buf();
Ok((path, Some(temp_dir)))
} else {
let path = Path::new(input).to_path_buf();
if !path.exists() {
return Err(format!("Path not found: '{input}'"));
}
Ok((path, None))
}
}
fn classify_clone_error(url: &str, err: &git2::Error) -> String {
let raw = err.message();
if raw.contains("not found")
|| raw.contains("404")
|| raw.contains("403")
|| raw.contains("authentication")
|| raw.contains("could not read Username")
{
return format!("Repository not found or access denied: {url}");
}
if raw.contains("resolve")
|| raw.contains("connect")
|| raw.contains("timed out")
|| raw.contains("SSL")
|| raw.contains("network")
{
return format!("Network error while cloning: {raw}");
}
if raw.contains("unsupported URL protocol") || raw.contains("invalid") {
return format!("Invalid repository URL: {url}");
}
format!("Failed to clone repository: {raw}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_https_url() {
assert!(is_git_url("https://github.com/user/repo"));
}
#[test]
fn detects_http_url() {
assert!(is_git_url("http://github.com/user/repo"));
}
#[test]
fn detects_git_ssh_url() {
assert!(is_git_url("git@github.com:user/repo.git"));
}
#[test]
fn rejects_local_dot_path() {
assert!(!is_git_url("."));
}
#[test]
fn rejects_local_relative_path() {
assert!(!is_git_url("./src"));
}
#[test]
fn rejects_local_absolute_path() {
assert!(!is_git_url("/home/user/project"));
}
#[test]
fn rejects_empty_string() {
assert!(!is_git_url(""));
}
#[test]
fn rejects_bare_word() {
assert!(!is_git_url("myproject"));
}
#[test]
#[cfg(not(target_os = "windows"))]
fn clone_nonexistent_repo_returns_error() {
let result = clone_repo("https://github.com/nonexistent-user-abc123/no-such-repo-xyz789");
assert!(result.is_err());
let msg = result.unwrap_err();
assert!(
msg.contains("not found")
|| msg.contains("Failed to clone")
|| msg.contains("Network error"),
"unexpected error message: {msg}"
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn clone_invalid_url_returns_error() {
let result = clone_repo("https://");
assert!(result.is_err());
}
#[test]
fn resolve_local_path_returns_none_tempdir() {
let (path, temp) = resolve_input(".").unwrap();
assert!(path.exists());
assert!(temp.is_none());
}
#[test]
fn resolve_missing_local_path_returns_error() {
let result = resolve_input("/nonexistent/path/that/does/not/exist");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Path not found"));
}
#[test]
fn temp_dir_is_deleted_on_drop() {
let temp = TempDir::new().unwrap();
let path = temp.path().to_path_buf();
assert!(path.exists());
drop(temp);
assert!(!path.exists(), "temp dir should be deleted after drop");
}
#[test]
#[ignore]
fn clone_public_repo_succeeds() {
let temp_dir = clone_repo("https://github.com/crafteraadarsh/codedna").unwrap();
let path = temp_dir.path();
assert!(
path.join("Cargo.toml").exists(),
"expected Cargo.toml in cloned repo"
);
assert!(
path.join("src").is_dir(),
"expected src/ directory in cloned repo"
);
}
#[test]
#[ignore]
fn temp_dir_deleted_after_clone_and_drop() {
let temp_dir = clone_repo("https://github.com/crafteraadarsh/codedna").unwrap();
let cloned_path = temp_dir.path().to_path_buf();
assert!(cloned_path.exists(), "cloned path should exist before drop");
drop(temp_dir);
assert!(
!cloned_path.exists(),
"cloned path should be deleted after drop"
);
}
#[test]
#[ignore]
fn full_analyze_with_git_url() {
let url = "https://github.com/crafteraadarsh/codedna";
let (local_path, _guard) = resolve_input(url).unwrap();
let result = crate::analysis::analyze(&local_path);
assert!(result.total_loc > 0, "total_loc should be > 0");
assert!(
!result.languages.is_empty(),
"languages should not be empty"
);
assert!(
!result.file_breakdown.is_empty(),
"file_breakdown should not be empty"
);
assert!(
result.languages.keys().any(|l| format!("{l}") == "Rust"),
"expected Rust in languages"
);
assert!(
local_path.exists(),
"temp dir should exist while _guard is alive"
);
let saved_path = local_path.clone();
drop(_guard);
assert!(
!saved_path.exists(),
"temp dir should be deleted after _guard is dropped"
);
}
}