use super::{MESSAGES_DIR, SKILLS_DIR, SUMMARY_DIR};
use crate::facts::FILE as FACTS_FILE;
use crate::prompt::Error;
use crate::prompt::dispatch::step_commit::DESCRIPTIONS_DIR;
use crate::template::GitRunner;
use std::collections::BTreeSet;
use std::path::Path;
pub(super) fn changed_paths(
worktree: &Path,
founding: &str,
terminal: &str,
git: &dyn GitRunner,
) -> Result<Vec<String>, Error> {
let (messages, summary, descriptions) = (
format!(":(exclude){MESSAGES_DIR}"),
format!(":(exclude){SUMMARY_DIR}"),
format!(":(exclude){DESCRIPTIONS_DIR}"),
);
let out = git
.run_capture(
worktree,
&[
"diff",
"--name-only",
founding,
terminal,
"--",
&messages,
&summary,
&descriptions,
],
)
.map_err(|source| Error::Git {
op: "proposal diff",
source,
})?;
Ok(out
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(str::to_string)
.collect())
}
pub(super) fn admitted(path: &str, pool: &BTreeSet<String>) -> bool {
if path == FACTS_FILE {
return true;
}
let mut parts = path.split('/');
if parts.next() != Some(SKILLS_DIR) {
return false;
}
let Some(name) = parts.next() else {
return false;
};
parts.next().is_some() && !pool.contains(name)
}
pub(super) fn write_patch(
worktree: &Path,
founding: &str,
terminal: &str,
out: &str,
git: &dyn GitRunner,
) -> Result<(), Error> {
let output = format!("--output={out}");
git.run(
worktree,
&[
"diff", founding, terminal, &output, "--", SKILLS_DIR, FACTS_FILE,
],
)
.map_err(|source| Error::Git {
op: "proposal patch",
source,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::io;
struct DiffGit(&'static str, std::cell::RefCell<Vec<String>>);
impl GitRunner for DiffGit {
fn run(&self, _d: &Path, _a: &[&str]) -> io::Result<()> {
unreachable!("the filter only captures")
}
fn run_capture(&self, _d: &Path, args: &[&str]) -> io::Result<String> {
*self.1.borrow_mut() = args.iter().map(|s| (*s).to_string()).collect();
if self.0.is_empty() {
return Err(io::Error::other("diff boom"));
}
Ok(self.0.to_string())
}
}
fn pool(names: &[&str]) -> BTreeSet<String> {
names.iter().map(|n| (*n).to_string()).collect()
}
#[test]
fn the_two_classes_are_admitted_and_nothing_else() {
let pool = pool(&["bash"]);
assert!(admitted("skills/notes/SKILL.md", &pool));
assert!(admitted("skills/notes/ref/table.md", &pool));
assert!(admitted(FACTS_FILE, &pool));
assert!(admitted("skills/archived/notes/SKILL.md", &pool));
assert!(!admitted("skills/bash/SKILL.md", &pool));
assert!(!admitted("out.txt", &pool));
assert!(!admitted("soul.md", &pool));
assert!(!admitted("skills/loose.txt", &pool));
assert!(!admitted("skills", &pool));
}
#[test]
fn the_transcript_is_excluded_by_pathspec_and_blank_lines_drop() {
let git = DiffGit(
"skills/notes/SKILL.md
",
std::cell::RefCell::new(Vec::new()),
);
let changed = changed_paths(Path::new("/wt"), "f", "t", &git).unwrap();
assert_eq!(changed, vec!["skills/notes/SKILL.md".to_string()]);
let args = git.1.borrow();
assert!(args.iter().any(|a| a == ":(exclude)messages"), "{args:?}");
assert!(args.iter().any(|a| a == ":(exclude)summary"), "{args:?}");
}
struct FailingRun;
impl GitRunner for FailingRun {
fn run(&self, _d: &Path, _a: &[&str]) -> io::Result<()> {
Err(io::Error::other("diff --output boom"))
}
fn run_capture(&self, _d: &Path, _a: &[&str]) -> io::Result<String> {
unreachable!("the patch write never captures")
}
}
#[test]
fn a_failing_patch_write_surfaces_as_a_git_error() {
let err = write_patch(Path::new("/wt"), "f", "t", "/tmp/p", &FailingRun).unwrap_err();
assert!(
matches!(
err,
Error::Git {
op: "proposal patch",
..
}
),
"{err:?}"
);
}
#[test]
fn a_failing_diff_surfaces_as_a_git_error() {
let git = DiffGit("", std::cell::RefCell::new(Vec::new()));
let err = changed_paths(Path::new("/wt"), "f", "t", &git).unwrap_err();
assert!(
matches!(
err,
Error::Git {
op: "proposal diff",
..
}
),
"{err:?}"
);
}
}