chore: split it into files

This commit is contained in:
əlemi 2024-02-14 20:06:49 +01:00
parent d37477d936
commit 80fce1d909
Signed by: alemi
GPG key ID: A4895B84D311642C
5 changed files with 197 additions and 148 deletions

33
src/config.rs Normal file
View file

@ -0,0 +1,33 @@
#[derive(Debug, Default, serde::Deserialize)]
pub struct TciServerConfig {
#[serde(default)]
pub allow_all: bool,
#[serde(default)]
pub run_setup_even_if_not_allowed: bool,
#[serde(default)]
pub branch: Option<String>,
#[serde(default)]
pub setup: Vec<String>,
#[serde(default)]
pub cleanup: Vec<String>,
}
#[derive(Debug, Default, serde::Deserialize)]
pub struct TciScriptConfig {
#[serde(default)]
pub branch: Option<String>,
#[serde(default)]
pub filter: Option<String>,
#[serde(default)]
pub commands: Vec<String>,
// #[serde(default)]
// concurrent: bool,
}

24
src/error.rs Normal file
View file

@ -0,0 +1,24 @@
#[derive(Debug, thiserror::Error)]
pub enum TciError {
#[error("could not understand file system structure, bailing out: {0}")]
FsError(&'static str),
#[error("missing tci script")]
MissingScript,
#[error("unexpected i/o error: {0}")]
IoError(#[from] std::io::Error),
#[error("git error: {0}")]
GitError(#[from] git2::Error),
#[error("subprocess finished with non-zero exit code: {0}")]
SubprocessError(i32),
#[error("subprocess terminated")]
SubprocessTerminated,
#[error("invalid toml configuration: {0}")]
ConfigError(#[from] toml::de::Error),
}

23
src/git.rs Normal file
View file

@ -0,0 +1,23 @@
pub struct RefUpdate {
pub from: String,
pub to: String,
pub branch: String,
}
pub trait RefUpdateVecHelper {
fn contains_branch(&self, branch: &str) -> bool;
}
impl RefUpdateVecHelper for Vec<RefUpdate> {
fn contains_branch(&self, branch: &str) -> bool {
self.iter().any(|rf| rf.branch == branch)
}
}
pub fn shell_out<S : AsRef<std::ffi::OsStr>>(cmd: S) -> Result<(), crate::error::TciError> {
match std::process::Command::new(cmd).status()?.code() {
Some(0) => Ok(()),
Some(x) => Err(crate::error::TciError::SubprocessError(x)),
None => Err(crate::error::TciError::SubprocessTerminated),
}
}

View file

@ -1,42 +1,7 @@
#[derive(Debug, thiserror::Error)]
enum TciError {
#[error("could not understand file system structure, bailing out: {0}")]
FsError(&'static str),
#[error("missing tci script")]
MissingScript,
#[error("unexpected i/o error: {0}")]
IoError(#[from] std::io::Error),
#[error("git error: {0}")]
GitError(#[from] git2::Error),
#[error("subprocess finished with non-zero exit code: {0}")]
SubprocessError(i32),
#[error("subprocess terminated")]
SubprocessTerminated,
}
#[derive(Debug, Default, serde::Deserialize)]
struct TciConfig {
#[serde(default)]
allow_all: bool,
#[serde(default)]
run_setup_even_if_not_allowed: bool,
#[serde(default)]
setup: Vec<String>,
#[serde(default)]
cleanup: Vec<String>,
}
lazy_static::lazy_static!{
static ref HOME : String = std::env::var("HOME").unwrap_or_default();
}
mod config;
mod error;
mod git;
mod tci;
fn main() {
// load tci config
@ -51,118 +16,11 @@ fn main() {
let cfg = toml::from_str(&cfg_raw)
.unwrap_or_else(|e| {
println!("[!] invalid config: {e}");
TciConfig::default()
crate::config::TciServerConfig::default()
});
if let Err(e) = tci(cfg) {
if let Err(e) = crate::tci::tci(cfg) {
println!("[!] error running tci: {e}");
}
}
fn shell_out<S : AsRef<std::ffi::OsStr>>(cmd: S) -> Result<(), TciError> {
match std::process::Command::new(cmd).status()?.code() {
Some(0) => Ok(()),
Some(x) => Err(TciError::SubprocessError(x)),
None => Err(TciError::SubprocessTerminated),
}
}
fn tci(cfg: TciConfig) -> Result<(), Box<dyn std::error::Error>> {
let repo_path = std::env::current_dir()?;
// check config before even creating temp dir and cloning repo
let git_config = git2::Config::open(&repo_path.join("config"))?;
let tci_script = git_config.get_str("tci.script").unwrap_or(".tci").to_string();
// check if CI is allowed
if !cfg.allow_all
&& !cfg.run_setup_even_if_not_allowed
&& !git_config.get_bool("tci.allow").unwrap_or(false)
{
return Ok(()); // we are in whitelist mode and this repo is not whitelisted
}
// run hooks
for setup in cfg.setup {
println!("[+] setting up ({setup})");
shell_out(setup)?;
}
if !cfg.allow_all && !git_config.get_bool("tci.allow").unwrap_or(false) {
return Ok(()); // ewwww!!! ugly fix to allow running cgit update-agefile on every repo anyway
}
let res = tci_hook(&repo_path, &tci_script);
for cleanup in cfg.cleanup {
println!("[-] cleaning up ({cleanup}) ");
shell_out(cleanup)?;
}
res?; // only check error AFTER running cleanup hooks
println!("[o] script run successfully");
Ok(())
}
fn tci_hook(repo_path: &std::path::PathBuf, tci_script: &str) -> Result<(), TciError> {
// TODO kind of ew but ehh should do its job
let mut name = repo_path.to_string_lossy()
.replace(HOME.as_str(), "");
if name.starts_with('/') {
name.remove(0);
}
if name.ends_with('/') {
name.remove(name.chars().count());
}
let tmp = tempdir::TempDir::new(&format!("tci-{}", name.replace('/', "_")))?;
// TODO allow customizing clone? just clone recursive? just let hook setup submodules?
git2::Repository::clone(
repo_path.to_str()
.ok_or(TciError::FsError("repo path is not a valid string"))?,
tmp.path(),
)?;
let tci_path = tmp.path().join(tci_script);
// if .tci is one script file, just run it
if tci_path.is_file() {
println!("[=] running tci script for repo '{name}'");
std::env::set_current_dir(tmp.path())?;
let res = shell_out(tci_path);
std::env::set_current_dir(repo_path)?;
res
// if .tci is a directory of scripts, run all of them sequentially
} else if tci_path.is_dir() {
let mut scripts : Vec<String> = std::fs::read_dir(&tci_path)?
.filter_map(|x| Some(x.ok()?.file_name().to_string_lossy().to_string()))
.collect();
scripts.sort(); // so that we get reliable execution order
std::env::set_current_dir(tmp.path())?;
for script in scripts {
println!("[=] running tci script '{script}' for repo '{name}'");
let res = shell_out(tci_path.clone().join(&script));
if let Err(e) = res {
eprintln!("[!] error executing script '{script}': {e}");
std::env::set_current_dir(repo_path)?;
return Err(e);
}
}
std::env::set_current_dir(repo_path)?;
Ok(())
// return error in any other case
} else {
return Err(TciError::MissingScript);
}
}

111
src/tci.rs Normal file
View file

@ -0,0 +1,111 @@
use std::io::Read;
use crate::{config::{TciServerConfig, TciScriptConfig}, error::TciError, git::{shell_out, RefUpdate, RefUpdateVecHelper}};
lazy_static::lazy_static!{
static ref HOME : String = std::env::var("HOME").unwrap_or_default();
}
pub fn tci(cfg: TciServerConfig) -> Result<(), Box<dyn std::error::Error>> {
let repo_path = std::path::PathBuf::from(&std::env::var("GIT_DIR")?);
// check which branches are being updated
let mut input = String::new();
std::io::stdin().read_to_string(&mut input)?;
let updates : Vec<RefUpdate> = input.split('\n')
.filter_map(|l| {
let split : Vec<&str> = l.split(' ').collect();
Some(RefUpdate {
from: (*split.get(0)?).to_string(),
to: (*split.get(1)?).to_string(),
branch: (*split.get(2)?).to_string(),
})
})
.collect();
if let Some(branch) = cfg.branch {
if !updates.contains_branch(&branch) {
return Ok(()); // tci is not allowed to run on changes for this branch
}
}
// check config before even creating temp dir and cloning repo
let git_config = git2::Config::open(&repo_path.join("config"))?;
let tci_script = git_config.get_str("tci.script").unwrap_or(".tci").to_string();
// check if CI is allowed
if !cfg.allow_all
&& !cfg.run_setup_even_if_not_allowed
&& !git_config.get_bool("tci.allow").unwrap_or(false)
{
return Ok(()); // we are in whitelist mode and this repo is not whitelisted
}
// run hooks
for setup in cfg.setup {
println!("[+] setting up ({setup})");
shell_out(setup)?;
}
if !cfg.allow_all && !git_config.get_bool("tci.allow").unwrap_or(false) {
return Ok(()); // ewwww!!! ugly fix to allow running cgit update-agefile on every repo anyway
}
let res = tci_hook(&repo_path, &updates, tci_script);
for cleanup in cfg.cleanup {
println!("[-] cleaning up ({cleanup}) ");
shell_out(cleanup)?;
}
res?; // only check error AFTER running cleanup hooks
println!("[o] script run successfully");
Ok(())
}
pub fn tci_hook(repo_path: &std::path::Path, updates: &Vec<RefUpdate>, tci_script: String) -> Result<(), TciError> {
// TODO kind of ew but ehh should do its job
let mut name = repo_path.to_string_lossy()
.replace(HOME.as_str(), "");
if name.starts_with('/') {
name.remove(0);
}
if name.ends_with('/') {
name.remove(name.chars().count());
}
let tmp = tempdir::TempDir::new(&format!("tci-{}", name.replace('/', "_")))?;
// TODO allow customizing clone? just clone recursive? just let hook setup submodules?
git2::Repository::clone(
repo_path.to_str()
.ok_or(TciError::FsError("repo path is not a valid string"))?,
tmp.path(),
)?;
let tci_path = tmp.path().join(&tci_script);
let tci_config_path = tmp.path().join(tci_script + ".toml");
if tci_config_path.is_file() { // if .tci.toml is a config file, parse it for advanced CI options
let tci_config_raw = std::fs::read_to_string(tci_config_path)?;
let tci_config : TciScriptConfig = toml::from_str(&tci_config_raw)?;
if let Some(branch) = tci_config.branch {
if !updates.contains_branch(&branch) {
return Ok(()); // tci script should not be invoked for this branch
}
}
println!("[=] running tci pipeline for repo '{name}'");
Ok(())
} else if tci_path.is_file() { // if .tci is just one script, simply run it
println!("[=] running tci script for repo '{name}'");
std::env::set_current_dir(tmp.path())?;
let res = shell_out(tci_path);
std::env::set_current_dir(repo_path)?;
res
} else {
return Err(TciError::MissingScript);
}
}