312 lines
9.7 KiB
Rust
312 lines
9.7 KiB
Rust
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<String, AskConfig>,
|
|
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<Vec<String>>,
|
|
pub order: Option<u64>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ActionsConfig {
|
|
pub replace: Vec<ReplaceAction>,
|
|
pub branch: Vec<BranchAction>,
|
|
pub script: Vec<ScriptAction>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct VarCompare {
|
|
pub var: String,
|
|
pub eq: String,
|
|
}
|
|
|
|
type WhenExpression = Either<Option<String>, Option<VarCompare>>;
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ReplaceAction {
|
|
pub when: Option<toml::Value>,
|
|
pub from: String,
|
|
pub to: Option<String>,
|
|
pub to_var: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct BranchAction {
|
|
pub when: Option<toml::Value>,
|
|
pub branch: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ScriptAction {
|
|
pub when: Option<toml::Value>,
|
|
pub script: String,
|
|
pub expose: Option<Vec<String>>,
|
|
}
|
|
|
|
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<Vec<String>>, 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::<Vec<_>>()
|
|
.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();
|
|
}
|