1use std::collections::BTreeSet;
6use std::io::Write;
7
8use mkit_core::hash::Hash;
9use mkit_core::ops::bisect::{
10 BisectState, BisectStep, cleanup_bisect, is_bisect_in_progress, next_step, read_state,
11 write_state,
12};
13use mkit_core::refs::{self, Head};
14use mkit_core::store::ObjectStore;
15
16use clap::{Parser, Subcommand};
17
18use crate::clap_shim;
19use crate::exit;
20use crate::format;
21
22#[derive(Debug, Parser)]
23#[command(
24 name = "mkit bisect",
25 about = "Binary-search for a regression-introducing commit."
26)]
27struct BisectOpts {
28 #[command(subcommand)]
29 sub: BisectCmd,
30}
31
32#[derive(Debug, Subcommand)]
33enum BisectCmd {
34 Start,
36 Good { commit: Option<String> },
38 Bad { commit: Option<String> },
40 Skip,
42 Reset,
44}
45
46#[must_use]
47pub fn run(args: &[String]) -> u8 {
48 let opts = match clap_shim::parse::<BisectOpts>("mkit bisect", args) {
49 Ok(o) => o,
50 Err(code) => return code,
51 };
52 let cwd = match std::env::current_dir() {
53 Ok(p) => p,
54 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
55 };
56 let store = match ObjectStore::open(&cwd) {
57 Ok(s) => s,
58 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
59 };
60 let mkit_dir = cwd.join(mkit_core::MKIT_DIR);
61
62 match opts.sub {
63 BisectCmd::Start => start(&mkit_dir),
64 BisectCmd::Good { commit } => mark(&store, &mkit_dir, commit.as_deref(), true),
65 BisectCmd::Bad { commit } => mark(&store, &mkit_dir, commit.as_deref(), false),
66 BisectCmd::Skip => skip(&store, &mkit_dir),
67 BisectCmd::Reset => reset(&mkit_dir),
68 }
69}
70
71fn start(mkit_dir: &std::path::Path) -> u8 {
72 if is_bisect_in_progress(mkit_dir) {
73 return emit_err(
74 "a bisect is already in progress (use `mkit bisect reset` first)",
75 exit::GENERAL_ERROR,
76 );
77 }
78 let orig_head = match refs::resolve_head(mkit_dir) {
79 Ok(Some(h)) => h,
80 Ok(None) => return emit_err("no commits yet", exit::GENERAL_ERROR),
81 Err(e) => return emit_err(&format!("resolve HEAD: {e}"), exit::GENERAL_ERROR),
82 };
83 let orig_branch = match refs::read_head(mkit_dir) {
84 Ok(Head::Branch(name)) => Some(name),
85 _ => None,
86 };
87 let state = BisectState {
88 orig_head,
89 orig_branch,
90 bad_hash: None,
91 good_hashes: Vec::new(),
92 skipped: BTreeSet::default(),
93 };
94 if let Err(e) = write_state(mkit_dir, &state) {
95 return emit_err(&format!("write state: {e}"), exit::CANTCREAT);
96 }
97 let mut stderr = std::io::stderr().lock();
98 let _ = writeln!(
99 stderr,
100 "bisect started; mark endpoints with `mkit bisect good <hash>` and `mkit bisect bad <hash>`"
101 );
102 exit::OK
103}
104
105fn mark(store: &ObjectStore, mkit_dir: &std::path::Path, arg: Option<&str>, good: bool) -> u8 {
106 if !is_bisect_in_progress(mkit_dir) {
107 return emit_err("no bisect in progress", exit::GENERAL_ERROR);
108 }
109 let mut state = match read_state(mkit_dir) {
110 Ok(s) => s,
111 Err(e) => return emit_err(&format!("read state: {e}"), exit::GENERAL_ERROR),
112 };
113 let hash_: Hash = match arg {
114 Some(s) => match super::revspec::resolve_revision(store, mkit_dir, s) {
115 Ok(h) => h,
116 Err(e) => return emit_err(&format!("bad commit: {e}"), exit::DATAERR),
117 },
118 None => match refs::resolve_head(mkit_dir) {
119 Ok(Some(h)) => h,
120 _ => return emit_err("no HEAD; provide an explicit hash", exit::GENERAL_ERROR),
121 },
122 };
123 if good {
124 state.good_hashes.push(hash_);
125 } else {
126 state.bad_hash = Some(hash_);
127 }
128 if let Err(e) = write_state(mkit_dir, &state) {
129 return emit_err(&format!("persist state: {e}"), exit::CANTCREAT);
130 }
131 report_step(store, &state)
132}
133
134fn skip(store: &ObjectStore, mkit_dir: &std::path::Path) -> u8 {
135 if !is_bisect_in_progress(mkit_dir) {
136 return emit_err("no bisect in progress", exit::GENERAL_ERROR);
137 }
138 let mut state = match read_state(mkit_dir) {
139 Ok(s) => s,
140 Err(e) => return emit_err(&format!("read state: {e}"), exit::GENERAL_ERROR),
141 };
142 let current_mid = match next_step(store, &state) {
144 Ok(BisectStep::Testing { hash, .. }) => hash,
145 Ok(_) => {
146 return emit_err("bisect skip: no current candidate to skip", exit::USAGE);
151 }
152 Err(e) => return emit_err(&format!("bisect skip: {e}"), exit::GENERAL_ERROR),
153 };
154 state.skipped.insert(current_mid);
156 if let Err(e) = write_state(mkit_dir, &state) {
157 return emit_err(&format!("persist state: {e}"), exit::CANTCREAT);
158 }
159 let mut stderr = std::io::stderr().lock();
160 let _ = writeln!(
161 stderr,
162 "skipped {}; advancing to next candidate",
163 format::short_hash(¤t_mid, 12)
164 );
165 drop(stderr);
166 report_step(store, &state)
167}
168
169fn reset(mkit_dir: &std::path::Path) -> u8 {
170 if !is_bisect_in_progress(mkit_dir) {
171 return emit_err("no bisect in progress", exit::GENERAL_ERROR);
172 }
173 let state = match read_state(mkit_dir) {
174 Ok(s) => s,
175 Err(e) => return emit_err(&format!("read state: {e}"), exit::GENERAL_ERROR),
176 };
177 if let Some(branch) = state.orig_branch.as_deref() {
178 let _ = refs::write_head_branch(mkit_dir, branch);
179 } else {
180 let _ = refs::write_head_detached(mkit_dir, &state.orig_head);
181 }
182 let _ = cleanup_bisect(mkit_dir);
183 let mut stderr = std::io::stderr().lock();
184 let _ = writeln!(stderr, "bisect reset");
185 exit::OK
186}
187
188fn report_step(store: &ObjectStore, state: &BisectState) -> u8 {
189 match next_step(store, state) {
190 Ok(BisectStep::NeedMore) => {
191 let mut stderr = std::io::stderr().lock();
192 let _ = writeln!(
193 stderr,
194 "need at least one good and a bad commit to start searching"
195 );
196 exit::OK
197 }
198 Ok(BisectStep::Testing { hash, remaining }) => {
199 let mut stderr = std::io::stderr().lock();
202 let _ = writeln!(stderr, "bisect: testing ({remaining} candidates remaining)");
203 drop(stderr);
204 let mut stdout = std::io::stdout().lock();
205 let _ = writeln!(stdout, "{}", format::short_hash(&hash, 12));
206 exit::OK
207 }
208 Ok(BisectStep::Found(h)) => {
209 let mut stderr = std::io::stderr().lock();
212 let _ = writeln!(stderr, "bisect found first bad commit:");
213 drop(stderr);
214 let mut stdout = std::io::stdout().lock();
215 let _ = writeln!(stdout, "{}", format::short_hash(&h, 12));
216 exit::OK
217 }
218 Err(e) => emit_err(&format!("bisect: {e}"), exit::GENERAL_ERROR),
219 }
220}
221
222fn emit_err(msg: &str, code: u8) -> u8 {
223 let mut stderr = std::io::stderr().lock();
224 let _ = writeln!(stderr, "error: {msg}");
225 code
226}