#![feature(coverage_attribute)]
use std::process::{ExitCode, Termination};
use anyhow::Result;
use ariadne::{ColorGenerator, Label, Report, ReportKind, Source};
use clap::Parser;
use clap_stdin::FileOrStdin;
#[repr(u8)]
enum Exit {
Success = 0,
Invalid = 1,
Usage = 2,
}
impl Termination for Exit {
fn report(self) -> ExitCode {
ExitCode::from(self as u8)
}
}
#[derive(Debug)]
#[derive(Parser)]
#[command(
version,
about = "Lint commit messages with atypical.",
long_about = "\
Lint commit messages with atypical.\n\
Check out the documentation for more details: https://github.com/kekkon-nexus/atypical\
",
after_help = "\
Exit codes:\n \
0 the commit message is valid\n \
1 the commit message failed linting, or input could not be read\n \
2 usage error, or no commit message to lint\
"
)]
struct Args {
input: Option<FileOrStdin>,
}
fn message_header(input: &str) -> Option<(usize, &str)> {
let mut offset = 0;
for line in input.split_inclusive('\n') {
let content = line.trim_end_matches(['\n', '\r']);
if !content.trim().is_empty() && !content.trim_start().starts_with('#')
{
return Some((offset, content));
}
offset += line.len();
}
None
}
#[coverage(off)]
fn main() -> Result<Exit> {
let args = Args::parse();
let Some(input) = args.input else {
eprintln!("No input provided.");
return Ok(Exit::Usage);
};
let filename = input.filename().to_owned();
let input = input.contents()?;
let Some((offset, header)) = message_header(&input) else {
eprintln!("No commit message to lint.");
return Ok(Exit::Usage);
};
use chumsky::Parser;
let result = atypical_commit::header().parse(header);
if !result.has_errors() {
return Ok(Exit::Success);
}
let mut report = Report::build(
ReportKind::Error,
(&filename, offset..offset + header.len()),
)
.with_message("Failed to parse commit message")
.with_code(3);
let mut colors = ColorGenerator::new();
for error in result.errors() {
let range = error.span().into_range();
report = report.with_label(
Label::new((&filename, offset + range.start..offset + range.end))
.with_message(error.to_string())
.with_color(colors.next()),
);
}
report.finish().eprint((&filename, Source::from(&input)))?;
Ok(Exit::Invalid)
}