use super::{
BloatDir, EnforcePolicy, PackageManager, dir_size, enforce_two_tier, run_command_with_timeout,
};
use anyhow::{Result, anyhow};
use std::path::Path;
pub struct Go;
fn vendor_has_uncommitted_changes(path: &Path) -> Result<bool> {
let output = crate::scanner::git::git_in(path)
.args(["status", "--porcelain", "--", "vendor"])
.output()
.map_err(|e| anyhow!("could not run `git status` to check `vendor/`: {e}"))?;
if !output.status.success() {
return Err(anyhow!(
"`git status` could not inspect `vendor/` for uncommitted changes: {}",
String::from_utf8_lossy(&output.stderr).trim()
));
}
Ok(String::from_utf8_lossy(&output.stdout)
.lines()
.any(|line| !line.trim().is_empty() && !line.starts_with("??")))
}
impl PackageManager for Go {
fn name(&self) -> &'static str {
"go"
}
fn detect(&self, path: &Path) -> bool {
path.join("go.mod").exists()
}
fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
let mut dirs = Vec::new();
let vendor_path = path.join("vendor");
if vendor_path.exists() && vendor_path.join("modules.txt").exists() {
dirs.push(BloatDir {
name: "vendor".to_string(),
path: vendor_path.clone(),
size_bytes: dir_size(&vendor_path),
shared_bytes: 0,
});
}
dirs
}
fn enforce_lockfile(&self, path: &Path, policy: EnforcePolicy) -> Result<()> {
if path.join("vendor").exists() && vendor_has_uncommitted_changes(path)? {
return Err(anyhow!(
"`vendor/` has uncommitted changes — deleting it would lose them, and \
`go mod vendor` would rebuild the tree without them. Commit or stash \
the changes first."
));
}
enforce_two_tier(
&path.join("go.sum"),
"go",
&["mod", "download"],
&["mod", "tidy"],
path,
policy,
)
}
fn restore(&self, path: &Path, timeout: std::time::Duration) -> Result<()> {
if path.join("vendor").exists() {
run_command_with_timeout("go", &["mod", "vendor"], path, timeout)
} else {
run_command_with_timeout("go", &["mod", "download"], path, timeout)
}
}
fn lockfiles(&self) -> &'static [&'static str] {
&["go.sum"]
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::fs::File;
use tempfile::tempdir;
#[test]
fn test_name() {
let adapter = Go;
assert_eq!(adapter.name(), "go");
}
#[test]
fn test_detect_positive() {
let dir = tempdir().unwrap();
File::create(dir.path().join("go.mod")).unwrap();
let adapter = Go;
assert!(adapter.detect(dir.path()));
}
#[test]
fn test_detect_negative() {
let dir = tempdir().unwrap();
let adapter = Go;
assert!(!adapter.detect(dir.path()));
}
#[test]
fn test_bloat_dirs_present() {
let dir = tempdir().unwrap();
fs::create_dir(dir.path().join("vendor")).unwrap();
File::create(dir.path().join("vendor").join("modules.txt")).unwrap();
let adapter = Go;
let dirs = adapter.bloat_dirs(dir.path());
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].name, "vendor");
}
#[test]
fn a_vendor_tree_without_modules_txt_is_not_claimed() {
let dir = tempdir().unwrap();
fs::create_dir(dir.path().join("vendor")).unwrap();
File::create(dir.path().join("vendor").join("some_pkg.go")).unwrap();
assert!(Go.bloat_dirs(dir.path()).is_empty());
}
#[test]
fn staged_vendor_changes_refuse_the_prune() {
let dir = tempdir().unwrap();
let vendor = dir.path().join("vendor");
fs::create_dir(&vendor).unwrap();
fs::write(vendor.join("modules.txt"), "# github.com/x/y v1.0.0\n").unwrap();
assert!(vendor_has_uncommitted_changes(dir.path()).is_err());
let git = |args: &[&str]| {
std::process::Command::new("git")
.args(args)
.current_dir(dir.path())
.output()
.unwrap()
};
assert!(git(&["init", "-q"]).status.success());
git(&["add", "vendor"]);
assert!(vendor_has_uncommitted_changes(dir.path()).unwrap());
}
#[test]
fn test_bloat_dirs_absent() {
let dir = tempdir().unwrap();
let adapter = Go;
let dirs = adapter.bloat_dirs(dir.path());
assert!(dirs.is_empty());
}
}