use std::io::Read; use crate::{cfg::{TciConfig, TciScriptConfig}, error::{TciErr, TciResult}, git::{RefUpdateVecHelper, TciRepo}}; pub struct Tci { cfg: TciConfig, repo: TciRepo, // stdin: String, } impl Tci { pub fn new() -> TciResult { let cfg = TciConfig::load() .unwrap_or_else(|e| { eprintln!("[!] invalid config: {e}"); TciConfig::default() }); // check which branches are being updated let mut stdin = String::new(); std::io::stdin().read_to_string(&mut stdin) .unwrap_or_else(|e| { eprintln!("[!] could not read refs from stdin: {e}"); 0 }); let repo = TciRepo::new(&stdin)?; Ok(Tci{cfg, repo}) } /// check if tci is allowed to run for this repository pub fn allowed(&self) -> bool { if !self.cfg.allow_all && !self.repo.cfg.get_bool("tci.allow").unwrap_or(false) { return false; // we are in whitelist mode and this repo is not whitelisted } if let Some(ref branch) = self.cfg.branch { if !self.repo.updates.contains_branch(branch) { return false; // tci is not allowed to run on changes for this branch } } true } pub fn run(&self) -> TciResult<()> { // run hooks for hook in &self.cfg.hooks { println!("[*] running hook ({hook})"); Tci::exec(&self.repo.path, hook)?; } if !self.allowed() { return Ok(()) } let env = self.prepare_env()?; let tci_script = self.repo.cfg.get_string("tci.path") .unwrap_or_else(|_| ".tci".into()); let tci_path = env.path().join(tci_script); if !tci_path.is_file() && !tci_path.is_dir() { return Err(TciErr::Missing); } for setup in &self.cfg.setup { println!("[+] setting up ({setup})"); Tci::exec(&self.repo.path, setup)?; } let res = if tci_path.is_file() { println!("[=] running tci script for {}", self.repo.name); Tci::exec(env.path(), tci_path) } else if tci_path.is_dir() { self.run_tci_hook(env.path(), &tci_path) } else { Ok(()) }; // will never happen but rustc doesn't know for cleanup in &self.cfg.cleanup { println!("[-] cleaning up ({cleanup}) "); Tci::exec(&self.repo.path, cleanup)?; } res?; // if there was an error, return it now println!("[o] tci complete"); Ok(()) } fn prepare_env(&self) -> TciResult { let tmp = tempdir::TempDir::new( &format!("tci-{}", self.repo.name.replace('/', "_")) )?; // TODO allow customizing clone? just clone recursive? just let hook setup submodules? git2::Repository::clone( self.repo.path.to_str() .ok_or(TciErr::FsError("repo path is not a valid string"))?, tmp.path(), )?; Ok(tmp) } fn run_tci_hook(&self, cwd: &std::path::Path, dir: &std::path::Path) -> TciResult<()> { let cfg = TciScriptConfig::load(&dir.join("config.toml"))?; if let Some(branch) = cfg.branch { if !self.repo.updates.contains_branch(&branch) { return Ok(()); // tci is not configured to run on this branch } } // TODO calculate which files changed and decide if we should // run depending on cfg.filter (regex match idk?) for script in cfg.scripts { println!("[=] running tci script '{script}' for {}", self.repo.name); Tci::exec(cwd, dir.join(script))?; } Ok(()) } fn exec>(cwd: &std::path::Path, s: S) -> TciResult<()> { match std::process::Command::new(s) .current_dir(cwd) // .stdin(self.stdin.clone()) // TODO not this easy .status()? .code() { Some(0) => Ok(()), Some(x) => Err(crate::error::TciErr::SubprocessError(x)), None => Err(crate::error::TciErr::SubprocessTerminated), } } }