use std::io;
use std::path::Path;
use crate::git::Anvil;
use crate::log::{Level, Log};
use crate::op::Phase;
use crate::registry::PluginRef;
use crate::verb::Verb;
pub trait BaseChange {
fn stage(&self, dir: &Path) -> io::Result<()>;
fn finalize(&self, dir: &Path) -> io::Result<String>;
fn narrated(&self) -> bool {
false
}
}
#[derive(Debug, Clone, Copy)]
pub struct Sealed<'a> {
pub commit: &'a str,
pub previous_commit: &'a str,
pub message: Option<&'a str>,
}
pub trait Plugins {
fn run(
&self,
plugin: &PluginRef,
op: Verb,
phase: Phase,
dir: &Path,
sealed: Option<&Sealed>,
) -> io::Result<()>;
fn rollback(&self, plugin: &PluginRef, op: Verb, phase: Phase, dir: &Path, sealed: Option<&Sealed>);
}
#[derive(Debug)]
pub enum OpError {
Author(io::Error),
Anvil(io::Error),
Substrate(io::Error),
Plugin { name: String, source: io::Error },
Invalid(String),
Narration,
}
impl std::fmt::Display for OpError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OpError::Author(e) => write!(f, "authoring the base change failed: {e}"),
OpError::Anvil(e) => write!(f, "sealing onto the anvil failed: {e}"),
OpError::Substrate(e) => write!(f, "materializing the store failed: {e}"),
OpError::Plugin { source, .. } => write!(f, "{source}"),
OpError::Invalid(msg) => f.write_str(msg),
OpError::Narration => write!(
f,
"nothing changed, so nothing sealed — the -m note rides only a commit and would be lost; retry in a second or drop -m"
),
}
}
}
impl std::error::Error for OpError {}
struct SealRecord {
previous_commit: String,
commit: String,
message: Option<String>,
}
impl SealRecord {
fn facts(&self) -> Sealed<'_> {
Sealed {
commit: &self.commit,
previous_commit: &self.previous_commit,
message: self.message.as_deref(),
}
}
}
#[derive(Default)]
struct Trace {
ran: Vec<(PluginRef, Phase)>,
opened: bool,
seal: Option<SealRecord>,
}
pub struct Engine<'a> {
anvil: &'a dyn Anvil,
plugins: &'a dyn Plugins,
log: &'a Log,
}
impl<'a> Engine<'a> {
pub fn new(anvil: &'a dyn Anvil, plugins: &'a dyn Plugins, log: &'a Log) -> Self {
Self { anvil, plugins, log }
}
pub fn seal(
&self,
base: &dyn BaseChange,
op: Verb,
change_dir: &Path,
pre: &[PluginRef],
post: &[PluginRef],
) -> Result<String, OpError> {
self.log.record(Level::Info, "core", None, "begin");
let mut trace = Trace::default();
match self.run_inner(base, op, change_dir, pre, post, &mut trace) {
Ok(sha) => {
let _ = self.anvil.close(change_dir); self.log.record(Level::Info, "core", None, &format!("seal {sha}"));
Ok(sha)
}
Err(e) => {
self.rollback(op, change_dir, &trace);
self.log.record(Level::Error, "core", None, &format!("abort {e}"));
Err(e)
}
}
}
fn run_inner(
&self,
base: &dyn BaseChange,
op: Verb,
change_dir: &Path,
pre: &[PluginRef],
post: &[PluginRef],
trace: &mut Trace,
) -> Result<String, OpError> {
self.anvil.open(change_dir).map_err(OpError::Anvil)?; trace.opened = true;
base.stage(change_dir).map_err(OpError::Author)?; run_phase(self.plugins, op, Phase::Pre, change_dir, pre, None, &mut trace.ran)?; validate::changed_balls(self.anvil, change_dir, &trace.ran)?; let prev = self.anvil.head().map_err(OpError::Anvil)?;
let message = base.finalize(change_dir).map_err(OpError::Author)?;
let sha = self.anvil.seal(change_dir, &message).map_err(OpError::Anvil)?; if base.narrated() && sha == prev {
return Err(OpError::Narration); }
let sealed = trace
.seal
.insert(SealRecord { previous_commit: prev, commit: sha.clone(), message: Some(message) })
.facts();
run_phase(self.plugins, op, Phase::Post, change_dir, post, Some(&sealed), &mut trace.ran)?; Ok(sha)
}
fn rollback(&self, op: Verb, change_dir: &Path, trace: &Trace) {
unwind(self.plugins, op, change_dir, &trace.ran, trace.seal.as_ref());
if let Some(record) = &trace.seal {
let _ = self.anvil.unseal(&record.previous_commit); }
if trace.opened {
let _ = self.anvil.close(change_dir); }
}
}
fn run_phase(
plugins: &dyn Plugins,
op: Verb,
phase: Phase,
dir: &Path,
list: &[PluginRef],
sealed: Option<&Sealed>,
ran: &mut Vec<(PluginRef, Phase)>,
) -> Result<(), OpError> {
for plugin in list {
plugins
.run(plugin, op, phase, dir, sealed)
.map_err(|source| OpError::Plugin { name: plugin.name.clone(), source })?;
ran.push((plugin.clone(), phase));
}
Ok(())
}
fn unwind(plugins: &dyn Plugins, op: Verb, dir: &Path, ran: &[(PluginRef, Phase)], seal: Option<&SealRecord>) {
let sealed = seal.map(SealRecord::facts);
for (plugin, phase) in ran.iter().rev() {
plugins.rollback(plugin, op, *phase, dir, sealed.as_ref());
}
}
#[path = "lifecycle_diffless.rs"]
mod diffless;
#[path = "lifecycle_validate.rs"]
mod validate;
#[cfg(test)]
#[path = "lifecycle_tests.rs"]
mod tests;