Normalize a path for storage, optionally stripping a repo root prefix. This handles the case where absolute paths are accidentally passed in. On macOS, `/tmp` is a symlink to `/private/tmp`, so we try both the given root and its canonical form. # Arguments `path` - The path to normalize `repo_root` - Optional repository root to strip from absolute paths
(path: &Path, repo_root: Option<&Path>)
| 26 | /// * `path` - The path to normalize |
| 27 | /// * `repo_root` - Optional repository root to strip from absolute paths |
| 28 | pub fn normalize_path_with_root(path: &Path, repo_root: Option<&Path>) -> String { |
| 29 | let mut path_to_normalize = path.to_path_buf(); |
| 30 | |
| 31 | // If path is absolute and we have a repo root, try to make it relative. |
| 32 | // We check both Path::is_absolute() (handles native absolute paths) and |
| 33 | // a leading '/' in the string representation (handles Unix-style paths on |
| 34 | // Windows, where "/repo/src" is not considered absolute by the OS but must |
| 35 | // still be treated as absolute for prefix-stripping purposes). |
| 36 | let path_str_raw = path.to_string_lossy(); |
| 37 | let is_absolute = path_to_normalize.is_absolute() || path_str_raw.starts_with('/'); |
| 38 | |
| 39 | if is_absolute { |
| 40 | if let Some(root) = repo_root { |
| 41 | // Try stripping the root directly |
| 42 | if let Ok(rel) = path_to_normalize.strip_prefix(root) { |
| 43 | path_to_normalize = rel.to_path_buf(); |
| 44 | } else if let Ok(canonical_root) = root.canonicalize() { |
| 45 | // On macOS, /tmp -> /private/tmp, so try canonical |
| 46 | if let Ok(rel) = path_to_normalize.strip_prefix(&canonical_root) { |
| 47 | path_to_normalize = rel.to_path_buf(); |
| 48 | } |
| 49 | } else { |
| 50 | // Path::strip_prefix uses OS path semantics, which on Windows |
| 51 | // won't match Unix-style "/repo" against "/repo/src/main.rs" |
| 52 | // as a proper prefix. Fall back to string-level stripping so |
| 53 | // that Unix-style paths work correctly on Windows in tests and |
| 54 | // cross-platform scenarios. |
| 55 | let root_str = root.to_string_lossy(); |
| 56 | let root_str = root_str.replace('\\', "/"); |
| 57 | let path_str = path_str_raw.replace('\\', "/"); |
| 58 | let root_with_sep = if root_str.ends_with('/') { |
| 59 | root_str.to_string() |
| 60 | } else { |
| 61 | format!("{}/", root_str) |
| 62 | }; |
| 63 | if let Some(rel) = path_str.strip_prefix(root_with_sep.as_str()) { |
| 64 | path_to_normalize = PathBuf::from(rel); |
| 65 | } else if path_str == root_str { |
| 66 | path_to_normalize = PathBuf::new(); |
| 67 | } |
| 68 | } |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | let path_str = path_to_normalize.to_string_lossy(); |
| 73 | |
| 74 | // Convert to forward slashes and remove trailing slash |
| 75 | let normalized = path_str |
| 76 | .replace('\\', "/") |
| 77 | .trim_end_matches('/') |
| 78 | .to_string(); |
| 79 | |
| 80 | // Handle empty path (current directory) |
| 81 | if normalized.is_empty() { |
| 82 | ".".to_string() |
| 83 | } else { |
| 84 | normalized |
| 85 | } |
no test coverage detected