use std::io;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::change::{Create, FieldEdit, Occupancy, Retire, Update};
use crate::checkout;
use crate::edge::Edge;
use crate::git::Git;
use crate::hooks::Hooks;
use crate::id::IdScheme;
use crate::lifecycle::{BaseChange, Engine};
use crate::log::{self, Log};
use crate::plugin::Subprocess;
use crate::registry::Registry;
use crate::task::Task;
use crate::taskfile::{read_task, task_ids};
use crate::verb::Verb;
use crate::wire::{Command, OpContext};
#[path = "mutate_build.rs"]
mod build;
#[path = "mutate_edit.rs"]
mod edit;
#[path = "mutate_report.rs"]
mod report;
pub fn run(edge: &Edge, verb: Verb, args: &[String]) -> io::Result<()> {
dispatch(edge, verb, args, &mut edit::Editor::live())
}
fn dispatch(edge: &Edge, verb: Verb, args: &[String], editor: &mut edit::Editor) -> io::Result<()> {
let flags = parse(args, &edge.default_actor)?;
let clone = edge.xdg.clone_dir(&edge.invocation_path);
let (landing, store) = (clone.landing(), clone.store());
primed(&landing)?;
let Some((base, before)) = base_change(verb, &store, &flags, now(), editor)? else {
return Ok(());
};
let ctx = Op {
actor: flags.actor.clone(),
remote: flags.remote.clone(),
command: command(verb, &flags),
};
let sha = seal_op(edge, verb, &ctx, base.as_ref(), before)?;
report::emit(verb, &store, &sha)
}
pub(crate) struct Op {
pub actor: String,
pub remote: Option<String>,
pub command: Command,
}
pub(crate) fn primed(landing: &Path) -> io::Result<()> {
if !landing.join("config").is_dir() {
return Err(other("no balls checkout here — run `bl prime` first"));
}
Ok(())
}
pub(crate) fn seal_op(edge: &Edge, verb: Verb, op: &Op, base: &dyn BaseChange, before: Option<Task>) -> io::Result<String> {
let clone = edge.xdg.clone_dir(&edge.invocation_path);
let (landing, store) = (clone.landing(), clone.store());
primed(&landing)?;
let (binding, level) = checkout::bind(edge, &landing, &store, op.remote.clone(), None)?;
let log = Log::new(clone.op_log(), level, verb, log::wall);
let ctx = OpContext {
actor: op.actor.clone(),
binding,
command: Some(op.command.clone()),
before,
};
let hooks = Hooks::effective(&landing, &edge.xdg.user_config())?;
let reg = Registry::at(&landing);
let pre = hooks.resolve(®, verb.token(), "pre");
let post = hooks.resolve(®, verb.token(), "post");
let change_dir = clone.change(&change_token());
let plugins = Subprocess::new(ctx, &log, edge.depth);
let anvil = Git::at(&store);
Engine::new(&anvil, &plugins, &log)
.seal(base, verb, &change_dir, &pre, &post)
.map_err(|e| other(e.to_string()))
}
type Authored = (Box<dyn BaseChange>, Option<Task>);
fn base_change(
verb: Verb,
store: &Path,
flags: &Flags,
now: i64,
editor: &mut edit::Editor,
) -> io::Result<Option<Authored>> {
let actor = flags.actor.clone();
match verb {
Verb::Create => {
build::forbid_removals_on_create(flags)?;
let title = one_positional(flags, "create")?;
let parent = build::effective_parent(flags)?;
let blockers = build::needs_blockers(flags)?;
let blocks = build::blocks_edges(flags, parent.as_deref())?;
build::require_live(
store,
verb,
blockers.iter().map(|b| b.id.as_str()).chain(blocks.iter().map(|(id, _)| id.as_str())),
)?;
let base = Create {
id: IdScheme::default().generate(),
actor,
now,
title,
parent: parent.clone(),
priority: flags.priority,
tags: flags.tags.clone(),
blockers,
blocks,
body: flags.body.clone(),
message: flags.message.clone(),
existing: task_ids(store)?,
};
Ok(Some((Box::new(base), None)))
}
Verb::Claim | Verb::Unclaim => {
build::forbid_shaping(flags, verb)?;
let id = one_positional(flags, verb.token())?;
let before = read_task(store, &id)?;
let claimant = (verb == Verb::Claim).then(|| actor.clone());
let base = Occupancy { verb, id, claimant, actor, now, message: flags.message.clone() };
Ok(Some((Box::new(base), Some(before))))
}
Verb::Update => {
build::forbid_foreign_blocks(flags, verb)?;
build::forbid_contradictions(flags)?;
let mut positionals = flags.positionals.iter();
let id = positionals.next().ok_or_else(|| other("update: needs a task id"))?.clone();
let before = read_task(store, &id)?;
let edits = if flags.edit {
build::forbid_fields_with_edit(flags)?;
if positionals.next().is_some() {
return Err(other("update: --edit and key=value extras are mutually exclusive — the buffer is the payload"));
}
let Some(after) = editor.edited(&before, &id)? else { return Ok(None) };
vec![FieldEdit::Replace(Box::new(after))]
} else {
build::edits(positionals, flags)?
};
build::require_live(
store,
verb,
edits.iter().filter_map(|e| match e {
FieldEdit::AddBlocker(b) => Some(b.id.as_str()),
_ => None,
}),
)?;
let base = Update { id, actor, now, edits, message: flags.message.clone() };
Ok(Some((Box::new(base), Some(before))))
}
Verb::Close => {
build::forbid_shaping(flags, verb)?;
let id = one_positional(flags, verb.token())?;
let before = read_task(store, &id)?;
let base = Retire { id, title: before.title.clone(), actor, message: flags.message.clone() };
Ok(Some((Box::new(base), Some(before))))
}
_ => Err(other(format!("{}: not a mutating verb", verb.token()))),
}
}
fn command(verb: Verb, flags: &Flags) -> Command {
Command { op: verb.token().to_string(), body_change: flags.body.clone() }
}
fn one_positional(flags: &Flags, verb: &str) -> io::Result<String> {
match flags.positionals.as_slice() {
[only] => Ok(only.clone()),
_ => Err(other(format!("{verb}: expects exactly one positional argument"))),
}
}
#[path = "mutate_args.rs"]
mod args;
use args::{parse, Flags};
fn now() -> i64 {
#[allow(clippy::cast_possible_wrap)]
SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs() as i64)
}
fn change_token() -> String {
IdScheme { prefix: String::new(), length: 32, alphabet: "0123456789abcdef".to_string() }.generate()
}
fn other(msg: impl Into<String>) -> io::Error {
io::Error::other(msg.into())
}
#[cfg(test)]
#[path = "mutate_tests.rs"]
mod tests;