use super::*;
pub(crate) const TAG_PATH_MIGRATED: &str = "path-migrated";
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct RenameMigration {
pub(super) key: String,
pub(super) old_files: Vec<String>,
pub(super) new_files: Vec<String>,
pub(super) followed: Vec<(String, String)>,
}
pub(crate) struct AppliedRename {
pub(super) key: String,
pub(super) followed: Vec<(String, String)>,
pub(super) sandbox_tagged: bool,
}
pub(crate) fn plan_rename_migrations(
renames: &[(String, String)],
gotchas: &[Record],
root: &std::path::Path,
) -> Vec<RenameMigration> {
if renames.is_empty() {
return Vec::new();
}
let mut moves: HashMap<&str, &str> = HashMap::new();
let mut decided: HashSet<&str> = HashSet::new();
for (old, new) in renames {
if old == new || !decided.insert(old.as_str()) {
continue;
}
if root.join(old).exists() || !root.join(new).exists() {
continue;
}
moves.insert(old.as_str(), new.as_str());
}
if moves.is_empty() {
return Vec::new();
}
let mut plans = Vec::new();
for rec in gotchas {
if !matches!(rec.lifecycle, RecordLifecycle::Active) || is_auto_gotcha(&rec.key) {
continue;
}
let Some(g) = rec.payload_as::<GotchaRecord>() else {
continue; };
let mut followed: Vec<(String, String)> = Vec::new();
let rewritten: Vec<String> = g
.affected_files
.iter()
.map(|f| match moves.get(f.as_str()) {
Some(new) => {
followed.push((f.clone(), (*new).to_string()));
(*new).to_string()
}
None => f.clone(),
})
.collect();
if followed.is_empty() {
continue;
}
let new_files = mati_core::store::gotcha_ops::normalize_affected_files_with_root(
&rewritten,
root.to_str(),
);
if new_files == g.affected_files {
continue;
}
plans.push(RenameMigration {
key: rec.key.clone(),
old_files: g.affected_files.clone(),
new_files,
followed,
});
}
plans
}
pub(crate) async fn migrate_renamed_gotchas(
store: &Store,
root: &std::path::Path,
renames: &[(String, String)],
gotchas: &mut [Record],
) -> Vec<AppliedRename> {
let plans = plan_rename_migrations(renames, gotchas, root);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let mut applied = Vec::new();
for plan in plans {
let Some(rec) = gotchas.iter_mut().find(|r| r.key == plan.key) else {
continue;
};
let sandbox_tagged = rec
.tags
.iter()
.any(|t| t == super::sandbox::TAG_DENY_WRITE || t == super::sandbox::TAG_DENY_READ);
if !rec.tags.iter().any(|t| t == TAG_PATH_MIGRATED) {
rec.tags.push(TAG_PATH_MIGRATED.to_string());
}
rec.updated_at = now;
rec.version.logical_clock += 1;
rec.version.wall_clock = now;
if let Err(e) = mati_core::store::gotcha_ops::apply_gotcha_write(
store,
root,
rec,
&plan.old_files,
&plan.new_files,
false,
)
.await
{
tracing::warn!("init: rename migration failed for {}: {e}", plan.key);
continue;
}
if let Ok(Some(fresh)) = store.get(&plan.key).await {
*rec = fresh;
}
for (old, _) in &plan.followed {
if root.join(old).exists() {
continue;
}
let file_key = format!("file:{old}");
let still_linked = match store.get(&file_key).await {
Ok(Some(r)) => r
.payload_as::<FileRecord>()
.map(|f| !f.gotcha_keys.is_empty())
.unwrap_or(true),
_ => continue,
};
if !still_linked {
if let Err(e) = store.delete(&file_key).await {
tracing::warn!("init: retiring orphaned {file_key} failed: {e}");
}
}
}
applied.push(AppliedRename {
key: plan.key,
followed: plan.followed,
sandbox_tagged,
});
}
applied
}