use super::Error;
use crate::template::GitRunner;
use std::path::Path;
pub(crate) const WRITE_SUMMARY: &str = "write_summary";
pub(crate) const MARK_FOR_DELETION: &str = "mark_for_deletion";
pub(crate) const SUMMARY_DIR: &str = "summary";
const SUMMARY_SEQ_WIDTH: usize = 3;
pub(crate) fn write_summary(worktree: &Path, content: &str) -> std::io::Result<String> {
let dir_abs = worktree.join(SUMMARY_DIR);
std::fs::create_dir_all(&dir_abs)?;
let seq = next_seq(&dir_abs)?;
let file_name = format!("{seq:0width$}.md", width = SUMMARY_SEQ_WIDTH);
let path_abs = dir_abs.join(&file_name);
std::fs::write(&path_abs, content)?;
Ok(format!("{SUMMARY_DIR}/{file_name}"))
}
pub(crate) fn mark_for_deletion(
worktree: &Path,
path: &str,
git: &dyn GitRunner,
) -> Result<(), Error> {
git.run(worktree, &["rm", "-r", "-q", "--", path])
.map_err(|source| Error::Git {
op: "mark_for_deletion rm",
source,
})
}
fn next_seq(dir: &Path) -> std::io::Result<u32> {
let mut max = 0u32;
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let name = entry.file_name();
let Some(stem) = Path::new(&name).file_stem().and_then(|s| s.to_str()) else {
continue;
};
if Path::new(&name).extension().and_then(|e| e.to_str()) != Some("md") {
continue;
}
if let Ok(n) = stem.parse::<u32>() {
max = max.max(n);
}
}
Ok(max + 1)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::template::RealGit;
fn tmpdir() -> tempfile::TempDir {
tempfile::TempDir::new().unwrap()
}
#[test]
fn write_summary_picks_001_when_dir_is_empty() {
let wt = tmpdir();
let rel = write_summary(wt.path(), "body\n").unwrap();
assert_eq!(rel, "summary/001.md");
assert_eq!(
std::fs::read_to_string(wt.path().join(&rel)).unwrap(),
"body\n"
);
}
#[test]
fn write_summary_increments_past_existing_files() {
let wt = tmpdir();
let dir = wt.path().join("summary");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("001.md"), "old").unwrap();
std::fs::write(dir.join("007.md"), "also old").unwrap();
let rel = write_summary(wt.path(), "new\n").unwrap();
assert_eq!(rel, "summary/008.md");
}
#[test]
fn write_summary_skips_non_md_and_unparseable_stems() {
let wt = tmpdir();
let dir = wt.path().join("summary");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("README.txt"), "").unwrap();
std::fs::write(dir.join("notes.md"), "").unwrap();
std::fs::write(dir.join("002.md"), "").unwrap();
let rel = write_summary(wt.path(), "x").unwrap();
assert_eq!(rel, "summary/003.md");
}
#[test]
fn write_summary_skips_a_non_utf8_file_name() {
use std::os::unix::ffi::OsStrExt;
let wt = tmpdir();
let dir = wt.path().join("summary");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(std::ffi::OsStr::from_bytes(b"\xFF\xFE")), "").unwrap();
std::fs::write(dir.join("004.md"), "").unwrap();
let rel = write_summary(wt.path(), "x").unwrap();
assert_eq!(rel, "summary/005.md");
}
fn repo_with(rel: &str) -> tempfile::TempDir {
let dir = tmpdir();
let wt = dir.path();
let g = RealGit::new();
g.run(wt, &["init", "-b", "agents/p1"]).unwrap();
g.run(wt, &["config", "user.email", "t@t"]).unwrap();
g.run(wt, &["config", "user.name", "t"]).unwrap();
let f = wt.join(rel);
std::fs::create_dir_all(f.parent().unwrap()).unwrap();
std::fs::write(&f, "content\n").unwrap();
g.run(wt, &["add", "-A"]).unwrap();
g.run(wt, &["commit", "-m", "c"]).unwrap();
dir
}
#[test]
fn mark_for_deletion_stages_a_real_removal() {
let dir = repo_with("messages/001-user.md");
let wt = dir.path();
mark_for_deletion(wt, "messages/001-user.md", &RealGit::new()).unwrap();
assert!(!wt.join("messages/001-user.md").exists());
let staged = RealGit::new()
.run_capture(wt, &["diff", "--cached", "--name-status"])
.unwrap();
assert!(staged.starts_with('D'), "staged deletion: {staged:?}");
}
#[test]
fn mark_for_deletion_declines_a_nonexistent_path() {
let dir = repo_with("keep.txt");
let err = mark_for_deletion(dir.path(), "no/such.md", &RealGit::new()).unwrap_err();
assert!(
matches!(
err,
Error::Git {
op: "mark_for_deletion rm",
..
}
),
"{err:?}"
);
}
}