use std::io::Write;
use clap::{Parser, ValueEnum};
use mkit_core::hash::Hash;
use mkit_core::layout::RepoLayout;
use mkit_core::object::{Commit, Object};
use mkit_core::ops::cherry_pick::{CherryPickError, cherry_pick};
use mkit_core::ops::conflict_state::{
self, CherryPickState, in_progress_op_name, is_cherry_pick_in_progress,
};
use mkit_core::refs::{self, Head};
use mkit_core::serialize;
use mkit_core::store::ObjectStore;
use mkit_core::worktree;
use super::{advance_head, error as emit_err, load_tree_hash};
use crate::clap_shim;
use crate::config;
use crate::exit;
use crate::format::{self, JsonObject, json_string_array};
#[derive(Debug, Clone, Copy, ValueEnum)]
enum CherryPickFormat {
Default,
Json,
}
#[derive(Debug, Parser)]
#[command(name = "mkit cherry-pick", about = "Apply a single commit onto HEAD.")]
struct CherryPickOpts {
#[arg(long = "continue", conflicts_with_all = ["abort", "commit"])]
cont: bool,
#[arg(long, conflicts_with_all = ["cont", "commit"])]
abort: bool,
#[arg(short = 'n', long = "no-commit", conflicts_with_all = ["cont", "abort"])]
no_commit: bool,
#[arg(short = 'm', long = "mainline", value_name = "PARENT-NUMBER", conflicts_with_all = ["cont", "abort"])]
mainline: Option<usize>,
#[arg(long, value_enum, default_value = "default")]
format: CherryPickFormat,
commit: Option<String>,
}
fn emit_err_json(msg: &str, code: u8, json: bool) -> u8 {
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", false).field_str("error", msg);
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{}", obj.finish());
}
emit_err(msg, code)
}
#[must_use]
pub fn run(args: &[String]) -> u8 {
let opts = match clap_shim::parse::<CherryPickOpts>("mkit cherry-pick", args) {
Ok(o) => o,
Err(code) => return code,
};
let json = matches!(opts.format, CherryPickFormat::Json);
let cwd = match std::env::current_dir() {
Ok(p) => p,
Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
};
let layout = match super::resolve_layout(&cwd) {
Ok(layout) => layout,
Err(code) => return code,
};
let store = match ObjectStore::open(&layout) {
Ok(s) => s,
Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
};
let _lock = match super::acquire_worktree_lock(&layout) {
Ok(l) => l,
Err(code) => return code,
};
if opts.abort {
abort(&layout, &store, json)
} else if opts.cont {
cont(&layout, &store, json)
} else if let Some(hex) = opts.commit.as_deref() {
start(&layout, &store, hex, opts.no_commit, opts.mainline, json)
} else {
super::usage_error("usage: mkit cherry-pick <commit> | --continue | --abort")
}
}
#[allow(clippy::too_many_lines)]
fn start(
layout: &RepoLayout,
store: &ObjectStore,
hex: &str,
no_commit: bool,
mainline: Option<usize>,
json: bool,
) -> u8 {
let emit_err = |msg: &str, code: u8| emit_err_json(msg, code, json);
if let Some(op) = in_progress_op_name(layout) {
return emit_err(
&format!("a {op} is already in progress (use --continue or --abort)"),
exit::GENERAL_ERROR,
);
}
let target: Hash = match super::revspec::resolve_revision(store, layout, hex) {
Ok(h) => super::log::peel_tags(store, h),
Err(e) => return emit_err(&format!("bad commit: {e}"), exit::DATAERR),
};
let ours = match refs::resolve_head(layout) {
Ok(Some(h)) => h,
Ok(None) => return emit_err("no commits on current branch", exit::GENERAL_ERROR),
Err(e) => return emit_err(&format!("resolve HEAD: {e}"), exit::GENERAL_ERROR),
};
let ours_tree = match store.read_object(&ours) {
Ok(Object::Commit(c)) => c.tree_hash,
Ok(_) => return emit_err("HEAD is not a commit", exit::DATAERR),
Err(e) => return emit_err(&format!("read HEAD: {e}"), exit::GENERAL_ERROR),
};
let result = match cherry_pick(store, target, ours_tree, mainline) {
Ok(r) => r,
Err(
e @ (CherryPickError::MergeNeedsMainline
| CherryPickError::MainlineForNonMerge
| CherryPickError::BadMainline { .. }),
) => return emit_err(&format!("cherry-pick: {e}"), exit::USAGE),
Err(e) => return emit_err(&format!("cherry-pick: {e}"), exit::GENERAL_ERROR),
};
if result.has_conflicts() {
if no_commit {
return emit_err(
&format!(
"cherry-pick -n of {} conflicts; mkit cannot stage an unresolved \
conflict without committing — re-run without -n, then resolve and \
`mkit cherry-pick --continue`",
format::short_hash(&target, 8)
),
exit::GENERAL_ERROR,
);
}
if let Err(e) = super::ensure_restore_safe(layout, store, result.tree_hash) {
return emit_err(&e, exit::GENERAL_ERROR);
}
let records = match super::conflict::materialize_conflicts(
layout,
store,
result.tree_hash,
&result.conflicts,
) {
Ok(r) => r,
Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
};
let state = CherryPickState {
cherry_pick_head: target,
orig_head: ours,
message: result.original_message.clone(),
};
if let Err(e) = conflict_state::write_cherry_pick_state(layout, &state, &records) {
return emit_err(&format!("write cherry-pick state: {e}"), exit::CANTCREAT);
}
if let Err(e) =
conflict_state::write_result_tree(layout.worktree_state_dir(), &result.tree_hash)
{
return emit_err(&format!("write cherry-pick state: {e}"), exit::CANTCREAT);
}
let mut stderr = std::io::stderr().lock();
for rec in &records {
let _ = writeln!(stderr, "CONFLICT (content): Merge conflict in {}", rec.path);
}
let _ = writeln!(
stderr,
"hint: resolve the files above, `mkit add` them, then run \
`mkit cherry-pick --continue` (or `mkit cherry-pick --abort`)"
);
drop(stderr);
if json {
let paths: Vec<&str> = records.iter().map(|r| r.path.as_str()).collect();
let mut obj = JsonObject::new();
obj.field_bool("ok", false)
.field_str("kind", "conflict")
.field_raw("conflicts", &json_string_array(&paths))
.field_str(
"error",
"cherry-pick conflict; resolve and continue or abort",
);
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{}", obj.finish());
}
return exit::GENERAL_ERROR;
}
if let Err(e) = super::ensure_restore_safe(layout, store, result.tree_hash) {
return emit_err(&e, exit::GENERAL_ERROR);
}
if no_commit {
if let Err(e) = super::restore_worktree_and_index(layout, store, result.tree_hash) {
return emit_err(&e, exit::GENERAL_ERROR);
}
if let Err(e) =
super::stage_removed_tombstones(layout, store, Some(ours_tree), result.tree_hash)
{
return emit_err(&e, exit::GENERAL_ERROR);
}
let mut stderr = std::io::stderr().lock();
let _ = writeln!(
stderr,
"staged cherry-pick of {} (no commit; run `mkit commit` when ready)",
format::short_hash(&target, 8),
);
drop(stderr);
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", true)
.field_str("kind", "no-commit")
.field_hash("picked", &target)
.field_hash("tree", &result.tree_hash);
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{}", obj.finish());
}
return exit::OK;
}
let commit_hash = match create_commit(
layout,
store,
result.tree_hash,
ours,
&result.original_message,
target,
) {
Ok(h) => h,
Err(code) => return code,
};
if let Err(e) = super::restore_worktree_and_index(layout, store, result.tree_hash) {
return emit_err(&e, exit::GENERAL_ERROR);
}
if let Err(e) = advance_head(layout, &commit_hash) {
return emit_err(&e, exit::CANTCREAT);
}
let subject = String::from_utf8_lossy(&result.original_message)
.lines()
.next()
.unwrap_or("")
.to_owned();
let branch_name = match refs::read_head(layout) {
Ok(Head::Branch(b)) => Some(b),
_ => None,
};
let head_ref = match &branch_name {
Some(b) => super::summary::HeadRef::Branch(b),
None => super::summary::HeadRef::Detached,
};
let mut stderr = std::io::stderr().lock();
super::summary::print_commit_summary(
&mut stderr,
store,
&head_ref,
&commit_hash,
&subject,
false,
Some(ours_tree),
Some(result.tree_hash),
);
drop(stderr);
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", true)
.field_str("kind", "commit")
.field_hash("hash", &commit_hash)
.field_hash("picked", &target)
.field_hash("tree", &result.tree_hash);
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{}", obj.finish());
}
exit::OK
}
fn cont(layout: &RepoLayout, store: &ObjectStore, json: bool) -> u8 {
let emit_err = |msg: &str, code: u8| emit_err_json(msg, code, json);
if !is_cherry_pick_in_progress(layout) {
return emit_err("no cherry-pick in progress", exit::GENERAL_ERROR);
}
let state = match conflict_state::read_cherry_pick_state(layout) {
Ok(Some(s)) => s,
Ok(None) => return emit_err("no cherry-pick in progress", exit::GENERAL_ERROR),
Err(e) => return emit_err(&format!("read cherry-pick state: {e}"), exit::GENERAL_ERROR),
};
let records = match conflict_state::read_conflicts(layout.worktree_state_dir()) {
Ok(r) => r,
Err(e) => return emit_err(&format!("read conflicts: {e}"), exit::GENERAL_ERROR),
};
match super::conflict::first_unresolved_marker(layout.worktree_root(), &records) {
Ok(Some(path)) => {
return emit_err(
&format!(
"unresolved conflict markers remain in '{path}'; resolve and `mkit add` it"
),
exit::GENERAL_ERROR,
);
}
Ok(None) => {}
Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
}
if let Err(e) = super::conflict::ensure_conflict_paths_staged(layout, store, &records) {
return emit_err(&e, exit::GENERAL_ERROR);
}
let idx = match super::read_or_seed_index_from_head(layout, store) {
Ok(i) => i,
Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
};
let tree_hash = match worktree::build_tree_from_index(store, &idx) {
Ok(t) => t,
Err(e) => return emit_err(&format!("build tree from index: {e}"), exit::GENERAL_ERROR),
};
let parent = match refs::resolve_head(layout) {
Ok(Some(h)) => h,
Ok(None) => state.orig_head,
Err(e) => return emit_err(&format!("resolve HEAD: {e}"), exit::GENERAL_ERROR),
};
let commit_hash = match create_commit(
layout,
store,
tree_hash,
parent,
&state.message,
state.cherry_pick_head,
) {
Ok(h) => h,
Err(code) => return code,
};
if let Err(e) = super::sync_index_to_tree(layout, store, tree_hash) {
return emit_err(&e, exit::GENERAL_ERROR);
}
if let Err(e) = advance_head(layout, &commit_hash) {
return emit_err(&e, exit::CANTCREAT);
}
if let Err(e) = conflict_state::clear_cherry_pick_state(layout) {
return emit_err(
&format!("clear cherry-pick state: {e}"),
exit::GENERAL_ERROR,
);
}
let mut stderr = std::io::stderr().lock();
let _ = writeln!(
stderr,
"cherry-picked {} as {}",
format::short_hash(&state.cherry_pick_head, 8),
format::short_hash(&commit_hash, 8),
);
drop(stderr);
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", true)
.field_str("kind", "commit")
.field_hash("hash", &commit_hash)
.field_hash("picked", &state.cherry_pick_head)
.field_hash("tree", &tree_hash);
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{}", obj.finish());
}
exit::OK
}
fn abort(layout: &RepoLayout, store: &ObjectStore, json: bool) -> u8 {
let emit_err = |msg: &str, code: u8| emit_err_json(msg, code, json);
if !is_cherry_pick_in_progress(layout) {
return emit_err("no cherry-pick in progress", exit::GENERAL_ERROR);
}
let state = match conflict_state::read_cherry_pick_state(layout) {
Ok(Some(s)) => s,
Ok(None) => return emit_err("no cherry-pick in progress", exit::GENERAL_ERROR),
Err(e) => return emit_err(&format!("read cherry-pick state: {e}"), exit::GENERAL_ERROR),
};
let records = match conflict_state::read_conflicts(layout.worktree_state_dir()) {
Ok(r) => r,
Err(e) => return emit_err(&format!("read conflicts: {e}"), exit::GENERAL_ERROR),
};
if let Err(code) = restore_to(layout, store, state.orig_head, &records) {
return code;
}
if let Err(e) = conflict_state::clear_cherry_pick_state(layout) {
return emit_err(
&format!("clear cherry-pick state: {e}"),
exit::GENERAL_ERROR,
);
}
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "cherry-pick aborted; HEAD restored");
drop(stderr);
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", true)
.field_str("kind", "aborted")
.field_hash("hash", &state.orig_head);
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{}", obj.finish());
}
exit::OK
}
fn restore_to(
layout: &RepoLayout,
store: &ObjectStore,
target: Hash,
records: &[mkit_core::ops::conflict_state::ConflictRecord],
) -> Result<(), u8> {
let target_tree = load_tree_hash(store, target)?;
let op_result = conflict_state::read_result_tree(layout.worktree_state_dir())
.ok()
.flatten();
if let Err(e) =
super::conflict::ensure_abort_safe(layout, store, records, target_tree, op_result)
{
return Err(emit_err(&e, exit::GENERAL_ERROR));
}
if let Err(e) =
super::conflict::reset_conflict_paths(layout, store, records, target_tree, op_result)
{
return Err(emit_err(&e, exit::GENERAL_ERROR));
}
if let Err(e) = super::ensure_restore_safe(layout, store, target_tree) {
return Err(emit_err(&e, exit::GENERAL_ERROR));
}
if let Err(e) = super::restore_worktree_and_index(layout, store, target_tree) {
return Err(emit_err(&e, exit::GENERAL_ERROR));
}
super::restore_head_ref(layout, &target)
}
fn create_commit(
layout: &RepoLayout,
store: &ObjectStore,
tree_hash: Hash,
parent: Hash,
message: &[u8],
picked: Hash,
) -> Result<Hash, u8> {
let cfg = config::read_or_default(layout)
.map_err(|e| emit_err(&format!("config: {e}"), exit::CONFIG_ERROR))?;
let mut signer = super::commit::load_commit_signer(layout, &cfg)
.map_err(|(msg, code)| emit_err(&msg, code))?;
let signer_public = signer
.public_key()
.map_err(|(msg, code)| emit_err(&msg, code))?;
let (author, timestamp) = match store.read_object(&picked) {
Ok(Object::Commit(c)) => (c.author, c.timestamp),
Ok(_) => return Err(emit_err("picked object is not a commit", exit::DATAERR)),
Err(e) => {
return Err(emit_err(
&format!("read picked commit: {e}"),
exit::GENERAL_ERROR,
));
}
};
let mut unsigned = Commit::new_unannotated(
tree_hash,
vec![parent],
author,
signer_public,
message.to_vec(),
timestamp,
[0u8; 64],
);
let sig = signer
.sign_commit(&unsigned)
.map_err(|(msg, code)| emit_err(&msg, code))?;
unsigned.signature = sig;
let bytes = serialize::serialize(&Object::Commit(unsigned))
.map_err(|e| emit_err(&format!("serialize: {e}"), exit::DATAERR))?;
store
.write(&bytes)
.map_err(|e| emit_err(&format!("store commit: {e}"), exit::CANTCREAT))
}