use std::fmt::Debug;
use std::path::Path;
use std::{fs, mem};
use tempfile::NamedTempFile;
use toml_edit::{DocumentMut, Item};
use crate::cli::MigrateOptions;
use crate::config;
mod yaml;
pub fn migrate(options: MigrateOptions) {
let config_path = options
.config_file
.clone()
.or_else(|| config::installed_config("toml"))
.or_else(|| config::installed_config("yml"));
let config_path = match config_path {
Some(config_path) => config_path,
None => {
eprintln!("No configuration file found");
std::process::exit(1);
},
};
if !options.dry_run {
#[allow(clippy::redundant_clone)]
let mut options = options.clone();
options.silent = true;
options.dry_run = true;
if let Err(err) = migrate_config(&options, &config_path, config::IMPORT_RECURSION_LIMIT) {
eprintln!("Configuration file migration failed:");
eprintln!(" {config_path:?}: {err}");
std::process::exit(1);
}
}
match migrate_config(&options, &config_path, config::IMPORT_RECURSION_LIMIT) {
Ok(migration) => {
if !options.silent {
println!("{}", migration.success_message(false));
}
},
Err(err) => {
eprintln!("Configuration file migration failed:");
eprintln!(" {config_path:?}: {err}");
std::process::exit(1);
},
}
}
fn migrate_config<'a>(
options: &MigrateOptions,
path: &'a Path,
recursion_limit: usize,
) -> Result<Migration<'a>, String> {
let path_str = path.to_string_lossy();
let (prefix, suffix) = match path_str.rsplit_once('.') {
Some((prefix, suffix)) => (prefix, suffix),
None => return Err("missing file extension".to_string()),
};
if suffix == "yml" {
let new_path = yaml::migrate(options, path, recursion_limit, prefix)?;
return Ok(Migration::Yaml((path, new_path)));
}
if options.skip_renames {
if options.dry_run {
eprintln!("Ignoring TOML file {path:?} since `--skip-renames` was supplied");
}
return Ok(Migration::Toml(path));
}
let toml = fs::read_to_string(path).map_err(|err| format!("{err}"))?;
let mut migrated = migrate_toml(toml)?;
migrate_imports(options, path, &mut migrated, recursion_limit)?;
write_results(options, path, &migrated.to_string())?;
Ok(Migration::Toml(path))
}
fn migrate_toml(toml: String) -> Result<DocumentMut, String> {
let mut document = match toml.parse::<DocumentMut>() {
Ok(document) => document,
Err(err) => return Err(format!("TOML parsing error: {err}")),
};
move_value(&mut document, &["draw_bold_text_with_bright_colors"], &[
"colors",
"draw_bold_text_with_bright_colors",
])?;
move_value(&mut document, &["key_bindings"], &["keyboard", "bindings"])?;
move_value(&mut document, &["mouse_bindings"], &["mouse", "bindings"])?;
move_value(&mut document, &["live_config_reload"], &["general", "live_config_reload"])?;
move_value(&mut document, &["working_directory"], &["general", "working_directory"])?;
move_value(&mut document, &["ipc_socket"], &["general", "ipc_socket"])?;
move_value(&mut document, &["import"], &["general", "import"])?;
move_value(&mut document, &["shell"], &["terminal", "shell"])?;
Ok(document)
}
fn migrate_imports(
options: &MigrateOptions,
path: &Path,
document: &mut DocumentMut,
recursion_limit: usize,
) -> Result<(), String> {
let imports = match document
.get("general")
.and_then(|general| general.get("import"))
.and_then(|import| import.as_array())
{
Some(array) if !array.is_empty() => array,
_ => return Ok(()),
};
if recursion_limit == 0 {
return Err("Exceeded maximum configuration import depth".into());
}
for import in imports.into_iter().filter_map(|item| item.as_str()) {
let normalized_path = config::normalize_import(path, import);
if !normalized_path.exists() {
if options.dry_run {
println!("Skipping migration for nonexistent path: {}", normalized_path.display());
}
continue;
}
let migration = migrate_config(options, &normalized_path, recursion_limit - 1)?;
if options.dry_run {
println!("{}", migration.success_message(true));
}
}
Ok(())
}
fn move_value(document: &mut DocumentMut, origin: &[&str], target: &[&str]) -> Result<(), String> {
let (mut origin_key, mut origin_item) = (None, document.as_item_mut());
for element in origin {
let table = match origin_item.as_table_like_mut() {
Some(table) => table,
None => panic!("Moving from unsupported TOML structure"),
};
let (key, item) = match table.get_key_value_mut(element) {
Some((key, item)) => (key, item),
None => return Ok(()),
};
origin_key = Some(key);
origin_item = item;
if let Some(table) = origin_item.as_table_mut() {
table.set_implicit(true)
}
}
let origin_key_decor =
origin_key.map(|key| (key.leaf_decor().clone(), key.dotted_decor().clone()));
let origin_item = mem::replace(origin_item, Item::None);
let mut target_item = document.as_item_mut();
for (i, element) in target.iter().enumerate() {
let table = match target_item.as_table_like_mut() {
Some(table) => table,
None => panic!("Moving into unsupported TOML structure"),
};
if i + 1 == target.len() {
table.insert(element, origin_item);
if let Some((leaf, dotted)) = origin_key_decor {
let mut key = table.key_mut(element).unwrap();
*key.leaf_decor_mut() = leaf;
*key.dotted_decor_mut() = dotted;
}
break;
} else {
target_item = target_item[element].or_insert(toml_edit::table());
}
}
Ok(())
}
fn write_results<P>(options: &MigrateOptions, path: P, toml: &str) -> Result<(), String>
where
P: AsRef<Path> + Debug,
{
let path = path.as_ref();
if options.dry_run && !options.silent {
println!(
"\nv-----Start TOML for {path:?}-----v\n\n{toml}\n^-----End TOML for {path:?}-----^\n"
);
} else if !options.dry_run {
let tmp = NamedTempFile::new_in(path.parent().unwrap())
.map_err(|err| format!("could not create temporary file: {err}"))?;
fs::write(tmp.path(), toml).map_err(|err| format!("filesystem error: {err}"))?;
tmp.persist(path).map_err(|err| format!("atomic replacement failed: {err}"))?;
}
Ok(())
}
enum Migration<'a> {
Toml(&'a Path),
Yaml((&'a Path, String)),
}
impl Migration<'_> {
fn success_message(&self, import: bool) -> String {
match self {
Self::Yaml((original_path, new_path)) if import => {
format!("Successfully migrated import {original_path:?} to {new_path:?}")
},
Self::Yaml((original_path, new_path)) => {
format!("Successfully migrated {original_path:?} to {new_path:?}")
},
Self::Toml(original_path) if import => {
format!("Successfully migrated import {original_path:?}")
},
Self::Toml(original_path) => format!("Successfully migrated {original_path:?}"),
}
}
fn new_path(&self) -> String {
match self {
Self::Toml(path) => path.to_string_lossy().into(),
Self::Yaml((_, path)) => path.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn move_values() {
let input = r#"
# This is a root_value.
#
# Use it with care.
root_value = 3
[table]
table_value = 5
[preexisting]
not_moved = 9
"#;
let mut document = input.parse::<DocumentMut>().unwrap();
move_value(&mut document, &["root_value"], &["new_table", "root_value"]).unwrap();
move_value(&mut document, &["table", "table_value"], &[
"preexisting",
"subtable",
"new_name",
])
.unwrap();
let output = document.to_string();
let expected = r#"
[preexisting]
not_moved = 9
[preexisting.subtable]
new_name = 5
[new_table]
# This is a root_value.
#
# Use it with care.
root_value = 3
"#;
assert_eq!(output, expected);
}
#[test]
fn migrate_empty() {
assert!(migrate_toml(String::new()).unwrap().to_string().is_empty());
}
}