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,
}
}
pub fn resolve_on_desk(start: &Path, desk: &Path) -> ProjectInfo {
let found = resolve(start);
let desk = desk.canonicalize().unwrap_or_else(|_| desk.to_path_buf());
let start = start.canonicalize().unwrap_or_else(|_| start.to_path_buf());
if start.starts_with(&desk)
&& desk != found.root
&& desk.starts_with(&found.root)
&& holds_a_repository(&found.root)
{
return ProjectInfo {
name: dir_name(&desk),
root: desk,
};
}
found
}
const HOLD_DEPTH: usize = 3;
const HOLD_DIRS: usize = 2000;
fn holds_a_repository(root: &Path) -> bool {
let mut seen = 0;
let mut level = vec![root.to_path_buf()];
for _ in 0..HOLD_DEPTH {
let mut next = Vec::new();
for dir in level {
let Ok(rd) = std::fs::read_dir(&dir) else {
continue;
};
for e in rd.flatten() {
if !e.file_type().is_ok_and(|t| t.is_dir()) {
continue;
}
let name = e.file_name();
let name = name.to_string_lossy();
if name.starts_with('.') || name == "node_modules" || name == "target" {
continue;
}
if e.path().join(".git").is_dir() {
return true;
}
seen += 1;
if seen >= HOLD_DIRS {
return false;
}
next.push(e.path());
}
}
level = next;
}
false
}
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 mut cmd = std::process::Command::new("git");
cmd.args([
"-c",
"core.fsmonitor=false",
"--no-optional-locks",
"status",
"--porcelain",
"--untracked-files=no",
])
.current_dir(dunce::simplified(dir))
.stderr(std::process::Stdio::null());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(crate::platform::CREATE_NO_WINDOW);
}
let out = cmd.output().ok()?;
out.status.success().then_some(!out.stdout.is_empty())
}
pub fn branch(root: &Path) -> Option<String> {
if !root.join(".git").exists() {
return None;
}
head_of(root)
}
#[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"));
assert_eq!(branch(&tree).as_deref(), Some("side"));
assert_eq!(branch(&tree.join("sub")), None, "only the root's own");
}
#[test]
fn a_desk_under_a_repository_a_folder_too_high_is_its_own_project() {
let dir = crate::store::tempdir::Dir::new("snyvi-floor");
let desk = dir.path.join("taskflow");
let app = desk.join("app");
std::fs::create_dir_all(dir.path.join(".git")).unwrap();
std::fs::create_dir_all(app.join(".git")).unwrap();
std::fs::create_dir_all(desk.join("supabase")).unwrap();
let on = |p: &Path| resolve_on_desk(p, &desk);
assert_eq!(on(&desk).name, "taskflow");
assert_eq!(on(&desk.join("supabase")).name, "taskflow");
assert_eq!(
on(&app).name,
"app",
"a repository inside the desk still wins"
);
assert_eq!(on(&dir.path).root, resolve(&dir.path).root);
assert_eq!(resolve_on_desk(&app, &app).root, resolve(&app).root);
}
#[test]
fn a_desk_on_a_monorepos_folder_is_the_monorepo() {
let dir = crate::store::tempdir::Dir::new("snyvi-mono");
let web = dir.path.join("packages/web");
std::fs::create_dir_all(dir.path.join(".git")).unwrap();
std::fs::create_dir_all(web.join("src")).unwrap();
std::fs::create_dir_all(dir.path.join("vendor/lib")).unwrap();
std::fs::write(
dir.path.join("vendor/lib/.git"),
"gitdir: ../../.git/modules/lib
",
)
.unwrap();
std::fs::create_dir_all(web.join("node_modules/dep/.git")).unwrap();
let got = resolve_on_desk(&web.join("src"), &web);
assert_eq!(got.root, resolve(&web).root);
assert_ne!(got.name, "web");
}
#[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);
}
}