git_sumi/
lib.rs

1extern crate git_conventional;
2extern crate prettytable;
3extern crate serde;
4extern crate toml;
5
6mod args;
7mod config;
8mod errors;
9pub mod lint;
10mod parser;
11
12use crate::errors::SumiError;
13use args::Opt;
14use clap::{CommandFactory, Parser};
15use config::{
16    assemble_config, count_active_rules, generate_commit_msg_hook_content, init_config, Config,
17};
18use env_logger::Builder;
19use lint::{run_lint, run_lint_on_each_line};
20use log::{error, info, LevelFilter};
21use parser::ParsedCommit;
22use std::io::{self, Read, Write};
23
24pub fn run() -> Result<(), SumiError> {
25    let args = Opt::parse();
26
27    if let Some(init_option) = args.init {
28        init_config(init_option)?;
29        return Ok(());
30    }
31
32    if let Some(shell_type) = args.generate_shell_completion {
33        generate_shell_completion(shell_type);
34        return Ok(());
35    }
36
37    let config = assemble_config(&args)?;
38    init_logger_from_config(&config);
39
40    if args.prepare_commit_message {
41        generate_commit_msg_hook_content(&config)?;
42        return Ok(());
43    }
44
45    // It's Lintin' Time.
46    if !args.commit && !config.display && count_active_rules(&config) == 0 {
47        return Err(SumiError::NoRulesEnabled);
48    }
49
50    let commit_message = get_commit_from_arg_or_stdin(args.commit_message, args.commit_file)?;
51
52    let lint_result = if config.split_lines {
53        run_lint_on_each_line(&commit_message, &config)
54    } else {
55        run_lint(&commit_message, &config).map(|pc| vec![pc])
56    };
57
58    if args.commit {
59        handle_commit_based_on_lint(lint_result, &commit_message, args.force)?;
60    } else {
61        lint_result?;
62    }
63
64    Ok(())
65}
66
67fn init_logger_from_config(config: &Config) {
68    Builder::new()
69        .format(|buf, record| {
70            if record.level() == log::Level::Error {
71                writeln!(buf, "❌ Error: {}", record.args())
72            } else {
73                writeln!(buf, "{}", record.args())
74            }
75        })
76        .filter(
77            None,
78            if config.quiet {
79                LevelFilter::Error
80            } else {
81                LevelFilter::Info
82            },
83        )
84        .target(env_logger::Target::Stdout)
85        .init();
86}
87
88fn get_commit_from_arg_or_stdin(
89    commit: Option<String>,
90    commit_file: Option<String>,
91) -> Result<String, SumiError> {
92    match (commit, commit_file) {
93        (Some(message), _) => Ok(message),
94        (None, Some(path)) => get_commit_from_file(&path),
95        (None, None) => get_commit_from_stdin(),
96    }
97}
98
99fn get_commit_from_file(path: &str) -> Result<String, SumiError> {
100    std::fs::read_to_string(path)
101        .map(|content| content.trim().to_string())
102        .map_err(|e| SumiError::GeneralError {
103            details: format!("Could not read commit message from '{path}': {e}"),
104        })
105}
106
107fn get_commit_from_stdin() -> Result<String, SumiError> {
108    let mut buffer = String::new();
109    io::stdin().read_to_string(&mut buffer)?;
110    Ok(buffer.trim().to_string())
111}
112
113fn handle_commit_based_on_lint(
114    lint_result: Result<Vec<ParsedCommit>, SumiError>,
115    commit_message: &str,
116    force: bool,
117) -> Result<(), SumiError> {
118    match lint_result {
119        Ok(_) => commit_with_message(commit_message),
120        Err(lint_errors) if force => {
121            error!(
122                "🚨 Forced commit with lint errors: {lint_errors}. Force flag is set, committing despite errors."
123            );
124            commit_with_message(commit_message)
125        }
126        Err(lint_errors) => {
127            info!("πŸ’‘ Use the --force flag to commit despite errors.");
128            Err(lint_errors)
129        }
130    }
131}
132
133fn commit_with_message(commit_message: &str) -> Result<(), SumiError> {
134    info!("πŸš€ Running git commit…");
135    let commit_result = execute_git_commit(commit_message)?;
136
137    if commit_result.status.success() {
138        info!("πŸŽ‰ Commit successful!");
139        Ok(())
140    } else {
141        Err(construct_commit_error(commit_result))
142    }
143}
144
145fn execute_git_commit(commit_message: &str) -> Result<std::process::Output, std::io::Error> {
146    std::process::Command::new("git")
147        .args(["commit", "-m", commit_message])
148        .output()
149}
150
151fn construct_commit_error(commit_output: std::process::Output) -> SumiError {
152    let git_output = [&commit_output.stderr[..], &commit_output.stdout[..]].concat();
153    let git_error_message = extract_error_message(&git_output);
154    SumiError::ErrorWhileCommitting(git_error_message)
155}
156
157fn extract_error_message(git_output: &[u8]) -> String {
158    let git_error_cow = String::from_utf8_lossy(git_output);
159    let git_error = git_error_cow.trim();
160
161    if git_error.is_empty() {
162        "Commit failed. No additional error information available.".to_string()
163    } else {
164        format!("git output:\n{git_error}")
165    }
166}
167
168fn generate_shell_completion(shell: clap_complete::Shell) {
169    let cmd = &mut Opt::command();
170    clap_complete::generate(
171        shell,
172        cmd,
173        cmd.get_name().to_string(),
174        &mut std::io::stdout(),
175    );
176}