pub mod archive;
pub mod backup;
pub mod fs;
pub mod git;
pub mod hook;
pub mod jj;
pub mod local;
pub mod notify;
pub mod vcs;
use crate::config::project::ProjectConfig;
use crate::error::FrostxError;
use std::path::{Path, PathBuf};
pub type ActionFactory = fn(&ProjectConfig, Option<&str>) -> Result<Box<dyn Action>, FrostxError>;
const ALL_REGISTRIES: &[&[(&str, ActionFactory)]] = &[
git::REGISTRY,
jj::REGISTRY,
vcs::REGISTRY,
fs::REGISTRY,
archive::REGISTRY,
backup::REGISTRY,
local::REGISTRY,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActionKind {
Check,
Mutation,
}
#[derive(Debug, Clone)]
pub struct ActionOutcome {
pub status: crate::pipeline::ActionStatus,
pub message: String,
pub new_project_path: Option<PathBuf>,
}
impl ActionOutcome {
pub fn ok(msg: impl Into<String>) -> Self {
Self {
status: crate::pipeline::ActionStatus::Ok,
message: msg.into(),
new_project_path: None,
}
}
pub fn failed(msg: impl Into<String>) -> Self {
Self {
status: crate::pipeline::ActionStatus::Failed,
message: msg.into(),
new_project_path: None,
}
}
pub fn skipped(msg: impl Into<String>) -> Self {
Self {
status: crate::pipeline::ActionStatus::Skipped,
message: msg.into(),
new_project_path: None,
}
}
pub fn dry_run(msg: impl Into<String>) -> Self {
Self {
status: crate::pipeline::ActionStatus::DryRun,
message: msg.into(),
new_project_path: None,
}
}
}
pub struct ActionContext<'a> {
pub project_path: &'a Path,
pub config: &'a ProjectConfig,
pub dry_run: bool,
pub yes: bool,
}
pub trait Action: Send + Sync {
#[allow(dead_code)]
fn name(&self) -> &'static str;
fn kind(&self) -> ActionKind;
fn supports_compressed_archive(&self) -> bool {
false
}
fn run(&self, ctx: &ActionContext<'_>) -> Result<ActionOutcome, FrostxError>;
}
pub fn create(name: &str, config: &ProjectConfig) -> Result<Box<dyn Action>, FrostxError> {
let (base_name, tag) = name
.split_once('#')
.map_or((name, None), |(b, t)| (b, Some(t)));
for registry in ALL_REGISTRIES {
for (action_name, factory) in *registry {
if *action_name == base_name {
return factory(config, tag);
}
}
}
if let Some(notify_name) = base_name.strip_prefix("notify.") {
let notify_cfg = config.config.notifies.get(notify_name).ok_or_else(|| {
FrostxError::Config(format!(
"notify '{notify_name}' not defined in [config.notify.{notify_name}]"
))
})?;
return Ok(Box::new(notify::Notify::new(notify_cfg.clone())));
}
if let Some(hook_name) = base_name.strip_prefix("hook.") {
let hook_cfg = config.config.hooks.get(hook_name).ok_or_else(|| {
FrostxError::Config(format!(
"hook '{hook_name}' not defined in [config.hook.{hook_name}]"
))
})?;
return Ok(Box::new(hook::Hook::new(hook_name, hook_cfg.clone())));
}
Err(FrostxError::UnknownAction(
crate::diagnostics::unknown_action_message(name),
))
}
#[must_use]
pub fn cwd_is_inside(project_path: &Path) -> bool {
let Ok(cwd) = std::env::current_dir() else {
return false;
};
project_path
.canonicalize()
.is_ok_and(|p| cwd.starts_with(&p))
}
#[must_use]
pub fn all_static_actions() -> &'static [&'static str] {
static CACHE: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
CACHE.get_or_init(|| {
let mut names: Vec<&'static str> = ALL_REGISTRIES
.iter()
.flat_map(|r| r.iter().map(|(n, _)| *n))
.collect();
names.sort_unstable();
names
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cwd_is_inside_current_dir() {
let cwd = std::env::current_dir().expect("current_dir must be readable in tests");
assert!(
cwd_is_inside(&cwd),
"cwd should be detected as inside itself"
);
}
#[test]
fn cwd_is_inside_nonexistent_returns_false() {
assert!(!cwd_is_inside(Path::new("/nonexistent/frostx/xyz/abc")));
}
}