1use std::io::Write;
17
18use clap::Parser;
19use mkit_core::hash::Hash;
20use mkit_core::object::{Commit, Object};
21use mkit_core::ops::cherry_pick::cherry_pick;
22use mkit_core::ops::conflict_state::{
23 self, CherryPickState, in_progress_op_name, is_cherry_pick_in_progress,
24};
25use mkit_core::refs::{self, Head};
26use mkit_core::serialize;
27use mkit_core::store::ObjectStore;
28use mkit_core::worktree;
29
30use crate::clap_shim;
31use crate::config;
32use crate::exit;
33use crate::format;
34
35#[derive(Debug, Parser)]
36#[command(name = "mkit cherry-pick", about = "Apply a single commit onto HEAD.")]
37struct CherryPickOpts {
38 #[arg(long = "continue", conflicts_with_all = ["abort", "commit"])]
40 cont: bool,
41 #[arg(long, conflicts_with_all = ["cont", "commit"])]
43 abort: bool,
44 commit: Option<String>,
46}
47
48#[must_use]
49pub fn run(args: &[String]) -> u8 {
50 let opts = match clap_shim::parse::<CherryPickOpts>("mkit cherry-pick", args) {
51 Ok(o) => o,
52 Err(code) => return code,
53 };
54 let cwd = match std::env::current_dir() {
55 Ok(p) => p,
56 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
57 };
58 let store = match ObjectStore::open(&cwd) {
59 Ok(s) => s,
60 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
61 };
62 let mkit_dir = cwd.join(mkit_core::MKIT_DIR);
63 let _lock = match super::acquire_worktree_lock(&cwd) {
64 Ok(l) => l,
65 Err(code) => return code,
66 };
67
68 if opts.abort {
69 abort(&cwd, &mkit_dir, &store)
70 } else if opts.cont {
71 cont(&cwd, &mkit_dir, &store)
72 } else if let Some(hex) = opts.commit.as_deref() {
73 start(&cwd, &mkit_dir, &store, hex)
74 } else {
75 super::usage_error("usage: mkit cherry-pick <commit> | --continue | --abort")
76 }
77}
78
79#[allow(clippy::too_many_lines)]
80fn start(cwd: &std::path::Path, mkit_dir: &std::path::Path, store: &ObjectStore, hex: &str) -> u8 {
81 if let Some(op) = in_progress_op_name(mkit_dir) {
82 return emit_err(
83 &format!("a {op} is already in progress (use --continue or --abort)"),
84 exit::GENERAL_ERROR,
85 );
86 }
87 let target: Hash = match super::revspec::resolve_revision(store, mkit_dir, hex) {
88 Ok(h) => h,
89 Err(e) => return emit_err(&format!("bad commit: {e}"), exit::DATAERR),
90 };
91
92 let ours = match refs::resolve_head(mkit_dir) {
93 Ok(Some(h)) => h,
94 Ok(None) => return emit_err("no commits on current branch", exit::GENERAL_ERROR),
95 Err(e) => return emit_err(&format!("resolve HEAD: {e}"), exit::GENERAL_ERROR),
96 };
97 let ours_tree = match store.read_object(&ours) {
98 Ok(Object::Commit(c)) => c.tree_hash,
99 Ok(_) => return emit_err("HEAD is not a commit", exit::DATAERR),
100 Err(e) => return emit_err(&format!("read HEAD: {e}"), exit::GENERAL_ERROR),
101 };
102
103 let result = match cherry_pick(store, target, ours_tree) {
104 Ok(r) => r,
105 Err(e) => return emit_err(&format!("cherry-pick: {e}"), exit::GENERAL_ERROR),
106 };
107
108 if result.has_conflicts() {
109 if let Err(e) = super::ensure_restore_safe(cwd, store, result.tree_hash) {
110 return emit_err(&e, exit::GENERAL_ERROR);
111 }
112 let records = match super::conflict::materialize_conflicts(
113 cwd,
114 store,
115 result.tree_hash,
116 &result.conflicts,
117 ) {
118 Ok(r) => r,
119 Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
120 };
121 let state = CherryPickState {
122 cherry_pick_head: target,
123 orig_head: ours,
124 message: result.original_message.clone(),
125 };
126 if let Err(e) = conflict_state::write_cherry_pick_state(mkit_dir, &state, &records) {
127 return emit_err(&format!("write cherry-pick state: {e}"), exit::CANTCREAT);
128 }
129 let mut stderr = std::io::stderr().lock();
130 let _ = writeln!(
131 stderr,
132 "cherry-pick conflict; resolve the files above, `mkit add` them, then run \
133 `mkit cherry-pick --continue` (or `mkit cherry-pick --abort`)"
134 );
135 return exit::GENERAL_ERROR;
136 }
137
138 if let Err(e) = super::ensure_restore_safe(cwd, store, result.tree_hash) {
139 return emit_err(&e, exit::GENERAL_ERROR);
140 }
141
142 let commit_hash = match create_commit(
143 cwd,
144 store,
145 result.tree_hash,
146 ours,
147 &result.original_message,
148 target,
149 ) {
150 Ok(h) => h,
151 Err(code) => return code,
152 };
153 if let Err(e) = super::restore_worktree_and_index(cwd, store, result.tree_hash) {
154 return emit_err(&e, exit::GENERAL_ERROR);
155 }
156 if let Err(e) = advance_head(mkit_dir, &commit_hash) {
157 return emit_err(&e, exit::CANTCREAT);
158 }
159 let mut stderr = std::io::stderr().lock();
160 let _ = writeln!(
161 stderr,
162 "cherry-picked {} onto {} as {}",
163 format::short_hash(&target, 8),
164 format::short_hash(&ours, 8),
165 format::short_hash(&commit_hash, 8),
166 );
167 exit::OK
168}
169
170fn cont(cwd: &std::path::Path, mkit_dir: &std::path::Path, store: &ObjectStore) -> u8 {
171 if !is_cherry_pick_in_progress(mkit_dir) {
172 return emit_err("no cherry-pick in progress", exit::GENERAL_ERROR);
173 }
174 let state = match conflict_state::read_cherry_pick_state(mkit_dir) {
175 Ok(Some(s)) => s,
176 Ok(None) => return emit_err("no cherry-pick in progress", exit::GENERAL_ERROR),
177 Err(e) => return emit_err(&format!("read cherry-pick state: {e}"), exit::GENERAL_ERROR),
178 };
179 let records = match conflict_state::read_conflicts(mkit_dir) {
180 Ok(r) => r,
181 Err(e) => return emit_err(&format!("read conflicts: {e}"), exit::GENERAL_ERROR),
182 };
183 match super::conflict::first_unresolved_marker(cwd, &records) {
184 Ok(Some(path)) => {
185 return emit_err(
186 &format!(
187 "unresolved conflict markers remain in '{path}'; resolve and `mkit add` it"
188 ),
189 exit::GENERAL_ERROR,
190 );
191 }
192 Ok(None) => {}
193 Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
194 }
195 if let Err(e) = super::conflict::ensure_conflict_paths_staged(cwd, store, &records) {
196 return emit_err(&e, exit::GENERAL_ERROR);
197 }
198
199 let idx = match super::read_or_seed_index_from_head(cwd, store) {
202 Ok(i) => i,
203 Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
204 };
205 let tree_hash = match worktree::build_tree_from_index(store, &idx) {
206 Ok(t) => t,
207 Err(e) => return emit_err(&format!("build tree from index: {e}"), exit::GENERAL_ERROR),
208 };
209
210 let parent = match refs::resolve_head(mkit_dir) {
211 Ok(Some(h)) => h,
212 Ok(None) => state.orig_head,
213 Err(e) => return emit_err(&format!("resolve HEAD: {e}"), exit::GENERAL_ERROR),
214 };
215 let commit_hash = match create_commit(
216 cwd,
217 store,
218 tree_hash,
219 parent,
220 &state.message,
221 state.cherry_pick_head,
222 ) {
223 Ok(h) => h,
224 Err(code) => return code,
225 };
226 if let Err(e) = super::restore_worktree_and_index(cwd, store, tree_hash) {
227 return emit_err(&e, exit::GENERAL_ERROR);
228 }
229 if let Err(e) = advance_head(mkit_dir, &commit_hash) {
230 return emit_err(&e, exit::CANTCREAT);
231 }
232 if let Err(e) = conflict_state::clear_cherry_pick_state(mkit_dir) {
233 return emit_err(
234 &format!("clear cherry-pick state: {e}"),
235 exit::GENERAL_ERROR,
236 );
237 }
238 let mut stderr = std::io::stderr().lock();
239 let _ = writeln!(
240 stderr,
241 "cherry-picked {} as {}",
242 format::short_hash(&state.cherry_pick_head, 8),
243 format::short_hash(&commit_hash, 8),
244 );
245 exit::OK
246}
247
248fn abort(cwd: &std::path::Path, mkit_dir: &std::path::Path, store: &ObjectStore) -> u8 {
249 if !is_cherry_pick_in_progress(mkit_dir) {
250 return emit_err("no cherry-pick in progress", exit::GENERAL_ERROR);
251 }
252 let state = match conflict_state::read_cherry_pick_state(mkit_dir) {
253 Ok(Some(s)) => s,
254 Ok(None) => return emit_err("no cherry-pick in progress", exit::GENERAL_ERROR),
255 Err(e) => return emit_err(&format!("read cherry-pick state: {e}"), exit::GENERAL_ERROR),
256 };
257 let records = match conflict_state::read_conflicts(mkit_dir) {
258 Ok(r) => r,
259 Err(e) => return emit_err(&format!("read conflicts: {e}"), exit::GENERAL_ERROR),
260 };
261 if let Err(code) = restore_to(cwd, mkit_dir, store, state.orig_head, &records) {
262 return code;
263 }
264 if let Err(e) = conflict_state::clear_cherry_pick_state(mkit_dir) {
265 return emit_err(
266 &format!("clear cherry-pick state: {e}"),
267 exit::GENERAL_ERROR,
268 );
269 }
270 let mut stderr = std::io::stderr().lock();
271 let _ = writeln!(stderr, "cherry-pick aborted; HEAD restored");
272 exit::OK
273}
274
275fn restore_to(
276 cwd: &std::path::Path,
277 mkit_dir: &std::path::Path,
278 store: &ObjectStore,
279 target: Hash,
280 records: &[mkit_core::ops::conflict_state::ConflictRecord],
281) -> Result<(), u8> {
282 let target_tree = load_tree_hash(store, target)?;
283 if let Err(e) = super::conflict::ensure_abort_safe(cwd, store, records, target_tree) {
288 return Err(emit_err(&e, exit::GENERAL_ERROR));
289 }
290 if let Err(e) = super::conflict::reset_conflict_paths(cwd, store, records, target_tree) {
291 return Err(emit_err(&e, exit::GENERAL_ERROR));
292 }
293 if let Err(e) = super::ensure_restore_safe(cwd, store, target_tree) {
294 return Err(emit_err(&e, exit::GENERAL_ERROR));
295 }
296 if let Err(e) = super::restore_worktree_and_index(cwd, store, target_tree) {
297 return Err(emit_err(&e, exit::GENERAL_ERROR));
298 }
299 let head = refs::read_head(mkit_dir).unwrap_or(Head::Branch("main".to_string()));
300 match head {
301 Head::Branch(name) => {
302 if let Err(e) = super::write_ref_recording_history(
303 mkit_dir,
304 &name,
305 refs::RefWriteCondition::Any,
306 &target,
307 ) {
308 return Err(emit_err(&format!("restore ref: {e}"), exit::CANTCREAT));
309 }
310 }
311 Head::Detached(_) => {
312 if let Err(e) = refs::write_head_detached(mkit_dir, &target) {
313 return Err(emit_err(&format!("restore HEAD: {e}"), exit::CANTCREAT));
314 }
315 }
316 }
317 Ok(())
318}
319
320fn create_commit(
321 cwd: &std::path::Path,
322 store: &ObjectStore,
323 tree_hash: Hash,
324 parent: Hash,
325 message: &[u8],
326 picked: Hash,
327) -> Result<Hash, u8> {
328 let cfg = config::read_or_default(cwd)
329 .map_err(|e| emit_err(&format!("config: {e}"), exit::CONFIG_ERROR))?;
330 let mut signer =
331 super::commit::load_commit_signer(cwd, &cfg).map_err(|(msg, code)| emit_err(&msg, code))?;
332 let signer_public = signer
333 .public_key()
334 .map_err(|(msg, code)| emit_err(&msg, code))?;
335 let (author, timestamp) = match store.read_object(&picked) {
338 Ok(Object::Commit(c)) => (c.author, c.timestamp),
339 Ok(_) => return Err(emit_err("picked object is not a commit", exit::DATAERR)),
340 Err(e) => {
341 return Err(emit_err(
342 &format!("read picked commit: {e}"),
343 exit::GENERAL_ERROR,
344 ));
345 }
346 };
347 let mut unsigned = Commit::new_unannotated(
348 tree_hash,
349 vec![parent],
350 author,
351 signer_public,
352 message.to_vec(),
353 timestamp,
354 [0u8; 64],
355 );
356 let sig = signer
357 .sign_commit(&unsigned)
358 .map_err(|(msg, code)| emit_err(&msg, code))?;
359 unsigned.signature = sig;
360 let bytes = serialize::serialize(&Object::Commit(unsigned))
361 .map_err(|e| emit_err(&format!("serialize: {e}"), exit::DATAERR))?;
362 store
363 .write(&bytes)
364 .map_err(|e| emit_err(&format!("store commit: {e}"), exit::CANTCREAT))
365}
366
367fn load_tree_hash(store: &ObjectStore, commit_hash: Hash) -> Result<Hash, u8> {
368 match store.read_object(&commit_hash) {
369 Ok(Object::Commit(c)) => Ok(c.tree_hash),
370 Ok(_) => Err(emit_err("object is not a commit", exit::DATAERR)),
371 Err(e) => Err(emit_err(&format!("read commit: {e}"), exit::GENERAL_ERROR)),
372 }
373}
374
375fn advance_head(mkit_dir: &std::path::Path, new_head: &Hash) -> Result<(), String> {
376 let head = refs::read_head(mkit_dir).unwrap_or(Head::Branch("main".to_string()));
377 match head {
378 Head::Branch(name) => super::write_ref_recording_history(
379 mkit_dir,
380 &name,
381 refs::RefWriteCondition::Any,
382 new_head,
383 )
384 .map_err(|e| format!("write ref: {e}")),
385 Head::Detached(_) => {
386 refs::write_head_detached(mkit_dir, new_head).map_err(|e| format!("update HEAD: {e}"))
387 }
388 }
389}
390
391fn emit_err(msg: &str, code: u8) -> u8 {
392 let mut stderr = std::io::stderr().lock();
393 let _ = writeln!(stderr, "error: {msg}");
394 code
395}