use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn detect_project(explicit: Option<&str>) -> String {
let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
detect_project_at(&cwd, explicit)
}
pub fn detect_project_at(root: &Path, explicit: Option<&str>) -> String {
detect_project_at_internal(root, explicit, None)
}
pub(crate) fn detect_project_at_internal(
root: &Path,
explicit: Option<&str>,
env_project: Option<String>,
) -> String {
if let Some(project) = explicit {
if !project.trim().is_empty() {
return project.trim().to_string();
}
}
if let Some(project) = env_project {
let trimmed = project.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
} else 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_url_at(root) {
let project = parse_git_remote(&remote);
if !project.is_empty() {
return project;
}
}
if let Some(git_root) = find_git_root_at(root) {
if let Some(name) = git_root.file_name() {
if let Some(s) = name.to_str() {
emit_fallback_warning(s, &git_root);
return s.to_string();
}
}
}
root.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_string())
.unwrap_or_else(|| "unknown".to_string())
}
fn get_git_remote_url_at(root: &Path) -> Option<String> {
let output = Command::new("git")
.args(["-C", root.to_str()?, "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_at(root: &Path) -> Option<PathBuf> {
let output = Command::new("git")
.args(["-C", root.to_str()?, "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 get_other_remotes_at(root: &Path) -> Vec<String> {
let root_str = match root.to_str() {
Some(s) => s,
None => return Vec::new(),
};
let output = match Command::new("git")
.args(["-C", root_str, "remote"])
.env("GIT_TERMINAL_PROMPT", "0")
.output()
{
Ok(out) => out,
Err(_) => return Vec::new(),
};
if output.status.success() {
return String::from_utf8_lossy(&output.stdout)
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect();
}
Vec::new()
}
pub(crate) fn build_fallback_warning_message(project_id: &str, git_root: &Path) -> String {
let remotes = get_other_remotes_at(git_root);
let mut msg = format!(
"Warning: no git remote 'origin' found, using directory name as project_id: '{}'",
project_id
);
if !remotes.is_empty() {
msg.push_str(&format!(" (other remotes: {})", remotes.join(", ")));
}
msg.push_str(". This project_id may differ from the remote-derived one.");
msg
}
fn emit_fallback_warning(project_id: &str, git_root: &Path) {
eprintln!("{}", build_fallback_warning_message(project_id, git_root));
}
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)]
#[path = "project_tests.rs"]
mod project_tests;