1use agtrace_types::{ProjectHash, RepositoryHash};
2use sha2::{Digest, Sha256};
3use std::path::{Path, PathBuf};
4
5pub type Result<T> = std::result::Result<T, Error>;
6
7#[derive(Debug)]
8pub enum Error {
9 Io(std::io::Error),
10 Config(String),
11}
12
13impl std::fmt::Display for Error {
14 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15 match self {
16 Error::Io(err) => write!(f, "IO error: {}", err),
17 Error::Config(msg) => write!(f, "Config error: {}", msg),
18 }
19 }
20}
21
22impl std::error::Error for Error {
23 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
24 match self {
25 Error::Io(err) => Some(err),
26 Error::Config(_) => None,
27 }
28 }
29}
30
31impl From<std::io::Error> for Error {
32 fn from(err: std::io::Error) -> Self {
33 Error::Io(err)
34 }
35}
36
37pub fn resolve_workspace_path(explicit_path: Option<&str>) -> Result<PathBuf> {
43 if let Some(path) = explicit_path {
45 return Ok(expand_tilde(path));
46 }
47
48 if let Ok(env_path) = std::env::var("AGTRACE_PATH") {
50 return Ok(expand_tilde(&env_path));
51 }
52
53 if let Some(data_dir) = dirs::data_dir() {
55 return Ok(data_dir.join("agtrace"));
56 }
57
58 if let Some(home) = std::env::var_os("HOME") {
60 return Ok(PathBuf::from(home).join(".agtrace"));
61 }
62
63 Err(Error::Config(
65 "Could not determine workspace path: no HOME directory or system data directory found"
66 .to_string(),
67 ))
68}
69
70pub fn expand_tilde(path: &str) -> PathBuf {
72 if let Some(stripped) = path.strip_prefix("~/")
73 && let Some(home) = std::env::var_os("HOME")
74 {
75 return PathBuf::from(home).join(stripped);
76 }
77 PathBuf::from(path)
78}
79
80pub fn project_hash_from_root(project_root: &str) -> ProjectHash {
87 let normalized = normalize_path(Path::new(project_root));
89 let path_str = normalized.to_string_lossy();
90
91 let mut hasher = Sha256::new();
92 hasher.update(path_str.as_bytes());
93 ProjectHash::new(format!("{:x}", hasher.finalize()))
94}
95
96pub fn normalize_path(path: &Path) -> PathBuf {
98 path.canonicalize().unwrap_or_else(|_| {
99 if path.is_absolute() {
100 path.to_path_buf()
101 } else {
102 std::env::current_dir()
103 .map(|cwd| cwd.join(path))
104 .unwrap_or_else(|_| path.to_path_buf())
105 }
106 })
107}
108
109pub fn paths_equal(path1: &Path, path2: &Path) -> bool {
111 normalize_path(path1) == normalize_path(path2)
112}
113
114pub fn discover_project_root(explicit_project_root: Option<&str>) -> Result<PathBuf> {
119 if let Some(root) = explicit_project_root {
120 return Ok(PathBuf::from(root));
121 }
122
123 if let Ok(env_root) = std::env::var("AGTRACE_PROJECT_ROOT") {
124 return Ok(PathBuf::from(env_root));
125 }
126
127 let cwd = std::env::current_dir()?;
128 Ok(cwd)
129}
130
131pub fn resolve_effective_project_hash(
133 explicit_hash: Option<&ProjectHash>,
134 all_projects: bool,
135) -> Result<(Option<ProjectHash>, bool)> {
136 if let Some(hash) = explicit_hash {
137 Ok((Some(hash.clone()), false))
138 } else if all_projects {
139 Ok((None, true))
140 } else {
141 let project_root_path = discover_project_root(None)?;
142 let current_project_hash = project_hash_from_root(&project_root_path.to_string_lossy());
143 Ok((Some(current_project_hash), false))
144 }
145}
146
147pub fn project_hash_from_log_path(log_path: &Path) -> ProjectHash {
150 let mut hasher = Sha256::new();
151 hasher.update(log_path.to_string_lossy().as_bytes());
152 ProjectHash::new(format!("{:x}", hasher.finalize()))
153}
154
155pub fn repository_hash_from_path(path: &Path) -> Option<RepositoryHash> {
162 use std::process::Command;
163
164 let git_common_dir = Command::new("git")
165 .args(["rev-parse", "--git-common-dir"])
166 .current_dir(path)
167 .output()
168 .ok()?;
169
170 if !git_common_dir.status.success() {
171 return None;
172 }
173
174 let common_dir_str = String::from_utf8_lossy(&git_common_dir.stdout);
175 let common_dir_path = Path::new(common_dir_str.trim());
176
177 let normalized = normalize_path(common_dir_path);
179 let path_str = normalized.to_string_lossy();
180
181 let mut hasher = Sha256::new();
182 hasher.update(path_str.as_bytes());
183 Some(RepositoryHash::new(format!("{:x}", hasher.finalize())))
184}