1use crate::cli::BisectAction;
2use crate::core::git::GitOperations;
3use crate::core::validation::Validate;
4use crate::{GitXError, Result};
5use console::style;
6use std::process::Command;
7
8pub fn run(action: BisectAction) -> Result<String> {
9 match action {
10 BisectAction::Start { good, bad } => start_bisect(good, bad),
11 BisectAction::Good => mark_good(),
12 BisectAction::Bad => mark_bad(),
13 BisectAction::Skip => skip_commit(),
14 BisectAction::Reset => reset_bisect(),
15 BisectAction::Status => show_status(),
16 }
17}
18
19fn start_bisect(good: String, bad: String) -> Result<String> {
20 if is_bisecting()? {
22 return Err(GitXError::GitCommand(
23 "Already in bisect mode. Use 'git x bisect reset' to exit first.".to_string(),
24 ));
25 }
26
27 validate_commit_exists(&good)?;
29 validate_commit_exists(&bad)?;
30
31 let mut output = Vec::new();
32 output.push(format!(
33 "{} Starting bisect between {} (good) and {} (bad)",
34 style("đ").bold(),
35 style(&good).green().bold(),
36 style(&bad).red().bold()
37 ));
38
39 let git_output = Command::new("git")
41 .args(["bisect", "start", &bad, &good])
42 .output()?;
43
44 if !git_output.status.success() {
45 return Err(GitXError::GitCommand(format!(
46 "Failed to start bisect: {}",
47 String::from_utf8_lossy(&git_output.stderr).trim()
48 )));
49 }
50
51 let current_commit = get_current_commit_info()?;
53 output.push(format!(
54 "{} Checked out commit: {}",
55 style("đ").bold(),
56 style(¤t_commit).cyan()
57 ));
58
59 let remaining = get_remaining_steps()?;
60 output.push(format!(
61 "{} Approximately {} steps remaining",
62 style("âŗ").bold(),
63 style(remaining).yellow().bold()
64 ));
65
66 output.push(format!(
67 "\n{} Test this commit and run:",
68 style("đĄ").bold()
69 ));
70 output.push(format!(
71 " {} if commit is good",
72 style("git x bisect good").green()
73 ));
74 output.push(format!(
75 " {} if commit is bad",
76 style("git x bisect bad").red()
77 ));
78 output.push(format!(
79 " {} if commit is untestable",
80 style("git x bisect skip").yellow()
81 ));
82
83 Ok(output.join("\n"))
84}
85
86fn mark_good() -> Result<String> {
87 ensure_bisecting()?;
88
89 let current_commit = get_current_commit_info()?;
90 let mut output = Vec::new();
91 output.push(format!(
92 "{} Marked {} as good",
93 style("â
").bold(),
94 style(¤t_commit).green()
95 ));
96
97 let git_output = Command::new("git").args(["bisect", "good"]).output()?;
98
99 if !git_output.status.success() {
100 return Err(GitXError::GitCommand(format!(
101 "Failed to mark commit as good: {}",
102 String::from_utf8_lossy(&git_output.stderr).trim()
103 )));
104 }
105
106 let stdout = String::from_utf8_lossy(&git_output.stdout);
107 if stdout.contains("is the first bad commit") {
108 output.push(format!(
109 "\n{} Found the first bad commit!",
110 style("đ¯").bold()
111 ));
112 output.push(parse_bisect_result(&stdout));
113 output.push(format!(
114 "\n{} Run {} to return to your original branch",
115 style("đĄ").bold(),
116 style("git x bisect reset").cyan()
117 ));
118 } else {
119 let new_commit = get_current_commit_info()?;
120 output.push(format!(
121 "{} Checked out commit: {}",
122 style("đ").bold(),
123 style(&new_commit).cyan()
124 ));
125
126 let remaining = get_remaining_steps()?;
127 output.push(format!(
128 "{} Approximately {} steps remaining",
129 style("âŗ").bold(),
130 style(remaining).yellow().bold()
131 ));
132 }
133
134 Ok(output.join("\n"))
135}
136
137fn mark_bad() -> Result<String> {
138 ensure_bisecting()?;
139
140 let current_commit = get_current_commit_info()?;
141 let mut output = Vec::new();
142 output.push(format!(
143 "{} Marked {} as bad",
144 style("â").bold(),
145 style(¤t_commit).red()
146 ));
147
148 let git_output = Command::new("git").args(["bisect", "bad"]).output()?;
149
150 if !git_output.status.success() {
151 return Err(GitXError::GitCommand(format!(
152 "Failed to mark commit as bad: {}",
153 String::from_utf8_lossy(&git_output.stderr).trim()
154 )));
155 }
156
157 let stdout = String::from_utf8_lossy(&git_output.stdout);
158 if stdout.contains("is the first bad commit") {
159 output.push(format!(
160 "\n{} Found the first bad commit!",
161 style("đ¯").bold()
162 ));
163 output.push(parse_bisect_result(&stdout));
164 output.push(format!(
165 "\n{} Run {} to return to your original branch",
166 style("đĄ").bold(),
167 style("git x bisect reset").cyan()
168 ));
169 } else {
170 let new_commit = get_current_commit_info()?;
171 output.push(format!(
172 "{} Checked out commit: {}",
173 style("đ").bold(),
174 style(&new_commit).cyan()
175 ));
176
177 let remaining = get_remaining_steps()?;
178 output.push(format!(
179 "{} Approximately {} steps remaining",
180 style("âŗ").bold(),
181 style(remaining).yellow().bold()
182 ));
183 }
184
185 Ok(output.join("\n"))
186}
187
188fn skip_commit() -> Result<String> {
189 ensure_bisecting()?;
190
191 let current_commit = get_current_commit_info()?;
192 let mut output = Vec::new();
193 output.push(format!(
194 "{} Skipped {} (untestable)",
195 style("âī¸").bold(),
196 style(¤t_commit).yellow()
197 ));
198
199 let git_output = Command::new("git").args(["bisect", "skip"]).output()?;
200
201 if !git_output.status.success() {
202 return Err(GitXError::GitCommand(format!(
203 "Failed to skip commit: {}",
204 String::from_utf8_lossy(&git_output.stderr).trim()
205 )));
206 }
207
208 let new_commit = get_current_commit_info()?;
209 output.push(format!(
210 "{} Checked out commit: {}",
211 style("đ").bold(),
212 style(&new_commit).cyan()
213 ));
214
215 let remaining = get_remaining_steps()?;
216 output.push(format!(
217 "{} Approximately {} steps remaining",
218 style("âŗ").bold(),
219 style(remaining).yellow().bold()
220 ));
221
222 Ok(output.join("\n"))
223}
224
225fn reset_bisect() -> Result<String> {
226 if !is_bisecting()? {
227 return Ok(format!(
228 "{} Not currently in bisect mode",
229 style("âšī¸").bold()
230 ));
231 }
232
233 let git_output = Command::new("git").args(["bisect", "reset"]).output()?;
234
235 if !git_output.status.success() {
236 return Err(GitXError::GitCommand(format!(
237 "Failed to reset bisect: {}",
238 String::from_utf8_lossy(&git_output.stderr).trim()
239 )));
240 }
241
242 Ok(format!(
243 "{} Bisect session ended, returned to original branch",
244 style("đ").bold()
245 ))
246}
247
248fn show_status() -> Result<String> {
249 if !is_bisecting()? {
250 return Ok(format!(
251 "{} Not currently in bisect mode",
252 style("âšī¸").bold()
253 ));
254 }
255
256 let mut output = Vec::new();
257 output.push(format!("{} Bisect Status", style("đ").bold()));
258
259 let current_commit = get_current_commit_info()?;
261 output.push(format!(
262 "{} Current commit: {}",
263 style("đ").bold(),
264 style(¤t_commit).cyan()
265 ));
266
267 let remaining = get_remaining_steps()?;
269 output.push(format!(
270 "{} Approximately {} steps remaining",
271 style("âŗ").bold(),
272 style(remaining).yellow().bold()
273 ));
274
275 if let Ok(log) = get_bisect_log() {
277 output.push(format!("\n{} Bisect log:", style("đ").bold()));
278 for entry in log.lines().take(5) {
279 if !entry.trim().is_empty() {
280 output.push(format!(" {}", style(entry.trim()).dim()));
281 }
282 }
283 }
284
285 output.push(format!("\n{} Available commands:", style("đĄ").bold()));
286 output.push(format!(
287 " {} - Mark current commit as good",
288 style("git x bisect good").green()
289 ));
290 output.push(format!(
291 " {} - Mark current commit as bad",
292 style("git x bisect bad").red()
293 ));
294 output.push(format!(
295 " {} - Skip current commit",
296 style("git x bisect skip").yellow()
297 ));
298 output.push(format!(
299 " {} - End bisect session",
300 style("git x bisect reset").cyan()
301 ));
302
303 Ok(output.join("\n"))
304}
305
306fn is_bisecting() -> Result<bool> {
307 let output = Command::new("git")
309 .args(["rev-parse", "--git-dir"])
310 .output()?;
311
312 if !output.status.success() {
313 return Ok(false);
314 }
315
316 let git_dir_output = String::from_utf8_lossy(&output.stdout);
317 let git_dir = git_dir_output.trim();
318 let bisect_start_path = format!("{git_dir}/BISECT_START");
319
320 Ok(std::path::Path::new(&bisect_start_path).exists())
321}
322
323fn ensure_bisecting() -> Result<()> {
324 if !is_bisecting()? {
325 return Err(GitXError::GitCommand(
326 "Not currently in bisect mode. Use 'git x bisect start <good> <bad>' first."
327 .to_string(),
328 ));
329 }
330 Ok(())
331}
332
333fn validate_commit_exists(commit: &str) -> Result<()> {
334 Validate::commit_exists(commit)
335}
336
337fn get_current_commit_info() -> Result<String> {
338 GitOperations::run(&["log", "-1", "--pretty=format:%h %s"])
339}
340
341fn get_remaining_steps() -> Result<String> {
342 match GitOperations::run(&["bisect", "view", "--pretty=oneline"]) {
343 Ok(output) => {
344 let count = output.lines().count();
345 let steps = (count as f64).log2().ceil() as usize;
346 Ok(steps.to_string())
347 }
348 Err(_) => Ok("unknown".to_string()),
349 }
350}
351
352fn get_bisect_log() -> Result<String> {
353 GitOperations::run(&["bisect", "log"])
354}
355
356pub fn parse_bisect_result(output: &str) -> String {
357 for line in output.lines() {
358 if line.contains("is the first bad commit") {
359 if let Some(commit_hash) = line.split_whitespace().next() {
360 return format!(
361 "{} First bad commit: {}",
362 style("đ¯").bold(),
363 style(commit_hash).red().bold()
364 );
365 }
366 }
367 }
368 "Bisect completed".to_string()
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use crate::GitXError;
375
376 #[test]
377 fn test_parse_bisect_result() {
378 let sample_output =
379 "abc123def456 is the first bad commit\ncommit abc123def456\nAuthor: Test User";
380 let result = parse_bisect_result(sample_output);
381 assert!(result.contains("abc123def456"));
382 assert!(result.contains("First bad commit"));
383 }
384
385 #[test]
386 fn test_parse_bisect_result_no_match() {
387 let sample_output = "Some other git output\nNo bad commit found";
388 let result = parse_bisect_result(sample_output);
389 assert_eq!(result, "Bisect completed");
390 }
391
392 #[test]
393 fn test_get_current_commit_info_error_handling() {
394 let test_result: Result<String> = Err(GitXError::GitCommand("test".to_string()));
398 assert!(test_result.is_err());
399 }
400
401 #[test]
402 fn test_validate_commit_exists_logic() {
403 let _validate_fn = validate_commit_exists;
406 }
408
409 #[test]
410 fn test_is_bisecting_logic() {
411 let _bisect_fn = is_bisecting;
413 }
415
416 #[test]
417 fn test_ensure_bisecting_logic() {
418 let result = ensure_bisecting();
421 match result {
424 Ok(_) => {} Err(GitXError::GitCommand(_)) => {} Err(GitXError::Io(_)) => {} Err(_) => panic!("Unexpected error type"),
428 }
429 }
430
431 #[test]
432 fn test_get_remaining_steps_fallback() {
433 let _steps_fn = get_remaining_steps;
436 }
438
439 #[test]
440 fn test_get_bisect_log_error_handling() {
441 let _log_fn = get_bisect_log;
443 }
445
446 #[test]
447 fn test_bisect_workflow_functions_exist() {
448 let _start_fn = start_bisect;
450 let _good_fn = mark_good;
451 let _bad_fn = mark_bad;
452 let _skip_fn = skip_commit;
453 let _reset_fn = reset_bisect;
454 let _status_fn = show_status;
455 }
457}