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