Files
sandcage/src/workspace.rs
T
2026-05-21 23:15:51 +02:00

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:?}");
}
}