use crate::config_edit::branch::edit::DraftFile;
use std::ops::Range;
use std::path::Path;
#[cfg(test)]
mod tests;
pub const MANIFEST_YAML: &str = "manifest.yaml";
pub const GLOB: &str = "instructions/**";
const ROLE: &str = "worker:";
const ROLES: &str = "roles:";
const PINNED: &str = "pinned:";
pub fn drift(workspace: &Path, config: &str) -> Option<DraftFile> {
let base = crate::control::author::committed(workspace, config, MANIFEST_YAML)?;
let want = authored(&base);
(want != base).then(|| DraftFile {
rel_path: MANIFEST_YAML.to_owned(),
bytes: want.into_bytes(),
})
}
pub fn authored(base: &str) -> String {
let mut lines: Vec<String> = base.lines().map(str::to_owned).collect();
if let Some((at, pad)) = insertion(&lines) {
lines.insert(at, format!("{pad}- {GLOB}"));
}
let mut out = lines.join("\n");
out.push('\n');
out
}
fn insertion(lines: &[String]) -> Option<(usize, String)> {
let roles = block(lines, 0..lines.len(), 0, ROLES)?;
let role_depth = indent(lines.get(roles.start)?);
let worker = block(lines, roles, role_depth, ROLE)?;
let key_depth = indent(lines.get(worker.start)?);
let pinned = block(lines, worker, key_depth, PINNED)?;
let want = format!("- {GLOB}");
let item = |i: usize| lines.get(i).filter(|l| is_item(l));
if pinned
.clone()
.any(|i| lines.get(i).is_some_and(|l| l.trim() == want))
{
return None;
}
let pad = pinned.clone().find_map(item).map_or_else(
|| " ".repeat(key_depth + 2),
|line| " ".repeat(indent(line)),
);
Some((pinned.end, pad))
}
fn block(lines: &[String], span: Range<usize>, depth: usize, key: &str) -> Option<Range<usize>> {
let end = span.end;
let start = span.into_iter().find(|&i| {
lines
.get(i)
.is_some_and(|line| indent(line) == depth && line.trim() == key)
})?;
let close = ((start + 1)..end)
.find(|&i| {
lines.get(i).is_some_and(|line| {
let level_item = indent(line) == depth && is_item(line);
!line.trim().is_empty() && indent(line) <= depth && !level_item
})
})
.unwrap_or(end);
Some((start + 1)..close)
}
fn is_item(line: &str) -> bool {
line.trim_start().starts_with("- ")
}
fn indent(line: &str) -> usize {
line.len() - line.trim_start_matches(' ').len()
}