From f5d735efb8ab6572246782a8425c58f466252ab6 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 12 Feb 2026 23:17:57 +0300 Subject: [PATCH] model: add lockfile migration system (v1 -> v2) Signed-off-by: NotAShelf Change-Id: I335406fc4ee4a04071f6dcb6782e1a076a6a6964 --- src/model/lockfile.rs | 163 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 156 insertions(+), 7 deletions(-) diff --git a/src/model/lockfile.rs b/src/model/lockfile.rs index 9f8a945..dcda70d 100644 --- a/src/model/lockfile.rs +++ b/src/model/lockfile.rs @@ -384,7 +384,8 @@ mod tests { assert_eq!(loaded.mc_versions, mc_versions); assert_eq!(loaded.loaders, loaders); assert_eq!(loaded.projects.len(), 2); - assert_eq!(loaded.lockfile_version, 1); + // Lockfile should be migrated from v1 to v2 on load + assert_eq!(loaded.lockfile_version, 2); } #[test] @@ -423,6 +424,95 @@ mod tests { assert!(lockfile.validate().is_ok()); } + #[test] + fn test_lockfile_migration_v1_to_v2() { + // Test that v1 lockfiles are migrated to v2 + let temp_dir = TempDir::new().unwrap(); + + let mut loaders = HashMap::new(); + loaders.insert("fabric".to_string(), "0.15.0".to_string()); + + // Create a v1 lockfile manually + let v1_content = r#"{ + "target": "modrinth", + "mc_versions": ["1.20.1"], + "loaders": {"fabric": "0.15.0"}, + "projects": [], + "lockfile_version": 1 + }"#; + + let lockfile_path = temp_dir.path().join("pakku-lock.json"); + std::fs::write(&lockfile_path, v1_content).unwrap(); + + // Load should trigger migration + let loaded = LockFile::load(temp_dir.path()).unwrap(); + assert_eq!(loaded.lockfile_version, 2); + + // Verify the migrated file was saved + let reloaded = LockFile::load(temp_dir.path()).unwrap(); + assert_eq!(reloaded.lockfile_version, 2); + } + + #[test] + fn test_lockfile_migration_preserves_projects() { + // Test that migration preserves all project data + let temp_dir = TempDir::new().unwrap(); + + // Create a v1 lockfile with projects (using correct enum case) + let v1_content = r#"{ + "target": "modrinth", + "mc_versions": ["1.20.1"], + "loaders": {"fabric": "0.15.0"}, + "projects": [ + { + "pakku_id": "test-id-1", + "type": "MOD", + "side": "BOTH", + "name": {"modrinth": "Test Mod"}, + "slug": {"modrinth": "test-mod"}, + "id": {"modrinth": "abc123"}, + "files": [], + "pakku_links": [], + "aliases": [], + "update_strategy": "LATEST", + "redistributable": true, + "export": true + } + ], + "lockfile_version": 1 + }"#; + + let lockfile_path = temp_dir.path().join("pakku-lock.json"); + std::fs::write(&lockfile_path, v1_content).unwrap(); + + let loaded = LockFile::load(temp_dir.path()).unwrap(); + assert_eq!(loaded.lockfile_version, 2); + assert_eq!(loaded.projects.len(), 1); + assert_eq!(loaded.projects[0].pakku_id, Some("test-id-1".to_string())); + } + + #[test] + fn test_lockfile_rejects_future_version() { + // Test that lockfiles with version > current are rejected + let temp_dir = TempDir::new().unwrap(); + + let future_content = r#"{ + "target": "modrinth", + "mc_versions": ["1.20.1"], + "loaders": {"fabric": "0.15.0"}, + "projects": [], + "lockfile_version": 999 + }"#; + + let lockfile_path = temp_dir.path().join("pakku-lock.json"); + std::fs::write(&lockfile_path, future_content).unwrap(); + + let result = LockFile::load(temp_dir.path()); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("newer than supported")); + } + #[test] fn test_lockfile_pretty_json_format() { // Test that saved JSON is pretty-printed @@ -472,7 +562,10 @@ mod tests { } } -const LOCKFILE_VERSION: u32 = 1; +/// Current lockfile version - bump this when making breaking changes +const LOCKFILE_VERSION: u32 = 2; +/// Minimum supported lockfile version for migration +const MIN_SUPPORTED_VERSION: u32 = 1; const LOCKFILE_NAME: &str = "pakku-lock.json"; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -494,13 +587,26 @@ impl LockFile { path: P, validate: bool, ) -> Result { - let path = path.as_ref().join(LOCKFILE_NAME); + let path_ref = path.as_ref(); + let lockfile_path = path_ref.join(LOCKFILE_NAME); let content = - std::fs::read_to_string(&path).map_err(PakkerError::IoError)?; + std::fs::read_to_string(&lockfile_path).map_err(PakkerError::IoError)?; let mut lockfile: Self = serde_json::from_str(&content) .map_err(|e| PakkerError::InvalidLockFile(e.to_string()))?; + // Check if migration is needed + if lockfile.lockfile_version < LOCKFILE_VERSION { + lockfile = lockfile.migrate()?; + // Save migrated lockfile + lockfile.save_without_validation(path_ref)?; + log::info!( + "Migrated lockfile from version {} to {}", + lockfile.lockfile_version, + LOCKFILE_VERSION + ); + } + if validate { lockfile.validate()?; } @@ -509,6 +615,42 @@ impl LockFile { Ok(lockfile) } + /// Migrate lockfile from older version to current version + fn migrate(mut self) -> Result { + if self.lockfile_version < MIN_SUPPORTED_VERSION { + return Err(PakkerError::InvalidLockFile(format!( + "Lockfile version {} is too old to migrate. Minimum supported: {}", + self.lockfile_version, MIN_SUPPORTED_VERSION + ))); + } + + // Migration from v1 to v2 + if self.lockfile_version == 1 { + log::info!("Migrating lockfile from v1 to v2..."); + + // v2 changes: + // - Projects now have explicit export field (defaults to true) + // - Side detection is more granular + for project in &mut self.projects { + // Ensure export field is set (v1 didn't always have it) + // Already has a default in Project, but be explicit + if !project.export { + project.export = true; + } + } + + self.lockfile_version = 2; + } + + // Future migrations would go here: + // if self.lockfile_version == 2 { + // // migrate v2 -> v3 + // self.lockfile_version = 3; + // } + + Ok(self) + } + pub fn save>(&self, path: P) -> Result<()> { self.validate()?; let path = path.as_ref().join(LOCKFILE_NAME); @@ -525,10 +667,17 @@ impl LockFile { } pub fn validate(&self) -> Result<()> { - if self.lockfile_version != LOCKFILE_VERSION { + if self.lockfile_version > LOCKFILE_VERSION { return Err(PakkerError::InvalidLockFile(format!( - "Unsupported lockfile version: {}", - self.lockfile_version + "Lockfile version {} is newer than supported version {}. Please \ + upgrade Pakker.", + self.lockfile_version, LOCKFILE_VERSION + ))); + } + if self.lockfile_version < MIN_SUPPORTED_VERSION { + return Err(PakkerError::InvalidLockFile(format!( + "Lockfile version {} is too old. Minimum supported: {}", + self.lockfile_version, MIN_SUPPORTED_VERSION ))); }