nils-semantic-commit 1.21.9

CLI crate for nils-semantic-commit in the nils-cli workspace.
Documentation
use clap::{Arg, ArgAction, Command, ValueHint};
use clap_complete::{Shell, generate};
use std::io::{self, Write};

pub fn run(args: &[String]) -> i32 {
    match args.first().map(String::as_str) {
        None => {
            eprintln!("usage: semantic-commit completion <bash|zsh>");
            1
        }
        Some("bash") if args.len() == 1 => generate_script(Shell::Bash),
        Some("zsh") if args.len() == 1 => generate_script(Shell::Zsh),
        Some(shell) if args.len() == 1 => {
            eprintln!("semantic-commit: error: unsupported completion shell '{shell}'");
            eprintln!("usage: semantic-commit completion <bash|zsh>");
            1
        }
        _ => {
            eprintln!("semantic-commit: error: expected `semantic-commit completion <bash|zsh>`");
            1
        }
    }
}

fn generate_script(generator: Shell) -> i32 {
    let mut command = build_completion_command();
    let bin_name = command.get_name().to_string();
    if matches!(generator, Shell::Bash) {
        let mut output = Vec::new();
        generate(generator, &mut command, bin_name.clone(), &mut output);
        let normalized = normalize_bash_completion(
            String::from_utf8(output).expect("bash completion should be valid UTF-8"),
        );
        io::stdout()
            .write_all(normalized.as_bytes())
            .expect("failed to write bash completion");
        return 0;
    }

    generate(generator, &mut command, bin_name, &mut io::stdout());
    0
}

fn normalize_bash_completion(script: String) -> String {
    script.replace("__subcmd__", "__")
}

