From 2baa90cc6770df66e135f823d94de826b1f0ec61 Mon Sep 17 00:00:00 2001 From: Ryan Daigle Date: Sun, 15 Feb 2026 11:47:46 -0500 Subject: [PATCH] Clean up stale migration files when `up --baseline` has no pending work When running `migrate up --baseline` and all migrations are already applied, stale migration files (e.g. reintroduced via git merge) at or below the existing baseline version are now deleted. Previously they were silently ignored because the early return skipped baseline cleanup logic. Bump version to 0.5.1 for patch release. Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/commands/up.rs | 45 ++++++++++++++++++++++++ tests/integration.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae13f32..b34cb96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -285,7 +285,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "migrate" -version = "0.5.0" +version = "0.5.1" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 4d25a03..cad5325 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "migrate" -version = "0.5.0" +version = "0.5.1" edition = "2021" description = "Generic file migration tool for applying ordered transformations to a project directory" license = "MIT" diff --git a/src/commands/up.rs b/src/commands/up.rs index 465fade..dde797b 100644 --- a/src/commands/up.rs +++ b/src/commands/up.rs @@ -42,6 +42,51 @@ pub fn run( if pending.is_empty() { println!("No pending migrations."); + + // Even with no pending migrations, --baseline should clean up stale + // migration files that are at or below the existing baseline version. + 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(()); } diff --git a/tests/integration.rs b/tests/integration.rs index 6a795b3..81a4733 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -396,3 +396,85 @@ fn test_status_shows_applied_and_pending() { assert!(stdout.contains("Pending (1)")); assert!(stdout.contains("00002-second")); } + +#[test] +fn test_up_baseline_cleans_stale_migrations() { + let temp_dir = create_temp_dir(); + let migrations_dir = temp_dir.path().join("migrations"); + fs::create_dir(&migrations_dir).unwrap(); + + // Create a migration and apply it with --baseline + let migration = migrations_dir.join("00001-init.sh"); + fs::write( + &migration, + "#!/usr/bin/env bash\nset -euo pipefail\necho init\n", + ) + .unwrap(); + let mut perms = fs::metadata(&migration).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&migration, perms).unwrap(); + + let output = Command::new(get_binary_path()) + .args([ + "--root", + temp_dir.path().to_str().unwrap(), + "up", + "--baseline", + ]) + .output() + .expect("Failed to execute command"); + assert!(output.status.success()); + + // Migration file should be deleted by baseline + assert!( + !migrations_dir.join("00001-init.sh").exists(), + "Migration file should have been deleted by baseline" + ); + + // Now simulate the old migration file reappearing (e.g., git merge) + let reappeared = migrations_dir.join("00001-init.sh"); + fs::write( + &reappeared, + "#!/usr/bin/env bash\nset -euo pipefail\necho init\n", + ) + .unwrap(); + let mut perms = fs::metadata(&reappeared).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&reappeared, perms).unwrap(); + + assert!( + migrations_dir.join("00001-init.sh").exists(), + "Migration file should exist again after simulated reappearance" + ); + + // Run up --baseline again. No migrations should be applied, but the stale + // file should be cleaned up. + let output = Command::new(get_binary_path()) + .args([ + "--root", + temp_dir.path().to_str().unwrap(), + "up", + "--baseline", + ]) + .output() + .expect("Failed to execute command"); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(output.status.success(), "Should succeed: {}", stdout); + assert!( + stdout.contains("No pending migrations"), + "Should report no pending migrations: {}", + stdout + ); + assert!( + stdout.contains("stale migration"), + "Should report cleaning stale migrations: {}", + stdout + ); + + // The reappeared migration file should be deleted + assert!( + !migrations_dir.join("00001-init.sh").exists(), + "Stale migration file should have been cleaned up" + ); +}