rivox 1.0.0

Universal polyglot build coordination layer for Python, Rust, and Node monorepos
Documentation
use super::EcosystemAdapter;
use crate::graph::GraphNode;
use anyhow::{Context, Result, bail};
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

pub struct GoAdapter;

impl EcosystemAdapter for GoAdapter {
    fn name(&self) -> &'static str {
        "go"
    }

    fn detect_tool(&self) -> Result<String> {
        let output = Command::new("go")
            .arg("version")
            .output()
            .context("Failed to execute `go version`. Ensure `go` is installed and in PATH.")?;

        if !output.status.success() {
            bail!("`go version` failed with exit status: {}", output.status);
        }

        let version_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
        Ok(version_str)
    }

    fn resolve(&self, workspace_path: &Path, frozen: bool) -> Result<PathBuf> {
        let lockfile_path = workspace_path.join("go.sum");

        if frozen && lockfile_path.exists() {
            return Ok(lockfile_path);
        }

        let mut cmd = Command::new("go");
        cmd.arg("mod").arg("download").current_dir(workspace_path);

        let output = cmd
            .output()
            .context("Failed to run `go mod download` in workspace")?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            bail!(
                "`go mod download` failed in {}: {}",
                workspace_path.display(),
                stderr
            );
        }

        if !lockfile_path.exists() {
            let mod_path = workspace_path.join("go.mod");
            if mod_path.exists() {
                return Ok(mod_path);
            }
            bail!(
                "`go mod download` completed but neither `go.sum` nor `go.mod` found at {}",
                workspace_path.display()
            );
        }

        Ok(lockfile_path)
    }

    fn parse_lockfile(&self, lockfile_path: &Path) -> Result<Vec<GraphNode>> {
        let content = fs::read_to_string(lockfile_path).with_context(|| {
            format!("Failed to read Go lockfile at {}", lockfile_path.display())
        })?;

        let mut nodes = Vec::new();

        for line in content.lines() {
            let line = line.trim();
            if line.is_empty() || line.starts_with("//") {
                continue;
            }

            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts.len() >= 3 {
                let pkg_name = parts[0].to_string();
                let version = parts[1].trim_end_matches("/go.mod").to_string();
                let raw_hash = parts[2];

                let content_hash = format!(
                    "sha256:{}",
                    hex::encode(Sha256::digest(
                        format!("go:{}:{}:{}", pkg_name, version, raw_hash).as_bytes()
                    ))
                );

                nodes.push(GraphNode {
                    ecosystem: "go".to_string(),
                    package_name: pkg_name,
                    resolved_version: version,
                    content_hash,
                    platform_markers: None,
                    lock_ref: lockfile_path.to_string_lossy().to_string(),
                });
            }
        }

        nodes.sort_by(|a, b| {
            a.package_name
                .cmp(&b.package_name)
                .then(a.resolved_version.cmp(&b.resolved_version))
        });
        nodes.dedup_by(|a, b| {
            a.package_name == b.package_name && a.resolved_version == b.resolved_version
        });

        Ok(nodes)
    }
}