Skip to main content

mkit_cli/commands/
summary.rs

1//! Shared git-shaped post-commit summary: the `[<branch> <hash>]
2//! <subject>` headline, the diffstat, and the `create/delete mode`
3//! trailers. Used by commit / cherry-pick / revert / merge so their
4//! human output matches git's. All lines go to the caller-provided
5//! writer (the commands route it to stderr, per mkit's channel rule).
6
7use std::io::Write;
8
9use mkit_core::hash::Hash;
10use mkit_core::object::EntryMode;
11use mkit_core::ops::{DiffKind, diff_trees};
12use mkit_core::store::ObjectStore;
13
14use crate::format;
15
16/// Where HEAD points, for the summary headline.
17#[derive(Debug)]
18pub enum HeadRef<'a> {
19    /// On a branch — `[<name> <hash>] <subject>`.
20    Branch(&'a str),
21    /// Detached HEAD — `[detached HEAD <hash>] <subject>`.
22    Detached,
23}
24
25/// Render git's post-commit summary on `out`:
26/// ```text
27/// [main (root-commit) 1a2b3c4] subject
28///  2 files changed, 2 insertions(+)
29///  create mode 100644 a.txt
30/// ```
31/// `old_tree`/`new_tree` bound the diffstat (`None` = the empty tree, e.g.
32/// a root commit). The diffstat + mode trailers are best-effort: a failure
33/// to compute them still leaves the `[...]` headline intact. Object ids are
34/// BLAKE3 prefixes (the documented hash-length divergence).
35pub fn print_commit_summary(
36    out: &mut impl Write,
37    store: &ObjectStore,
38    head: &HeadRef,
39    hash: &Hash,
40    subject: &str,
41    is_root: bool,
42    old_tree: Option<Hash>,
43    new_tree: Option<Hash>,
44) {
45    let short = format::short_hash(hash, format::SUMMARY_ABBREV);
46    let head_part = match head {
47        HeadRef::Detached => format!("detached HEAD {short}"),
48        HeadRef::Branch(b) if is_root => format!("{b} (root-commit) {short}"),
49        HeadRef::Branch(b) => format!("{b} {short}"),
50    };
51    let _ = writeln!(out, "[{head_part}] {subject}");
52    let Ok(result) = diff_trees(store, old_tree, new_tree) else {
53        return;
54    };
55    // `render_stat` hoists its own `DisplaySource` wrapping (#625).
56    let _ = super::diff::render_stat(out, store, result.entries.iter());
57    for e in &result.entries {
58        match e.kind {
59            DiffKind::Added => {
60                let _ = writeln!(out, " create mode {} {}", octal(e.new_mode), e.path);
61            }
62            DiffKind::Removed => {
63                let _ = writeln!(out, " delete mode {} {}", octal(e.old_mode), e.path);
64            }
65            DiffKind::Modified | DiffKind::ModeChanged | DiffKind::Renamed => {}
66        }
67    }
68}
69
70/// git octal mode string for a diff entry side (`None` → regular file).
71/// Mirrors `diff::git_octal`.
72fn octal(mode: Option<EntryMode>) -> &'static str {
73    match mode {
74        Some(EntryMode::Executable) => "100755",
75        Some(EntryMode::Symlink) => "120000",
76        Some(EntryMode::Tree) => "040000",
77        _ => "100644",
78    }
79}