extern crate git_conventional;
extern crate prettytable;
extern crate serde;
extern crate toml;
mod args;
mod config;
mod errors;
mod git;
pub mod lint;
mod parser;
use crate::errors::SumiError;
use args::Opt;
use clap::{CommandFactory, Parser};
use config::{
assemble_config, count_active_rules, generate_commit_msg_hook_content, init_config, Config,
};
use env_logger::Builder;
use git::{execute_git_commit, remove_verbose_output};
use lint::{run_lint, run_lint_on_commit_range, run_lint_on_each_line};
use log::{error, info, LevelFilter};
use parser::ParsedCommit;
use std::io::{self, Read, Write};
pub fn run() -> Result<(), SumiError> {
let args = Opt::parse();
if let Some(init_option) = args.init {
init_config(init_option)?;
return Ok(());
}
if let Some(shell_type) = args.generate_shell_completion {
generate_shell_completion(shell_type);
return Ok(());
}
let config = assemble_config(&args)?;
init_logger_from_config(&config);
if args.prepare_commit_message {
generate_commit_msg_hook_content(&config)?;
return Ok(());
}
if !args.commit && !config.display && count_active_rules(&config) == 0 {
return Err(SumiError::NoRulesEnabled);
}
if let (Some(from), Some(to)) = (&args.from, &args.to) {
let commits = git::get_commits_in_range(from, to)?;
if commits.is_empty() {
info!("No commits found in range {from}..{to}");
return Ok(());
}
let result = run_lint_on_commit_range(commits, &config);
return result.map(|_| ());
}
let commit_message = get_commit_from_arg_or_stdin(args.commit_message, args.commit_file)?;
let lint_result = if config.split_lines {
run_lint_on_each_line(&commit_message, &config, None)
} else {
run_lint(&commit_message, &config, None).map(|pc| vec![pc])
};
if args.commit {
handle_commit_based_on_lint(lint_result, &commit_message, args.force)?;
} else {
lint_result?;
}
Ok(())
}
fn init_logger_from_config(config: &Config) {
Builder::new()
.format(|buf, record| {
if record.level() == log::Level::Error {
writeln!(buf, "β Error: {}", record.args())
} else {
writeln!(buf, "{}", record.args())
}
})
.filter(
None,
if config.quiet {
LevelFilter::Error
} else {
LevelFilter::Info
},
)
.target(env_logger::Target::Stdout)
.init();
}
fn get_commit_from_arg_or_stdin(
commit: Option<String>,
commit_file: Option<String>,
) -> Result<String, SumiError> {
let msg = match (commit, commit_file) {
(Some(message), _) => message,
(None, Some(path)) => get_commit_from_file(&path)?,
(None, None) => get_commit_from_stdin()?,
};
remove_verbose_output(&msg)
}
fn get_commit_from_file(path: &str) -> Result<String, SumiError> {
std::fs::read_to_string(path)
.map(|content| content.trim().to_string())
.map_err(|e| SumiError::GeneralError {
details: format!("Could not read commit message from '{path}': {e}"),
})
}
fn get_commit_from_stdin() -> Result<String, SumiError> {
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
Ok(buffer.trim().to_string())
}
fn handle_commit_based_on_lint(
lint_result: Result<Vec<ParsedCommit>, SumiError>,
commit_message: &str,
force: bool,
) -> Result<(), SumiError> {
match lint_result {
Ok(_) => commit_with_message(commit_message),
Err(lint_errors) if force => {
error!(
"π¨ Forced commit with lint errors: {lint_errors}. Force flag is set, committing despite errors."
);
commit_with_message(commit_message)
}
Err(lint_errors) => {
info!("π‘ Use the --force flag to commit despite errors.");
Err(lint_errors)
}
}
}
fn commit_with_message(commit_message: &str) -> Result<(), SumiError> {
info!("π Running git commitβ¦");
let commit_result = execute_git_commit(commit_message)?;
if commit_result.status.success() {
info!("π Commit successful!");
Ok(())
} else {
Err(construct_commit_error(commit_result))
}
}
fn construct_commit_error(commit_output: std::process::Output) -> SumiError {
let git_output = [&commit_output.stderr[..], &commit_output.stdout[..]].concat();
let git_error_message = extract_error_message(&git_output);
SumiError::ErrorWhileCommitting(git_error_message)
}
fn extract_error_message(git_output: &[u8]) -> String {
let git_error_cow = String::from_utf8_lossy(git_output);
let git_error = git_error_cow.trim();
if git_error.is_empty() {
"Commit failed. No additional error information available.".to_string()
} else {
format!("git output:\n{git_error}")
}
}
fn generate_shell_completion(shell: clap_complete::Shell) {
let cmd = &mut Opt::command();
clap_complete::generate(
shell,
cmd,
cmd.get_name().to_string(),
&mut std::io::stdout(),
);
}