use std::env;
use std::path::PathBuf;
use std::process::Command;
pub fn detect_project(explicit: Option<&str>) -> String {
if let Some(project) = explicit {
if !project.trim().is_empty() {
return project.trim().to_string();
}
}
if let Ok(project) = env::var("VIPUNE_PROJECT") {
let trimmed = project.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
}
if let Some(remote) = get_git_remote_origin() {
let project = parse_git_remote(&remote);
if !project.is_empty() {
return project;
}
}
if let Some(root) = find_git_root() {
if let Some(name) = root.file_name() {
if let Some(s) = name.to_str() {
return s.to_string();
}
}
}
env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.unwrap_or_else(|| "unknown".to_string())
}
fn get_git_remote_origin() -> Option<String> {
let output = Command::new("git")
.args(["remote", "get-url", "origin"])
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.ok()?;
if output.status.success() {
let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !url.is_empty() {
return Some(url);
}
}
None
}
fn find_git_root() -> Option<PathBuf> {
let output = Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.ok()?;
if output.status.success() {
let path_str = String::from_utf8_lossy(&output.stdout);
let path = path_str.trim();
if !path.is_empty() {
return Some(PathBuf::from(path));
}
}
None
}
fn parse_git_remote(url: &str) -> String {
let url = url.trim().trim_end_matches(".git");
if let Some(rest) = url.strip_prefix("git@") {
if let Some(colon_pos) = rest.find(':') {
return rest[colon_pos + 1..].to_string();
}
}
if let Some(rest) = url.split("://").nth(1) {
let parts: Vec<&str> = rest.split('/').collect();
if parts.len() >= 3 {
return format!("{}/{}", parts[parts.len() - 2], parts[parts.len() - 1]);
}
}
url.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_ssh_remote() {
assert_eq!(
parse_git_remote("git@github.com:owner/repo.git"),
"owner/repo"
);
assert_eq!(parse_git_remote("git@github.com:owner/repo"), "owner/repo");
}
#[test]
fn test_parse_https_remote() {
assert_eq!(
parse_git_remote("https://github.com/owner/repo.git"),
"owner/repo"
);
assert_eq!(
parse_git_remote("https://github.com/owner/repo"),
"owner/repo"
);
}
#[test]
fn test_parse_ssh_url_with_protocol() {
assert_eq!(
parse_git_remote("ssh://git@github.com/owner/repo.git"),
"owner/repo"
);
}
#[test]
fn test_git_suffix_stripping() {
assert_eq!(parse_git_remote("owner/repo.git"), "owner/repo");
}
#[test]
fn test_fallback_when_no_domain() {
assert_eq!(parse_git_remote("just-name"), "just-name");
}
#[test]
fn test_explicit_override() {
assert_eq!(detect_project(Some("my-project")), "my-project");
}
#[test]
fn test_explicit_override_empty() {
let project = detect_project(Some(""));
assert!(!project.is_empty());
}
#[test]
fn test_explicit_override_whitespace() {
let project = detect_project(Some(" \t "));
assert!(!project.is_empty());
}
#[test]
fn test_detect_fallback_to_current_dir() {
let project = detect_project(None);
assert!(!project.is_empty());
}
#[test]
fn test_env_var_whitespace() {
unsafe {
std::env::set_var("VIPUNE_PROJECT", " ");
}
let project = detect_project(None);
assert!(!project.is_empty()); unsafe {
std::env::remove_var("VIPUNE_PROJECT");
}
}
}