use clap::{Parser, Subcommand, ValueEnum};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
pub enum Provider {
#[default]
Copilot,
Codex,
}
#[derive(Debug, Parser)]
#[command(
name = "git-ca",
version,
about = "Draft git commit messages with GitHub Copilot",
disable_help_subcommand = true
)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
#[arg(short = 'n', long = "no-verify", global = true)]
pub no_verify: bool,
#[arg(short = 'm', long = "model", global = true)]
pub model: Option<String>,
#[arg(short = 'y', long = "yes", global = true)]
pub yes: bool,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Pr {
#[arg(long)]
base: Option<String>,
#[arg(long, value_enum, default_value_t = PrSource::Diff)]
source: PrSource,
},
Auth {
#[command(subcommand)]
action: AuthAction,
},
Models,
Config {
#[command(subcommand)]
action: ConfigAction,
},
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
pub enum PrSource {
#[default]
Diff,
Commits,
}
#[derive(Debug, Subcommand)]
pub enum AuthAction {
Login {
#[arg(long, value_enum)]
provider: Option<Provider>,
account: Option<String>,
},
SetToken {
#[arg(long, default_value = "default")]
account: String,
token: String,
},
Logout { account: Option<String> },
Use { account: String },
Status,
}
#[derive(Debug, Subcommand)]
pub enum ConfigAction {
List,
SetModel { id: String },
GetModel,
SetAutoAccept {
#[arg(action = clap::ArgAction::Set)]
value: bool,
},
GetAutoAccept,
SetAutoAcceptPr {
#[arg(action = clap::ArgAction::Set)]
value: bool,
},
GetAutoAcceptPr,
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn parses_yes_long_flag() {
let cli = Cli::try_parse_from(["git-ca", "--yes"]).unwrap();
assert!(cli.yes);
}
#[test]
fn parses_model_short_flag() {
let cli = Cli::try_parse_from(["git-ca", "-m", "gpt-4o"]).unwrap();
assert_eq!(cli.model.as_deref(), Some("gpt-4o"));
}
#[test]
fn parses_yes_short_flag() {
let cli = Cli::try_parse_from(["git-ca", "-y"]).unwrap();
assert!(cli.yes);
}
#[test]
fn rejects_auto_accept_flag_alias() {
let err = Cli::try_parse_from(["git-ca", "--auto-accept"]).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
}
#[test]
fn parses_set_auto_accept_value() {
let cli = Cli::try_parse_from(["git-ca", "config", "set-auto-accept", "true"]).unwrap();
assert!(matches!(
cli.command,
Some(Command::Config {
action: ConfigAction::SetAutoAccept { value: true }
})
));
}
#[test]
fn parses_set_auto_accept_pr_value() {
let cli = Cli::try_parse_from(["git-ca", "config", "set-auto-accept-pr", "true"]).unwrap();
assert!(matches!(
cli.command,
Some(Command::Config {
action: ConfigAction::SetAutoAcceptPr { value: true }
})
));
}
#[test]
fn parses_get_auto_accept_pr() {
let cli = Cli::try_parse_from(["git-ca", "config", "get-auto-accept-pr"]).unwrap();
assert!(matches!(
cli.command,
Some(Command::Config {
action: ConfigAction::GetAutoAcceptPr
})
));
}
#[test]
fn parses_config_list() {
let cli = Cli::try_parse_from(["git-ca", "config", "list"]).unwrap();
assert!(matches!(
cli.command,
Some(Command::Config {
action: ConfigAction::List
})
));
}
#[test]
fn parses_pr_defaults_to_diff_source() {
let cli = Cli::try_parse_from(["git-ca", "pr"]).unwrap();
assert!(matches!(
cli.command,
Some(Command::Pr {
base: None,
source: PrSource::Diff,
})
));
}
#[test]
fn parses_pr_base_and_commit_source() {
let cli = Cli::try_parse_from(["git-ca", "pr", "--base", "develop", "--source", "commits"])
.unwrap();
assert!(matches!(
cli.command,
Some(Command::Pr {
base,
source: PrSource::Commits,
}) if base.as_deref() == Some("develop")
));
}
#[test]
fn parses_global_model_after_pr_subcommand() {
let cli = Cli::try_parse_from(["git-ca", "pr", "--model", "gpt-4o"]).unwrap();
assert_eq!(cli.model.as_deref(), Some("gpt-4o"));
}
#[test]
fn parses_auth_login_account_name() {
let cli = Cli::try_parse_from(["git-ca", "auth", "login", "work"]).unwrap();
assert!(matches!(
cli.command,
Some(Command::Auth {
action: AuthAction::Login { provider: None, account }
}) if account.as_deref() == Some("work")
));
}
#[test]
fn parses_auth_login_without_provider_or_account() {
let cli = Cli::try_parse_from(["git-ca", "auth", "login"]).unwrap();
assert!(matches!(
cli.command,
Some(Command::Auth {
action: AuthAction::Login {
provider: None,
account: None,
}
})
));
}
#[test]
fn parses_auth_login_codex_provider() {
let cli = Cli::try_parse_from(["git-ca", "auth", "login", "--provider", "codex"]).unwrap();
assert!(matches!(
cli.command,
Some(Command::Auth {
action: AuthAction::Login {
provider: Some(Provider::Codex),
account: None,
}
})
));
}
#[test]
fn parses_auth_login_codex_provider_with_account() {
let cli =
Cli::try_parse_from(["git-ca", "auth", "login", "--provider", "codex", "personal"])
.unwrap();
assert!(matches!(
cli.command,
Some(Command::Auth {
action: AuthAction::Login {
provider: Some(Provider::Codex),
account,
}
}) if account.as_deref() == Some("personal")
));
}
#[test]
fn parses_auth_set_token_default_account() {
let cli = Cli::try_parse_from(["git-ca", "auth", "set-token", "gho_manual"]).unwrap();
assert!(matches!(
cli.command,
Some(Command::Auth {
action: AuthAction::SetToken { account, token }
}) if account == "default" && token == "gho_manual"
));
}
#[test]
fn parses_auth_set_token_named_account() {
let cli = Cli::try_parse_from([
"git-ca",
"auth",
"set-token",
"--account",
"work",
"gho_manual",
])
.unwrap();
assert!(matches!(
cli.command,
Some(Command::Auth {
action: AuthAction::SetToken { account, token }
}) if account == "work" && token == "gho_manual"
));
}
#[test]
fn parses_auth_use_account_name() {
let cli = Cli::try_parse_from(["git-ca", "auth", "use", "personal"]).unwrap();
assert!(matches!(
cli.command,
Some(Command::Auth {
action: AuthAction::Use { account }
}) if account == "personal"
));
}
#[test]
fn git_help_man_page_is_packaged() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("docs")
.join("man")
.join("git-ca.1");
let man_page = std::fs::read_to_string(path).unwrap();
assert!(man_page.contains(".TH GIT-CA 1"));
assert!(man_page.contains("git ca \\-h"));
assert!(man_page.contains("git ca \\--yes"));
}
}