use std::io;
use std::path::Path;
use super::{other, Flags};
use crate::change::FieldEdit;
use crate::id;
use crate::reads::resolve_dead;
use crate::task::{Blocker, On};
use crate::taskfile::exists;
use crate::verb::Verb;
pub(super) fn require_live<'a>(store: &Path, verb: Verb, targets: impl Iterator<Item = &'a str>) -> io::Result<()> {
for id in targets {
require_id_shape(verb, "edge target", id)?;
if exists(store, id) {
continue;
}
let fate = if resolve_dead(store, id)?.is_some() {
"already closed — a dead blocker can never block"
} else {
"not a known id"
};
return Err(other(format!("{}: edge target '{id}' is {fate}; drop the flag", verb.token())));
}
Ok(())
}
fn require_id_shape(verb: Verb, what: &str, id: &str) -> io::Result<()> {
if id::is_valid(id) {
return Ok(());
}
Err(other(format!(
"{}: {what} '{id}' is not a task id — a flag token was read as one; check the flag order",
verb.token()
)))
}
pub(super) fn needs_blockers(flags: &Flags) -> io::Result<Vec<Blocker>> {
flags
.needs
.iter()
.map(|spec| match spec.split_once(':') {
Some((id, op)) => Ok(Blocker { id: id.to_string(), on: verb_of(op)? }),
None => Ok(Blocker { id: spec.clone(), on: On::Claim }),
})
.collect()
}
pub(super) fn effective_parent(flags: &Flags) -> io::Result<Option<String>> {
if flags.subtask_of.is_some() && flags.parent.is_some() {
return Err(other("create: --subtask-of and --parent conflict — --subtask-of IS a parent spelling (parent + close-gate)"));
}
let parent = flags.subtask_of.clone().or_else(|| flags.parent.clone());
if let Some(p) = &parent {
require_id_shape(Verb::Create, "parent", p)?;
}
Ok(parent)
}
pub(super) fn blocks_edges(flags: &Flags, parent: Option<&str>) -> io::Result<Vec<(String, On)>> {
let mut edges: Vec<(String, On)> = flags
.blocks
.iter()
.map(|spec| {
if let Some((id, op)) = spec.split_once(':') {
Ok((id.to_string(), verb_of(op)?))
} else {
let parent = parent.map(ToString::to_string).ok_or_else(|| {
other("create: --blocks OP needs --parent/--subtask-of; gate a non-parent with --blocks ID:OP")
})?;
Ok((parent, verb_of(spec)?))
}
})
.collect::<io::Result<_>>()?;
if let Some(e) = &flags.subtask_of {
let gate = (e.clone(), On::Close);
if !edges.contains(&gate) {
edges.push(gate);
}
}
Ok(edges)
}
fn verb_of(token: &str) -> io::Result<On> {
Verb::parse(token).ok_or_else(|| other(format!("'{token}' is not a known op")))
}
pub(super) fn edits<'a>(extras: impl Iterator<Item = &'a String>, flags: &Flags) -> io::Result<Vec<FieldEdit>> {
let mut edits = Vec::new();
for kv in extras {
let (k, v) = kv.split_once('=').ok_or_else(|| other(format!("update: '{kv}' is not key=value")))?;
edits.push(extra_edit(k, v)?);
}
if let Some(t) = &flags.title {
edits.push(FieldEdit::Title(t.clone()));
}
if let Some(b) = &flags.body {
edits.push(FieldEdit::Body(b.clone()));
}
if let Some(p) = &flags.parent {
require_id_shape(Verb::Update, "parent", p)?;
edits.push(FieldEdit::Parent(Some(p.clone())));
} else if flags.no_parent {
edits.push(FieldEdit::Parent(None));
}
if let Some(p) = flags.priority {
edits.push(FieldEdit::Priority(Some(p)));
} else if flags.no_priority {
edits.push(FieldEdit::Priority(None));
}
edits.extend(flags.tags.iter().map(|t| FieldEdit::AddTag(t.clone())));
edits.extend(flags.no_tags.iter().map(|t| FieldEdit::RemoveTag(t.clone())));
edits.extend(needs_blockers(flags)?.into_iter().map(FieldEdit::AddBlocker));
edits.extend(flags.no_needs.iter().map(|spec| {
let id = spec.split_once(':').map_or(spec.as_str(), |(id, _)| id);
FieldEdit::RemoveBlocker(id.to_string())
}));
Ok(edits)
}
const RESERVED: [&str; 5] = ["id", "body", "created", "updated", "root_commit"];
fn extra_edit(k: &str, v: &str) -> io::Result<FieldEdit> {
if RESERVED.contains(&k) {
return Err(other(format!(
"update: '{k}' is reserved, not an extra — the id is the filename, the body is --body, created/updated are the seal's stamps, root_commit is the create-time repo identity"
)));
}
Ok(if v.is_empty() {
FieldEdit::RemoveExtra(k.to_string())
} else {
FieldEdit::SetExtra(k.to_string(), toml::Value::String(v.to_string()))
})
}