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" + ); +}