use std::path::PathBuf;
use crate::FileOperationKind;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OutputEvent {
IgnoredInitScript {
path: PathBuf,
},
WouldRunInitScript {
path: PathBuf,
root_path: PathBuf,
},
RunInitScript {
path: PathBuf,
},
NoConfigDetected,
RootWorktreeDetected,
ConfigDetected {
path: PathBuf,
},
FileApplied {
operation: FileOperationKind,
source: PathBuf,
target: PathBuf,
},
FileWouldApply {
operation: FileOperationKind,
source: PathBuf,
target: PathBuf,
},
FileSkipped {
operation: FileOperationKind,
target: PathBuf,
reason: String,
},
FileWouldSkip {
operation: FileOperationKind,
target: PathBuf,
reason: String,
},
FileDeleted {
path: PathBuf,
},
FileWouldDelete {
path: PathBuf,
},
FileWarning {
path: PathBuf,
reason: String,
},
CommandStarted {
label: String,
},
CommandWouldRun {
label: String,
},
CommandAllowedFailure {
label: String,
reason: String,
},
InitCreated {
path: PathBuf,
},
}
impl OutputEvent {
#[must_use]
pub fn message(&self) -> String {
match self {
Self::IgnoredInitScript { path } => {
format!("treeboot: ignore {}; not executable", path.display())
}
Self::WouldRunInitScript { path, root_path } => format!(
"treeboot: would run {} {}",
path.display(),
root_path.display()
),
Self::RunInitScript { path } => {
format!("treeboot: run {}", path.display())
}
Self::NoConfigDetected => "treeboot: no config detected".to_owned(),
Self::RootWorktreeDetected => "treeboot: This is not a work tree".to_owned(),
Self::ConfigDetected { path } => {
format!("treeboot: config detected {}", path.display())
}
Self::FileApplied {
operation,
source,
target,
} => format!(
"treeboot: {} {} -> {}",
operation.as_str(),
source.display(),
target.display()
),
Self::FileWouldApply {
operation,
source,
target,
} => format!(
"treeboot: would {} {} -> {}",
operation.as_str(),
source.display(),
target.display()
),
Self::FileSkipped {
operation,
target,
reason,
} => format!(
"treeboot: skip {} {}; {}",
operation.as_str(),
target.display(),
reason
),
Self::FileWouldSkip {
operation,
target,
reason,
} => format!(
"treeboot: would skip {} {}; {}",
operation.as_str(),
target.display(),
reason
),
Self::FileDeleted { path } => {
format!("treeboot: delete {}", path.display())
}
Self::FileWouldDelete { path } => {
format!("treeboot: would delete {}", path.display())
}
Self::FileWarning { path, reason } => {
format!("treeboot: warning: {} {}", path.display(), reason)
}
Self::CommandStarted { label } => {
format!("treeboot: run {label}")
}
Self::CommandWouldRun { label } => {
format!("treeboot: would run {label}")
}
Self::CommandAllowedFailure { label, reason } => {
format!("treeboot: warning: command {label} {reason}")
}
Self::InitCreated { path } => {
format!("treeboot: created {}", path.display())
}
}
}
}
pub trait Reporter {
fn report(&mut self, event: OutputEvent) -> std::io::Result<()>;
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
use crate::FileOperationKind;
#[test]
fn message_should_format_ignored_init_script() {
let event = OutputEvent::IgnoredInitScript {
path: PathBuf::from(".treeboot.sh"),
};
assert_eq!(
event.message(),
"treeboot: ignore .treeboot.sh; not executable"
);
}
#[test]
fn message_should_format_dry_run_init_script() {
let event = OutputEvent::WouldRunInitScript {
path: PathBuf::from(".treeboot.sh"),
root_path: PathBuf::from("/repo"),
};
assert_eq!(event.message(), "treeboot: would run .treeboot.sh /repo");
}
#[test]
fn message_should_format_config_detected() {
let event = OutputEvent::ConfigDetected {
path: PathBuf::from(".treeboot.toml"),
};
assert_eq!(event.message(), "treeboot: config detected .treeboot.toml");
}
#[test]
fn message_should_format_file_applied() {
let event = OutputEvent::FileApplied {
operation: FileOperationKind::Copy,
source: PathBuf::from(".env"),
target: PathBuf::from(".env"),
};
assert_eq!(event.message(), "treeboot: copy .env -> .env");
}
#[test]
fn message_should_format_file_would_apply() {
let event = OutputEvent::FileWouldApply {
operation: FileOperationKind::Symlink,
source: PathBuf::from("tool"),
target: PathBuf::from(".tool"),
};
assert_eq!(event.message(), "treeboot: would symlink tool -> .tool");
}
#[test]
fn message_should_format_file_skipped() {
let event = OutputEvent::FileSkipped {
operation: FileOperationKind::Copy,
target: PathBuf::from(".env"),
reason: "target exists".to_owned(),
};
assert_eq!(event.message(), "treeboot: skip copy .env; target exists");
}
#[test]
fn message_should_format_file_would_skip() {
let event = OutputEvent::FileWouldSkip {
operation: FileOperationKind::Sync,
target: PathBuf::from("shared"),
reason: "missing source".to_owned(),
};
assert_eq!(
event.message(),
"treeboot: would skip sync shared; missing source"
);
}
#[test]
fn message_should_format_file_deleted() {
let event = OutputEvent::FileDeleted {
path: PathBuf::from(".config/old.toml"),
};
assert_eq!(event.message(), "treeboot: delete .config/old.toml");
}
#[test]
fn message_should_format_file_would_delete() {
let event = OutputEvent::FileWouldDelete {
path: PathBuf::from(".config/old.toml"),
};
assert_eq!(event.message(), "treeboot: would delete .config/old.toml");
}
#[test]
fn message_should_format_file_warning() {
let event = OutputEvent::FileWarning {
path: PathBuf::from("shared/link"),
reason: "symlink target does not exist".to_owned(),
};
assert_eq!(
event.message(),
"treeboot: warning: shared/link symlink target does not exist"
);
}
#[test]
fn message_should_format_root_worktree_detected() {
let event = OutputEvent::RootWorktreeDetected;
assert_eq!(event.message(), "treeboot: This is not a work tree");
}
#[test]
fn message_should_format_command_started() {
let event = OutputEvent::CommandStarted {
label: "Install packages: npm install".to_owned(),
};
assert_eq!(
event.message(),
"treeboot: run Install packages: npm install"
);
}
#[test]
fn message_should_format_command_allowed_failure() {
let event = OutputEvent::CommandAllowedFailure {
label: "lint".to_owned(),
reason: "failed with exit status: 1".to_owned(),
};
assert_eq!(
event.message(),
"treeboot: warning: command lint failed with exit status: 1"
);
}
}