use std::path::{Path, PathBuf};
#[derive(Clone, Debug)]
pub struct ProjectInfo {
pub root: PathBuf,
pub name: String,
}
pub fn resolve(start: &Path) -> ProjectInfo {
let start = if start.is_file() {
start
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| start.to_path_buf())
} else {
start.to_path_buf()
};
let start = start.canonicalize().unwrap_or(start);
let mut cur: Option<&Path> = Some(&start);
while let Some(dir) = cur {
if dir.join(".git").exists() {
return ProjectInfo {
root: dir.to_path_buf(),
name: dir_name(dir),
};
}
cur = dir.parent();
}
ProjectInfo {
name: dir_name(&start),
root: start,
}
}
fn dir_name(p: &Path) -> String {
p.file_name()
.map(|s| s.to_string_lossy().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "root".to_string())
}
pub fn head_of(dir: &Path) -> Option<String> {
let mut cur: Option<&Path> = Some(dir);
let root = loop {
let d = cur?;
if d.join(".git").exists() {
break d;
}
cur = d.parent();
};
let dot = root.join(".git");
let git = if dot.is_file() {
let line = std::fs::read_to_string(&dot).ok()?;
let named = Path::new(line.trim().strip_prefix("gitdir: ")?).to_path_buf();
if named.is_absolute() {
named
} else {
root.join(named)
}
} else {
dot
};
let head = std::fs::read_to_string(git.join("HEAD")).ok()?;
let head = head.trim();
Some(
head.strip_prefix("ref: refs/heads/")
.map(str::to_string)
.unwrap_or_else(|| head.chars().take(8).collect()),
)
}
pub fn modified(dir: &Path) -> Option<bool> {
let out = std::process::Command::new("git")
.args([
"--no-optional-locks",
"status",
"--porcelain",
"--untracked-files=no",
])
.current_dir(dir)
.stderr(std::process::Stdio::null())
.output()
.ok()?;
out.status.success().then_some(!out.stdout.is_empty())
}
pub fn branch(root: &Path) -> Option<String> {
let head = std::fs::read_to_string(root.join(".git/HEAD")).ok()?;
let head = head.trim();
head.strip_prefix("ref: refs/heads/")
.map(str::to_string)
.or_else(|| Some(head.chars().take(8).collect()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_branch_is_read_from_a_folder_inside_the_repository() {
let dir = crate::store::tempdir::Dir::new("snyvi-head");
let deep = dir.path.join("a/b/c");
std::fs::create_dir_all(&deep).unwrap();
std::fs::create_dir_all(dir.path.join(".git")).unwrap();
std::fs::write(
dir.path.join(".git/HEAD"),
"ref: refs/heads/claude/snappy-reading\n",
)
.unwrap();
assert_eq!(head_of(&deep).as_deref(), Some("claude/snappy-reading"));
}
#[test]
fn a_detached_head_is_the_commit_it_is_on() {
let dir = crate::store::tempdir::Dir::new("snyvi-head");
std::fs::create_dir_all(dir.path.join(".git")).unwrap();
std::fs::write(
dir.path.join(".git/HEAD"),
"9adceca1234567890abcdef1234567890abcdef1\n",
)
.unwrap();
assert_eq!(head_of(&dir.path).as_deref(), Some("9adceca1"));
}
#[test]
fn a_worktree_keeps_git_as_a_file_naming_the_real_one() {
let dir = crate::store::tempdir::Dir::new("snyvi-head");
let real = dir.path.join("store/worktrees/one");
std::fs::create_dir_all(&real).unwrap();
std::fs::write(real.join("HEAD"), "ref: refs/heads/side\n").unwrap();
let tree = dir.path.join("tree");
std::fs::create_dir_all(&tree).unwrap();
std::fs::write(tree.join(".git"), format!("gitdir: {}\n", real.display())).unwrap();
assert_eq!(head_of(&tree).as_deref(), Some("side"));
}
#[test]
fn a_folder_outside_any_repository_is_on_no_branch() {
let dir = crate::store::tempdir::Dir::new("snyvi-head");
assert_eq!(head_of(&dir.path), None);
}
}