use std::collections::HashSet;
use std::io::Seek;
use std::path::{Path, PathBuf};
use anyhow::Context;
use colored::Colorize;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug)]
pub struct Config {
#[serde(default)]
pub checks: Vec<Check>,
bundle: Option<BundleConfig>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct BundleConfig {
url: String,
sha256: String,
#[serde(skip)]
path: PathBuf,
}
impl Config {
pub fn download_bundle(&mut self) -> anyhow::Result<()> {
if let Some(config) = &mut self.bundle {
config.path = dirs::cache_dir()
.context("Failed to find cache dir")?
.join("checkalot")
.join(&config.sha256);
std::fs::create_dir_all(&config.path)?;
let cache_done = config.path.join("done");
if !cache_done.exists() {
println!("Downloading bundle from {}...", config.url);
let mut reader = ureq::get(&config.url).call()?.into_reader();
let mut tempfile = tempfile::tempfile()?;
std::io::copy(&mut reader, &mut tempfile)?;
tempfile.seek(std::io::SeekFrom::Start(0))?;
let sha256 = crate::sha256(&mut tempfile)?;
anyhow::ensure!(
config.sha256 == sha256,
"Mismatching checksum: got {}, wanted {}",
sha256,
config.sha256
);
tempfile.seek(std::io::SeekFrom::Start(0))?;
let reader = flate2::read::GzDecoder::new(tempfile);
let mut tar = tar::Archive::new(reader);
tar.unpack(&config.path)?;
std::fs::write(cache_done, "")?;
} else {
println!("Using bundle from {:?}", config.path);
}
let path = std::env::var("PATH").unwrap_or_default();
std::env::set_var(
"PATH",
format!("{}:{}", config.path.to_str().unwrap(), path),
);
}
Ok(())
}
pub fn load(path: &Path) -> anyhow::Result<Self> {
let config = std::fs::read_to_string(path)
.with_context(|| format!("Failed to open configuration at {:?}", path))?;
let mut config: Config =
serde_yaml::from_str(&config).context("Failed to deserialize configuration")?;
config
.download_bundle()
.context("Failed to download bundle")?;
Ok(config)
}
pub fn filter(&mut self, only: HashSet<&str>, skip: HashSet<&str>) -> anyhow::Result<()> {
let checks: HashSet<_> = self.checks.iter().map(Check::name).collect();
anyhow::ensure!(
skip.is_subset(&checks),
"The --skip checks are not a subset of available checks {:?}",
checks
);
anyhow::ensure!(
only.is_subset(&checks),
"The --only checks are not a subset of available checks {:?}",
checks
);
if !only.is_empty() {
println!("{} {:?}", "Executing only".yellow(), only);
self.checks.retain(|c| only.contains(&c.name()));
}
if !skip.is_empty() {
self.checks.retain(|c| !skip.contains(&c.name()));
println!(" {} {:?}", "Skipping".yellow(), skip);
}
Ok(())
}
}
#[derive(serde::Deserialize, serde::Serialize, Debug)]
#[serde(untagged)]
pub enum CommandSpec {
Simple(String),
Complex {
command: String,
success_statuses: Vec<i32>,
},
}
impl CommandSpec {
pub fn command(&self) -> &String {
match self {
CommandSpec::Simple(command) => command,
CommandSpec::Complex { command, .. } => command,
}
}
pub fn success_statuses(&self) -> &[i32] {
match self {
CommandSpec::Simple(_) => &[0],
CommandSpec::Complex {
success_statuses: ok_returns,
..
} => ok_returns,
}
}
}
#[derive(serde::Deserialize, serde::Serialize, Debug)]
#[serde(rename_all = "lowercase", tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum Check {
Version {
version: semver::VersionReq,
},
#[serde(rename = "git-is-clean")]
GitClean,
#[serde(rename = "git-is-rebased")]
GitRebased,
Command {
name: String,
command: CommandSpec,
fix_command: Option<CommandSpec>,
folder: Option<PathBuf>,
version_command: Option<CommandSpec>,
version: Option<semver::VersionReq>,
output: Option<PathBuf>,
},
}