feat: parse config file, run setup and cleanup

This commit is contained in:
əlemi 2024-02-13 19:42:03 +01:00
parent 60e7edc765
commit 183e659839
Signed by: alemi
GPG key ID: A4895B84D311642C
2 changed files with 98 additions and 37 deletions

View file

@ -12,7 +12,10 @@ repository = "https://git.alemi.dev/tci.git"
[dependencies]
git2 = "0.18.2"
serde = { version = "1.0.196", features = ["derive"] }
tempdir = "0.3.7"
thiserror = "1.0.57"
toml = { version = "0.8.10", features = ["parse"] }
[dev-dependencies]

View file

@ -1,58 +1,116 @@
use git2::{Config, Repository};
#[derive(Debug, thiserror::Error)]
enum TciError {
#[error("could not understand file system structure, bailing out")]
FsError,
#[error("missing tci script")]
MissingScript,
#[error("there is no argv0 (wtf??), can't figure out which hook invoked tci!")]
MissingArgv0,
}
#[derive(Debug, Default, serde::Deserialize)]
struct TciConfig {
#[serde(default)]
allow_all: bool,
#[serde(default)]
setup: Option<String>,
#[serde(default)]
cleanup: Option<String>,
}
fn main() {
let callback = std::path::Path::new(&std::env::args().next().expect("no argv0??"))
.file_name()
.expect("path terminates with '...'")
.to_str()
.expect("could not represent OsStr")
if let Err(e) = tci() {
println!("[!] error running tci: {e}");
}
}
fn tci() -> Result<(), Box<dyn std::error::Error>> {
// load tci config
let config_path = std::env::args().nth(1)
.as_deref()
.unwrap_or("/etc/tci/tci.toml")
.to_string();
let repo_path = std::env::current_dir()
.expect("not in a directory???");
let cfg_raw = std::fs::read_to_string(config_path)
.unwrap_or_default();
let cfg = toml::from_str(&cfg_raw)
.unwrap_or_else(|e| {
println!("[!] invalid config: {e}");
TciConfig::default()
});
// find out which hook is invoking tci
let callback = std::path::Path::new(&std::env::args().next().ok_or(TciError::MissingArgv0)?)
.file_name().ok_or(TciError::FsError)?
.to_str().ok_or(TciError::FsError)?
.to_string();
let repo_path = std::env::current_dir()?;
let repo_name = repo_path
.parent()
.expect("in root???")
.file_name()
.expect("path terminates with '...'")
.to_str()
.expect("could not represent OsStr")
.parent().ok_or(TciError::FsError)?
.file_name().ok_or(TciError::FsError)?
.to_str().ok_or(TciError::FsError)?
.to_string();
// check config before even creating temp dir and cloning repo
let cfg = Config::open(&repo_path.join("config")).expect("could not read git config");
let Ok(true) = cfg.get_bool("tci.allow") else {
std::process::exit(0);
};
let git_config = Config::open(&repo_path.join("config"))?;
if !cfg.allow_all && !git_config.get_bool("tci.allow").unwrap_or(false) {
return Ok(()); // we are in whitelist mode and this repo is not whitelisted
}
let tmp = tempdir::TempDir::new(&format!("tci-{repo_name}")).expect("could not create tempdir");
let tmp = tempdir::TempDir::new(&format!("tci-{repo_name}"))?;
let repo = Repository::clone(repo_path.to_str().unwrap(), tmp.path()).expect("could not clone repository");
std::env::set_current_dir(tmp.path()).expect("could not change directory");
Repository::clone(repo_path.to_str().ok_or(TciError::FsError)?, tmp.path())?;
std::env::set_current_dir(tmp.path())?;
println!("[+] running tci hook [{callback}] for repo '{repo_name}'");
let tci_script = cfg.get_str("tci.script").unwrap_or(".tci").to_string();
if let Some(setup) = cfg.setup {
print!("[+] setting up ('{setup}')");
let res = shell(setup)?;
if !res.is_empty() {
print!(": {res}");
}
println!();
}
println!("[o] running tci hook [{callback}] for repo '{repo_name}'");
let tci_script = git_config.get_str("tci.script").unwrap_or(".tci").to_string();
if !tmp.path().join(&tci_script).is_file() {
println!("[?] tci enabled for repo, but no .tci script found");
std::process::exit(1);
return Err(Box::new(TciError::MissingScript));
}
let output = match std::process::Command::new(tmp.path().join(&tci_script)).output() {
Ok(out) => out,
Err(e) => {
println!("[!] error running tci script: {e}");
std::process::exit(1);
}
};
let res = shell(tmp.path().join(&tci_script))?;
println!("{res}");
match std::str::from_utf8(&output.stdout) {
Ok(s) => println!("{s}"),
Err(e) => {
println!("[?] script produced non-utf8 output ({e}), showing as bytes");
println!("{:?}", output.stdout);
},
if let Some(cleanup) = cfg.cleanup {
print!("[-] cleaning up ('{cleanup}')");
let res = shell(cleanup)?;
if !res.is_empty() {
print!(": {res}");
}
println!();
}
Ok(())
}
fn shell<S : AsRef<std::ffi::OsStr>>(cmd: S) -> std::io::Result<String> {
let output = std::process::Command::new(cmd).output()?;
match std::str::from_utf8(&output.stdout) {
Ok(s) => Ok(s.to_string()),
Err(e) => {
println!("[?] script produced non-utf8 output ({e}), showing as bytes");
Ok(format!("{:?}", output.stdout))
},
}
}