relay_knowledge/paths/
repository_root.rs1use std::{
8 error::Error,
9 fmt, fs, io,
10 path::{Path, PathBuf},
11};
12
13use crate::project::{
14 CODESPEC_MAP_RELATIVE_PATH, KNOWLEDGE_MAP_RELATIVE_PATH, LEGACY_KNOWLEDGE_MAP_RELATIVE_PATH,
15};
16
17#[derive(Debug)]
19pub enum RepositoryRootDiscoveryError {
20 StartUnavailable { path: PathBuf, source: io::Error },
21 StartNotDirectory { path: PathBuf },
22 MarkerProbeFailed { path: PathBuf, source: io::Error },
23}
24
25impl fmt::Display for RepositoryRootDiscoveryError {
26 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27 match self {
28 Self::StartUnavailable { path, source } => {
29 write!(
30 formatter,
31 "failed to inspect start directory '{}': {source}",
32 path.display()
33 )
34 }
35 Self::StartNotDirectory { path } => {
36 write!(
37 formatter,
38 "repository root search must start from a directory, got '{}'",
39 path.display()
40 )
41 }
42 Self::MarkerProbeFailed { path, source } => {
43 write!(
44 formatter,
45 "failed to inspect repository marker '{}': {source}",
46 path.display()
47 )
48 }
49 }
50 }
51}
52
53impl Error for RepositoryRootDiscoveryError {
54 fn source(&self) -> Option<&(dyn Error + 'static)> {
55 match self {
56 Self::StartUnavailable { source, .. } | Self::MarkerProbeFailed { source, .. } => {
57 Some(source)
58 }
59 Self::StartNotDirectory { .. } => None,
60 }
61 }
62}
63
64pub fn discover_repository_root(
70 start: &Path,
71) -> Result<Option<PathBuf>, RepositoryRootDiscoveryError> {
72 let metadata =
73 fs::metadata(start).map_err(|source| RepositoryRootDiscoveryError::StartUnavailable {
74 path: start.to_path_buf(),
75 source,
76 })?;
77 if !metadata.is_dir() {
78 return Err(RepositoryRootDiscoveryError::StartNotDirectory {
79 path: start.to_path_buf(),
80 });
81 }
82
83 let mut agents_root = None;
84 for path in start.ancestors() {
85 if marker_exists(path.join(".git"))?
86 || marker_exists(path.join(KNOWLEDGE_MAP_RELATIVE_PATH))?
87 || marker_exists(path.join(CODESPEC_MAP_RELATIVE_PATH))?
88 || marker_exists(path.join(LEGACY_KNOWLEDGE_MAP_RELATIVE_PATH))?
89 {
90 return Ok(Some(path.to_path_buf()));
91 }
92 if agents_root.is_none() && marker_exists(path.join("AGENTS.md"))? {
93 agents_root = Some(path.to_path_buf());
94 }
95 }
96
97 Ok(agents_root)
98}
99
100fn marker_exists(path: PathBuf) -> Result<bool, RepositoryRootDiscoveryError> {
101 path.try_exists()
102 .map_err(|source| RepositoryRootDiscoveryError::MarkerProbeFailed { path, source })
103}
104
105#[cfg(test)]
106#[path = "repository_root_tests.rs"]
107mod tests;