use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH};
use clap::{Parser, ValueEnum};
use mkit_core::hash::Hash;
use mkit_core::layout::RepoLayout;
use mkit_core::object::{Commit, Object};
use mkit_core::ops::conflict_state::{self, MergeState, in_progress_op_name, is_merge_in_progress};
use mkit_core::ops::merge::{find_merge_base, merge_trees};
use mkit_core::refs;
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 MergeFormat {
Default,
Json,
}
#[derive(Debug, Parser)]
#[command(name = "mkit merge", about = "Three-way merge a branch into HEAD.")]
struct MergeOpts {
#[arg(long = "continue", conflicts_with_all = ["abort", "branch"])]
cont: bool,
#[arg(long, conflicts_with_all = ["cont", "branch"])]
abort: bool,
#[arg(long = "no-commit", conflicts_with_all = ["cont", "abort"])]
no_commit: bool,
#[arg(short = 'm', long = "message", conflicts_with_all = ["cont", "abort"])]
message: Option<String>,
#[arg(long, value_enum, default_value = "default")]
format: MergeFormat,
branch: Option<String>,
}
#[must_use]
pub fn run(args: &[String]) -> u8 {
let opts = match clap_shim::parse::<MergeOpts>("mkit merge", args) {
Ok(o) => o,
Err(code) => return code,
};
let json = matches!(opts.format, MergeFormat::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(branch) = opts.branch.as_deref() {
start(
&layout,
&store,
branch,
opts.no_commit,
opts.message.as_deref(),
json,
)
} else {
super::usage_error("usage: mkit merge <branch> | --continue | --abort")
}
}
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)
}
#[allow(clippy::too_many_lines)]
fn start(
layout: &RepoLayout,
store: &ObjectStore,
branch: &str,
no_commit: bool,
message: Option<&str>,
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 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 theirs = match super::revspec::resolve_revision(store, layout, branch) {
Ok(h) => super::log::peel_tags(store, h),
Err(e) => return emit_err(&format!("merge target: {e}"), exit::GENERAL_ERROR),
};
if ours == theirs {
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "Already up to date.");
drop(stderr);
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", true)
.field_str("kind", "up-to-date")
.field_hash("hash", &ours);
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{}", obj.finish());
}
return exit::OK;
}
let base = match find_merge_base(store, ours, theirs) {
Ok(b) => b,
Err(e) => return emit_err(&format!("find merge base: {e}"), exit::GENERAL_ERROR),
};
if let Some(bh) = base
&& bh == ours
{
let theirs_tree = match load_tree_hash(store, theirs) {
Ok(t) => t,
Err(code) => return code,
};
if let Err(e) = super::ensure_restore_safe(layout, store, theirs_tree) {
return emit_err(&e, exit::GENERAL_ERROR);
}
if let Err(e) = super::restore_worktree_and_index(layout, store, theirs_tree) {
return emit_err(&e, exit::GENERAL_ERROR);
}
if let Err(e) = advance_head(layout, &theirs) {
return emit_err(&e, exit::CANTCREAT);
}
let mut stderr = std::io::stderr().lock();
let _ = writeln!(
stderr,
"Updating {}..{}",
format::short_hash(&ours, format::SUMMARY_ABBREV),
format::short_hash(&theirs, format::SUMMARY_ABBREV),
);
let _ = writeln!(stderr, "Fast-forward");
drop(stderr);
print_merge_stat(store, ours, theirs);
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", true)
.field_str("kind", "fast-forward")
.field_hash("old", &ours)
.field_hash("new", &theirs);
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{}", obj.finish());
}
return exit::OK;
}
let ours_tree = match load_tree_hash(store, ours) {
Ok(t) => t,
Err(code) => return code,
};
let theirs_tree = match load_tree_hash(store, theirs) {
Ok(t) => t,
Err(code) => return code,
};
let base_tree: Option<Hash> = match base {
Some(b) => match load_tree_hash(store, b) {
Ok(t) => Some(t),
Err(code) => return code,
},
None => None,
};
let result = match merge_trees(store, base_tree, Some(ours_tree), Some(theirs_tree)) {
Ok(r) => r,
Err(e) => return emit_err(&format!("merge: {e}"), exit::GENERAL_ERROR),
};
let msg = match message {
Some(m) => m.to_string(),
None if merge_source_is_remote_tracking(layout, branch) => {
let short = branch.strip_prefix("refs/remotes/").unwrap_or(branch);
format!("Merge remote-tracking branch '{short}'")
}
None => format!("Merge branch '{branch}'"),
};
if result.has_conflicts() {
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 = MergeState {
merge_head: theirs,
orig_head: ours,
message: msg.into_bytes(),
};
if let Err(e) = conflict_state::write_merge_state(layout, &state, &records) {
return emit_err(&format!("write merge 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 merge 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,
"Automatic merge failed; fix conflicts and then commit the result."
);
let _ = writeln!(
stderr,
"hint: resolve the files above, `mkit add` them, then run \
`mkit merge --continue` (or `mkit merge --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",
"automatic merge failed; fix conflicts and then commit the result",
);
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 state = MergeState {
merge_head: theirs,
orig_head: ours,
message: msg.into_bytes(),
};
if let Err(e) = conflict_state::write_merge_state(layout, &state, &[]) {
return emit_err(&format!("write merge 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 merge state: {e}"), exit::CANTCREAT);
}
let mut stderr = std::io::stderr().lock();
let _ = writeln!(
stderr,
"automatic merge went well; stopped before committing as requested\n\
commit the result with `mkit commit` (or `mkit merge --continue`)"
);
drop(stderr);
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", true)
.field_str("kind", "no-commit")
.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_merge_commit(
layout,
store,
result.tree_hash,
ours,
theirs,
msg.as_bytes(),
) {
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 mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "Merge made by the 'ort' strategy.");
}
print_merge_stat_trees(store, Some(ours_tree), Some(result.tree_hash));
if json {
let mut obj = JsonObject::new();
obj.field_bool("ok", true)
.field_str("kind", "merge-commit")
.field_hash("hash", &commit_hash)
.field_raw(
"parents",
&json_string_array(&[format::hex_hash(&ours), format::hex_hash(&theirs)]),
)
.field_hash("tree", &result.tree_hash);
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{}", obj.finish());
}
exit::OK
}
fn print_merge_stat(store: &ObjectStore, old: Hash, new: Hash) {
let old_tree = load_tree_hash(store, old).ok();
let new_tree = load_tree_hash(store, new).ok();
print_merge_stat_trees(store, old_tree, new_tree);
}
fn print_merge_stat_trees(store: &ObjectStore, old_tree: Option<Hash>, new_tree: Option<Hash>) {
if let Ok(result) = mkit_core::ops::diff_trees(store, old_tree, new_tree) {
let mut stderr = std::io::stderr().lock();
let _ = super::diff::render_stat(&mut stderr, store, result.entries.iter());
}
}
fn cont(layout: &RepoLayout, store: &ObjectStore, json: bool) -> u8 {
let emit_err = |msg: &str, code: u8| emit_err_json(msg, code, json);
if !is_merge_in_progress(layout) {
return emit_err("no merge in progress", exit::GENERAL_ERROR);
}
let state = match conflict_state::read_merge_state(layout) {
Ok(Some(s)) => s,
Ok(None) => return emit_err("no merge in progress", exit::GENERAL_ERROR),
Err(e) => return emit_err(&format!("read merge 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 commit_hash = match create_merge_commit(
layout,
store,
tree_hash,
state.orig_head,
state.merge_head,
&state.message,
) {
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_merge_state(layout) {
return emit_err(&format!("clear merge state: {e}"), exit::GENERAL_ERROR);
}
let mut stderr = std::io::stderr().lock();
let _ = writeln!(
stderr,
"merge {} into HEAD ({})",
format::short_hash(&state.merge_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", "merge-commit")
.field_hash("hash", &commit_hash)
.field_raw(
"parents",
&json_string_array(&[
format::hex_hash(&state.orig_head),
format::hex_hash(&state.merge_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_merge_in_progress(layout) {
return emit_err("no merge in progress", exit::GENERAL_ERROR);
}
let state = match conflict_state::read_merge_state(layout) {
Ok(Some(s)) => s,
Ok(None) => return emit_err("no merge in progress", exit::GENERAL_ERROR),
Err(e) => return emit_err(&format!("read merge 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_merge_state(layout) {
return emit_err(&format!("clear merge state: {e}"), exit::GENERAL_ERROR);
}
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "merge 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_merge_commit(
layout: &RepoLayout,
store: &ObjectStore,
tree_hash: Hash,
parent_ours: Hash,
parent_theirs: Hash,
message: &[u8],
) -> 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 = super::commit::resolve_author(None, &cfg.user_identity, &signer_public)
.map_err(|e| emit_err(&format!("author: {e}"), exit::CONFIG_ERROR))?;
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let mut unsigned = Commit::new_unannotated(
tree_hash,
vec![parent_ours, parent_theirs],
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))
}
fn merge_source_is_remote_tracking(layout: &RepoLayout, spec: &str) -> bool {
let rel = spec.strip_prefix("refs/remotes/").map_or(spec, |r| r);
let Some((remote, branch)) = rel.split_once('/') else {
return false;
};
if refs::read_ref(layout, spec).is_ok_and(|r| r.is_some())
|| refs::read_tag(layout, spec).is_ok_and(|r| r.is_some())
{
return false; }
refs::read_remote_ref(layout, remote, branch).is_ok_and(|r| r.is_some())
}