mirror of
https://git.alemi.dev/tci.git
synced 2024-11-23 16:44:48 +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)]
|
mod config;
|
||||||
enum TciError {
|
mod error;
|
||||||
#[error("could not understand file system structure, bailing out: {0}")]
|
mod git;
|
||||||
FsError(&'static str),
|
mod tci;
|
||||||
|
|
||||||
#[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();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// load tci config
|
// load tci config
|
||||||
|
@ -51,118 +16,11 @@ fn main() {
|
||||||
let cfg = toml::from_str(&cfg_raw)
|
let cfg = toml::from_str(&cfg_raw)
|
||||||
.unwrap_or_else(|e| {
|
.unwrap_or_else(|e| {
|
||||||
println!("[!] invalid config: {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}");
|
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