use owo_colors::OwoColorize;
use crate::error::ShipItError;
pub fn build_plan_yaml<T: serde::Serialize>(plan: &T, filename: &str) -> Result<String, ShipItError> {
let mut val = serde_yaml::to_value(plan)
.map_err(|e| ShipItError::Error(format!("Failed to serialize plan: {}", e)))?;
if let serde_yaml::Value::Mapping(ref mut map) = val {
map.insert(
serde_yaml::Value::String("plan_file".into()),
serde_yaml::Value::String(filename.to_string()),
);
}
serde_yaml::to_string(&val)
.map_err(|e| ShipItError::Error(format!("Failed to serialize plan: {}", e)))
}
pub fn start_spinner(msg: &str) -> indicatif::ProgressBar {
let pb = indicatif::ProgressBar::new_spinner();
pb.set_style(
indicatif::ProgressStyle::with_template("{spinner:.cyan} {msg}")
.unwrap()
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
);
pb.set_message(msg.to_string());
pb.enable_steady_tick(std::time::Duration::from_millis(80));
pb
}
pub fn print_content(header: &str, body: &str) {
let rule = "─".repeat(60);
eprintln!("\n{}\n{}\n\n{}\n\n{}", header.bold().cyan(), rule.dimmed(), body.trim_end(), rule.dimmed());
}
pub fn print_url(url: &str) {
println!("\n\nAvailable at:\n\n {}", url.cyan().underline());
}
pub fn print_plan_saved(path: &std::path::Path) {
eprintln!("{} Plan saved to: {}", "✓".bright_green().bold(), path.display().bold());
}
pub fn print_success(msg: &str) {
println!("{} {}", "✓".bright_green().bold(), msg);
}
pub fn print_skipped(msg: &str) {
println!("{}", msg.dimmed());
}
pub fn print_token_prompt(label: &str) {
use std::io::Write;
eprint!(" {} ", label.bold());
std::io::stderr().flush().ok();
}
pub fn prompt_mr_title(suggested: &str) -> std::io::Result<String> {
use std::io::Write;
eprint!("\n\n{} {} ", "Merge request title".bold().cyan(), format!("[{}]:", suggested).dimmed());
std::io::stderr().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_string();
Ok(if trimmed.is_empty() {
suggested.to_string()
} else {
trimmed
})
}
pub fn prompt_push(branch: &str) -> std::io::Result<bool> {
use std::io::Write;
eprint!("\nLocal branch '{}' is ahead of remote. Push now? [y/N]: ", branch);
std::io::stderr().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
Ok(input.trim().eq_ignore_ascii_case("y"))
}
pub fn prompt_pull(branch: &str) -> std::io::Result<bool> {
use std::io::Write;
eprint!("\nRemote branch '{}' is ahead of local. Pull now? [y/N]: ", branch);
std::io::stderr().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
Ok(input.trim().eq_ignore_ascii_case("y"))
}
pub fn prompt_tag_name(suggested: Option<&str>) -> std::io::Result<String> {
use std::io::Write;
match suggested {
Some(s) => eprint!("\n\nTag name [{}]: ", s),
None => eprint!("\n\nTag name: "),
}
std::io::stderr().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_string();
Ok(if trimmed.is_empty() {
suggested.unwrap_or("").to_string()
} else {
trimmed
})
}
#[cfg(test)]
mod tests {
use super::build_plan_yaml;
use serde::Serialize;
#[derive(Serialize)]
struct DummyPlan {
title: String,
commits: Vec<String>,
}
fn dummy_plan() -> DummyPlan {
DummyPlan {
title: "Release v1.0.0".to_string(),
commits: vec![
"feat: add payments abc123".to_string(),
"fix: handle timeout def456".to_string(),
],
}
}
#[test]
fn test_plan_file_field_is_injected() {
let yaml = build_plan_yaml(&dummy_plan(), "abc123.yml").unwrap();
assert!(yaml.contains("plan_file: abc123.yml"));
}
#[test]
fn test_plan_fields_are_present() {
let yaml = build_plan_yaml(&dummy_plan(), "abc123.yml").unwrap();
assert!(yaml.contains("title: Release v1.0.0"));
assert!(yaml.contains("feat: add payments abc123"));
assert!(yaml.contains("fix: handle timeout def456"));
}
#[test]
fn test_output_is_valid_yaml() {
let yaml = build_plan_yaml(&dummy_plan(), "abc123.yml").unwrap();
let parsed: serde_yaml::Value = serde_yaml::from_str(&yaml)
.expect("output should be valid YAML");
assert_eq!(parsed["plan_file"].as_str().unwrap(), "abc123.yml");
assert_eq!(parsed["title"].as_str().unwrap(), "Release v1.0.0");
}
#[test]
fn test_commits_accessible_by_index() {
let yaml = build_plan_yaml(&dummy_plan(), "abc123.yml").unwrap();
let parsed: serde_yaml::Value = serde_yaml::from_str(&yaml).unwrap();
let commits = parsed["commits"].as_sequence().unwrap();
let last_sha = commits[0].as_str().unwrap().split_whitespace().last().unwrap();
let first_sha = commits[commits.len() - 1].as_str().unwrap().split_whitespace().last().unwrap();
assert_eq!(last_sha, "abc123");
assert_eq!(first_sha, "def456");
}
#[test]
fn test_plan_file_not_duplicated_in_other_fields() {
let yaml = build_plan_yaml(&dummy_plan(), "abc123.yml").unwrap();
assert_eq!(yaml.matches("plan_file").count(), 1);
}
}