agtrace_types/
util.rs

1use anyhow::Result;
2use sha2::{Digest, Sha256};
3use std::path::{Path, PathBuf};
4
5/// Calculate project_hash from project_root using SHA256
6///
7/// This function canonicalizes the path before hashing to ensure consistency
8/// across symlinks and different path representations.
9/// For example, `/var/folders/...` and `/private/var/folders/...` will produce
10/// the same hash on macOS where `/var` is a symlink to `/private/var`.
11pub fn project_hash_from_root(project_root: &str) -> String {
12    // Normalize path to resolve symlinks and relative paths
13    let normalized = normalize_path(Path::new(project_root));
14    let path_str = normalized.to_string_lossy();
15
16    let mut hasher = Sha256::new();
17    hasher.update(path_str.as_bytes());
18    format!("{:x}", hasher.finalize())
19}
20
21/// Check if string is 64-character hexadecimal
22pub fn is_64_char_hex(s: &str) -> bool {
23    s.len() == 64 && s.chars().all(|c| c.is_ascii_hexdigit())
24}
25
26/// Normalize a path for comparison (resolve to absolute, canonicalize if possible)
27pub fn normalize_path(path: &Path) -> PathBuf {
28    path.canonicalize().unwrap_or_else(|_| {
29        if path.is_absolute() {
30            path.to_path_buf()
31        } else {
32            std::env::current_dir()
33                .map(|cwd| cwd.join(path))
34                .unwrap_or_else(|_| path.to_path_buf())
35        }
36    })
37}
38
39/// Check if two paths are equivalent after normalization
40pub fn paths_equal(path1: &Path, path2: &Path) -> bool {
41    normalize_path(path1) == normalize_path(path2)
42}
43
44/// Discover project root based on priority:
45/// 1. explicit_project_root (--project-root flag)
46/// 2. AGTRACE_PROJECT_ROOT environment variable
47/// 3. Current working directory
48pub fn discover_project_root(explicit_project_root: Option<&str>) -> Result<PathBuf> {
49    if let Some(root) = explicit_project_root {
50        return Ok(PathBuf::from(root));
51    }
52
53    if let Ok(env_root) = std::env::var("AGTRACE_PROJECT_ROOT") {
54        return Ok(PathBuf::from(env_root));
55    }
56
57    let cwd = std::env::current_dir()?;
58    Ok(cwd)
59}
60
61/// Resolve effective project hash based on explicit hash or all_projects flag
62pub fn resolve_effective_project_hash(
63    explicit_hash: Option<&str>,
64    all_projects: bool,
65) -> Result<(Option<String>, bool)> {
66    if let Some(hash) = explicit_hash {
67        Ok((Some(hash.to_string()), false))
68    } else if all_projects {
69        Ok((None, true))
70    } else {
71        let project_root_path = discover_project_root(None)?;
72        let current_project_hash = project_hash_from_root(&project_root_path.to_string_lossy());
73        Ok((Some(current_project_hash), false))
74    }
75}
76
77/// Truncate a string to a maximum length
78pub fn truncate(s: &str, max: usize) -> String {
79    if s.chars().count() <= max {
80        s.to_string()
81    } else {
82        s.chars().take(max).collect::<String>() + "...(truncated)"
83    }
84}