omni_dev/utils/
ai_scratch.rs1use std::env;
4use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result};
7
8use crate::utils::env::{EnvSource, SystemEnv};
9
10fn resolve_scratch_dir(
18 env: &impl EnvSource,
19 git_root: impl FnOnce() -> Result<PathBuf>,
20) -> Result<PathBuf> {
21 if let Some(ai_scratch) = env.var("AI_SCRATCH") {
23 if let Some(git_root_path) = ai_scratch.strip_prefix("git-root:") {
24 Ok(git_root()?.join(git_root_path))
26 } else {
27 Ok(PathBuf::from(ai_scratch))
29 }
30 } else {
31 let tmpdir = env.var("TMPDIR").unwrap_or_else(|| "/tmp".to_string());
33 Ok(PathBuf::from(tmpdir))
34 }
35}
36
37pub fn get_ai_scratch_dir() -> Result<PathBuf> {
39 resolve_scratch_dir(&SystemEnv, find_git_root)
40}
41
42pub fn get_ai_scratch_dir_at(repo_root: &Path) -> Result<PathBuf> {
49 resolve_scratch_dir(&SystemEnv, || find_git_root_from_path(repo_root))
50}
51
52fn find_git_root() -> Result<PathBuf> {
54 let current_dir = env::current_dir().context("Failed to get current directory")?;
55 find_git_root_from_path(¤t_dir)
56}
57
58fn find_git_root_from_path(start_path: &Path) -> Result<PathBuf> {
60 let mut current = start_path;
61
62 loop {
63 let git_dir = current.join(".git");
64 if git_dir.exists() {
65 return Ok(current.to_path_buf());
66 }
67
68 match current.parent() {
69 Some(parent) => current = parent,
70 None => {
71 return Err(anyhow::anyhow!(
72 "No git repository found in current directory or any parent directory"
73 ))
74 }
75 }
76 }
77}
78
79#[cfg(test)]
80#[allow(clippy::unwrap_used, clippy::expect_used)]
81mod tests {
82 use super::*;
83 use crate::test_support::env::MapEnv;
84 use tempfile::TempDir;
85
86 fn scratch_dir_with(env: &impl EnvSource, repo_root: &Path) -> Result<PathBuf> {
91 resolve_scratch_dir(env, || super::find_git_root_from_path(repo_root))
92 }
93
94 #[test]
95 fn get_ai_scratch_dir_with_direct_path() {
96 let env = MapEnv::new().with("AI_SCRATCH", "/custom/scratch/path");
97
98 let result = scratch_dir_with(&env, Path::new("/unused")).unwrap();
99 assert_eq!(result, PathBuf::from("/custom/scratch/path"));
100 }
101
102 #[test]
103 fn get_ai_scratch_dir_fallback_to_tmpdir() {
104 let env = MapEnv::new().with("TMPDIR", "/custom/tmp");
107
108 let result = scratch_dir_with(&env, Path::new("/unused")).unwrap();
109 assert_eq!(result, PathBuf::from("/custom/tmp"));
110 }
111
112 #[test]
113 fn get_ai_scratch_dir_at_resolves_git_root_from_injected_path() {
114 let temp_dir = {
115 std::fs::create_dir_all("tmp").ok();
116 TempDir::new_in("tmp").unwrap()
117 };
118 std::fs::create_dir(temp_dir.path().join(".git")).unwrap();
119 let env = MapEnv::new().with("AI_SCRATCH", "git-root:scratch");
120
121 let result = scratch_dir_with(&env, temp_dir.path()).unwrap();
124 assert_eq!(result, temp_dir.path().join("scratch"));
125 }
126
127 #[test]
128 fn public_wrappers_resolve_without_panicking() {
129 let _ = get_ai_scratch_dir();
134 let _ = get_ai_scratch_dir_at(Path::new("/tmp"));
135 }
136
137 #[test]
138 fn find_git_root_from_path() {
139 let temp_dir = {
140 std::fs::create_dir_all("tmp").ok();
141 TempDir::new_in("tmp").unwrap()
142 };
143 let git_dir = temp_dir.path().join(".git");
144 std::fs::create_dir(&git_dir).unwrap();
145
146 let sub_dir = temp_dir.path().join("subdir").join("deeper");
147 std::fs::create_dir_all(&sub_dir).unwrap();
148
149 let result = super::find_git_root_from_path(&sub_dir).unwrap();
150 assert_eq!(result, temp_dir.path());
151 }
152}