use std::collections::HashMap; use either::Either; use serde::Deserialize; use subprocess::Exec; use crate::git::switch_branch; #[derive(Debug, Deserialize)] pub struct BootstrapConfig { pub ask: HashMap, pub actions: ActionsConfig, } #[derive(Debug, Deserialize)] pub enum AskKind { #[serde(rename = "text")] Text, #[serde(rename = "number")] Number, #[serde(rename = "selection")] Selection, #[serde(rename = "bool")] Bool, } pub enum AskValue { Text(String, String), Number(String, i64), Selection(String, String), Bool(String, bool), } impl AskValue { pub fn has_name(&self, name: &str) -> bool { match self { AskValue::Text(n, _) => n.as_str() == name, AskValue::Number(n, _) => n.as_str() == name, AskValue::Selection(n, _) => n.as_str() == name, AskValue::Bool(n, _) => n.as_str() == name, } } pub fn text(&self) -> String { match self { AskValue::Text(_, t) => t.clone(), AskValue::Number(_, n) => n.to_string(), AskValue::Selection(_, s) => s.clone(), AskValue::Bool(_, b) => b.to_string(), } } } #[derive(Debug, Deserialize)] pub struct AskConfig { pub kind: AskKind, pub prompt: String, pub options: Option>, pub order: Option, } #[derive(Debug, Deserialize)] pub struct ActionsConfig { pub replace: Vec, pub branch: Vec, pub script: Vec, } #[derive(Debug, Deserialize)] pub struct VarCompare { pub var: String, pub eq: String, } type WhenExpression = Either, Option>; #[derive(Debug, Deserialize)] pub struct ReplaceAction { pub when: Option, pub from: String, pub to: Option, pub to_var: Option, } #[derive(Debug, Deserialize)] pub struct BranchAction { pub when: Option, pub branch: String, } #[derive(Debug, Deserialize)] pub struct ScriptAction { pub when: Option, pub script: String, pub expose: Option>, } pub fn eval(when: &WhenExpression, vars: &[AskValue]) -> bool { if when.is_left() { let when = when.as_ref().left().unwrap().as_ref().unwrap(); let var = vars.into_iter().find(|x| x.has_name(&when)).unwrap(); if let AskValue::Bool(_, ret) = var { return *ret; } } else { let when = when.as_ref().right().unwrap().as_ref().unwrap(); let var = vars.into_iter().find(|x| x.has_name(&when.var)).unwrap(); match var { AskValue::Text(_, t) => { return *t == when.eq; } AskValue::Number(_, n) => { return n.to_string() == when.eq; } AskValue::Selection(_, s) => { return *s == when.eq; } AskValue::Bool(_, b) => { return b.to_string() == when.eq; } } } panic!("Could not evaluate condition"); } pub fn build_script_vars(expose: Option>, vars: &[AskValue]) -> String { if let Some(expose) = expose { let mut exp = Vec::new(); for ex in expose { let v = vars.iter().find(|x| x.has_name(&ex)).unwrap(); exp.push(v); } return exp .iter() .map(|x| match x { AskValue::Text(name, text) => { format!("export {name}={}", enquote::enquote('\'', text)) } AskValue::Number(name, num) => format!("export {name}={num}"), AskValue::Selection(name, select) => { format!("export {name}={}", enquote::enquote('\'', select)) } AskValue::Bool(name, b) => format!("export {name}='{}'", b.to_string()), }) .collect::>() .join("\n"); } String::new() } pub fn do_script(action: &ScriptAction, vars: &[AskValue], name: &str, verify: bool) { let pre = build_script_vars(action.expose.clone(), &vars); if verify { let script = std::fs::read_to_string(std::path::Path::new(name).join(&action.script)).unwrap(); println!("Running script:\n{pre}\n{script}"); if !inquire::prompt_confirmation("Run this script?").unwrap() { return; } } println!("Running script '{}'", action.script); Exec::cmd("sh") .arg("-c") .arg(format!("{pre}\nbash {}", action.script)) .cwd(std::path::Path::new(name)) .popen() .unwrap() .wait() .unwrap(); } pub fn do_replace(action: &ReplaceAction, name: &str, vars: &[AskValue]) { let to = if let Some(var) = &action.to_var { vars.iter().find(|x| x.has_name(var)).unwrap().text() } else { action.to.clone().unwrap() }; println!("Replacing '{}' -> '{}'", action.from, to); Exec::cmd("fd") .arg(".") .arg("-tf") .arg("-x") .arg("sd") .arg(&action.from) .arg(&to) .cwd(std::path::Path::new(name)) .popen() .unwrap() .wait() .unwrap(); } /// Bootstrap a git repository using a base template pub fn bootstrap(base: &str, name: &str, verify: bool) { Exec::cmd("git") .arg("clone") .arg(base) .arg(name) .popen() .unwrap() .wait() .unwrap(); if std::fs::exists(format!("{name}/bootstrap.toml")).unwrap_or(false) { let config: BootstrapConfig = toml::from_str(&std::fs::read_to_string(&format!("{name}/bootstrap.toml")).unwrap()) .unwrap(); let mut vars = Vec::new(); let mut ask_vec: Vec<(&String, &AskConfig)> = config.ask.iter().collect(); ask_vec.sort_by(|a, b| a.1.order.unwrap_or(0).cmp(&b.1.order.unwrap_or(0))); for (var_name, def) in ask_vec { match def.kind { AskKind::Text => { let a = inquire::prompt_text(&def.prompt).unwrap(); vars.push(AskValue::Text(var_name.clone(), a)); } AskKind::Number => { let a = inquire::prompt_text(&def.prompt).unwrap(); let a: i64 = a.parse().unwrap(); vars.push(AskValue::Number(var_name.clone(), a)); } AskKind::Selection => { let a = inquire::Select::new(&def.prompt, def.options.clone().unwrap()) .prompt() .unwrap(); vars.push(AskValue::Selection(var_name.clone(), a)); } AskKind::Bool => { let a = inquire::prompt_confirmation(&def.prompt).unwrap(); vars.push(AskValue::Bool(var_name.clone(), a)); } } } for action in config.actions.branch { if let Some(when) = &action.when { let when = match when { toml::Value::String(s) => WhenExpression::Left(Some(s.clone())), toml::Value::Table(map) => WhenExpression::Right(Some(VarCompare { var: map.get("var").unwrap().as_str().unwrap().to_string(), eq: map.get("eq").unwrap().as_str().unwrap().to_string(), })), _ => panic!("Invalid value in when condition"), }; if eval(&when, &vars) { println!("Switching to '{}' branch", action.branch); switch_branch(&action.branch, name); } } else { println!("Switching to '{}' branch", action.branch); switch_branch(&action.branch, name); } } for action in config.actions.replace { if let Some(when) = &action.when { let when = match when { toml::Value::String(s) => WhenExpression::Left(Some(s.clone())), toml::Value::Table(map) => WhenExpression::Right(Some(VarCompare { var: map.get("var").unwrap().as_str().unwrap().to_string(), eq: map.get("eq").unwrap().as_str().unwrap().to_string(), })), _ => panic!("Invalid value in when condition"), }; if eval(&when, &vars) { do_replace(&action, name, &vars); } } else { do_replace(&action, name, &vars); } } for action in config.actions.script { if let Some(when) = &action.when { let when = match when { toml::Value::String(s) => WhenExpression::Left(Some(s.clone())), toml::Value::Table(map) => WhenExpression::Right(Some(VarCompare { var: map.get("var").unwrap().as_str().unwrap().to_string(), eq: map.get("eq").unwrap().as_str().unwrap().to_string(), })), _ => panic!("Invalid value in when condition"), }; if eval(&when, &vars) { do_script(&action, &vars, name, verify); } } else { do_script(&action, &vars, name, verify); } } } let _ = std::fs::remove_file("bootstrap.toml"); std::fs::remove_dir_all(std::path::Path::new(name).join(".git")).unwrap(); Exec::cmd("git") .arg("init") .arg("--quiet") .cwd(std::path::Path::new(name)) .popen() .unwrap() .wait() .unwrap(); }