ambient-ci 0.14.0

A continuous integration engine
Documentation
use std::{
    collections::HashMap,
    path::{Path, PathBuf},
};

use ambient_ci::{
    project::{self, ProjectError},
    runlog::RunLog,
};
use clap::Parser;
use serde::Serialize;
use walkdir::WalkDir;

use super::{AmbientError, Config, Leaf};

/// Show per-project statistics for the state directory.
#[derive(Debug, Parser)]
pub struct State {}

impl Leaf for State {
    fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> {
        let mut projects = StateDir::new(config.state());
        let statedir = config.state().to_path_buf();
        for entry in WalkDir::new(&statedir).min_depth(1).max_depth(1) {
            let entry =
                entry.map_err(|err| StateError::ListDir(config.state().to_path_buf(), err))?;
            if let Some(project) = entry.path().file_name() {
                if let Some(project) = project.to_str() {
                    projects.insert(project)?;
                }
            }
        }

        let json = serde_json::to_string_pretty(&projects).map_err(StateError::ToJson)?;
        println!("{json}");
        Ok(())
    }
}

#[derive(Default, Serialize)]
struct StateDir {
    #[serde(skip)]
    statedir: PathBuf,
    projects: HashMap<String, ProjectState>,
    project_count: usize,
}

impl StateDir {
    fn new(statedir: &Path) -> Self {
        Self {
            statedir: statedir.into(),
            ..Default::default()
        }
    }

    fn insert(&mut self, project: &str) -> Result<(), StateError> {
        let project_state = ProjectState::new(&self.statedir, project)?;
        self.projects.insert(project.into(), project_state);
        self.project_count = self.projects.len();
        Ok(())
    }
}

#[derive(Debug, Serialize)]
struct ProjectState {
    latest_commit: Option<String>,
    run_log: Option<u64>,
    dependencies: Option<u64>,
    cache: Option<u64>,
    artifacts: Option<u64>,
}

impl ProjectState {
    fn new(statedir: &Path, project: &str) -> Result<Self, StateError> {
        let state = project::State::from_file(statedir, project)
            .map_err(|err| StateError::LoadState(project.to_string(), err))?;

        Ok(Self {
            latest_commit: state.latest_commit.clone(),
            run_log: Some(Self::file_length(statedir, &state.run_log_filename())?),
            dependencies: Some(Self::dir_size(&state.dependenciesdir())?),
            cache: Some(Self::dir_size(&state.cachedir())?),
            artifacts: Some(Self::dir_size(&state.artifactsdir())?),
        })
    }

    fn file_length(statedir: &Path, filename: &Path) -> Result<u64, StateError> {
        let pathname = statedir.join(filename);
        let meta = pathname
            .metadata()
            .map_err(|err| StateError::Metadata(pathname, err))?;
        Ok(meta.len())
    }

    fn dir_size(path: &Path) -> Result<u64, StateError> {
        let mut bytes = 0;
        for entry in WalkDir::new(path) {
            let entry = entry.map_err(|err| StateError::ListDir(path.to_path_buf(), err))?;
            let meta = entry
                .metadata()
                .map_err(|err| StateError::MetadataWalkDir(entry.path().to_path_buf(), err))?;
            bytes += meta.len();
        }
        Ok(bytes)
    }
}

#[derive(Debug, thiserror::Error)]
pub enum StateError {
    #[error("failed list contents of state directory {0}")]
    ListDir(PathBuf, #[source] walkdir::Error),

    #[error("failed to load project state: {0}")]
    LoadState(String, #[source] ProjectError),

    #[error("failed to serialize project states as JSON")]
    ToJson(#[source] serde_json::Error),

    #[error("failed to get meta data for {0}")]
    Metadata(PathBuf, #[source] std::io::Error),

    #[error("failed to get meta data for {0}")]
    MetadataWalkDir(PathBuf, #[source] walkdir::Error),
}