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