Skip to main content

mkit_cli/commands/
merge_base.rs

1//! `mkit merge-base [--is-ancestor] <a> <b>` — print the best common
2//! ancestor of two commits, or test ancestry. Reuses the merge engine's
3//! `find_merge_base` / `is_ancestor` (the algorithms already power
4//! `merge`/`rebase`). Object ids are full 64-hex BLAKE3 (the documented
5//! hash-length divergence); everything else matches git.
6
7use std::io::Write;
8
9use clap::Parser;
10use mkit_core::ops::merge::{find_merge_base, is_ancestor};
11use mkit_core::store::ObjectStore;
12
13use crate::clap_shim;
14use crate::exit;
15use crate::format;
16
17#[derive(Debug, Parser)]
18#[command(
19    name = "mkit merge-base",
20    about = "Find a common ancestor of two commits."
21)]
22struct MergeBaseOpts {
23    /// Test whether <a> is an ancestor of <b>: exit 0 if yes, 1 if no
24    /// (no output), like `git merge-base --is-ancestor`.
25    #[arg(long = "is-ancestor")]
26    is_ancestor: bool,
27    a: String,
28    b: String,
29}
30
31#[must_use]
32pub fn run(args: &[String]) -> u8 {
33    let opts = match clap_shim::parse::<MergeBaseOpts>("mkit merge-base", args) {
34        Ok(o) => o,
35        Err(code) => return code,
36    };
37    let cwd = match std::env::current_dir() {
38        Ok(p) => p,
39        Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
40    };
41    let layout = match super::resolve_layout(&cwd) {
42        Ok(layout) => layout,
43        Err(code) => return code,
44    };
45    let store = match ObjectStore::open(&layout) {
46        Ok(s) => s,
47        Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
48    };
49    // Peel annotated/signed tags to the commit they point at, like the
50    // sibling history commands (resolve_revision returns the tag object).
51    let a = match super::revspec::resolve_revision(&store, &layout, &opts.a) {
52        Ok(h) => super::log::peel_tags(&store, h),
53        Err(e) => {
54            return emit_err(
55                &format!("bad revision '{}': {e}", opts.a),
56                exit::GENERAL_ERROR,
57            );
58        }
59    };
60    let b = match super::revspec::resolve_revision(&store, &layout, &opts.b) {
61        Ok(h) => super::log::peel_tags(&store, h),
62        Err(e) => {
63            return emit_err(
64                &format!("bad revision '{}': {e}", opts.b),
65                exit::GENERAL_ERROR,
66            );
67        }
68    };
69
70    if opts.is_ancestor {
71        return match is_ancestor(&store, a, b) {
72            Ok(true) => exit::OK,
73            Ok(false) => exit::GENERAL_ERROR,
74            Err(e) => emit_err(&format!("merge-base: {e}"), exit::GENERAL_ERROR),
75        };
76    }
77
78    match find_merge_base(&store, a, b) {
79        Ok(Some(base)) => {
80            let mut stdout = std::io::stdout().lock();
81            let _ = writeln!(stdout, "{}", format::hex_hash(&base));
82            exit::OK
83        }
84        // No common ancestor — git exits 1 with no output.
85        Ok(None) => exit::GENERAL_ERROR,
86        Err(e) => emit_err(&format!("merge-base: {e}"), exit::GENERAL_ERROR),
87    }
88}
89
90use super::error as emit_err;