1use super::{
7 BloatDir, EnforcePolicy, PackageManager, dir_size, enforce_two_tier, run_command_with_timeout,
8};
9use anyhow::{Result, anyhow};
10use std::path::Path;
11
12pub struct Go;
14
15fn vendor_has_uncommitted_changes(path: &Path) -> Result<bool> {
27 let output = crate::scanner::git::git_in(path)
28 .args(["status", "--porcelain", "--", "vendor"])
29 .output()
30 .map_err(|e| anyhow!("could not run `git status` to check `vendor/`: {e}"))?;
31 if !output.status.success() {
32 return Err(anyhow!(
33 "`git status` could not inspect `vendor/` for uncommitted changes: {}",
34 String::from_utf8_lossy(&output.stderr).trim()
35 ));
36 }
37 Ok(String::from_utf8_lossy(&output.stdout)
38 .lines()
39 .any(|line| !line.trim().is_empty() && !line.starts_with("??")))
40}
41
42impl PackageManager for Go {
43 fn name(&self) -> &'static str {
44 "go"
45 }
46
47 fn detect(&self, path: &Path) -> bool {
48 path.join("go.mod").exists()
49 }
50
51 fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
52 let mut dirs = Vec::new();
53 let vendor_path = path.join("vendor");
54 if vendor_path.exists() && vendor_path.join("modules.txt").exists() {
58 dirs.push(BloatDir {
59 name: "vendor".to_string(),
60 path: vendor_path.clone(),
61 size_bytes: dir_size(&vendor_path),
62 shared_bytes: 0,
63 });
64 }
65 dirs
66 }
67
68 fn enforce_lockfile(&self, path: &Path, policy: EnforcePolicy) -> Result<()> {
74 if path.join("vendor").exists() && vendor_has_uncommitted_changes(path)? {
78 return Err(anyhow!(
79 "`vendor/` has uncommitted changes — deleting it would lose them, and \
80 `go mod vendor` would rebuild the tree without them. Commit or stash \
81 the changes first."
82 ));
83 }
84 enforce_two_tier(
85 &path.join("go.sum"),
86 "go",
87 &["mod", "download"],
88 &["mod", "tidy"],
89 path,
90 policy,
91 )
92 }
93
94 fn restore(&self, path: &Path, timeout: std::time::Duration) -> Result<()> {
95 if path.join("vendor").exists() {
96 run_command_with_timeout("go", &["mod", "vendor"], path, timeout)
97 } else {
98 run_command_with_timeout("go", &["mod", "download"], path, timeout)
99 }
100 }
101
102 fn lockfiles(&self) -> &'static [&'static str] {
103 &["go.sum"]
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use std::fs;
111 use std::fs::File;
112 use tempfile::tempdir;
113
114 #[test]
115 fn test_name() {
116 let adapter = Go;
117 assert_eq!(adapter.name(), "go");
118 }
119
120 #[test]
121 fn test_detect_positive() {
122 let dir = tempdir().unwrap();
123 File::create(dir.path().join("go.mod")).unwrap();
124
125 let adapter = Go;
126 assert!(adapter.detect(dir.path()));
127 }
128
129 #[test]
130 fn test_detect_negative() {
131 let dir = tempdir().unwrap();
132
133 let adapter = Go;
134 assert!(!adapter.detect(dir.path()));
135 }
136
137 #[test]
138 fn test_bloat_dirs_present() {
139 let dir = tempdir().unwrap();
140 fs::create_dir(dir.path().join("vendor")).unwrap();
141 File::create(dir.path().join("vendor").join("modules.txt")).unwrap();
142
143 let adapter = Go;
144 let dirs = adapter.bloat_dirs(dir.path());
145 assert_eq!(dirs.len(), 1);
146 assert_eq!(dirs[0].name, "vendor");
147 }
148
149 #[test]
150 fn a_vendor_tree_without_modules_txt_is_not_claimed() {
151 let dir = tempdir().unwrap();
154 fs::create_dir(dir.path().join("vendor")).unwrap();
155 File::create(dir.path().join("vendor").join("some_pkg.go")).unwrap();
156
157 assert!(Go.bloat_dirs(dir.path()).is_empty());
158 }
159
160 #[test]
161 fn staged_vendor_changes_refuse_the_prune() {
162 let dir = tempdir().unwrap();
163 let vendor = dir.path().join("vendor");
164 fs::create_dir(&vendor).unwrap();
165 fs::write(vendor.join("modules.txt"), "# github.com/x/y v1.0.0\n").unwrap();
166
167 assert!(vendor_has_uncommitted_changes(dir.path()).is_err());
170
171 let git = |args: &[&str]| {
174 std::process::Command::new("git")
175 .args(args)
176 .current_dir(dir.path())
177 .output()
178 .unwrap()
179 };
180 assert!(git(&["init", "-q"]).status.success());
181 git(&["add", "vendor"]);
182 assert!(vendor_has_uncommitted_changes(dir.path()).unwrap());
183 }
184
185 #[test]
186 fn test_bloat_dirs_absent() {
187 let dir = tempdir().unwrap();
188
189 let adapter = Go;
190 let dirs = adapter.bloat_dirs(dir.path());
191 assert!(dirs.is_empty());
192 }
193}