1use std::io::Write;
25use std::time::{SystemTime, UNIX_EPOCH};
26
27use clap::{Parser, ValueEnum};
28use mkit_core::hash::Hash;
29use mkit_core::layout::RepoLayout;
30use mkit_core::object::{Commit, Object};
31use mkit_core::ops::conflict_state::{self, MergeState, in_progress_op_name, is_merge_in_progress};
32use mkit_core::ops::merge::{find_merge_base, merge_trees};
33use mkit_core::refs;
34use mkit_core::serialize;
35use mkit_core::store::ObjectStore;
36use mkit_core::worktree;
37
38use super::{advance_head, error as emit_err, load_tree_hash};
39use crate::clap_shim;
40use crate::config;
41use crate::exit;
42use crate::format::{self, JsonObject, json_string_array};
43
44#[derive(Debug, Clone, Copy, ValueEnum)]
45enum MergeFormat {
46 Default,
47 Json,
48}
49
50#[derive(Debug, Parser)]
51#[command(name = "mkit merge", about = "Three-way merge a branch into HEAD.")]
52struct MergeOpts {
53 #[arg(long = "continue", conflicts_with_all = ["abort", "branch"])]
55 cont: bool,
56 #[arg(long, conflicts_with_all = ["cont", "branch"])]
58 abort: bool,
59 #[arg(long = "no-commit", conflicts_with_all = ["cont", "abort"])]
65 no_commit: bool,
66 #[arg(short = 'm', long = "message", conflicts_with_all = ["cont", "abort"])]
68 message: Option<String>,
69 #[arg(long, value_enum, default_value = "default")]
73 format: MergeFormat,
74 branch: Option<String>,
76}
77
78#[must_use]
79pub fn run(args: &[String]) -> u8 {
80 let opts = match clap_shim::parse::<MergeOpts>("mkit merge", args) {
81 Ok(o) => o,
82 Err(code) => return code,
83 };
84 let json = matches!(opts.format, MergeFormat::Json);
85 let cwd = match std::env::current_dir() {
86 Ok(p) => p,
87 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
88 };
89 let layout = match super::resolve_layout(&cwd) {
90 Ok(layout) => layout,
91 Err(code) => return code,
92 };
93 let store = match ObjectStore::open(&layout) {
94 Ok(s) => s,
95 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
96 };
97 let _lock = match super::acquire_worktree_lock(&layout) {
98 Ok(l) => l,
99 Err(code) => return code,
100 };
101
102 if opts.abort {
103 abort(&layout, &store, json)
104 } else if opts.cont {
105 cont(&layout, &store, json)
106 } else if let Some(branch) = opts.branch.as_deref() {
107 start(
108 &layout,
109 &store,
110 branch,
111 opts.no_commit,
112 opts.message.as_deref(),
113 json,
114 )
115 } else {
116 super::usage_error("usage: mkit merge <branch> | --continue | --abort")
117 }
118}
119
120fn emit_err_json(msg: &str, code: u8, json: bool) -> u8 {
125 if json {
126 let mut obj = JsonObject::new();
127 obj.field_bool("ok", false).field_str("error", msg);
128 let mut stdout = std::io::stdout().lock();
129 let _ = writeln!(stdout, "{}", obj.finish());
130 }
131 emit_err(msg, code)
132}
133
134#[allow(clippy::too_many_lines)]
135fn start(
136 layout: &RepoLayout,
137 store: &ObjectStore,
138 branch: &str,
139 no_commit: bool,
140 message: Option<&str>,
141 json: bool,
142) -> u8 {
143 let emit_err = |msg: &str, code: u8| emit_err_json(msg, code, json);
148
149 if let Some(op) = in_progress_op_name(layout) {
150 return emit_err(
151 &format!("a {op} is already in progress (use --continue or --abort)"),
152 exit::GENERAL_ERROR,
153 );
154 }
155
156 let ours = match refs::resolve_head(layout) {
157 Ok(Some(h)) => h,
158 Ok(None) => return emit_err("no commits on current branch", exit::GENERAL_ERROR),
159 Err(e) => return emit_err(&format!("resolve HEAD: {e}"), exit::GENERAL_ERROR),
160 };
161 let theirs = match super::revspec::resolve_revision(store, layout, branch) {
166 Ok(h) => super::log::peel_tags(store, h),
167 Err(e) => return emit_err(&format!("merge target: {e}"), exit::GENERAL_ERROR),
168 };
169
170 if ours == theirs {
171 let mut stderr = std::io::stderr().lock();
172 let _ = writeln!(stderr, "Already up to date.");
173 drop(stderr);
174 if json {
175 let mut obj = JsonObject::new();
176 obj.field_bool("ok", true)
177 .field_str("kind", "up-to-date")
178 .field_hash("hash", &ours);
179 let mut stdout = std::io::stdout().lock();
180 let _ = writeln!(stdout, "{}", obj.finish());
181 }
182 return exit::OK;
183 }
184
185 let base = match find_merge_base(store, ours, theirs) {
186 Ok(b) => b,
187 Err(e) => return emit_err(&format!("find merge base: {e}"), exit::GENERAL_ERROR),
188 };
189
190 if let Some(bh) = base
192 && bh == ours
193 {
194 let theirs_tree = match load_tree_hash(store, theirs) {
195 Ok(t) => t,
196 Err(code) => return code,
197 };
198 if let Err(e) = super::ensure_restore_safe(layout, store, theirs_tree) {
199 return emit_err(&e, exit::GENERAL_ERROR);
200 }
201 if let Err(e) = super::restore_worktree_and_index(layout, store, theirs_tree) {
202 return emit_err(&e, exit::GENERAL_ERROR);
203 }
204 if let Err(e) = advance_head(layout, &theirs) {
205 return emit_err(&e, exit::CANTCREAT);
206 }
207 let mut stderr = std::io::stderr().lock();
210 let _ = writeln!(
211 stderr,
212 "Updating {}..{}",
213 format::short_hash(&ours, format::SUMMARY_ABBREV),
214 format::short_hash(&theirs, format::SUMMARY_ABBREV),
215 );
216 let _ = writeln!(stderr, "Fast-forward");
217 drop(stderr);
218 print_merge_stat(store, ours, theirs);
219 if json {
220 let mut obj = JsonObject::new();
221 obj.field_bool("ok", true)
222 .field_str("kind", "fast-forward")
223 .field_hash("old", &ours)
224 .field_hash("new", &theirs);
225 let mut stdout = std::io::stdout().lock();
226 let _ = writeln!(stdout, "{}", obj.finish());
227 }
228 return exit::OK;
229 }
230
231 let ours_tree = match load_tree_hash(store, ours) {
232 Ok(t) => t,
233 Err(code) => return code,
234 };
235 let theirs_tree = match load_tree_hash(store, theirs) {
236 Ok(t) => t,
237 Err(code) => return code,
238 };
239 let base_tree: Option<Hash> = match base {
240 Some(b) => match load_tree_hash(store, b) {
241 Ok(t) => Some(t),
242 Err(code) => return code,
243 },
244 None => None,
245 };
246
247 let result = match merge_trees(store, base_tree, Some(ours_tree), Some(theirs_tree)) {
248 Ok(r) => r,
249 Err(e) => return emit_err(&format!("merge: {e}"), exit::GENERAL_ERROR),
250 };
251
252 let msg = match message {
255 Some(m) => m.to_string(),
256 None if merge_source_is_remote_tracking(layout, branch) => {
257 let short = branch.strip_prefix("refs/remotes/").unwrap_or(branch);
258 format!("Merge remote-tracking branch '{short}'")
259 }
260 None => format!("Merge branch '{branch}'"),
261 };
262
263 if result.has_conflicts() {
264 if let Err(e) = super::ensure_restore_safe(layout, store, result.tree_hash) {
266 return emit_err(&e, exit::GENERAL_ERROR);
267 }
268 let records = match super::conflict::materialize_conflicts(
269 layout,
270 store,
271 result.tree_hash,
272 &result.conflicts,
273 ) {
274 Ok(r) => r,
275 Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
276 };
277 let state = MergeState {
278 merge_head: theirs,
279 orig_head: ours,
280 message: msg.into_bytes(),
281 };
282 if let Err(e) = conflict_state::write_merge_state(layout, &state, &records) {
283 return emit_err(&format!("write merge state: {e}"), exit::CANTCREAT);
284 }
285 if let Err(e) =
288 conflict_state::write_result_tree(layout.worktree_state_dir(), &result.tree_hash)
289 {
290 return emit_err(&format!("write merge state: {e}"), exit::CANTCREAT);
291 }
292 let mut stderr = std::io::stderr().lock();
293 for rec in &records {
296 let _ = writeln!(stderr, "CONFLICT (content): Merge conflict in {}", rec.path);
297 }
298 let _ = writeln!(
299 stderr,
300 "Automatic merge failed; fix conflicts and then commit the result."
301 );
302 let _ = writeln!(
303 stderr,
304 "hint: resolve the files above, `mkit add` them, then run \
305 `mkit merge --continue` (or `mkit merge --abort`)"
306 );
307 drop(stderr);
308 if json {
309 let paths: Vec<&str> = records.iter().map(|r| r.path.as_str()).collect();
310 let mut obj = JsonObject::new();
311 obj.field_bool("ok", false)
312 .field_str("kind", "conflict")
313 .field_raw("conflicts", &json_string_array(&paths))
314 .field_str(
315 "error",
316 "automatic merge failed; fix conflicts and then commit the result",
317 );
318 let mut stdout = std::io::stdout().lock();
319 let _ = writeln!(stdout, "{}", obj.finish());
320 }
321 return exit::GENERAL_ERROR;
322 }
323
324 if let Err(e) = super::ensure_restore_safe(layout, store, result.tree_hash) {
325 return emit_err(&e, exit::GENERAL_ERROR);
326 }
327
328 if no_commit {
333 if let Err(e) = super::restore_worktree_and_index(layout, store, result.tree_hash) {
334 return emit_err(&e, exit::GENERAL_ERROR);
335 }
336 if let Err(e) =
343 super::stage_removed_tombstones(layout, store, Some(ours_tree), result.tree_hash)
344 {
345 return emit_err(&e, exit::GENERAL_ERROR);
346 }
347 let state = MergeState {
348 merge_head: theirs,
349 orig_head: ours,
350 message: msg.into_bytes(),
351 };
352 if let Err(e) = conflict_state::write_merge_state(layout, &state, &[]) {
353 return emit_err(&format!("write merge state: {e}"), exit::CANTCREAT);
354 }
355 if let Err(e) =
358 conflict_state::write_result_tree(layout.worktree_state_dir(), &result.tree_hash)
359 {
360 return emit_err(&format!("write merge state: {e}"), exit::CANTCREAT);
361 }
362 let mut stderr = std::io::stderr().lock();
363 let _ = writeln!(
364 stderr,
365 "automatic merge went well; stopped before committing as requested\n\
366 commit the result with `mkit commit` (or `mkit merge --continue`)"
367 );
368 drop(stderr);
369 if json {
370 let mut obj = JsonObject::new();
371 obj.field_bool("ok", true)
372 .field_str("kind", "no-commit")
373 .field_hash("tree", &result.tree_hash);
374 let mut stdout = std::io::stdout().lock();
375 let _ = writeln!(stdout, "{}", obj.finish());
376 }
377 return exit::OK;
378 }
379
380 let commit_hash = match create_merge_commit(
382 layout,
383 store,
384 result.tree_hash,
385 ours,
386 theirs,
387 msg.as_bytes(),
388 ) {
389 Ok(h) => h,
390 Err(code) => return code,
391 };
392 if let Err(e) = super::restore_worktree_and_index(layout, store, result.tree_hash) {
393 return emit_err(&e, exit::GENERAL_ERROR);
394 }
395 if let Err(e) = advance_head(layout, &commit_hash) {
396 return emit_err(&e, exit::CANTCREAT);
397 }
398 {
401 let mut stderr = std::io::stderr().lock();
402 let _ = writeln!(stderr, "Merge made by the 'ort' strategy.");
403 }
404 print_merge_stat_trees(store, Some(ours_tree), Some(result.tree_hash));
405 if json {
406 let mut obj = JsonObject::new();
407 obj.field_bool("ok", true)
408 .field_str("kind", "merge-commit")
409 .field_hash("hash", &commit_hash)
410 .field_raw(
411 "parents",
412 &json_string_array(&[format::hex_hash(&ours), format::hex_hash(&theirs)]),
413 )
414 .field_hash("tree", &result.tree_hash);
415 let mut stdout = std::io::stdout().lock();
416 let _ = writeln!(stdout, "{}", obj.finish());
417 }
418 exit::OK
419}
420
421fn print_merge_stat(store: &ObjectStore, old: Hash, new: Hash) {
425 let old_tree = load_tree_hash(store, old).ok();
426 let new_tree = load_tree_hash(store, new).ok();
427 print_merge_stat_trees(store, old_tree, new_tree);
428}
429
430fn print_merge_stat_trees(store: &ObjectStore, old_tree: Option<Hash>, new_tree: Option<Hash>) {
431 if let Ok(result) = mkit_core::ops::diff_trees(store, old_tree, new_tree) {
432 let mut stderr = std::io::stderr().lock();
433 let _ = super::diff::render_stat(&mut stderr, store, result.entries.iter());
435 }
436}
437
438fn cont(layout: &RepoLayout, store: &ObjectStore, json: bool) -> u8 {
439 let emit_err = |msg: &str, code: u8| emit_err_json(msg, code, json);
440 if !is_merge_in_progress(layout) {
441 return emit_err("no merge in progress", exit::GENERAL_ERROR);
442 }
443 let state = match conflict_state::read_merge_state(layout) {
444 Ok(Some(s)) => s,
445 Ok(None) => return emit_err("no merge in progress", exit::GENERAL_ERROR),
446 Err(e) => return emit_err(&format!("read merge state: {e}"), exit::GENERAL_ERROR),
447 };
448 let records = match conflict_state::read_conflicts(layout.worktree_state_dir()) {
449 Ok(r) => r,
450 Err(e) => return emit_err(&format!("read conflicts: {e}"), exit::GENERAL_ERROR),
451 };
452 match super::conflict::first_unresolved_marker(layout.worktree_root(), &records) {
453 Ok(Some(path)) => {
454 return emit_err(
455 &format!(
456 "unresolved conflict markers remain in '{path}'; resolve and `mkit add` it"
457 ),
458 exit::GENERAL_ERROR,
459 );
460 }
461 Ok(None) => {}
462 Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
463 }
464 if let Err(e) = super::conflict::ensure_conflict_paths_staged(layout, store, &records) {
465 return emit_err(&e, exit::GENERAL_ERROR);
466 }
467
468 let idx = match super::read_or_seed_index_from_head(layout, store) {
471 Ok(i) => i,
472 Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
473 };
474 let tree_hash = match worktree::build_tree_from_index(store, &idx) {
475 Ok(t) => t,
476 Err(e) => return emit_err(&format!("build tree from index: {e}"), exit::GENERAL_ERROR),
477 };
478
479 let commit_hash = match create_merge_commit(
480 layout,
481 store,
482 tree_hash,
483 state.orig_head,
484 state.merge_head,
485 &state.message,
486 ) {
487 Ok(h) => h,
488 Err(code) => return code,
489 };
490 if let Err(e) = super::sync_index_to_tree(layout, store, tree_hash) {
496 return emit_err(&e, exit::GENERAL_ERROR);
497 }
498 if let Err(e) = advance_head(layout, &commit_hash) {
499 return emit_err(&e, exit::CANTCREAT);
500 }
501 if let Err(e) = conflict_state::clear_merge_state(layout) {
502 return emit_err(&format!("clear merge state: {e}"), exit::GENERAL_ERROR);
503 }
504 let mut stderr = std::io::stderr().lock();
505 let _ = writeln!(
506 stderr,
507 "merge {} into HEAD ({})",
508 format::short_hash(&state.merge_head, 8),
509 format::short_hash(&commit_hash, 8)
510 );
511 drop(stderr);
512 if json {
513 let mut obj = JsonObject::new();
514 obj.field_bool("ok", true)
515 .field_str("kind", "merge-commit")
516 .field_hash("hash", &commit_hash)
517 .field_raw(
518 "parents",
519 &json_string_array(&[
520 format::hex_hash(&state.orig_head),
521 format::hex_hash(&state.merge_head),
522 ]),
523 )
524 .field_hash("tree", &tree_hash);
525 let mut stdout = std::io::stdout().lock();
526 let _ = writeln!(stdout, "{}", obj.finish());
527 }
528 exit::OK
529}
530
531fn abort(layout: &RepoLayout, store: &ObjectStore, json: bool) -> u8 {
532 let emit_err = |msg: &str, code: u8| emit_err_json(msg, code, json);
533 if !is_merge_in_progress(layout) {
534 return emit_err("no merge in progress", exit::GENERAL_ERROR);
535 }
536 let state = match conflict_state::read_merge_state(layout) {
537 Ok(Some(s)) => s,
538 Ok(None) => return emit_err("no merge in progress", exit::GENERAL_ERROR),
539 Err(e) => return emit_err(&format!("read merge state: {e}"), exit::GENERAL_ERROR),
540 };
541 let records = match conflict_state::read_conflicts(layout.worktree_state_dir()) {
542 Ok(r) => r,
543 Err(e) => return emit_err(&format!("read conflicts: {e}"), exit::GENERAL_ERROR),
544 };
545 if let Err(code) = restore_to(layout, store, state.orig_head, &records) {
546 return code;
547 }
548 if let Err(e) = conflict_state::clear_merge_state(layout) {
549 return emit_err(&format!("clear merge state: {e}"), exit::GENERAL_ERROR);
550 }
551 let mut stderr = std::io::stderr().lock();
552 let _ = writeln!(stderr, "merge aborted; HEAD restored");
553 drop(stderr);
554 if json {
555 let mut obj = JsonObject::new();
556 obj.field_bool("ok", true)
557 .field_str("kind", "aborted")
558 .field_hash("hash", &state.orig_head);
559 let mut stdout = std::io::stdout().lock();
560 let _ = writeln!(stdout, "{}", obj.finish());
561 }
562 exit::OK
563}
564
565fn restore_to(
568 layout: &RepoLayout,
569 store: &ObjectStore,
570 target: Hash,
571 records: &[mkit_core::ops::conflict_state::ConflictRecord],
572) -> Result<(), u8> {
573 let target_tree = load_tree_hash(store, target)?;
574 let op_result = conflict_state::read_result_tree(layout.worktree_state_dir())
580 .ok()
581 .flatten();
582 if let Err(e) =
586 super::conflict::ensure_abort_safe(layout, store, records, target_tree, op_result)
587 {
588 return Err(emit_err(&e, exit::GENERAL_ERROR));
589 }
590 if let Err(e) =
594 super::conflict::reset_conflict_paths(layout, store, records, target_tree, op_result)
595 {
596 return Err(emit_err(&e, exit::GENERAL_ERROR));
597 }
598 if let Err(e) = super::ensure_restore_safe(layout, store, target_tree) {
599 return Err(emit_err(&e, exit::GENERAL_ERROR));
600 }
601 if let Err(e) = super::restore_worktree_and_index(layout, store, target_tree) {
602 return Err(emit_err(&e, exit::GENERAL_ERROR));
603 }
604 super::restore_head_ref(layout, &target)
605}
606
607fn create_merge_commit(
608 layout: &RepoLayout,
609 store: &ObjectStore,
610 tree_hash: Hash,
611 parent_ours: Hash,
612 parent_theirs: Hash,
613 message: &[u8],
614) -> Result<Hash, u8> {
615 let cfg = config::read_or_default(layout)
616 .map_err(|e| emit_err(&format!("config: {e}"), exit::CONFIG_ERROR))?;
617 let mut signer = super::commit::load_commit_signer(layout, &cfg)
618 .map_err(|(msg, code)| emit_err(&msg, code))?;
619 let signer_public = signer
620 .public_key()
621 .map_err(|(msg, code)| emit_err(&msg, code))?;
622 let author = super::commit::resolve_author(None, &cfg.user_identity, &signer_public)
623 .map_err(|e| emit_err(&format!("author: {e}"), exit::CONFIG_ERROR))?;
624 let timestamp = SystemTime::now()
625 .duration_since(UNIX_EPOCH)
626 .map_or(0, |d| d.as_secs());
627 let mut unsigned = Commit::new_unannotated(
628 tree_hash,
629 vec![parent_ours, parent_theirs],
630 author,
631 signer_public,
632 message.to_vec(),
633 timestamp,
634 [0u8; 64],
635 );
636 let sig = signer
637 .sign_commit(&unsigned)
638 .map_err(|(msg, code)| emit_err(&msg, code))?;
639 unsigned.signature = sig;
640 let bytes = serialize::serialize(&Object::Commit(unsigned))
641 .map_err(|e| emit_err(&format!("serialize: {e}"), exit::DATAERR))?;
642 store
643 .write(&bytes)
644 .map_err(|e| emit_err(&format!("store commit: {e}"), exit::CANTCREAT))
645}
646
647fn merge_source_is_remote_tracking(layout: &RepoLayout, spec: &str) -> bool {
650 let rel = spec.strip_prefix("refs/remotes/").map_or(spec, |r| r);
651 let Some((remote, branch)) = rel.split_once('/') else {
652 return false;
653 };
654 if refs::read_ref(layout, spec).is_ok_and(|r| r.is_some())
655 || refs::read_tag(layout, spec).is_ok_and(|r| r.is_some())
656 {
657 return false; }
659 refs::read_remote_ref(layout, remote, branch).is_ok_and(|r| r.is_some())
660}