creditlint 0.1.2

CLI for enforcing Git credit and authorship metadata policy
Documentation
use clap::ValueEnum;
use serde::Serialize;

use crate::policy::Violation;

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum OutputFormat {
    Human,
    Json,
}

pub fn render_violations(
    format: OutputFormat,
    violations: &[Violation],
) -> Result<String, serde_json::Error> {
    match format {
        OutputFormat::Human => Ok(render_human(violations)),
        OutputFormat::Json => render_json(violations),
    }
}

fn render_human(violations: &[Violation]) -> String {
    if violations.is_empty() {
        return String::new();
    }

    let mut lines = Vec::new();
    lines.push(format!(
        "creditlint found {} violation(s)",
        violations.len()
    ));

    for violation in violations {
        lines.push(format!("rule: {}", violation.rule_id));
        if let Some(commit_sha) = &violation.source.commit_sha {
            lines.push(format!("commit: {commit_sha}"));
        }
        if let Some(field) = &violation.field {
            lines.push(format!("field: {field}"));
        }
        if let Some(line) = violation.line {
            lines.push(format!("line: {line}"));
        }
        lines.push(format!("message: {}", violation.message));
    }

    lines.join("\n")
}

fn render_json(violations: &[Violation]) -> Result<String, serde_json::Error> {
    let report = ViolationReport {
        ok: violations.is_empty(),
        violations,
    };

    serde_json::to_string_pretty(&report)
}

#[derive(Debug, Serialize)]
struct ViolationReport<'a> {
    ok: bool,
    violations: &'a [Violation],
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use super::{OutputFormat, render_violations};
    use crate::policy::{Source, SourceKind, Violation};

    #[test]
    fn human_output_includes_commit_sha_when_present() {
        let report = render_violations(
            OutputFormat::Human,
            &[Violation {
                source: Source {
                    kind: SourceKind::Commit,
                    path: Some(PathBuf::from(".git/COMMIT_EDITMSG")),
                    commit_sha: Some("abc123".to_string()),
                },
                rule_id: "forbidden-ai-coauthor".to_string(),
                field: Some("Co-authored-by".to_string()),
                line: Some(2),
                message: "AI/tool authorship marker is not allowed".to_string(),
            }],
        )
        .expect("render report");

        assert!(report.contains("commit: abc123"));
    }
}