mise 2026.9.14

Dev tools, env vars, and tasks in one CLI
//! `mise dot history`: the checkpoint browser for the tracked
//! configuration files, and the helpers every history command shares.

use eyre::Result;

pub(crate) use crate::system::history::{open, resolve, short};

mod describe;
mod diff;
mod ls;
pub(crate) mod show;

/// Browse the checkpoints of your dotfiles
///
/// Every save, every mutating bootstrap command, and the watcher record a
/// checkpoint of explicitly enrolled `[dotfiles]` entries with `mode = "track"`.
/// Configuration and deployment sources are not implicitly enrolled. A checkpoint holds files,
/// never package or service state: restoring one restores files. Without a
/// subcommand this lists them, newest first.
#[derive(Debug, usage_rs::Args)]
#[usage(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub(crate) struct DotfilesHistory {
    #[usage(subcommand)]
    command: Option<HistoryCommands>,

    #[usage(flatten)]
    ls: ls::HistoryLs,
}

#[derive(Debug, usage_rs::Subcommands)]
enum HistoryCommands {
    Describe(describe::HistoryDescribe),
    Diff(diff::HistoryDiff),
    Ls(ls::HistoryLs),
    Show(show::HistoryShow),
}

impl DotfilesHistory {
    pub(crate) async fn run(self) -> Result<()> {
        match self.command {
            Some(HistoryCommands::Describe(cmd)) => cmd.run().await,
            Some(HistoryCommands::Diff(cmd)) => cmd.run().await,
            Some(HistoryCommands::Ls(cmd)) => cmd.run().await,
            Some(HistoryCommands::Show(cmd)) => cmd.run().await,
            None => self.ls.run().await,
        }
    }
}

/// The display form of a path argument (`~/…` under `$HOME`).
pub(crate) fn display_arg(path: &str) -> String {
    // Use the checkpoint's portable root mapping. Normalizing the target and
    // abbreviating against the unnormalized HOME loses root aliases. The tree
    // conversion also preserves a symlink leaf rather than following it.
    use crate::system::history::tracked::{display_to_tree_path, tree_path_to_display};
    tree_path_to_display(&display_to_tree_path(path))
}

pub(crate) fn local_time(rfc3339: &str) -> String {
    chrono::DateTime::parse_from_rfc3339(rfc3339)
        .map(|time| {
            time.with_timezone(&chrono::Local)
                .format("%Y-%m-%d %H:%M")
                .to_string()
        })
        .unwrap_or_else(|_| rfc3339.to_string())
}

static AFTER_LONG_HELP: &str = color_print::cstr!(
    r#"<bold><underline>Examples:</underline></bold>

    $ <bold>mise dot history</bold>
    $ <bold>mise dot history --path ~/.config/hypr/bindings.lua</bold>
    $ <bold>mise dot history show latest</bold>
    $ <bold>mise dot history diff</bold>          # the working tree against the latest checkpoint
    $ <bold>mise dot history diff 11 12 --patch</bold>
    $ <bold>mise dot save --description "before the theme change"</bold>
    $ <bold>mise dot rollback ~/.config/hypr/bindings.lua</bold>
    $ <bold>mise dot undo</bold>
"#
);