extern crate clap; extern crate serde; extern crate toml; extern crate git2; use std::{path::{PathBuf, Path}, io::{Read, Cursor}, collections::{HashMap, HashSet}, ffi::OsStr}; use clap::{Command, Arg}; use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize,Default)] struct Subcommands{ //a hashmap of subcommands which can be created via cloning a skeleton repository skeletons:HashMap, // a hashmap of subcommands which can be created via running an external executable scripts: HashMap } #[derive(Serialize, Deserialize,Default)] struct Config{ subcommands: Subcommands } #[derive(Debug)] enum Errors{IoErr(std::io::Error), GitErr(git2::Error), CliErr(CliError), Unknown} impl From for Errors { fn from(e: std::io::Error) -> Self { Self::IoErr(e) } } impl From for Errors { fn from(e: git2::Error) -> Self { Self::GitErr(e) } } impl From for Errors{ fn from(e: CliError) -> Self{ Self::CliErr(e) } } fn main() -> Result<(),Errors> { let config:Config = toml::from_str({ let mut buf = String::new(); let _ = std::fs::File::open( {let mut n = dirs::config_dir().expect("no config dir so edit the source code to make it work buddy"); n.push(clap::crate_name!().to_owned() + "/config.toml"); n }).expect("error opening file").read_to_string(&mut buf); buf }.as_str()).unwrap_or_default(); let Config {subcommands:Subcommands { skeletons, scripts }} = config; // Making sure the skeletons and scripts don't have overlap that will cause issue later { let scripts = scripts.keys().map(String::clone).collect::>(); let skeletons = skeletons.keys().map(String::clone).collect::>(); if scripts.intersection(&skeletons).count() > 0 { panic!("Overlap between skeletons and scripts"); } } let mut program = Command::new(clap::crate_name!()) .version(clap::crate_version!()) .author(clap::crate_authors!()) .subcommands(skeletons.keys().map(|v|Command::new(v).arg(Arg::new("output_dir").takes_value(true).action(clap::ArgAction::Append)))) .subcommands(scripts.keys().map(|v|Command::new(v).arg(Arg::new("script_args").takes_value(true)))); let mut short_help = String::new(); let mut long_help = String::new(); { let mut short_help_buf = Vec::new(); let mut long_help_buf = Vec::new(); program.write_help(&mut Cursor::new(&mut short_help_buf))?; program.write_help(&mut Cursor::new(&mut long_help_buf))?; short_help_buf.as_slice().read_to_string(&mut short_help)?; long_help_buf.as_slice().read_to_string(&mut long_help)?; } program.build(); let matches = program.get_matches(); for (skelly_name, (skelly_src, skelly_branch)) in skeletons { if let Some(sub) = matches.subcommand_matches(&skelly_name){ match sub.get_one::("output_dir"){ Some(loc)=>{ //cloning the repo let repo = match git2::Repository::clone(skelly_src.as_ref(), loc){ Ok(repo)=>{ repo } Err(_)=>{ eprintln!("libgit2 failed clone falling back to cli"); cli_fallback(skelly_src,loc)?; git2::Repository::open(loc)? } }; #[cfg(debug_assertions)] { eprintln!("repo cloned"); std::io::stdin().read_line(&mut String::new())?; } eprintln!("checking out appropriate branch"); handle_process(std::process::Command::new("git") .args(["checkout", skelly_branch.as_str()]))?; // let branch = repo.branches(Some(git2::BranchType::Remote))?.find(|branch|{ // if let Ok(branch) = branch{ // if let Ok(Some(name)) = branch.0.name(){ // name == format!("origin/{}",skelly_branch) // }else{false} // }else{false} // }).ok_or(git2::Error::from_str(format!("no branch with name {}",skelly_branch).as_str()))??; // match repo.set_head(branch.0.into_reference().name().ok_or(git2::Error::from_str("the branch with a name somehow has a reference without a name"))?){ // Err(e)=>{ // panic!("{}",e.message()); // } // _=>{} // } //#[cfg(debug_assertions)] //{ // eprintln!("head set"); // std::io::stdin().read_line(&mut String::new())?; //} repo.remote_delete("origin")?; #[cfg(debug_assertions)] { eprintln!("origin deleted"); std::io::stdin().read_line(&mut String::new())?; } let mut walk = repo.revwalk()?; walk.set_sorting(git2::Sort::TIME)?; walk.push_head()?; //let head = walk.next().ok_or(git2::Error::from_str("no head"))?.map(|oid|repo.find_commit(oid))??; let oldest_commit = repo.find_commit(walk.last().ok_or(git2::Error::from_str("No Oldest commit"))??)?; // #[cfg(debug_assertions)] { eprintln!("oldest commit found"); std::io::stdin().read_line(&mut String::new())?; } let pwd = repo.path().parent().expect("very bad cloning into the root dir happening"); eprintln!("rebasing skeleton's commits down into single commit {:?}", pwd); // I give up git2 documentation/api is just too bad for me to do this with it handle_process(std::process::Command::new("git") .current_dir(pwd) .args(["reset", "--mixed" , oldest_commit.id().as_bytes().iter().map(|byte|format!("{:x}",byte)).collect::().chars().take(7).collect::().as_str()]))?; handle_process(std::process::Command::new("git") .current_dir(pwd) .args(["add", "--all"]))?; handle_process(std::process::Command::new("git") .current_dir(pwd) .args(["commit", "--amend", "-am", format!("initialized from {} skeleton", skelly_name).as_str()]))?; std::process::exit(0) } None=>{ eprintln!("failed to provide a path to clone the skeleton directory into"); std::process::exit(1); } } } } for script in scripts { if let Some(sub) = matches.subcommand_matches(script.0){ std::process::exit(std::process::Command::new(script.1).args(sub.get_many::("script_args").map(Iterator::collect).unwrap_or(Vec::new())).spawn()?.wait()?.code().ok_or(Errors::Unknown)?); } } println!("{}", short_help); Ok(()) } #[derive(Debug)] enum CliError{Io(std::io::Error), NonZero} impl From for CliError { fn from(e: std::io::Error) -> Self { Self::Io(e) } } fn cli_fallback, P:AsRef>(source:S, dest: P)->Result<(),CliError>{ let mut child = std::process::Command::new("git"); child .arg("clone") .arg(source) .arg(dest.as_ref()); handle_process(&mut child) } fn handle_process<'proc,P:Into<&'proc mut std::process::Command>>(cmd:P) -> Result<(),CliError>{ if !cmd.into().status()?.success() { Err(CliError::NonZero) } else{ Ok(()) } }