use std::collections::BTreeMap;
use std::collections::btree_map::Entry;
use std::path::{Component, Path, PathBuf};
use std::str::FromStr;
use crate::BundleRelativePath;
use crate::transaction::{ConflictPolicy, PlannedWrite, TransactionError, WritePlan};
pub(super) fn resolve_renames(
plan: &WritePlan,
root: &Path,
) -> Result<WritePlan, TransactionError> {
let mappings = collect_mappings(plan, root)?;
let mut resolved = WritePlan::default();
for write in plan.writes() {
let path = apply_mapping(write.path.as_path(), &mappings);
let relative = parse_relative(&path)?;
resolved.insert(PlannedWrite::new(relative, write.content.clone()).with_mode(write.mode));
}
Ok(resolved)
}
fn collect_mappings(
plan: &WritePlan,
root: &Path,
) -> Result<BTreeMap<PathBuf, PathBuf>, TransactionError> {
let mut mappings = BTreeMap::new();
for write in plan.writes() {
if root.join(write.path.as_path()).exists() {
let prefix = resource_prefix(write.path.as_path()).ok_or(
TransactionError::UnsupportedConflictPolicy(ConflictPolicy::Rename),
)?;
if let Entry::Vacant(entry) = mappings.entry(prefix.clone()) {
let renamed = available_prefix(root, &prefix)?;
let _inserted = entry.insert(renamed);
}
}
}
Ok(mappings)
}
fn available_prefix(root: &Path, prefix: &Path) -> Result<PathBuf, TransactionError> {
for suffix in 1..=999 {
let renamed = renamed_resource_prefix(prefix, suffix).ok_or(
TransactionError::UnsupportedConflictPolicy(ConflictPolicy::Rename),
)?;
if !root.join(&renamed).exists() {
return Ok(renamed);
}
}
Err(TransactionError::RenameExhausted {
path: prefix.to_path_buf(),
})
}
fn apply_mapping(path: &Path, mappings: &BTreeMap<PathBuf, PathBuf>) -> PathBuf {
mappings
.iter()
.find_map(|(source, target)| {
path.strip_prefix(source)
.ok()
.map(|suffix| target.join(suffix))
})
.unwrap_or_else(|| path.to_path_buf())
}
fn resource_prefix(path: &Path) -> Option<PathBuf> {
let components = path
.components()
.map(Component::as_os_str)
.collect::<Vec<_>>();
let marker = components.iter().position(|part| {
part.to_str()
.is_some_and(|value| matches!(value, "skills" | "commands" | "agents"))
})?;
Some(components.iter().take(marker + 2).collect())
}
fn renamed_resource_prefix(path: &Path, suffix: u16) -> Option<PathBuf> {
let mut components = path
.components()
.map(Component::as_os_str)
.collect::<Vec<_>>();
let name = components.last()?.to_str()?;
let replacement = rename_component(name, suffix);
let last = components.len().checked_sub(1)?;
components[last] = replacement.as_ref();
Some(components.iter().collect())
}
fn rename_component(name: &str, suffix: u16) -> std::ffi::OsString {
let tag = if suffix == 1 {
"-migrated".to_owned()
} else {
format!("-migrated-{suffix}")
};
let path = Path::new(name);
match (path.file_stem(), path.extension()) {
(Some(stem), Some(extension)) => {
let mut value = stem.to_os_string();
value.push(tag);
value.push(".");
value.push(extension);
value
}
_ => format!("{name}{tag}").into(),
}
}
fn parse_relative(path: &Path) -> Result<BundleRelativePath, TransactionError> {
let value = path
.to_str()
.ok_or(TransactionError::UnsupportedConflictPolicy(
ConflictPolicy::Rename,
))?;
BundleRelativePath::from_str(value).map_err(|source| TransactionError::InvalidResolvedPath {
message: source.to_string(),
})
}