fn build_completion_command() -> Command {
    Command::new("semantic-commit")
        .version(env!("CARGO_PKG_VERSION"))
        .long_version(nils_build_info::long_version(env!("CARGO_PKG_VERSION")))
        .about("Commit workflow helper with semantic commit validation")
        .disable_help_subcommand(true)
        .subcommand(
            Command::new("staged-context")
                .about("Print staged change context for commit message generation")
                .arg(
                    Arg::new("format")
                        .long("format")
                        .help("Output format for staged context")
                        .value_name("bundle|json|patch")
                        .value_parser(["bundle", "json", "patch"]),
                )
                .arg(
                    Arg::new("json")
                        .long("json")
                        .help("Alias for --format json")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("repo")
                        .long("repo")
                        .help("Repository path override")
                        .value_name("path")
                        .value_hint(ValueHint::DirPath),
                ),
        )
        .subcommand(
            Command::new("commit")
                .about("Commit staged changes with a prepared commit message")
                .arg(
                    Arg::new("message")
                        .short('m')
                        .long("message")
                        .help("Inline commit message")
                        .value_name("text"),
                )
                .arg(
                    Arg::new("message-file")
                        .short('F')
                        .long("message-file")
                        .help("Read commit message from file")
                        .value_name("path")
                        .value_hint(ValueHint::FilePath),
                )
                .arg(
                    Arg::new("message-out")
                        .long("message-out")
                        .help("Write final commit message to file")
                        .value_name("path")
                        .value_hint(ValueHint::FilePath),
                )
                .arg(
                    Arg::new("summary")
                        .long("summary")
                        .help("Summary provider")
                        .value_name("git-scope|git-show|none")
                        .value_parser(["git-scope", "git-show", "none"]),
                )
                .arg(
                    Arg::new("no-summary")
                        .long("no-summary")
                        .help("Disable summary section")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("format")
                        .long("format")
                        .help("Output format")
                        .value_name("text|json")
                        .value_parser(["text", "json"]),
                )
                .arg(
                    Arg::new("json")
                        .long("json")
                        .help("Alias for --format json")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("repo")
                        .long("repo")
                        .help("Repository path override")
                        .value_name("path")
                        .value_hint(ValueHint::DirPath),
                )
                .arg(
                    Arg::new("max-header-width")
                        .long("max-header-width")
                        .help("Override commit header width")
                        .value_name("N"),
                )
                .arg(
                    Arg::new("automation")
                        .long("automation")
                        .help("Disable interactive prompts and stdin input")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("non-interactive")
                        .long("non-interactive")
                        .help("Fail instead of prompting for input")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("validate-only")
                        .long("validate-only")
                        .help("Validate message and exit without committing")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("dry-run")
                        .long("dry-run")
                        .help("Validate message and staged changes without running git commit")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("auto-fix")
                        .long("auto-fix")
                        .help("Normalize body wrap, bullet/type/scope case before validation")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("amend")
                        .long("amend")
                        .help("Amend HEAD instead of creating a new commit")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("no-edit")
                        .long("no-edit")
                        .help("Reuse the HEAD message with --amend")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("message-only")
                        .long("message-only")
                        .help("Amend only the HEAD message and require no staged changes")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("allow-empty")
                        .long("allow-empty")
                        .help("Allow commit operation without staged changes")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("require-clean")
                        .long("require-clean")
                        .help("Require no unstaged or untracked changes")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("no-unstaged")
                        .long("no-unstaged")
                        .help("Alias for --require-clean")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("expect-head")
                        .long("expect-head")
                        .help("Require HEAD to match revision before committing")
                        .value_name("rev"),
                )
                .arg(
                    Arg::new("signoff")
                        .long("signoff")
                        .help("Pass --signoff to git commit")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("trailer")
                        .long("trailer")
                        .help("Add a git trailer")
                        .value_name("token: value")
                        .action(ArgAction::Append),
                )
                .arg(
                    Arg::new("type")
                        .long("type")
                        .help("Structured message type")
                        .value_name("type"),
                )
                .arg(
                    Arg::new("scope")
                        .long("scope")
                        .help("Structured message scope")
                        .value_name("scope"),
                )
                .arg(
                    Arg::new("subject")
                        .long("subject")
                        .help("Structured message subject")
                        .value_name("subject"),
                )
                .arg(
                    Arg::new("body-bullet")
                        .long("body-bullet")
                        .alias("bullet")
                        .help("Structured message body bullet")
                        .value_name("text")
                        .action(ArgAction::Append),
                )
                .arg(
                    Arg::new("no-progress")
                        .long("no-progress")
                        .help("Disable progress UI")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("quiet")
                        .long("quiet")
                        .help("Reduce non-error output")
                        .action(ArgAction::SetTrue),
                ),
        )
        .subcommand(cleanup_completion_command(
            "fixup",
            "Create a fixup! commit for staged changes",
        ))
        .subcommand(cleanup_completion_command(
            "squash",
            "Create a squash! commit for staged changes",
        ))
        .subcommand(
            Command::new("completion")
                .about("Export shell completion script")
                .arg(
                    Arg::new("shell")
                        .value_name("shell")
                        .value_parser(["bash", "zsh"])
                        .required(true),
                ),
        )
        .subcommand(Command::new("help").about("Display help message"))
}

fn cleanup_completion_command(name: &'static str, about: &'static str) -> Command {
    Command::new(name)
        .about(about)
        .arg(
            Arg::new("target")
                .long("target")
                .help("Target commit revision")
                .value_name("rev"),
        )
        .arg(
            Arg::new("summary")
                .long("summary")
                .help("Summary provider")
                .value_name("git-scope|git-show|none")
                .value_parser(["git-scope", "git-show", "none"]),
        )
        .arg(
            Arg::new("no-summary")
                .long("no-summary")
                .help("Disable summary section")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("format")
                .long("format")
                .help("Output format")
                .value_name("text|json")
                .value_parser(["text", "json"]),
        )
        .arg(
            Arg::new("json")
                .long("json")
                .help("Alias for --format json")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("dry-run")
                .long("dry-run")
                .help("Validate target and staged checks without committing")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("allow-empty")
                .long("allow-empty")
                .help("Allow cleanup commit without staged changes")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("require-clean")
                .long("require-clean")
                .help("Require no unstaged or untracked changes")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("no-unstaged")
                .long("no-unstaged")
                .help("Alias for --require-clean")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("expect-head")
                .long("expect-head")
                .help("Require HEAD to match revision before committing")
                .value_name("rev"),
        )
        .arg(
            Arg::new("repo")
                .long("repo")
                .help("Repository path override")
                .value_name("path")
                .value_hint(ValueHint::DirPath),
        )
        .arg(
            Arg::new("no-progress")
                .long("no-progress")
                .help("Disable progress UI")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("quiet")
                .long("quiet")
                .help("Reduce non-error output")
                .action(ArgAction::SetTrue),
        )
}