use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
const ENV_PATH: &str = "PATH";
fn is_executable(path: &Path) -> bool {
if !path.is_file() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
path.metadata()
.is_ok_and(|m| m.permissions().mode() & 0o111 != 0)
}
#[cfg(not(unix))]
{
true
}
}
fn find_executable(name: &str) -> Option<PathBuf> {
find_executable_in_path(name, env::var_os(ENV_PATH)?)
}
fn find_executable_in_path(name: &str, path_os: std::ffi::OsString) -> Option<PathBuf> {
for path in env::split_paths(&path_os) {
if !path.is_absolute() {
continue;
}
let exe_path = path.join(name);
#[cfg(windows)]
{
if is_executable(&exe_path) {
return Some(exe_path);
}
let pathext = env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".into());
for ext in pathext.split(';') {
let ext = ext.trim_start_matches('.');
if ext.is_empty() {
continue;
}
let mut exe_with_ext = exe_path.clone();
exe_with_ext.set_extension(ext);
if is_executable(&exe_with_ext) {
return Some(exe_with_ext);
}
}
}
#[cfg(not(windows))]
{
if is_executable(&exe_path) {
return Some(exe_path);
}
}
}
None
}
pub fn resolve_git_local_path() -> Option<PathBuf> {
let mut cmd = if let Some(git_path) = find_executable("git") {
Command::new(git_path)
} else {
Command::new("git")
};
let output = cmd.args(["rev-parse", "--git-dir"]).output().ok()?;
if !output.status.success() {
return None;
}
let git_dir = String::from_utf8_lossy(&output.stdout).trim().to_string();
if git_dir.is_empty() {
return None;
}
let git_dir_path = PathBuf::from(&git_dir);
let absolute_git_dir = if git_dir_path.is_absolute() {
git_dir_path
} else {
std::env::current_dir()
.ok()?
.join(git_dir_path)
.canonicalize()
.ok()?
};
let memory_index_dir = absolute_git_dir.join("memory-index");
let db_path = memory_index_dir.join("csm.db");
Some(db_path)
}
pub fn ensure_git_local_dir(path: &Path) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_resolve_git_local_path_in_git_repo() {
let path = resolve_git_local_path();
if path.is_none() {
eprintln!(
"Skipping path assertions - git-local path not available (sandboxed environment)"
);
return;
}
let path = path.unwrap();
assert!(
path.ends_with("csm.db"),
"Path should end with csm.db: {path:?}"
);
assert!(
path.to_string_lossy().contains("memory-index"),
"Path should contain memory-index: {path:?}"
);
}
#[test]
fn test_resolve_git_local_path_outside_git_repo() {
let original_dir = env::current_dir().unwrap();
let temp_dir = tempfile::tempdir().unwrap();
env::set_current_dir(temp_dir.path()).unwrap();
let path = resolve_git_local_path();
if let Some(path) = path {
assert!(
path.ends_with("csm.db"),
"If path is returned, it should end with csm.db: {path:?}"
);
}
env::set_current_dir(original_dir).unwrap();
}
#[test]
fn test_ensure_git_local_dir() {
let temp_dir = tempfile::tempdir().unwrap();
let db_path = temp_dir
.path()
.join(".git")
.join("memory-index")
.join("csm.db");
assert!(!db_path.exists());
ensure_git_local_dir(&db_path).unwrap();
assert!(db_path.parent().unwrap().exists());
}
#[test]
fn test_find_executable_in_path() {
use std::ffi::OsString;
use std::fs::File;
let temp_dir = tempfile::tempdir().unwrap();
let bin_dir = temp_dir.path().join("bin");
std::fs::create_dir(&bin_dir).unwrap();
let exe_name = if cfg!(windows) {
"test_exe.exe"
} else {
"test_exe"
};
let exe_path = bin_dir.join(exe_name);
File::create(&exe_path).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&exe_path).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&exe_path, perms).unwrap();
}
let make_path = |paths: Vec<PathBuf>| -> OsString { env::join_paths(paths).unwrap() };
let path_os = make_path(vec![bin_dir.clone()]);
let found = find_executable_in_path("test_exe", path_os);
assert!(found.is_some());
assert!(found.unwrap().is_absolute());
let relative_path = PathBuf::from("relative_bin");
let path_os = make_path(vec![relative_path]);
let found = find_executable_in_path("test_exe", path_os);
assert!(found.is_none(), "Should ignore relative paths in PATH");
let path_os = make_path(vec![PathBuf::from("relative_bin"), bin_dir]);
let found = find_executable_in_path("test_exe", path_os);
assert!(found.is_some());
assert!(found.unwrap().is_absolute());
}
#[test]
fn test_resolve_git_local_path_structure() {
let test_git_dir = PathBuf::from("/tmp/test/.git");
let memory_index_dir = test_git_dir.join("memory-index");
let db_path = memory_index_dir.join("csm.db");
assert!(db_path.ends_with("csm.db"));
assert!(db_path.to_string_lossy().contains("memory-index"));
assert!(db_path.to_string_lossy().contains(".git"));
}
}