use std::{
path::{Path, PathBuf},
sync::Arc,
};
use crate::{
error::OxenError,
model::LocalRepository,
repositories,
util::{
self,
progress_bar::{ProgressBarType, oxen_progress_bar},
},
};
pub mod m20250111083535_add_child_counts_to_nodes;
use colored::Colorize;
pub use m20250111083535_add_child_counts_to_nodes::AddChildCountsToNodesMigration;
pub mod m20260408_add_workspace_name_index;
pub use m20260408_add_workspace_name_index::AddWorkspaceNameIndexMigration;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString, IntoStaticStr, VariantNames};
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
EnumString,
VariantNames,
Display,
IntoStaticStr,
Serialize,
Deserialize,
utoipa::ToSchema,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")] pub enum Direction {
Up,
Down,
}
impl Default for Direction {
fn default() -> Self {
Direction::Up
}
}
pub trait Migrate {
fn up(&self, repo: LocalRepository) -> Result<(), OxenError>;
fn down(&self, repo: LocalRepository) -> Result<(), OxenError>;
fn is_needed(&self, repo: &LocalRepository) -> Result<bool, OxenError>;
fn is_applicable(
&self,
direction: Direction,
repo: &LocalRepository,
) -> Result<bool, OxenError> {
match direction {
Direction::Up => self.is_needed(repo),
Direction::Down => Ok(true),
}
}
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
}
pub const ALL_MIGRATIONS: [&dyn Migrate; 2] = [
&AddChildCountsToNodesMigration,
&AddWorkspaceNameIndexMigration,
];
pub fn all_migrations(migration_name: &str) -> Option<&dyn Migrate> {
for migration in ALL_MIGRATIONS.iter() {
if migration_name == migration.name() {
return Some(*migration);
}
}
None
}
pub struct MigrationResults {
pub executed: Vec<(PathBuf, MigrationResult)>,
pub errored: Vec<(PathBuf, OxenError)>,
}
pub fn run_on_all_repos(
is_server: bool,
migration: &dyn Migrate,
direction: Direction,
run_optional: bool,
path: &Path,
) -> Result<MigrationResults, OxenError> {
let msg = |msg: &str| {
if is_server {
log::info!("{msg}");
} else {
println!("{msg}");
}
};
let path = util::fs::canonicalize(path)?;
msg(&format!(
"🐂 Collecting namespaces to migrate in {}",
path.display()
));
let namespaces = repositories::list_namespaces(&path)?;
msg(&format!("🐂 Migrating {} namespaces", namespaces.len()));
let bar = if is_server {
Arc::new(indicatif::ProgressBar::hidden())
} else {
oxen_progress_bar(namespaces.len() as u64, ProgressBarType::Counter)
};
let mut migration_results = MigrationResults {
executed: vec![],
errored: vec![],
};
for namespace in namespaces {
let namespace_path = path.join(namespace);
log::debug!(
"This is the namespace path we're walking: {}",
namespace_path.display()
);
for repo in repositories::list_repos_in_namespace(&namespace_path) {
log::debug!("Migrating repository: {}", repo.path.display());
let repo_path = repo.path.clone();
match try_apply_migration(migration, direction, run_optional, repo) {
Ok(mr) => {
if !mr.did_run() {
msg(&format!("Did not run migration: {}", mr.as_hint(is_server)));
migration_results.executed.push((repo_path, mr));
}
}
Err(err) => {
let message = format!(
"Could not run migration {direction} {} for repo {}\nError: {}",
migration.name(),
repo_path.display(),
err
);
if is_server {
log::error!("{message}");
} else {
println!("[ERROR] {message}");
}
migration_results.errored.push((repo_path, err));
}
}
}
bar.inc(1);
}
Ok(migration_results)
}
#[derive(Debug, thiserror::Error)]
pub enum MigrationResult {
#[error("Nothing to do: '{migration_name}' ({direction}) is applicable, but not needed.")]
IsApplicableButOptional {
direction: Direction,
migration_name: &'static str,
},
#[error("Nothing to do: '{migration_name}' ({direction}) is not needed nor is it applicable.")]
NotNeededNorApplicable {
direction: Direction,
migration_name: &'static str,
},
#[error("Successfully applied migration {direction} {migration_name}")]
Success {
direction: Direction,
migration_name: &'static str,
},
}
impl MigrationResult {
pub fn did_run(&self) -> bool {
matches!(self, Self::Success { .. })
}
pub fn as_hint(&self, is_server: bool) -> String {
let msg = self.to_string();
match self {
Self::IsApplicableButOptional { .. } => format!(
"{msg} You must run with {} to apply it.",
(if is_server {
"run_optional=true"
} else {
"--run-optional"
})
.yellow()
),
_ => msg,
}
}
}
pub fn try_apply_migration(
migration: &dyn Migrate,
direction: Direction,
run_optional: bool,
repo: LocalRepository,
) -> Result<MigrationResult, OxenError> {
let migration_name = migration.name();
if matches!(direction, Direction::Up) && migration.is_needed(&repo)? {
migration.up(repo)?;
return Ok(MigrationResult::Success {
direction,
migration_name,
});
}
if migration.is_applicable(direction, &repo)? {
match direction {
Direction::Down => {
migration.down(repo)?;
Ok(MigrationResult::Success {
direction,
migration_name,
})
}
Direction::Up if run_optional => {
migration.up(repo)?;
Ok(MigrationResult::Success {
direction,
migration_name,
})
}
_ => Ok(MigrationResult::IsApplicableButOptional {
direction,
migration_name,
}),
}
} else {
Ok(MigrationResult::NotNeededNorApplicable {
direction,
migration_name,
})
}
}