65 lines
2.4 KiB
Rust
65 lines
2.4 KiB
Rust
|
|
//! Shell I/O — the ONLY place in `service::shell/` that may perform side effects.
|
||
|
|
//!
|
||
|
|
//! Side effects here include: filesystem existence and canonicalization checks,
|
||
|
|
//! process spawning via `std::process::Command`, and reading pipe output.
|
||
|
|
//! All pure logic (pattern matching, output truncation, count parsing) lives in
|
||
|
|
//! `path_guard.rs`.
|
||
|
|
|
||
|
|
use super::Error;
|
||
|
|
use std::path::{Path, PathBuf};
|
||
|
|
|
||
|
|
/// Validate that `working_dir` is an absolute path that exists on disk and
|
||
|
|
/// lies inside the project's `.huskies/worktrees/` or `.huskies/merge_workspace/`
|
||
|
|
/// directory. Returns the canonicalized path on success.
|
||
|
|
///
|
||
|
|
/// # Errors
|
||
|
|
/// - [`Error::Validation`] if the path is relative or does not exist.
|
||
|
|
/// - [`Error::PathNotAllowed`] if the path is outside the allowed roots.
|
||
|
|
/// - [`Error::Io`] if canonicalization fails.
|
||
|
|
pub fn validate_working_dir(working_dir: &str, project_root: &Path) -> Result<PathBuf, Error> {
|
||
|
|
let wd = PathBuf::from(working_dir);
|
||
|
|
|
||
|
|
if !wd.is_absolute() {
|
||
|
|
return Err(Error::Validation(
|
||
|
|
"working_dir must be an absolute path".to_string(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
if !wd.exists() {
|
||
|
|
return Err(Error::Validation(format!(
|
||
|
|
"working_dir does not exist: {working_dir}"
|
||
|
|
)));
|
||
|
|
}
|
||
|
|
|
||
|
|
let worktrees_root = project_root.join(".huskies").join("worktrees");
|
||
|
|
|
||
|
|
let canonical_wd = wd
|
||
|
|
.canonicalize()
|
||
|
|
.map_err(|e| Error::Io(format!("Cannot canonicalize working_dir: {e}")))?;
|
||
|
|
|
||
|
|
let canonical_wt = if worktrees_root.exists() {
|
||
|
|
worktrees_root
|
||
|
|
.canonicalize()
|
||
|
|
.map_err(|e| Error::Io(format!("Cannot canonicalize worktrees root: {e}")))?
|
||
|
|
} else {
|
||
|
|
return Err(Error::PathNotAllowed(
|
||
|
|
"No worktrees directory found in project".to_string(),
|
||
|
|
));
|
||
|
|
};
|
||
|
|
|
||
|
|
// Also allow the merge workspace so mergemaster can fix conflicts.
|
||
|
|
let merge_workspace = project_root.join(".huskies").join("merge_workspace");
|
||
|
|
let canonical_mw = merge_workspace.canonicalize().unwrap_or_default();
|
||
|
|
|
||
|
|
let in_worktrees = canonical_wd.starts_with(&canonical_wt);
|
||
|
|
let in_merge_ws =
|
||
|
|
!canonical_mw.as_os_str().is_empty() && canonical_wd.starts_with(&canonical_mw);
|
||
|
|
|
||
|
|
if !in_worktrees && !in_merge_ws {
|
||
|
|
return Err(Error::PathNotAllowed(format!(
|
||
|
|
"working_dir must be inside .huskies/worktrees/ or .huskies/merge_workspace/. Got: {working_dir}"
|
||
|
|
)));
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(canonical_wd)
|
||
|
|
}
|