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 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}