1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// SPDX-License-Identifier: GPL-3.0-only
//! Project-root detection, shared by `install` and `skills`.
use std::path::{Path, PathBuf};
use anyhow::Context;
/// Default markers that identify a project root when walking up from CWD.
pub(crate) fn default_markers() -> Vec<String> {
vec![
".git".to_string(),
".jj".to_string(),
".project".to_string(),
"Cargo.toml".to_string(),
]
}
/// Resolve the project root.
///
/// An `explicit` path is used as-is. Otherwise walk up from CWD until a
/// directory contains any of `markers`.
pub(crate) fn find(explicit: Option<PathBuf>, markers: &[String]) -> anyhow::Result<PathBuf> {
if let Some(path) = explicit {
return Ok(path);
}
let cwd = std::env::current_dir().context("Failed to get current working directory")?;
find_from(&cwd, markers).ok_or_else(|| {
anyhow::anyhow!(
"No project root found. Walked up from '{}' looking for any of: {:?}",
cwd.display(),
markers,
)
})
}
/// Walk up from `start` (inclusive) to the first ancestor containing any of
/// `markers`. Unlike [`find`], this takes an explicit start directory — the
/// ambient-surfacing hook (SL-205) discovers the root from the stdin `cwd`, not
/// the process cwd — and returns `None` (rather than an error) when no marker is
/// found, so the caller can fold a missing root into a fail-open no-op.
pub(crate) fn find_from(start: &Path, markers: &[String]) -> Option<PathBuf> {
for ancestor in start.ancestors() {
for marker in markers {
if ancestor.join(marker).exists() {
return Some(ancestor.to_path_buf());
}
}
}
None
}