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)
}
}