Skip to main content

commit_wizard/core/usecases/commit/
check.rs

1use std::time::Instant;
2
3use crate::{
4    core::{Context, CoreResult},
5    engine::{
6        capabilities::commit::check::validate_commits,
7        constants::emoji::{ERROR, SUCCESS},
8        error::ErrorCode,
9        models::{git::CommitSummary, policy::commit::CommitModel},
10    },
11};
12
13pub fn run(
14    ctx: &Context,
15    tail: Option<u32>,
16    from: Option<String>,
17    to: Option<String>,
18    full_commit_hash: bool,
19) -> CoreResult<()> {
20    let ui = ctx.ui();
21    let start = Instant::now();
22
23    let format_hash = |hash: &str| {
24        if full_commit_hash {
25            hash.to_string()
26        } else {
27            hash[..8.min(hash.len())].to_string()
28        }
29    };
30
31    // Get the resolved config and build the commit policy
32    let resolved_config = ctx.config().ok_or_else(|| {
33        ErrorCode::ConfigUnreadable
34            .error()
35            .with_context("context", "Config not resolved")
36    })?;
37    let policy = CommitModel::from_config(resolved_config);
38
39    // Fetch commits in the specified range
40    let to_ref = to.as_deref().unwrap_or("HEAD").to_string();
41    let mut raw_commits = ctx.git().list_commits(from.as_deref(), &to_ref)?;
42
43    if let Some(n) = tail {
44        let n = n as usize;
45        if raw_commits.len() > n {
46            raw_commits = raw_commits.into_iter().take(n).collect();
47        }
48    }
49
50    let commits = raw_commits
51        .into_iter()
52        .map(|c| CommitSummary {
53            full_message: c.full_message,
54            hash: c.hash,
55            summary: c.summary,
56        })
57        .collect::<Vec<_>>();
58
59    // Validate commits against policy
60    let report = validate_commits(commits, &policy);
61
62    let all_valid = report.invalid_count == 0;
63
64    // Log summary
65    if all_valid {
66        ui.logger().ok(&format!(
67            "{} All {} commit(s) are valid",
68            SUCCESS, report.total
69        ));
70    } else {
71        ui.logger().warn(&format!(
72            "{} {} of {} commit(s) are invalid",
73            ERROR, report.invalid_count, report.total
74        ));
75    }
76
77    // Build output metadata
78    let duration_ms = start.elapsed().as_millis() as u64;
79    let meta = ui
80        .new_output_meta()
81        .with_duration_ms(duration_ms)
82        .with_timestamp(chrono::Utc::now().to_string())
83        .with_command("check".to_string())
84        .with_dry_run(ctx.dry_run());
85
86    // Build output content
87    let mut content = ui
88        .new_output_content()
89        .title(if all_valid {
90            "Commit History Valid"
91        } else {
92            "Commit History Invalid"
93        })
94        .subtitle(if all_valid {
95            "All commits conform to the active rules"
96        } else {
97            "Some commits do not conform to the active rules"
98        })
99        .data("total", report.total.to_string())
100        .data("invalid_count", report.invalid_count.to_string())
101        .data("valid", all_valid.to_string());
102
103    if let Some(from) = from {
104        content = content.data("from", from);
105    }
106    if let Some(to) = to {
107        content = content.data("to", to);
108    }
109    if let Some(tail) = tail {
110        content = content.data("tail", tail.to_string());
111    }
112
113    // Report invalid commits with violation details
114    let invalid_lines: Vec<String> = report
115        .commits
116        .iter()
117        .filter(|c| !c.valid)
118        .map(|c| {
119            let violations_str = c
120                .violations
121                .iter()
122                .map(|v| v.message())
123                .collect::<Vec<_>>()
124                .join(" | ");
125            format!(
126                "{} {} - {}\n       {}",
127                ERROR,
128                format_hash(&c.hash),
129                c.summary,
130                violations_str
131            )
132        })
133        .collect();
134
135    if !invalid_lines.is_empty() {
136        content = content.section(
137            "Invalid Commits",
138            invalid_lines.join("\n"),
139            "sh".to_string(),
140        );
141    }
142
143    // Report valid commits
144    let valid_lines: Vec<String> = report
145        .commits
146        .iter()
147        .filter(|c| c.valid)
148        .map(|c| format!("{} {} {}", SUCCESS, format_hash(&c.hash), c.summary))
149        .collect();
150
151    if !valid_lines.is_empty() {
152        content = content.section("Valid Commits", valid_lines.join("\n"), "sh".to_string());
153    }
154
155    // Plain output: invalid count for machine consumption
156    let plain = report.invalid_count.to_string();
157    content = content.plain(plain);
158
159    ui.print_with_meta(&content, Some(&meta), all_valid)
160}