use std::io;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
pub const DEFAULT_SCRATCH_DIR: &str = ".scratch";
static CONFIGURED_SCRATCH_DIR: OnceLock<String> = OnceLock::new();
pub fn set_scratch_dir(dir: impl Into<String>) {
let dir = dir.into();
if !dir.trim().is_empty() {
let _ = CONFIGURED_SCRATCH_DIR.set(dir);
}
}
#[must_use]
pub fn scratch_dir() -> String {
if let Ok(v) = std::env::var("NEWT_SCRATCH_DIR") {
if !v.trim().is_empty() {
return v;
}
}
CONFIGURED_SCRATCH_DIR
.get()
.cloned()
.unwrap_or_else(|| DEFAULT_SCRATCH_DIR.to_string())
}
#[must_use]
pub fn resolve_scratch_root(base: &Path, dir: &str) -> PathBuf {
let p = Path::new(dir);
if p.is_absolute() {
p.to_path_buf()
} else {
base.join(p)
}
}
#[must_use]
pub fn scratch_root(base: &Path) -> PathBuf {
resolve_scratch_root(base, &scratch_dir())
}
#[must_use]
pub fn scratch_workspace_subdir() -> String {
let dir = scratch_dir();
if Path::new(&dir).is_absolute() {
DEFAULT_SCRATCH_DIR.to_string()
} else {
dir
}
}
pub const SCRATCH_GITIGNORE: &str =
"# Auto-generated by newt: ephemeral scratch (#844). Never commit this dir.\n*\n";
pub fn ensure_scratch(base: &Path) -> io::Result<PathBuf> {
let root = scratch_root(base);
std::fs::create_dir_all(&root)?;
let ignore = root.join(".gitignore");
if !ignore.exists() {
std::fs::write(&ignore, SCRATCH_GITIGNORE)?;
}
Ok(root)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_relative_setting_is_joined_under_base() {
assert_eq!(
resolve_scratch_root(Path::new("/repo"), ".scratch"),
PathBuf::from("/repo/.scratch")
);
assert_eq!(
resolve_scratch_root(Path::new("/repo"), ".myscratch"),
PathBuf::from("/repo/.myscratch")
);
}
#[test]
fn resolve_absolute_setting_relocates_outside_the_repo() {
assert_eq!(
resolve_scratch_root(Path::new("/repo"), "/tmp/newt-scratch"),
PathBuf::from("/tmp/newt-scratch")
);
}
#[test]
fn workspace_subdir_falls_back_to_default_when_scratch_is_absolute() {
assert_eq!(scratch_workspace_subdir(), DEFAULT_SCRATCH_DIR);
}
#[test]
fn gitignore_body_ignores_everything() {
assert!(SCRATCH_GITIGNORE.lines().any(|l| l.trim() == "*"));
}
#[test]
fn ensure_scratch_creates_dir_and_self_ignore() {
let tmp = tempfile::tempdir().unwrap();
let root = ensure_scratch(tmp.path()).unwrap();
assert!(root.is_dir());
let ignore = root.join(".gitignore");
assert!(ignore.is_file());
assert_eq!(std::fs::read_to_string(&ignore).unwrap(), SCRATCH_GITIGNORE);
}
#[test]
fn ensure_scratch_is_idempotent_and_preserves_a_customized_ignore() {
let tmp = tempfile::tempdir().unwrap();
ensure_scratch(tmp.path()).unwrap();
let ignore = scratch_root(tmp.path()).join(".gitignore");
std::fs::write(&ignore, "*\n!keep.txt\n").unwrap();
ensure_scratch(tmp.path()).unwrap();
assert_eq!(std::fs::read_to_string(&ignore).unwrap(), "*\n!keep.txt\n");
}
}