use super::history;
use crate::config::Resolved;
use anyhow::{Context, Result};
use std::{
env,
path::{Path, PathBuf},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MigrationCheckResult {
Current,
Pending(Vec<&'static Migration>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Migration {
pub id: &'static str,
pub description: &'static str,
pub migration_type: MigrationType,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MigrationType {
ConfigKeyRename {
old_key: &'static str,
new_key: &'static str,
},
ConfigKeyRemove {
key: &'static str,
},
ConfigCiGateRewrite,
ConfigLegacyContractUpgrade,
FileRename {
old_path: &'static str,
new_path: &'static str,
},
ReadmeUpdate {
from_version: u32,
to_version: u32,
},
}
#[derive(Debug, Clone)]
pub struct MigrationContext {
pub repo_root: PathBuf,
pub project_config_path: PathBuf,
pub global_config_path: Option<PathBuf>,
pub resolved_config: crate::contracts::Config,
pub migration_history: history::MigrationHistory,
}
impl MigrationContext {
pub fn from_resolved(resolved: &Resolved) -> Result<Self> {
Self::build(
resolved.repo_root.clone(),
resolved
.project_config_path
.clone()
.unwrap_or_else(|| resolved.repo_root.join(".ralph/config.jsonc")),
resolved.global_config_path.clone(),
resolved.config.clone(),
)
}
pub fn discover_from_cwd() -> Result<Self> {
let cwd = env::current_dir().context("resolve current working directory")?;
Self::discover_from_dir(&cwd)
}
pub fn discover_from_dir(start: &Path) -> Result<Self> {
let repo_root = crate::config::find_repo_root(start);
let project_config_path = crate::config::project_config_path(&repo_root);
let global_config_path = crate::config::global_config_path();
Self::build(
repo_root,
project_config_path,
global_config_path,
crate::contracts::Config::default(),
)
}
fn build(
repo_root: PathBuf,
project_config_path: PathBuf,
global_config_path: Option<PathBuf>,
resolved_config: crate::contracts::Config,
) -> Result<Self> {
let migration_history =
history::load_migration_history(&repo_root).context("load migration history")?;
Ok(Self {
repo_root,
project_config_path,
global_config_path,
resolved_config,
migration_history,
})
}
pub fn is_migration_applied(&self, migration_id: &str) -> bool {
self.migration_history
.applied_migrations
.iter()
.any(|migration| migration.id == migration_id)
}
pub fn file_exists(&self, path: &str) -> bool {
self.repo_root.join(path).exists()
}
pub fn resolve_path(&self, path: &str) -> PathBuf {
self.repo_root.join(path)
}
}
#[derive(Debug, Clone)]
pub struct MigrationStatus<'a> {
pub migration: &'a Migration,
pub applied: bool,
pub applicable: bool,
}
impl<'a> MigrationStatus<'a> {
pub fn status_text(&self) -> &'static str {
if self.applied {
"applied"
} else if self.applicable {
"pending"
} else {
"not applicable"
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::migration::history;
use tempfile::TempDir;
fn create_test_context(dir: &TempDir) -> MigrationContext {
let repo_root = dir.path().to_path_buf();
let project_config_path = repo_root.join(".ralph/config.jsonc");
MigrationContext {
repo_root,
project_config_path,
global_config_path: None,
resolved_config: crate::contracts::Config::default(),
migration_history: history::MigrationHistory::default(),
}
}
#[test]
fn migration_context_detects_applied_migration() {
let dir = TempDir::new().unwrap();
let mut ctx = create_test_context(&dir);
assert!(!ctx.is_migration_applied("test_migration"));
ctx.migration_history
.applied_migrations
.push(history::AppliedMigration {
id: "test_migration".to_string(),
applied_at: chrono::Utc::now(),
migration_type: "test".to_string(),
});
assert!(ctx.is_migration_applied("test_migration"));
}
#[test]
fn migration_context_file_exists_check() {
let dir = TempDir::new().unwrap();
let ctx = create_test_context(&dir);
std::fs::create_dir_all(dir.path().join(".ralph")).unwrap();
std::fs::write(dir.path().join(".ralph/queue.json"), "{}").unwrap();
assert!(ctx.file_exists(".ralph/queue.json"));
assert!(!ctx.file_exists(".ralph/done.json"));
}
#[test]
fn migration_context_discovers_repo_without_resolving_config() {
let dir = TempDir::new().unwrap();
let ralph_dir = dir.path().join(".ralph");
std::fs::create_dir_all(&ralph_dir).unwrap();
std::fs::write(
ralph_dir.join("config.jsonc"),
r#"{"version":1,"agent":{"git_commit_push_enabled":true}}"#,
)
.unwrap();
let ctx = MigrationContext::discover_from_dir(dir.path()).unwrap();
assert_eq!(ctx.repo_root, dir.path());
assert_eq!(ctx.project_config_path, ralph_dir.join("config.jsonc"));
}
#[test]
fn migration_status_reports_display_text() {
let migration = Migration {
id: "test",
description: "test migration",
migration_type: MigrationType::ConfigCiGateRewrite,
};
assert_eq!(
MigrationStatus {
migration: &migration,
applied: true,
applicable: true,
}
.status_text(),
"applied"
);
assert_eq!(
MigrationStatus {
migration: &migration,
applied: false,
applicable: true,
}
.status_text(),
"pending"
);
assert_eq!(
MigrationStatus {
migration: &migration,
applied: false,
applicable: false,
}
.status_text(),
"not applicable"
);
}
}