1use std::io::Write;
17
18use clap::{Parser, ValueEnum};
19use mkit_core::hash::Hash;
20use mkit_core::layout::RepoLayout;
21use mkit_core::object::{Commit, Object};
22use mkit_core::ops::cherry_pick::{CherryPickError, cherry_pick};
23use mkit_core::ops::conflict_state::{
24 self, CherryPickState, in_progress_op_name, is_cherry_pick_in_progress,
25};
26use mkit_core::refs::{self, Head};
27use mkit_core::serialize;
28use mkit_core::store::ObjectStore;
29use mkit_core::worktree;
30
31use super::{advance_head, error as emit_err, load_tree_hash};
32use crate::clap_shim;
33use crate::config;
34use crate::exit;
35use crate::format::{self, JsonObject, json_string_array};
36
37#[derive(Debug, Clone, Copy, ValueEnum)]
38enum CherryPickFormat {
39 Default,
40 Json,
41}
42
43#[derive(Debug, Parser)]
44#[command(name = "mkit cherry-pick", about = "Apply a single commit onto HEAD.")]
45struct CherryPickOpts {
46 #[arg(long = "continue", conflicts_with_all = ["abort", "commit"])]
48 cont: bool,
49 #[arg(long, conflicts_with_all = ["cont", "commit"])]
51 abort: bool,
52 #[arg(short = 'n', long = "no-commit", conflicts_with_all = ["cont", "abort"])]
56 no_commit: bool,
57 #[arg(short = 'm', long = "mainline", value_name = "PARENT-NUMBER", conflicts_with_all = ["cont", "abort"])]
61 mainline: Option<usize>,
62 #[arg(long, value_enum, default_value = "default")]
66 format: CherryPickFormat,
67 commit: Option<String>,
69}
70
71fn emit_err_json(msg: &str, code: u8, json: bool) -> u8 {
74 if json {
75 let mut obj = JsonObject::new();
76 obj.field_bool("ok", false).field_str("error", msg);
77 let mut stdout = std::io::stdout().lock();
78 let _ = writeln!(stdout, "{}", obj.finish());
79 }
80 emit_err(msg, code)
81}
82
83#[must_use]
84pub fn run(args: &[String]) -> u8 {
85 let opts = match clap_shim::parse::<CherryPickOpts>("mkit cherry-pick", args) {
86 Ok(o) => o,
87 Err(code) => return code,
88 };
89 let json = matches!(opts.format, CherryPickFormat::Json);
90 let cwd = match std::env::current_dir() {
91 Ok(p) => p,
92 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
93 };
94 let layout = match super::resolve_layout(&cwd) {
95 Ok(layout) => layout,
96 Err(code) => return code,
97 };
98 let store = match ObjectStore::open(&layout) {
99 Ok(s) => s,
100 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
101 };
102 let _lock = match super::acquire_worktree_lock(&layout) {
103 Ok(l) => l,
104 Err(code) => return code,
105 };
106
107 if opts.abort {
108 abort(&layout, &store, json)
109 } else if opts.cont {
110 cont(&layout, &store, json)
111 } else if let Some(hex) = opts.commit.as_deref() {
112 start(&layout, &store, hex, opts.no_commit, opts.mainline, json)
113 } else {
114 super::usage_error("usage: mkit cherry-pick <commit> | --continue | --abort")
115 }
116}
117
118#[allow(clippy::too_many_lines)]
119fn start(
120 layout: &RepoLayout,
121 store: &ObjectStore,
122 hex: &str,
123 no_commit: bool,
124 mainline: Option<usize>,
125 json: bool,
126) -> u8 {
127 let emit_err = |msg: &str, code: u8| emit_err_json(msg, code, json);
128 if let Some(op) = in_progress_op_name(layout) {
129 return emit_err(
130 &format!("a {op} is already in progress (use --continue or --abort)"),
131 exit::GENERAL_ERROR,
132 );
133 }
134 let target: Hash = match super::revspec::resolve_revision(store, layout, hex) {
135 Ok(h) => super::log::peel_tags(store, h),
140 Err(e) => return emit_err(&format!("bad commit: {e}"), exit::DATAERR),
141 };
142
143 let ours = match refs::resolve_head(layout) {
144 Ok(Some(h)) => h,
145 Ok(None) => return emit_err("no commits on current branch", exit::GENERAL_ERROR),
146 Err(e) => return emit_err(&format!("resolve HEAD: {e}"), exit::GENERAL_ERROR),
147 };
148 let ours_tree = match store.read_object(&ours) {
149 Ok(Object::Commit(c)) => c.tree_hash,
150 Ok(_) => return emit_err("HEAD is not a commit", exit::DATAERR),
151 Err(e) => return emit_err(&format!("read HEAD: {e}"), exit::GENERAL_ERROR),
152 };
153
154 let result = match cherry_pick(store, target, ours_tree, mainline) {
155 Ok(r) => r,
156 Err(
159 e @ (CherryPickError::MergeNeedsMainline
160 | CherryPickError::MainlineForNonMerge
161 | CherryPickError::BadMainline { .. }),
162 ) => return emit_err(&format!("cherry-pick: {e}"), exit::USAGE),
163 Err(e) => return emit_err(&format!("cherry-pick: {e}"), exit::GENERAL_ERROR),
164 };
165
166 if result.has_conflicts() {
167 if no_commit {
175 return emit_err(
176 &format!(
177 "cherry-pick -n of {} conflicts; mkit cannot stage an unresolved \
178 conflict without committing — re-run without -n, then resolve and \
179 `mkit cherry-pick --continue`",
180 format::short_hash(&target, 8)
181 ),
182 exit::GENERAL_ERROR,
183 );
184 }
185 if let Err(e) = super::ensure_restore_safe(layout, store, result.tree_hash) {
186 return emit_err(&e, exit::GENERAL_ERROR);
187 }
188 let records = match super::conflict::materialize_conflicts(
189 layout,
190 store,
191 result.tree_hash,
192 &result.conflicts,
193 ) {
194 Ok(r) => r,
195 Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
196 };
197 let state = CherryPickState {
198 cherry_pick_head: target,
199 orig_head: ours,
200 message: result.original_message.clone(),
201 };
202 if let Err(e) = conflict_state::write_cherry_pick_state(layout, &state, &records) {
203 return emit_err(&format!("write cherry-pick state: {e}"), exit::CANTCREAT);
204 }
205 if let Err(e) =
208 conflict_state::write_result_tree(layout.worktree_state_dir(), &result.tree_hash)
209 {
210 return emit_err(&format!("write cherry-pick state: {e}"), exit::CANTCREAT);
211 }
212 let mut stderr = std::io::stderr().lock();
213 for rec in &records {
216 let _ = writeln!(stderr, "CONFLICT (content): Merge conflict in {}", rec.path);
217 }
218 let _ = writeln!(
219 stderr,
220 "hint: resolve the files above, `mkit add` them, then run \
221 `mkit cherry-pick --continue` (or `mkit cherry-pick --abort`)"
222 );
223 drop(stderr);
224 if json {
225 let paths: Vec<&str> = records.iter().map(|r| r.path.as_str()).collect();
226 let mut obj = JsonObject::new();
227 obj.field_bool("ok", false)
228 .field_str("kind", "conflict")
229 .field_raw("conflicts", &json_string_array(&paths))
230 .field_str(
231 "error",
232 "cherry-pick conflict; resolve and continue or abort",
233 );
234 let mut stdout = std::io::stdout().lock();
235 let _ = writeln!(stdout, "{}", obj.finish());
236 }
237 return exit::GENERAL_ERROR;
238 }
239
240 if let Err(e) = super::ensure_restore_safe(layout, store, result.tree_hash) {
241 return emit_err(&e, exit::GENERAL_ERROR);
242 }
243
244 if no_commit {
248 if let Err(e) = super::restore_worktree_and_index(layout, store, result.tree_hash) {
249 return emit_err(&e, exit::GENERAL_ERROR);
250 }
251 if let Err(e) =
255 super::stage_removed_tombstones(layout, store, Some(ours_tree), result.tree_hash)
256 {
257 return emit_err(&e, exit::GENERAL_ERROR);
258 }
259 let mut stderr = std::io::stderr().lock();
260 let _ = writeln!(
261 stderr,
262 "staged cherry-pick of {} (no commit; run `mkit commit` when ready)",
263 format::short_hash(&target, 8),
264 );
265 drop(stderr);
266 if json {
267 let mut obj = JsonObject::new();
268 obj.field_bool("ok", true)
269 .field_str("kind", "no-commit")
270 .field_hash("picked", &target)
271 .field_hash("tree", &result.tree_hash);
272 let mut stdout = std::io::stdout().lock();
273 let _ = writeln!(stdout, "{}", obj.finish());
274 }
275 return exit::OK;
276 }
277
278 let commit_hash = match create_commit(
279 layout,
280 store,
281 result.tree_hash,
282 ours,
283 &result.original_message,
284 target,
285 ) {
286 Ok(h) => h,
287 Err(code) => return code,
288 };
289 if let Err(e) = super::restore_worktree_and_index(layout, store, result.tree_hash) {
290 return emit_err(&e, exit::GENERAL_ERROR);
291 }
292 if let Err(e) = advance_head(layout, &commit_hash) {
293 return emit_err(&e, exit::CANTCREAT);
294 }
295 let subject = String::from_utf8_lossy(&result.original_message)
297 .lines()
298 .next()
299 .unwrap_or("")
300 .to_owned();
301 let branch_name = match refs::read_head(layout) {
302 Ok(Head::Branch(b)) => Some(b),
303 _ => None,
304 };
305 let head_ref = match &branch_name {
306 Some(b) => super::summary::HeadRef::Branch(b),
307 None => super::summary::HeadRef::Detached,
308 };
309 let mut stderr = std::io::stderr().lock();
310 super::summary::print_commit_summary(
311 &mut stderr,
312 store,
313 &head_ref,
314 &commit_hash,
315 &subject,
316 false,
317 Some(ours_tree),
318 Some(result.tree_hash),
319 );
320 drop(stderr);
321 if json {
322 let mut obj = JsonObject::new();
323 obj.field_bool("ok", true)
324 .field_str("kind", "commit")
325 .field_hash("hash", &commit_hash)
326 .field_hash("picked", &target)
327 .field_hash("tree", &result.tree_hash);
328 let mut stdout = std::io::stdout().lock();
329 let _ = writeln!(stdout, "{}", obj.finish());
330 }
331 exit::OK
332}
333
334fn cont(layout: &RepoLayout, store: &ObjectStore, json: bool) -> u8 {
335 let emit_err = |msg: &str, code: u8| emit_err_json(msg, code, json);
336 if !is_cherry_pick_in_progress(layout) {
337 return emit_err("no cherry-pick in progress", exit::GENERAL_ERROR);
338 }
339 let state = match conflict_state::read_cherry_pick_state(layout) {
340 Ok(Some(s)) => s,
341 Ok(None) => return emit_err("no cherry-pick in progress", exit::GENERAL_ERROR),
342 Err(e) => return emit_err(&format!("read cherry-pick state: {e}"), exit::GENERAL_ERROR),
343 };
344 let records = match conflict_state::read_conflicts(layout.worktree_state_dir()) {
345 Ok(r) => r,
346 Err(e) => return emit_err(&format!("read conflicts: {e}"), exit::GENERAL_ERROR),
347 };
348 match super::conflict::first_unresolved_marker(layout.worktree_root(), &records) {
349 Ok(Some(path)) => {
350 return emit_err(
351 &format!(
352 "unresolved conflict markers remain in '{path}'; resolve and `mkit add` it"
353 ),
354 exit::GENERAL_ERROR,
355 );
356 }
357 Ok(None) => {}
358 Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
359 }
360 if let Err(e) = super::conflict::ensure_conflict_paths_staged(layout, store, &records) {
361 return emit_err(&e, exit::GENERAL_ERROR);
362 }
363
364 let idx = match super::read_or_seed_index_from_head(layout, store) {
367 Ok(i) => i,
368 Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
369 };
370 let tree_hash = match worktree::build_tree_from_index(store, &idx) {
371 Ok(t) => t,
372 Err(e) => return emit_err(&format!("build tree from index: {e}"), exit::GENERAL_ERROR),
373 };
374
375 let parent = match refs::resolve_head(layout) {
376 Ok(Some(h)) => h,
377 Ok(None) => state.orig_head,
378 Err(e) => return emit_err(&format!("resolve HEAD: {e}"), exit::GENERAL_ERROR),
379 };
380 let commit_hash = match create_commit(
381 layout,
382 store,
383 tree_hash,
384 parent,
385 &state.message,
386 state.cherry_pick_head,
387 ) {
388 Ok(h) => h,
389 Err(code) => return code,
390 };
391 if let Err(e) = super::sync_index_to_tree(layout, store, tree_hash) {
396 return emit_err(&e, exit::GENERAL_ERROR);
397 }
398 if let Err(e) = advance_head(layout, &commit_hash) {
399 return emit_err(&e, exit::CANTCREAT);
400 }
401 if let Err(e) = conflict_state::clear_cherry_pick_state(layout) {
402 return emit_err(
403 &format!("clear cherry-pick state: {e}"),
404 exit::GENERAL_ERROR,
405 );
406 }
407 let mut stderr = std::io::stderr().lock();
408 let _ = writeln!(
409 stderr,
410 "cherry-picked {} as {}",
411 format::short_hash(&state.cherry_pick_head, 8),
412 format::short_hash(&commit_hash, 8),
413 );
414 drop(stderr);
415 if json {
416 let mut obj = JsonObject::new();
417 obj.field_bool("ok", true)
418 .field_str("kind", "commit")
419 .field_hash("hash", &commit_hash)
420 .field_hash("picked", &state.cherry_pick_head)
421 .field_hash("tree", &tree_hash);
422 let mut stdout = std::io::stdout().lock();
423 let _ = writeln!(stdout, "{}", obj.finish());
424 }
425 exit::OK
426}
427
428fn abort(layout: &RepoLayout, store: &ObjectStore, json: bool) -> u8 {
429 let emit_err = |msg: &str, code: u8| emit_err_json(msg, code, json);
430 if !is_cherry_pick_in_progress(layout) {
431 return emit_err("no cherry-pick in progress", exit::GENERAL_ERROR);
432 }
433 let state = match conflict_state::read_cherry_pick_state(layout) {
434 Ok(Some(s)) => s,
435 Ok(None) => return emit_err("no cherry-pick in progress", exit::GENERAL_ERROR),
436 Err(e) => return emit_err(&format!("read cherry-pick state: {e}"), exit::GENERAL_ERROR),
437 };
438 let records = match conflict_state::read_conflicts(layout.worktree_state_dir()) {
439 Ok(r) => r,
440 Err(e) => return emit_err(&format!("read conflicts: {e}"), exit::GENERAL_ERROR),
441 };
442 if let Err(code) = restore_to(layout, store, state.orig_head, &records) {
443 return code;
444 }
445 if let Err(e) = conflict_state::clear_cherry_pick_state(layout) {
446 return emit_err(
447 &format!("clear cherry-pick state: {e}"),
448 exit::GENERAL_ERROR,
449 );
450 }
451 let mut stderr = std::io::stderr().lock();
452 let _ = writeln!(stderr, "cherry-pick aborted; HEAD restored");
453 drop(stderr);
454 if json {
455 let mut obj = JsonObject::new();
456 obj.field_bool("ok", true)
457 .field_str("kind", "aborted")
458 .field_hash("hash", &state.orig_head);
459 let mut stdout = std::io::stdout().lock();
460 let _ = writeln!(stdout, "{}", obj.finish());
461 }
462 exit::OK
463}
464
465fn restore_to(
466 layout: &RepoLayout,
467 store: &ObjectStore,
468 target: Hash,
469 records: &[mkit_core::ops::conflict_state::ConflictRecord],
470) -> Result<(), u8> {
471 let target_tree = load_tree_hash(store, target)?;
472 let op_result = conflict_state::read_result_tree(layout.worktree_state_dir())
475 .ok()
476 .flatten();
477 if let Err(e) =
482 super::conflict::ensure_abort_safe(layout, store, records, target_tree, op_result)
483 {
484 return Err(emit_err(&e, exit::GENERAL_ERROR));
485 }
486 if let Err(e) =
487 super::conflict::reset_conflict_paths(layout, store, records, target_tree, op_result)
488 {
489 return Err(emit_err(&e, exit::GENERAL_ERROR));
490 }
491 if let Err(e) = super::ensure_restore_safe(layout, store, target_tree) {
492 return Err(emit_err(&e, exit::GENERAL_ERROR));
493 }
494 if let Err(e) = super::restore_worktree_and_index(layout, store, target_tree) {
495 return Err(emit_err(&e, exit::GENERAL_ERROR));
496 }
497 super::restore_head_ref(layout, &target)
498}
499
500fn create_commit(
501 layout: &RepoLayout,
502 store: &ObjectStore,
503 tree_hash: Hash,
504 parent: Hash,
505 message: &[u8],
506 picked: Hash,
507) -> Result<Hash, u8> {
508 let cfg = config::read_or_default(layout)
509 .map_err(|e| emit_err(&format!("config: {e}"), exit::CONFIG_ERROR))?;
510 let mut signer = super::commit::load_commit_signer(layout, &cfg)
511 .map_err(|(msg, code)| emit_err(&msg, code))?;
512 let signer_public = signer
513 .public_key()
514 .map_err(|(msg, code)| emit_err(&msg, code))?;
515 let (author, timestamp) = match store.read_object(&picked) {
518 Ok(Object::Commit(c)) => (c.author, c.timestamp),
519 Ok(_) => return Err(emit_err("picked object is not a commit", exit::DATAERR)),
520 Err(e) => {
521 return Err(emit_err(
522 &format!("read picked commit: {e}"),
523 exit::GENERAL_ERROR,
524 ));
525 }
526 };
527 let mut unsigned = Commit::new_unannotated(
528 tree_hash,
529 vec![parent],
530 author,
531 signer_public,
532 message.to_vec(),
533 timestamp,
534 [0u8; 64],
535 );
536 let sig = signer
537 .sign_commit(&unsigned)
538 .map_err(|(msg, code)| emit_err(&msg, code))?;
539 unsigned.signature = sig;
540 let bytes = serialize::serialize(&Object::Commit(unsigned))
541 .map_err(|e| emit_err(&format!("serialize: {e}"), exit::DATAERR))?;
542 store
543 .write(&bytes)
544 .map_err(|e| emit_err(&format!("store commit: {e}"), exit::CANTCREAT))
545}