malvin 0.2.5

Non-interactive research and coding agent
use std::path::{Path, PathBuf};

use super::DotfileBackupState;
use super::alloc::{DotfileBackupLabels, allocate_backup_dir, remove_if_exists};

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(super) struct DotfileSpecRow {
    pub rel: &'static str,
    pub home_subdir: &'static str,
    pub mkdir_lbl: &'static str,
    pub collision_lbl: &'static str,
    pub restore_lbl: &'static str,
    pub copy_err: &'static str,
    pub restore_copy_err: &'static str,
}

impl DotfileSpecRow {
    pub(super) const fn rel_path(self) -> &'static str {
        self.rel
    }
}

const fn labels(spec: &DotfileSpecRow) -> DotfileBackupLabels {
    DotfileBackupLabels {
        mkdir: spec.mkdir_lbl,
        collision: spec.collision_lbl,
        restore: spec.restore_lbl,
    }
}

pub(super) fn dotfile_source_path(slot: usize, work_dir: &Path) -> PathBuf {
    if DOTFILE_ROWS[slot].rel == crate::MALVIN_CHECKS_REL {
        crate::resolve_malvin_checks_path(work_dir)
    } else {
        work_dir.join(DOTFILE_ROWS[slot].rel)
    }
}

const GITIGNORE_FILE: &str = ".gitignore";
pub(super) const MALVIN_CONFIG_WORKSPACE_SLOT: usize = 2;

pub(super) const DOTFILE_ROWS: [DotfileSpecRow; 3] = [
    DotfileSpecRow {
        rel: crate::MALVIN_CHECKS_REL,
        home_subdir: "malvin_checks",
        mkdir_lbl: "malvin_checks backup mkdir",
        collision_lbl: "malvin_checks backup mkdir",
        restore_lbl: "malvin_checks restore",
        copy_err: ".malvin/gates backup copy",
        restore_copy_err: "malvin_checks restore",
    },
    DotfileSpecRow {
        rel: GITIGNORE_FILE,
        home_subdir: "gitignore",
        mkdir_lbl: "gitignore backup mkdir",
        collision_lbl: "gitignore backup mkdir",
        restore_lbl: "gitignore restore",
        copy_err: ".gitignore backup copy",
        restore_copy_err: "gitignore restore",
    },
    DotfileSpecRow {
        rel: crate::MALVIN_CONFIG_REL,
        home_subdir: "malvin_config_workspace",
        mkdir_lbl: "malvin_config_workspace backup mkdir",
        collision_lbl: "malvin_config_workspace backup mkdir",
        restore_lbl: "malvin_config_workspace restore",
        copy_err: "workspace .malvin/config.toml backup copy",
        restore_copy_err: "malvin_config_workspace restore",
    },
];

pub(super) fn backup_slot(
    slot: usize,
    work_dir: &Path,
    generate_id: &mut impl FnMut(usize) -> String,
) -> Result<DotfileBackupState, String> {
    let spec = &DOTFILE_ROWS[slot];
    let _ = spec.rel_path();
    let src = dotfile_source_path(slot, work_dir);
    if !src.is_file() {
        return Ok(DotfileBackupState::Missing);
    }
    let root = crate::workspace_paths::snapshot_category_dir(spec.home_subdir);
    let lbls = labels(spec);
    let dest_dir = allocate_backup_dir(&root, generate_id, &lbls)?;
    let dest_file = dest_dir.join(spec.rel);
    if let Some(parent) = dest_file.parent() {
        std::fs::create_dir_all(parent).map_err(|e| format!("{}: {e}", spec.mkdir_lbl))?;
    }
    let bytes = std::fs::read(&src).map_err(|e| format!("{}: {e}", spec.copy_err))?;
    if let Err(e) = std::fs::write(&dest_file, &bytes) {
        let _ = std::fs::remove_dir_all(&dest_dir);
        return Err(format!("{}: {e}", spec.copy_err));
    }
    Ok(DotfileBackupState::Present(super::DotfileBackupPayload {
        backup_path: dest_file,
        bytes,
    }))
}

pub(super) fn restore_slot(
    work_dir: &Path,
    backup: &DotfileBackupState,
    slot: usize,
) -> Result<(), String> {
    let spec = &DOTFILE_ROWS[slot];
    let _ = spec.rel_path();
    let dst = dotfile_source_path(slot, work_dir);
    let lbls = labels(spec);
    match backup {
        DotfileBackupState::Missing => remove_if_exists(&dst, lbls.restore),
        DotfileBackupState::Present(payload) => {
            if let Some(parent) = dst.parent() {
                std::fs::create_dir_all(parent)
                    .map_err(|e| format!("{}: {e}", spec.restore_lbl))?;
            }
            std::fs::write(&dst, &payload.bytes)
                .map_err(|e| format!("{}: {e}", spec.restore_copy_err))
        }
    }
}

#[cfg(test)]
pub(super) const fn labels_for_test(row: &DotfileSpecRow) -> DotfileBackupLabels {
    labels(row)
}