use anyhow::{Context, Result};
use std::io::ErrorKind;
use std::path::Path;
fn read_existing(path: &Path) -> Result<Option<String>> {
match std::fs::read_to_string(path) {
Ok(s) => Ok(Some(s)),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
Err(e) => Err(e).with_context(|| format!("reading {}", path.display())),
}
}
pub fn ensure(project_root: &Path, comment: &str, entry: &str) -> Result<()> {
let path = project_root.join(".gitignore");
let existing = read_existing(&path)?.unwrap_or_default();
if existing.lines().any(|l| l.trim() == entry) {
return Ok(());
}
let mut out = existing;
if !out.is_empty() {
if !out.ends_with('\n') {
out.push('\n');
}
out.push('\n'); }
out.push_str(comment);
out.push('\n');
out.push_str(entry);
out.push('\n');
std::fs::write(&path, out).with_context(|| format!("updating {}", path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
const COMMENT: &str = "# spm-managed test entry";
const ENTRY: &str = ".spm-test/";
#[test]
fn ensure_appends_block_and_is_idempotent() {
let tmp = std::env::temp_dir().join(format!("spm-gi-{}", std::process::id()));
std::fs::create_dir_all(&tmp).unwrap();
let gi = tmp.join(".gitignore");
std::fs::write(&gi, "target/\n").unwrap();
ensure(&tmp, COMMENT, ENTRY).unwrap();
let after = std::fs::read_to_string(&gi).unwrap();
assert!(after.contains(ENTRY), "{after}");
assert!(after.contains(COMMENT), "{after}");
assert!(
after.starts_with("target/\n"),
"preserves prior content: {after}"
);
ensure(&tmp, COMMENT, ENTRY).unwrap();
let twice = std::fs::read_to_string(&gi).unwrap();
assert_eq!(twice.matches(ENTRY).count(), 1, "{twice}");
std::fs::remove_dir_all(&tmp).unwrap();
}
#[test]
fn ensure_surfaces_read_error_instead_of_clobbering() {
let tmp = std::env::temp_dir().join(format!("spm-gi-badutf8-{}", std::process::id()));
std::fs::create_dir_all(&tmp).unwrap();
let gi = tmp.join(".gitignore");
std::fs::write(&gi, [0xff, 0xfe, 0x00, 0x9f]).unwrap();
let err = ensure(&tmp, COMMENT, ENTRY);
assert!(err.is_err(), "expected a surfaced read error");
assert_eq!(std::fs::read(&gi).unwrap(), vec![0xff, 0xfe, 0x00, 0x9f]);
std::fs::remove_dir_all(&tmp).unwrap();
}
}