use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProjectInfo {
pub root: PathBuf,
pub name: String,
pub branch: Option<String>,
}
pub fn find_git_root(start: &Path) -> Option<PathBuf> {
let mut current = if start.is_file() {
start.parent()?.to_path_buf()
} else {
start.to_path_buf()
};
loop {
let git_path = current.join(".git");
if git_path.exists() {
return Some(current);
}
if !current.pop() {
return None;
}
}
}
pub fn read_git_branch(git_dir: &Path) -> Option<String> {
let head_path = git_dir.join("HEAD");
let contents = std::fs::read_to_string(head_path).ok()?;
let trimmed = contents.trim();
if let Some(ref_target) = trimmed.strip_prefix("ref: ") {
ref_target
.strip_prefix("refs/heads/")
.map(|b| b.to_string())
} else {
None
}
}
pub fn detect_project(cwd: &Path) -> Option<ProjectInfo> {
let root = find_git_root(cwd)?;
let name = root.file_name()?.to_str()?.to_string();
let git_dir = root.join(".git");
let branch = if git_dir.is_dir() {
read_git_branch(&git_dir)
} else {
None
};
Some(ProjectInfo { root, name, branch })
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn find_git_root_returns_none_for_tmp() {
let dir = tempfile::tempdir().unwrap();
assert!(find_git_root(dir.path()).is_none());
}
#[test]
fn read_git_branch_symbolic_ref() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("HEAD"), "ref: refs/heads/feature/cool\n").unwrap();
assert_eq!(
read_git_branch(dir.path()),
Some("feature/cool".to_string())
);
}
#[test]
fn read_git_branch_detached_head() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join("HEAD"),
"abc1234567890abcdef1234567890abcdef123456\n",
)
.unwrap();
assert_eq!(read_git_branch(dir.path()), None);
}
#[test]
fn detect_project_returns_none_without_git() {
let dir = tempfile::tempdir().unwrap();
assert!(detect_project(dir.path()).is_none());
}
}