From d6d2909de05f2f0ab16531f9a578f035e1586a53 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Sun, 6 Apr 2025 01:10:11 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20bootstrap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 94 ++++++++++++++++- Cargo.toml | 3 + PKGBUILD | 2 +- src/bootstrap.rs | 265 +++++++++++++++++++++++++++++++++++++++++++++++ src/git.rs | 30 ++---- src/main.rs | 6 +- 6 files changed, 371 insertions(+), 29 deletions(-) create mode 100644 src/bootstrap.rs diff --git a/Cargo.lock b/Cargo.lock index 947272d..117a50c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,6 +146,21 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "fuzzy-matcher" version = "0.3.7" @@ -169,12 +184,31 @@ name = "g" version = "0.1.0" dependencies = [ "clap", + "either", "inquire", + "serde", "serde_json", "subprocess", + "toml", "yansi", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "inquire" version = "0.7.5" @@ -323,18 +357,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -353,6 +387,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "signal-hook" version = "0.3.17" @@ -426,6 +469,40 @@ dependencies = [ "once_cell", ] +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.14" @@ -617,6 +694,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +dependencies = [ + "memchr", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 13be6fc..2c3ca22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,10 @@ edition = "2024" [dependencies] clap = { version = "4.5.26", features = ["cargo"] } +either = { version = "1.15.0", features = ["serde"] } inquire = "0.7.5" +serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.137" subprocess = "0.2.9" +toml = "0.8.20" yansi = "1.0.1" diff --git a/PKGBUILD b/PKGBUILD index 5f02947..e20bead 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -6,7 +6,7 @@ pkgdesc="git workflow wrapper" arch=('x86_64') url="https://git.hydrar.de/jmarya/g" license=("MIT") -depends=("git" "serie" "ripgrep" "onefetch") +depends=("git" "serie" "ripgrep" "onefetch", "fd", "sd") makedepends=("rustup" "git") source=("${pkgname}::git+https://git.hydrar.de/jmarya/g.git") sha256sums=("SKIP") diff --git a/src/bootstrap.rs b/src/bootstrap.rs new file mode 100644 index 0000000..144b751 --- /dev/null +++ b/src/bootstrap.rs @@ -0,0 +1,265 @@ +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>, +} + +#[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, +} + +#[derive(Debug, Deserialize)] +pub struct ReplaceAction { + pub when: Option>, + pub from: String, + pub to: String, + 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: &Either, vars: &[AskValue]) -> bool { + if when.is_left() { + let when = when.as_ref().left().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(); + 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!("{name}='{text}'"), + AskValue::Number(name, num) => format!("{name}={num}"), + AskValue::Selection(name, select) => format!("{name}='{select}'"), + AskValue::Bool(name, b) => format!("{name}='{}'", b.to_string()), + }) + .collect::>() + .join("\n"); + } + + String::new() +} + +pub fn do_script(action: &ScriptAction, vars: &[AskValue], name: &str) { + let pre = build_script_vars(action.expose.clone(), &vars); + println!("Running script '{}'", action.script); + Exec::cmd("sh") + .arg("-c") + .arg(format!("{pre}\n./{}", 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() + }; + 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) { + 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(); + + for (var_name, def) in &config.ask { + 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 { + if eval(&when, &vars) { + println!("Switching to '{}' branch", action.branch); + switch_branch(&action.branch); + } + } else { + println!("Switching to '{}' branch", action.branch); + switch_branch(&action.branch); + } + } + + for action in config.actions.replace { + if let Some(when) = &action.when { + 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 { + if eval(when, &vars) { + do_script(&action, &vars, name); + } + } else { + do_script(&action, &vars, name); + } + } + } + + 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(); +} diff --git a/src/git.rs b/src/git.rs index f0c25a4..25e3cd6 100644 --- a/src/git.rs +++ b/src/git.rs @@ -13,6 +13,15 @@ pub fn has_remote() -> bool { !str.is_empty() } +pub fn switch_branch(branch: &str) { + let mut git = Exec::cmd("git") + .arg("checkout") + .arg(branch) + .popen() + .unwrap(); + git.wait().unwrap(); +} + /// Create a new branch and switch to it in the current repository pub fn create_new_branch(branch: &str) { let mut git = Exec::cmd("git") @@ -269,24 +278,3 @@ pub fn pull() { .wait() .unwrap(); } - -/// Bootstrap a git repository using a base template -pub fn bootstrap(base: &str, name: &str) { - Exec::cmd("git") - .arg("clone") - .arg(base) - .arg(name) - .popen() - .unwrap() - .wait() - .unwrap(); - 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(); -} diff --git a/src/main.rs b/src/main.rs index e2f7fa9..69beaac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,12 @@ -use git::{ - bootstrap, commit, create_new_branch, delete_branch, fetch, git_add, pull, push, revert_commits, -}; +use bootstrap::bootstrap; +use git::{commit, create_new_branch, delete_branch, fetch, git_add, pull, push, revert_commits}; use gitmoji::{CommitMessage, select_gitmoji}; use std::io::Read; use subprocess::{Exec, Redirection}; use yansi::{Color, Paint}; mod args; +mod bootstrap; mod git; mod gitmoji; mod precommit;