1use crate::core::{find_repo_root, read_head, Object, ObjectHash};
2use crate::response::BisectResponse;
3use crate::storage::ObjectStore;
4use serde::{Deserialize, Serialize};
5use std::fs;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8struct BisectState {
9 good: Vec<String>,
10 bad: Vec<String>,
11 current: Option<String>,
12 remaining: Vec<String>,
13 steps: usize,
14}
15
16pub fn execute(
17 command: Option<crate::BisectCommands>,
18) -> Result<BisectResponse, crate::errors::LitError> {
19 let repo_root = find_repo_root()?;
20
21 match command {
22 Some(crate::BisectCommands::Start) => bisect_start(&repo_root),
23 Some(crate::BisectCommands::Good { commit }) => bisect_mark(&repo_root, &commit, true),
24 Some(crate::BisectCommands::Bad { commit }) => bisect_mark(&repo_root, &commit, false),
25 Some(crate::BisectCommands::Reset) => bisect_reset(&repo_root),
26 None => {
27 let state = load_bisect_state(&repo_root)?;
29 Ok(BisectResponse {
30 action: "status".to_string(),
31 current: state.current,
32 remaining: state.remaining.len(),
33 steps: state.steps,
34 message: format!("Bisecting: {} commits left to test", state.remaining.len()),
35 })
36 }
37 }
38}
39
40fn bisect_start(repo_root: &std::path::Path) -> Result<BisectResponse, crate::errors::LitError> {
41 let state = BisectState {
42 good: Vec::new(),
43 bad: Vec::new(),
44 current: None,
45 remaining: Vec::new(),
46 steps: 0,
47 };
48
49 save_bisect_state(repo_root, &state)?;
50
51 Ok(BisectResponse {
52 action: "start".to_string(),
53 current: None,
54 remaining: 0,
55 steps: 0,
56 message: "Bisect started. Mark commits as good or bad.".to_string(),
57 })
58}
59
60fn bisect_mark(
61 repo_root: &std::path::Path,
62 commit: &str,
63 is_good: bool,
64) -> Result<BisectResponse, crate::errors::LitError> {
65 let mut state = load_bisect_state(repo_root)?;
66 let store = ObjectStore::new(repo_root);
67
68 let hash = if commit == "HEAD" {
69 read_head(repo_root)?
70 } else {
71 commit.to_string()
72 };
73
74 if is_good {
75 state.good.push(hash.clone());
76 } else {
77 state.bad.push(hash.clone());
78 }
79
80 if !state.good.is_empty() && !state.bad.is_empty() {
82 let bad_hash = state.bad.last().unwrap().clone();
83 let good_hash = state.good.last().unwrap().clone();
84
85 let commits = collect_commits_between(&store, &bad_hash, &good_hash)?;
87 state.remaining = commits;
88
89 if state.remaining.is_empty() {
90 save_bisect_state(repo_root, &state)?;
92 return Ok(BisectResponse {
93 action: if is_good { "good" } else { "bad" }.to_string(),
94 current: Some(bad_hash[..16.min(bad_hash.len())].to_string()),
95 remaining: 0,
96 steps: state.steps,
97 message: format!("First bad commit: {}", &bad_hash[..16.min(bad_hash.len())]),
98 });
99 }
100
101 let mid = state.remaining.len() / 2;
103 state.current = Some(state.remaining[mid].clone());
104 state.steps += 1;
105
106 let est_steps = (state.remaining.len() as f64).log2().ceil() as usize;
107
108 save_bisect_state(repo_root, &state)?;
109
110 let current_short = state
111 .current
112 .as_ref()
113 .map(|c| c[..16.min(c.len())].to_string());
114
115 Ok(BisectResponse {
116 action: if is_good { "good" } else { "bad" }.to_string(),
117 current: current_short,
118 remaining: state.remaining.len(),
119 steps: est_steps,
120 message: format!(
121 "Bisecting: {} commits left to test (~{} steps)",
122 state.remaining.len(),
123 est_steps
124 ),
125 })
126 } else {
127 save_bisect_state(repo_root, &state)?;
128 Ok(BisectResponse {
129 action: if is_good { "good" } else { "bad" }.to_string(),
130 current: None,
131 remaining: 0,
132 steps: 0,
133 message: format!(
134 "Marked {} as {}. Need both good and bad commits to start bisecting.",
135 &hash[..16.min(hash.len())],
136 if is_good { "good" } else { "bad" }
137 ),
138 })
139 }
140}
141
142fn bisect_reset(repo_root: &std::path::Path) -> Result<BisectResponse, crate::errors::LitError> {
143 let bisect_path = repo_root.join(".lit").join("bisect.json");
144 if bisect_path.exists() {
145 fs::remove_file(&bisect_path)
146 .map_err(|e| format!("Failed to remove bisect state: {}", e))?;
147 }
148
149 Ok(BisectResponse {
150 action: "reset".to_string(),
151 current: None,
152 remaining: 0,
153 steps: 0,
154 message: "Bisect reset".to_string(),
155 })
156}
157
158fn collect_commits_between(
159 store: &ObjectStore,
160 bad: &str,
161 good: &str,
162) -> Result<Vec<String>, crate::errors::LitError> {
163 let mut commits = Vec::new();
164 let mut current = bad.to_string();
165
166 loop {
167 if current == good {
168 break;
169 }
170
171 let hash = ObjectHash::from_hex(current.clone());
172 let commit = match store.read(&hash) {
173 Ok(Object::Commit(c)) => c,
174 _ => break,
175 };
176
177 commits.push(current);
178
179 match commit.parents.first() {
180 Some(p) => current = p.to_string(),
181 None => break,
182 }
183 }
184
185 Ok(commits)
186}
187
188fn load_bisect_state(repo_root: &std::path::Path) -> Result<BisectState, crate::errors::LitError> {
189 let path = repo_root.join(".lit").join("bisect.json");
190 if !path.exists() {
191 return Err("No bisect in progress. Run `lit bisect start` first.".into());
192 }
193 let data =
194 fs::read_to_string(&path).map_err(|e| format!("Failed to read bisect state: {}", e))?;
195 serde_json::from_str(&data).map_err(|e| format!("Failed to parse bisect state: {}", e).into())
196}
197
198fn save_bisect_state(
199 repo_root: &std::path::Path,
200 state: &BisectState,
201) -> Result<(), crate::errors::LitError> {
202 let path = repo_root.join(".lit").join("bisect.json");
203 let data = serde_json::to_string_pretty(state)
204 .map_err(|e| format!("Failed to serialize bisect state: {}", e))?;
205 fs::write(&path, data).map_err(|e| format!("Failed to write bisect state: {}", e).into())
206}