Skip to main content

git_sumi/
lib.rs

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