mirror of
https://git.alemi.dev/tci.git
synced 2024-11-12 19:59:19 +01:00
chore: split it into files
This commit is contained in:
parent
d37477d936
commit
80fce1d909
5 changed files with 197 additions and 148 deletions
33
src/config.rs
Normal file
33
src/config.rs
Normal 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
24
src/error.rs
Normal 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
23
src/git.rs
Normal 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),
|
||||
}
|
||||
}
|
154
src/main.rs
154
src/main.rs
|
@ -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
111
src/tci.rs
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue