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 if !args.commit && !config.display && count_active_rules(&config) == 0 {
49 return Err(SumiError::NoRulesEnabled);
50 }
51
52 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}