mirror of
https://git.alemi.dev/tci.git
synced 2024-11-23 08:34:51 +01:00
chore: big refactor
This commit is contained in:
parent
80fce1d909
commit
00c4e848e1
6 changed files with 205 additions and 131 deletions
|
@ -12,7 +12,6 @@ repository = "https://git.alemi.dev/tci.git"
|
|||
|
||||
[dependencies]
|
||||
git2 = "0.18.2"
|
||||
lazy_static = "1.4.0"
|
||||
serde = { version = "1.0.196", features = ["derive"] }
|
||||
tempdir = "0.3.7"
|
||||
thiserror = "1.0.57"
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
|
||||
|
||||
#[derive(Debug, Default, serde::Deserialize)]
|
||||
pub struct TciServerConfig {
|
||||
pub struct TciConfig {
|
||||
#[serde(default)]
|
||||
pub allow_all: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub run_setup_even_if_not_allowed: bool,
|
||||
pub branch: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub branch: Option<String>,
|
||||
pub hooks: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub setup: Vec<String>,
|
||||
|
@ -17,6 +15,20 @@ pub struct TciServerConfig {
|
|||
#[serde(default)]
|
||||
pub cleanup: Vec<String>,
|
||||
}
|
||||
|
||||
impl TciConfig {
|
||||
pub fn load() -> Result<Self, toml::de::Error> {
|
||||
let config_path = std::env::var("TCI_CONFIG")
|
||||
.as_deref()
|
||||
.unwrap_or("/etc/tci/config.toml");
|
||||
|
||||
let cfg_raw = std::fs::read_to_string(config_path)
|
||||
.unwrap_or_default();
|
||||
|
||||
toml::from_str(&cfg_raw)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Deserialize)]
|
||||
pub struct TciScriptConfig {
|
||||
#[serde(default)]
|
||||
|
@ -26,8 +38,17 @@ pub struct TciScriptConfig {
|
|||
pub filter: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub commands: Vec<String>,
|
||||
pub scripts: Vec<String>,
|
||||
|
||||
// #[serde(default)]
|
||||
// concurrent: bool,
|
||||
}
|
||||
|
||||
|
||||
impl TciScriptConfig {
|
||||
pub fn load(path: &std::path::PathBuf) -> Result<Self, toml::de::Error> {
|
||||
let cfg_raw = std::fs::read_to_string(path)
|
||||
.unwrap_or_default();
|
||||
toml::from_str(&cfg_raw)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ pub enum TciError {
|
|||
#[error("could not understand file system structure, bailing out: {0}")]
|
||||
FsError(&'static str),
|
||||
|
||||
#[error("missing tci script")]
|
||||
MissingScript,
|
||||
#[error("missing tci script or directory")]
|
||||
Missing,
|
||||
|
||||
#[error("unexpected i/o error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
@ -22,3 +22,5 @@ pub enum TciError {
|
|||
#[error("invalid toml configuration: {0}")]
|
||||
ConfigError(#[from] toml::de::Error),
|
||||
}
|
||||
|
||||
pub type TciResult<T> = Result<T, TciError>;
|
||||
|
|
50
src/git.rs
50
src/git.rs
|
@ -1,3 +1,45 @@
|
|||
|
||||
pub struct TciRepo {
|
||||
pub name: String,
|
||||
pub path: std::path::PathBuf,
|
||||
pub cfg: git2::Config,
|
||||
pub updates: Vec<RefUpdate>,
|
||||
}
|
||||
|
||||
impl TciRepo {
|
||||
pub fn new(updates_raw: &str, home: &str) -> crate::error::TciResult<Self> {
|
||||
let path = match std::env::var("GIT_DIR") {
|
||||
Ok(p) => std::path::PathBuf::from(&p),
|
||||
Err(e) => std::env::current_dir()?,
|
||||
};
|
||||
|
||||
// TODO kind of ew but ehh should do its job
|
||||
let mut name = path.to_string_lossy()
|
||||
.replace(home, "");
|
||||
if name.starts_with('/') {
|
||||
name.remove(0);
|
||||
}
|
||||
if name.ends_with('/') {
|
||||
name.remove(name.chars().count());
|
||||
}
|
||||
|
||||
let cfg = git2::Config::open(&path.join("config"))?;
|
||||
|
||||
let updates : Vec<RefUpdate> = updates_raw.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();
|
||||
|
||||
Ok(TciRepo { name, path, cfg, updates })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RefUpdate {
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
|
@ -13,11 +55,3 @@ impl RefUpdateVecHelper for Vec<RefUpdate> {
|
|||
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),
|
||||
}
|
||||
}
|
||||
|
|
24
src/main.rs
24
src/main.rs
|
@ -1,26 +1,14 @@
|
|||
mod config;
|
||||
mod error;
|
||||
mod git;
|
||||
mod config;
|
||||
mod tci;
|
||||
|
||||
fn main() {
|
||||
// load tci config
|
||||
// TODO how could we receive this?
|
||||
// arguments are used by git
|
||||
// idk how we could reliably set env vars???
|
||||
let config_path = "/etc/tci/config.toml";
|
||||
|
||||
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}");
|
||||
crate::config::TciServerConfig::default()
|
||||
});
|
||||
|
||||
if let Err(e) = crate::tci::tci(cfg) {
|
||||
println!("[!] error running tci: {e}");
|
||||
match crate::tci::Tci::new() {
|
||||
Err(e) => eprintln!("[!] error setting up tci: {e}"),
|
||||
Ok(tci) => if let Err(e) = tci.run() {
|
||||
eprintln!("[!] error running tci: {e}");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
172
src/tci.rs
172
src/tci.rs
|
@ -1,111 +1,141 @@
|
|||
use std::io::Read;
|
||||
|
||||
use crate::{config::{TciServerConfig, TciScriptConfig}, error::TciError, git::{shell_out, RefUpdate, RefUpdateVecHelper}};
|
||||
use crate::{config::{TciConfig, TciScriptConfig}, error::{TciError, TciResult}, git::{RefUpdate, RefUpdateVecHelper, TciRepo}};
|
||||
|
||||
lazy_static::lazy_static!{
|
||||
static ref HOME : String = std::env::var("HOME").unwrap_or_default();
|
||||
pub struct Tci {
|
||||
cfg: TciConfig,
|
||||
repo: TciRepo,
|
||||
home: String,
|
||||
stdin: String,
|
||||
}
|
||||
|
||||
pub fn tci(cfg: TciServerConfig) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let repo_path = std::path::PathBuf::from(&std::env::var("GIT_DIR")?);
|
||||
impl Tci {
|
||||
pub fn new() -> TciResult<Self> {
|
||||
let home = std::env::var("HOME")
|
||||
.unwrap_or_default();
|
||||
|
||||
let cfg = TciConfig::load()
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("[!] invalid config: {e}");
|
||||
TciConfig::default()
|
||||
});
|
||||
|
||||
// check which branches are being updated
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_to_string(&mut input)?;
|
||||
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 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();
|
||||
let repo = TciRepo::new(&stdin, &home)?;
|
||||
|
||||
if let Some(branch) = cfg.branch {
|
||||
if !updates.contains_branch(&branch) {
|
||||
return Ok(()); // tci is not allowed to run on changes for this branch
|
||||
Ok(Tci{cfg, repo, home, stdin})
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
true
|
||||
}
|
||||
|
||||
pub fn run(&self) -> TciResult<()> {
|
||||
// run hooks
|
||||
for setup in cfg.setup {
|
||||
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(".tci".into());
|
||||
let tci_path = env.path().join(&tci_script);
|
||||
|
||||
if !tci_path.is_file() && !tci_path.is_dir() {
|
||||
return Err(TciError::Missing);
|
||||
}
|
||||
|
||||
for setup in &self.cfg.setup {
|
||||
println!("[+] setting up ({setup})");
|
||||
shell_out(setup)?;
|
||||
Tci::exec(&self.repo.path, 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 = 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
|
||||
|
||||
let res = tci_hook(&repo_path, &updates, tci_script);
|
||||
|
||||
for cleanup in cfg.cleanup {
|
||||
for cleanup in &self.cfg.cleanup {
|
||||
println!("[-] cleaning up ({cleanup}) ");
|
||||
shell_out(cleanup)?;
|
||||
Tci::exec(&self.repo.path, cleanup)?;
|
||||
}
|
||||
|
||||
res?; // only check error AFTER running cleanup hooks
|
||||
res?; // if there was an error, return it now
|
||||
|
||||
println!("[o] script run successfully");
|
||||
println!("[o] tci complete");
|
||||
|
||||
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('/', "_")))?;
|
||||
fn prepare_env(&self) -> TciResult<tempdir::TempDir> {
|
||||
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(
|
||||
repo_path.to_str()
|
||||
self.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");
|
||||
Ok(tmp)
|
||||
}
|
||||
|
||||
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
|
||||
fn run_tci_hook(&self, cwd: &std::path::Path, dir: std::path::PathBuf) -> 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
|
||||
}
|
||||
}
|
||||
println!("[=] running tci pipeline for repo '{name}'");
|
||||
|
||||
// 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(())
|
||||
} 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);
|
||||
}
|
||||
|
||||
fn exec<S : AsRef<std::ffi::OsStr>>(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::TciError::SubprocessError(x)),
|
||||
None => Err(crate::error::TciError::SubprocessTerminated),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue