use std::path::{Path, PathBuf};
use std::process::ExitCode;
use anyhow::{bail, Context as _};
use clap::Subcommand;
use ontomark_compile::input::page::VaultKind;
use ontomark_compile::migrate::{self, ApplyOptions, Plan, PlanStatus};
use ontomark_compile::profile::Profile;
#[derive(Debug, Subcommand)]
pub enum MigrateCommand {
List,
Plan {
migration: String,
#[arg(long, default_value = "all", value_parser = VAULT_VALUES)]
vault: String,
#[arg(long)]
out: Option<PathBuf>,
},
Apply {
#[arg(long)]
plan: PathBuf,
#[arg(long)]
accept_semantic: bool,
},
Rollback {
migration: String,
digest: String,
},
Status,
}
pub(crate) const VAULT_VALUES: [&str; 3] = ["knowledge", "working", "all"];
pub(crate) fn vault_kinds(value: &str) -> Vec<VaultKind> {
match value.trim().to_ascii_lowercase().as_str() {
"working" => vec![VaultKind::Working],
"all" => vec![VaultKind::Knowledge, VaultKind::Working],
_ => vec![VaultKind::Knowledge],
}
}
fn kinds(value: &str) -> Vec<VaultKind> {
vault_kinds(value)
}
fn emit(json: bool, value: &serde_json::Value, prose: impl FnOnce()) -> anyhow::Result<()> {
if json {
println!("{}", serde_json::to_string_pretty(value)?);
} else {
prose();
}
Ok(())
}
#[allow(clippy::too_many_lines)]
pub fn run(
cmd: &MigrateCommand,
repo_root: &Path,
profile: &Profile,
json: bool,
) -> anyhow::Result<ExitCode> {
match cmd {
MigrateCommand::List => {
let all = migrate::registry();
let value = serde_json::Value::Array(
all.iter()
.map(|m| {
serde_json::json!({
"id": m.id(), "version": m.version(),
"kind": m.kind().as_str(), "description": m.description(),
})
})
.collect(),
);
emit(json, &value, || {
for m in &all {
println!(
"{}@{} ({}): {}",
m.id(),
m.version(),
m.kind().as_str(),
m.description()
);
}
})?;
Ok(ExitCode::SUCCESS)
}
MigrateCommand::Plan {
migration,
vault,
out,
} => {
let m = migrate::lookup(migration)
.with_context(|| format!("no migration `{migration}` (see `migrate list`)"))?;
for kind in kinds(vault) {
profile.layout.root_of(repo_root, kind)?;
}
let plan = migrate::plan(repo_root, &kinds(vault), m.as_ref(), profile)?;
if let Some(path) = out {
std::fs::write(path, plan.to_json()?)
.with_context(|| format!("writing {}", path.display()))?;
}
emit(json, &serde_json::to_value(&plan)?, || {
print!("{}", plan.report());
})?;
Ok(ExitCode::from(if plan.status == PlanStatus::Ready {
0
} else {
2
}))
}
MigrateCommand::Apply {
plan,
accept_semantic,
} => {
let text = std::fs::read_to_string(plan)
.with_context(|| format!("reading {}", plan.display()))?;
let plan = Plan::from_json(&text)?;
let m = migrate::lookup(&plan.migration.id)
.with_context(|| format!("no migration `{}`", plan.migration.id))?;
let opts = ApplyOptions {
accept_semantic: *accept_semantic,
..ApplyOptions::default()
};
let report = migrate::apply_with(repo_root, &plan, m.as_ref(), &opts)?;
emit(json, &serde_json::to_value(&report)?, || {
println!(
"{}: {} applied, {} already applied, {} conflict(s); journal {}",
report.migration,
report.applied,
report.already_applied,
report.conflicts.len(),
report.journal.display()
);
for c in &report.conflicts {
println!(" conflict {}: {}", c.path, c.reason);
}
})?;
Ok(ExitCode::from(u8::from(!report.complete)))
}
MigrateCommand::Rollback { migration, digest } => {
if migration.contains(['/', '\\'])
|| migration.contains("..")
|| digest.contains(['/', '\\'])
|| digest.contains("..")
{
bail!("migration id and digest must be plain names");
}
let dir = repo_root
.join(".ontomark/migrations")
.join(migration)
.join(digest);
let report = migrate::rollback(repo_root, &dir)?;
emit(json, &serde_json::to_value(&report)?, || {
println!(
"{} restored, {} already original, {} conflict(s)",
report.restored,
report.already_original,
report.conflicts.len()
);
for c in &report.conflicts {
println!(" conflict {}: {}", c.path, c.reason);
}
})?;
Ok(ExitCode::from(u8::from(!report.conflicts.is_empty())))
}
MigrateCommand::Status => {
let all = migrate::status(repo_root)?;
emit(json, &serde_json::to_value(&all)?, || {
if all.is_empty() {
println!("no migration journals");
}
for s in &all {
println!(
"{} {} {} intents {} applied {} conflicts {} restored {}",
s.migration,
s.input_digest,
s.state,
s.intents,
s.applied,
s.conflicts,
s.restored
);
}
})?;
Ok(ExitCode::SUCCESS)
}
}
}