use anyhow::Result;
use chrono::Utc;
use std::path::Path;
use crate::baseline::{delete_baselined_migrations, DeletedItem};
use crate::executor::execute;
use crate::loader::discover_migrations;
use crate::state::{append_baseline, append_history, get_pending, read_history, Baseline};
use crate::ExecutionContext;
pub fn run(
project_root: &Path,
migrations_dir: &Path,
dry_run: bool,
create_baseline: bool,
keep: bool,
) -> Result<()> {
let project_root = if project_root.is_absolute() {
project_root.to_path_buf()
} else {
std::env::current_dir()?.join(project_root)
};
let migrations_path = if migrations_dir.is_absolute() {
migrations_dir.to_path_buf()
} else {
project_root.join(migrations_dir)
};
if !migrations_path.exists() {
println!(
"No migrations directory found at: {}",
migrations_path.display()
);
return Ok(());
}
let available = discover_migrations(&migrations_path)?;
let state = read_history(&migrations_path)?;
let pending = get_pending(&available, &state);
if pending.is_empty() {
println!("No pending migrations.");
if create_baseline && !keep {
if let Some(baseline) = &state.baseline {
let stale: Vec<_> = available
.iter()
.filter(|m| m.version.as_str() <= baseline.version.as_str())
.collect();
if !stale.is_empty() {
if dry_run {
let asset_dir_count = stale
.iter()
.filter(|m| {
m.file_path
.parent()
.map(|p| p.join(&m.id).is_dir())
.unwrap_or(false)
})
.count();
if asset_dir_count > 0 {
println!(
"Would delete {} stale migration file(s) and {} asset directory(ies)",
stale.len(),
asset_dir_count
);
} else {
println!("Would delete {} stale migration file(s)", stale.len());
}
} else {
let deleted = delete_baselined_migrations(&baseline.version, &available)?;
let (files, dirs): (Vec<&DeletedItem>, Vec<&DeletedItem>) =
deleted.iter().partition(|d| !d.is_directory);
if !files.is_empty() {
println!("Deleted {} stale migration file(s)", files.len());
}
if !dirs.is_empty() {
println!("Deleted {} stale asset directory(ies)", dirs.len());
}
}
}
}
}
return Ok(());
}
println!(
"{} {} migration(s)...",
if dry_run { "Would apply" } else { "Applying" },
pending.len()
);
println!();
let mut last_applied_version: Option<String> = None;
for migration in &pending {
println!("→ {}", migration.id);
if dry_run {
println!(" (dry run - skipped)");
last_applied_version = Some(migration.version.clone());
continue;
}
let ctx = ExecutionContext {
project_root: project_root.clone(),
migrations_dir: migrations_path.clone(),
migration_id: migration.id.clone(),
dry_run,
};
let result = execute(migration, &ctx)?;
if result.success {
let applied_at = Utc::now();
append_history(&migrations_path, &migration.id, applied_at)?;
last_applied_version = Some(migration.version.clone());
println!(" ✓ completed");
} else {
println!(" ✗ failed (exit code {})", result.exit_code);
if let Some(error) = result.error {
println!(" {}", error);
}
return Err(anyhow::anyhow!(
"Migration {} failed with exit code {}",
migration.id,
result.exit_code
));
}
}
println!();
println!("All migrations applied successfully.");
if create_baseline {
if let Some(version) = last_applied_version {
println!();
if dry_run {
println!("Would create baseline at version '{}'", version);
if !keep {
let to_delete: Vec<_> = available
.iter()
.filter(|m| m.version.as_str() <= version.as_str())
.collect();
if !to_delete.is_empty() {
let asset_dir_count = to_delete
.iter()
.filter(|m| {
m.file_path
.parent()
.map(|p| p.join(&m.id).is_dir())
.unwrap_or(false)
})
.count();
if asset_dir_count > 0 {
println!(
"Would delete {} migration file(s) and {} asset directory(ies)",
to_delete.len(),
asset_dir_count
);
} else {
println!("Would delete {} migration file(s)", to_delete.len());
}
}
}
} else {
let new_baseline = Baseline {
version: version.clone(),
created: Utc::now(),
summary: None,
};
append_baseline(&migrations_path, &new_baseline)?;
println!("Created baseline at version '{}'", version);
if !keep {
let deleted = delete_baselined_migrations(&version, &available)?;
let (files, dirs): (Vec<&DeletedItem>, Vec<&DeletedItem>) =
deleted.iter().partition(|d| !d.is_directory);
if !files.is_empty() {
println!("Deleted {} migration file(s)", files.len());
}
if !dirs.is_empty() {
println!("Deleted {} asset directory(ies)", dirs.len());
}
}
}
}
}
Ok(())
}