115 lines
3.9 KiB
Rust
115 lines
3.9 KiB
Rust
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
use miette::Diagnostic;
|
|
use thiserror::Error;
|
|
|
|
#[derive(Debug, Error, Diagnostic)]
|
|
pub enum WorkspaceError {
|
|
#[error("Failed to resolve absolute path for {0}: {1}")]
|
|
#[diagnostic(code(sandcage::workspace::canonicalize_failed))]
|
|
CanonicalizeFailed(PathBuf, #[source] std::io::Error),
|
|
}
|
|
|
|
pub type Result<T> = std::result::Result<T, WorkspaceError>;
|
|
|
|
pub fn resolve_workspace(path: Option<&Path>) -> Result<PathBuf> {
|
|
// Determine starting path
|
|
let raw = match path {
|
|
Some(p) => p.to_path_buf(),
|
|
None => std::env::current_dir()
|
|
.map_err(|e| WorkspaceError::CanonicalizeFailed(PathBuf::from("."), e))?,
|
|
};
|
|
|
|
// Resolve to absolute, canonical path (follows symlinks, checks existence)
|
|
let absolute = raw
|
|
.canonicalize()
|
|
.map_err(|e| WorkspaceError::CanonicalizeFailed(raw.clone(), e))?;
|
|
|
|
// Ask git for the worktree root; silently fall back if not a git repo
|
|
let git_root = Command::new("git")
|
|
.args(["rev-parse", "--show-toplevel"])
|
|
.current_dir(&absolute)
|
|
.output();
|
|
|
|
match git_root {
|
|
Ok(output) if output.status.success() => {
|
|
let root = String::from_utf8_lossy(&output.stdout)
|
|
.trim()
|
|
.to_string();
|
|
Ok(PathBuf::from(root))
|
|
}
|
|
// Not a git repo, or git not installed — return the absolute path as-is
|
|
_ => Ok(absolute),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::process::Command;
|
|
use tempfile::TempDir;
|
|
|
|
/// Create a temporary directory and initialise it as a git repository.
|
|
/// Returns the `TempDir` guard (keep it alive for the test) and the path.
|
|
fn make_git_repo() -> (TempDir, PathBuf) {
|
|
let dir = TempDir::new().expect("create tempdir");
|
|
let path = dir.path().to_path_buf();
|
|
|
|
// git init
|
|
let status = Command::new("git")
|
|
.args(["init"])
|
|
.current_dir(&path)
|
|
.status()
|
|
.expect("run git init");
|
|
assert!(status.success(), "git init failed");
|
|
|
|
(dir, path)
|
|
}
|
|
|
|
#[test]
|
|
fn returns_repo_root_when_inside_git_repo() {
|
|
let (_guard, repo_root) = make_git_repo();
|
|
|
|
// Create a nested directory inside the repo
|
|
let nested = repo_root.join("a").join("b");
|
|
std::fs::create_dir_all(&nested).expect("create nested dirs");
|
|
|
|
// resolve_workspace from the nested dir should return the repo root
|
|
let resolved = resolve_workspace(Some(&nested)).expect("resolve_workspace");
|
|
assert_eq!(
|
|
resolved.canonicalize().unwrap(),
|
|
repo_root.canonicalize().unwrap(),
|
|
"expected repo root, got {resolved:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn returns_directory_itself_when_not_in_git_repo() {
|
|
// Create a temp dir that is NOT a git repo (no git init)
|
|
let dir = TempDir::new().expect("create tempdir");
|
|
|
|
// Make sure there is no ancestor git repo that would pollute the result.
|
|
// tempfile creates dirs under the system temp folder, which is typically
|
|
// outside any user repo, so this should be safe.
|
|
let path = dir.path().to_path_buf();
|
|
let resolved = resolve_workspace(Some(&path)).expect("resolve_workspace");
|
|
|
|
assert_eq!(
|
|
resolved.canonicalize().unwrap(),
|
|
path.canonicalize().unwrap(),
|
|
"expected the directory itself, got {resolved:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn none_path_uses_current_directory() {
|
|
// When None is passed the function should not error; it should return
|
|
// something (either the cwd or its git root).
|
|
let result = resolve_workspace(None);
|
|
assert!(result.is_ok(), "resolve_workspace(None) returned error: {result:?}");
|
|
let resolved = result.unwrap();
|
|
assert!(resolved.is_absolute(), "result should be absolute: {resolved:?}");
|
|
}
|
|
}
|