use std::path::{Path, PathBuf};
use clap::{Arg, ArgMatches, Command};
use crate::domain::usecases::migrate::{
self, Detail, DryRunCorpus, MigrationCorpus, MigrationReport, StepSummary,
};
use crate::infra::driven::fs::config::CURRENT_SCHEMA_VERSION;
use crate::infra::driven::fs::migration_corpus::FsMigrationCorpus;
use crate::infra::driving::cli::errors::{die1, CliError};
use crate::infra::driving::cli::Context;
pub(in super::super) fn subcommand() -> Command {
Command::new("migrate")
.about(format!(
"Bring all frontmatters to the current schema version (v{CURRENT_SCHEMA_VERSION})"
))
.arg(
Arg::new("dry-run")
.long("dry-run")
.action(clap::ArgAction::SetTrue)
.help("Print what would change without writing"),
)
}
pub(in super::super) fn execute(sub: &ArgMatches, ctx: &Context<'_>) {
let dry_run = sub.get_flag("dry-run");
let config = ctx.config();
let toml_path = ctx.root_dir.join("cartulary.toml");
if let Ok(text) = std::fs::read_to_string(&toml_path) {
if toml::from_str::<toml::Value>(&text).is_err() {
die1(
CliError::new(format!(
"cartulary.toml at {} is not valid TOML; \
fix it manually before re-running migrate",
toml_path.display()
))
.kind("migrate"),
ctx.output_fmt,
);
}
}
let record_dirs: Vec<PathBuf> = std::iter::once(config.issues_dir.clone())
.chain(config.decision_kinds.iter().map(|k| k.dir.clone()))
.collect();
let dr_dirs: Vec<PathBuf> = config
.decision_kinds
.iter()
.map(|k| k.dir.clone())
.collect();
let report = if dry_run {
let corpus = DryRunCorpus::new(FsMigrationCorpus);
run(&corpus, ctx.root_dir.as_path(), &record_dirs, dr_dirs, true)
} else {
run(
&FsMigrationCorpus,
ctx.root_dir.as_path(),
&record_dirs,
dr_dirs,
false,
)
};
let report = match report {
Ok(r) => r,
Err(e) => die1(
CliError::new(format!("migrate failed: {e:#}")).kind("migrate"),
ctx.output_fmt,
),
};
render_report(&report, dry_run);
}
fn run(
corpus: &dyn MigrationCorpus,
root_dir: &Path,
record_dirs: &[PathBuf],
dr_dirs: Vec<PathBuf>,
dry_run: bool,
) -> anyhow::Result<MigrationReport> {
let tsid_factory = |ms: i64| crate::infra::driven::fs::id_generator::tsid_from_millis(ms);
let ulid_factory = |ms: i64| crate::infra::driven::fs::id_generator::ulid_from_millis(ms);
migrate::run(
corpus,
root_dir,
record_dirs,
dr_dirs,
&tsid_factory,
&ulid_factory,
dry_run,
)
}
fn render_report(report: &MigrationReport, dry_run: bool) {
for step in &report.steps {
render_step(step, dry_run);
}
render_summary(report, dry_run);
}
fn render_step(step: &StepSummary, dry_run: bool) {
let short = step.id.rsplit('/').next().unwrap_or(step.id);
println!(
"==> v{:02} → v{:02} {short} — {}",
step.source_version,
step.source_version + 1,
step.description,
);
for d in &step.details {
println!(" {}", conjugate(d, dry_run));
}
}
fn conjugate(d: &Detail, dry_run: bool) -> String {
if dry_run {
format!("would {} {}", d.infinitive, d.subject)
} else {
format!("{} {}", d.past, d.subject)
}
}
fn render_summary(report: &MigrationReport, dry_run: bool) {
let label_width = report
.steps
.iter()
.map(|s| s.id.len())
.max()
.unwrap_or(0)
.max(14);
println!();
println!("Migration summary");
println!("{:<label_width$} files", "step");
for step in &report.steps {
println!("{:<label_width$} {}", step.id, step.files_changed);
}
let toml_status = if report.toml_already_current {
format!("already at version = {CURRENT_SCHEMA_VERSION}")
} else if dry_run {
format!("would bump to version = {CURRENT_SCHEMA_VERSION}")
} else {
format!("bumped to version = {CURRENT_SCHEMA_VERSION}")
};
println!("{:<label_width$} {toml_status}", "cartulary.toml");
let verb = if dry_run { "would migrate" } else { "migrated" };
println!(
"{verb} {} file(s) of {}",
report.total_changed(),
report.visited
);
}