canic-host 0.69.5

Host-side build, install, deployment, and fleet-template library for Canic workspaces
Documentation
use std::{
    path::{Path, PathBuf},
    process::Command,
};

use crate::evidence_envelope::sha256_hex;

use super::model::{DIRTY_SUMMARY_ALGORITHM, SourceDirtyPolicyV1, SourceProvenanceV1, SourceVcsV1};

pub(super) fn source_provenance(workspace_root: &Path) -> SourceProvenanceV1 {
    if !is_git_worktree_root(workspace_root) {
        return unknown_source_provenance();
    }

    let Some(revision) = git_output_text(workspace_root, ["rev-parse", "HEAD"]) else {
        return unknown_source_provenance();
    };
    let branch = git_output_text(workspace_root, ["rev-parse", "--abbrev-ref", "HEAD"]);
    let Some(status) = git_output_bytes(workspace_root, ["status", "--porcelain=v1", "-z"]) else {
        return SourceProvenanceV1 {
            schema_version: 1,
            vcs: SourceVcsV1::Git,
            revision: Some(revision),
            branch,
            dirty: None,
            dirty_policy: SourceDirtyPolicyV1::Unknown,
            dirty_summary_digest: None,
            dirty_summary_algorithm: None,
        };
    };

    let dirty = !status.is_empty();
    SourceProvenanceV1 {
        schema_version: 1,
        vcs: SourceVcsV1::Git,
        revision: Some(revision),
        branch,
        dirty: Some(dirty),
        dirty_policy: if dirty {
            SourceDirtyPolicyV1::DirtyRecorded
        } else {
            SourceDirtyPolicyV1::Clean
        },
        dirty_summary_digest: dirty.then(|| sha256_hex(&status)),
        dirty_summary_algorithm: dirty.then(|| DIRTY_SUMMARY_ALGORITHM.to_string()),
    }
}

fn is_git_worktree_root(workspace_root: &Path) -> bool {
    let Some(top_level) = git_output_text(workspace_root, ["rev-parse", "--show-toplevel"]) else {
        return false;
    };
    let Ok(top_level) = PathBuf::from(top_level).canonicalize() else {
        return false;
    };
    let Ok(workspace_root) = workspace_root.canonicalize() else {
        return false;
    };

    top_level == workspace_root
}

const fn unknown_source_provenance() -> SourceProvenanceV1 {
    SourceProvenanceV1 {
        schema_version: 1,
        vcs: SourceVcsV1::Unknown,
        revision: None,
        branch: None,
        dirty: None,
        dirty_policy: SourceDirtyPolicyV1::Unknown,
        dirty_summary_digest: None,
        dirty_summary_algorithm: None,
    }
}

fn git_output_text<const N: usize>(workspace_root: &Path, args: [&str; N]) -> Option<String> {
    String::from_utf8(git_output_bytes(workspace_root, args)?)
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
}

fn git_output_bytes<const N: usize>(workspace_root: &Path, args: [&str; N]) -> Option<Vec<u8>> {
    let mut command = Command::new("git");
    command.current_dir(workspace_root);
    clear_git_environment(&mut command);

    let output = command.args(args).output().ok()?;
    output.status.success().then_some(output.stdout)
}

fn clear_git_environment(command: &mut Command) {
    for key in [
        "GIT_ALTERNATE_OBJECT_DIRECTORIES",
        "GIT_CEILING_DIRECTORIES",
        "GIT_COMMON_DIR",
        "GIT_DIR",
        "GIT_DISCOVERY_ACROSS_FILESYSTEM",
        "GIT_INDEX_FILE",
        "GIT_NAMESPACE",
        "GIT_OBJECT_DIRECTORY",
        "GIT_PREFIX",
        "GIT_WORK_TREE",
    ] {
        command.env_remove(key);
    }
}