tldr_cli/commands/bugbot/
baseline.rs1use std::io::Write;
7use std::path::Path;
8use std::process::Command;
9
10use anyhow::{Context, Result};
11use tempfile::NamedTempFile;
12
13#[derive(Debug)]
15pub enum BaselineStatus {
16 Exists(String),
18 NewFile,
20 GitShowFailed(String),
22}
23
24pub fn get_baseline_content(project: &Path, file: &Path, base_ref: &str) -> Result<BaselineStatus> {
36 let relative = file.strip_prefix(project).unwrap_or(file);
39
40 let relative_str = relative
42 .components()
43 .map(|c| c.as_os_str().to_string_lossy().to_string())
44 .collect::<Vec<_>>()
45 .join("/");
46
47 let output = Command::new("git")
48 .args(["show", &format!("{}:{}", base_ref, relative_str)])
49 .current_dir(project)
50 .output()
51 .context("Failed to run git show")?;
52
53 if output.status.success() {
54 let content =
55 String::from_utf8(output.stdout).context("git show output is not valid UTF-8")?;
56 Ok(BaselineStatus::Exists(content))
57 } else {
58 let stderr = String::from_utf8_lossy(&output.stderr);
59 if stderr.contains("does not exist")
60 || stderr.contains("not exist in")
61 || stderr.contains("exists on disk, but not in")
62 || stderr.contains("did not match any")
63 {
64 Ok(BaselineStatus::NewFile)
65 } else {
66 Ok(BaselineStatus::GitShowFailed(stderr.to_string()))
67 }
68 }
69}
70
71pub fn write_baseline_tmpfile(content: &str, file_path: &Path) -> Result<NamedTempFile> {
77 let extension = file_path
78 .extension()
79 .and_then(|e| e.to_str())
80 .unwrap_or("txt");
81
82 let mut tmpfile = tempfile::Builder::new()
83 .prefix("bugbot_baseline_")
84 .suffix(&format!(".{}", extension))
85 .tempfile()
86 .context("Failed to create temp file for baseline")?;
87
88 tmpfile
89 .write_all(content.as_bytes())
90 .context("Failed to write baseline content to temp file")?;
91 tmpfile.flush()?;
92
93 Ok(tmpfile)
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use std::path::PathBuf;
100
101 fn init_git_repo() -> tempfile::TempDir {
103 let tmp = tempfile::TempDir::new().expect("create temp dir");
104 let dir = tmp.path();
105
106 Command::new("git")
107 .args(["init"])
108 .current_dir(dir)
109 .output()
110 .expect("git init");
111
112 Command::new("git")
113 .args(["config", "user.email", "test@test.com"])
114 .current_dir(dir)
115 .output()
116 .expect("git config email");
117
118 Command::new("git")
119 .args(["config", "user.name", "Test"])
120 .current_dir(dir)
121 .output()
122 .expect("git config name");
123
124 std::fs::write(dir.join("README.md"), "# test\n").expect("write readme");
126 Command::new("git")
127 .args(["add", "."])
128 .current_dir(dir)
129 .output()
130 .expect("git add");
131 Command::new("git")
132 .args(["commit", "-m", "init"])
133 .current_dir(dir)
134 .output()
135 .expect("git commit");
136
137 tmp
138 }
139
140 #[test]
141 fn test_get_baseline_existing_file() {
142 let tmp = init_git_repo();
143 let dir = tmp.path();
144
145 let original = "fn original() {}\n";
147 std::fs::write(dir.join("lib.rs"), original).expect("write lib.rs");
148 Command::new("git")
149 .args(["add", "lib.rs"])
150 .current_dir(dir)
151 .output()
152 .expect("git add");
153 Command::new("git")
154 .args(["commit", "-m", "add lib.rs"])
155 .current_dir(dir)
156 .output()
157 .expect("git commit");
158
159 std::fs::write(dir.join("lib.rs"), "fn modified() {}\n").expect("overwrite lib.rs");
161
162 let status =
164 get_baseline_content(dir, &dir.join("lib.rs"), "HEAD").expect("get_baseline_content");
165
166 match status {
167 BaselineStatus::Exists(content) => {
168 assert_eq!(
169 content, original,
170 "Baseline should return the committed content"
171 );
172 }
173 other => panic!("Expected BaselineStatus::Exists, got: {:?}", other),
174 }
175 }
176
177 #[test]
178 fn test_get_baseline_new_file() {
179 let tmp = init_git_repo();
180 let dir = tmp.path();
181
182 std::fs::write(dir.join("brand_new.rs"), "fn new() {}\n").expect("write new file");
184
185 let status = get_baseline_content(dir, &dir.join("brand_new.rs"), "HEAD")
186 .expect("get_baseline_content");
187
188 match status {
189 BaselineStatus::NewFile => {} other => panic!("Expected BaselineStatus::NewFile, got: {:?}", other),
191 }
192 }
193
194 #[test]
195 fn test_get_baseline_deleted_file() {
196 let tmp = init_git_repo();
197 let dir = tmp.path();
198
199 let original = "fn to_delete() {}\n";
201 std::fs::write(dir.join("doomed.rs"), original).expect("write doomed.rs");
202 Command::new("git")
203 .args(["add", "doomed.rs"])
204 .current_dir(dir)
205 .output()
206 .expect("git add");
207 Command::new("git")
208 .args(["commit", "-m", "add doomed.rs"])
209 .current_dir(dir)
210 .output()
211 .expect("git commit");
212
213 std::fs::remove_file(dir.join("doomed.rs")).expect("delete doomed.rs");
215
216 let status = get_baseline_content(dir, &dir.join("doomed.rs"), "HEAD")
218 .expect("get_baseline_content");
219
220 match status {
221 BaselineStatus::Exists(content) => {
222 assert_eq!(
223 content, original,
224 "Baseline should return the committed content even after deletion"
225 );
226 }
227 other => panic!("Expected BaselineStatus::Exists, got: {:?}", other),
228 }
229 }
230
231 #[test]
232 fn test_tmpfile_has_correct_extension() {
233 let tmpfile =
234 write_baseline_tmpfile("content", &PathBuf::from("src/lib.rs")).expect("write tmpfile");
235
236 let path = tmpfile.path();
237 let ext = path.extension().and_then(|e| e.to_str());
238 assert_eq!(ext, Some("rs"), "Temp file should have .rs extension");
239 }
240
241 #[test]
242 fn test_tmpfile_content_matches() {
243 let content = "fn hello() { println!(\"world\"); }\n";
244 let tmpfile =
245 write_baseline_tmpfile(content, &PathBuf::from("example.py")).expect("write tmpfile");
246
247 let read_back = std::fs::read_to_string(tmpfile.path()).expect("read tmpfile");
248 assert_eq!(
249 read_back, content,
250 "Content read back from temp file should match what was written"
251 );
252 }
253}