bootstrap
All checks were successful
ci/woodpecker/push/build Pipeline was successful

This commit is contained in:
JMARyA 2025-04-06 01:10:11 +02:00
parent 193036fab7
commit d6d2909de0
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
6 changed files with 371 additions and 29 deletions

94
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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")

265
src/bootstrap.rs Normal file
View file

@ -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<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>>,
}
#[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,
}
#[derive(Debug, Deserialize)]
pub struct ReplaceAction {
pub when: Option<Either<String, VarCompare>>,
pub from: String,
pub to: String,
pub to_var: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct BranchAction {
pub when: Option<Either<String, VarCompare>>,
pub branch: String,
}
#[derive(Debug, Deserialize)]
pub struct ScriptAction {
pub when: Option<Either<String, VarCompare>>,
pub script: String,
pub expose: Option<Vec<String>>,
}
pub fn eval(when: &Either<String, VarCompare>, 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<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!("{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::<Vec<_>>()
.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();
}

View file

@ -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();
}

View file

@ -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;