use clap::{Args, Parser, Subcommand, ValueEnum};
#[derive(Parser, Debug, Clone)]
#[command(name = "tang", author, version, about = "Tangled CLI", long_about = None)]
pub struct Cli {
#[arg(long, global = true)]
pub config: Option<String>,
#[arg(long, global = true, value_enum, default_value_t = OutputFormat::Table)]
pub format: OutputFormat,
#[arg(long, global = true, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(long, global = true, default_value_t = false)]
pub quiet: bool,
#[arg(long, global = true, default_value_t = false)]
pub no_color: bool,
#[command(subcommand)]
pub command: Command,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
pub enum OutputFormat {
Table,
Json,
#[value(alias = "yml")]
Yaml,
}
#[derive(Subcommand, Debug, Clone)]
pub enum Command {
#[command(subcommand)]
Auth(AuthCommand),
#[command(subcommand)]
Repo(RepoCommand),
#[command(subcommand)]
Issue(IssueCommand),
#[command(subcommand, alias = "pr")]
Pull(PullCommand),
#[command(subcommand)]
Knot(KnotCommand),
#[command(subcommand)]
Spindle(SpindleCommand),
}
#[derive(Subcommand, Debug, Clone)]
pub enum AuthCommand {
Login(AuthLoginArgs),
Status,
Logout,
}
#[derive(Args, Debug, Clone)]
pub struct AuthLoginArgs {
#[arg(long)]
pub handle: Option<String>,
#[arg(long, conflicts_with = "oauth")]
pub password: Option<String>,
#[arg(long)]
pub pds: Option<String>,
#[arg(long, conflicts_with = "oauth")]
pub app_password: bool,
#[arg(long)]
pub oauth: bool,
}
#[derive(Subcommand, Debug, Clone)]
pub enum RepoCommand {
List(RepoListArgs),
Create(RepoCreateArgs),
Clone(RepoCloneArgs),
Info(RepoInfoArgs),
Delete(RepoDeleteArgs),
Star(RepoRefArgs),
Unstar(RepoRefArgs),
}
#[derive(Args, Debug, Clone)]
pub struct RepoListArgs {
#[arg(long)]
pub knot: Option<String>,
#[arg(long)]
pub user: Option<String>,
#[arg(long, default_value_t = false)]
pub starred: bool,
#[arg(long)]
pub base: Option<String>,
}
#[derive(Args, Debug, Clone)]
pub struct RepoCreateArgs {
pub name: Option<String>,
#[arg(long)]
pub knot: Option<String>,
#[arg(long, default_value_t = false)]
pub private: bool,
#[arg(long)]
pub description: Option<String>,
#[arg(long, default_value_t = false)]
pub init: bool,
}
#[derive(Args, Debug, Clone)]
pub struct RepoCloneArgs {
pub repo: String,
#[arg(long, default_value_t = false)]
pub https: bool,
#[arg(long)]
pub depth: Option<usize>,
}
#[derive(Args, Debug, Clone)]
pub struct RepoInfoArgs {
pub repo: String,
#[arg(long, default_value_t = false)]
pub stats: bool,
#[arg(long, default_value_t = false)]
pub contributors: bool,
}
#[derive(Args, Debug, Clone)]
pub struct RepoDeleteArgs {
pub repo: String,
#[arg(long, default_value_t = false)]
pub force: bool,
}
#[derive(Args, Debug, Clone)]
pub struct RepoRefArgs {
pub repo: String,
}
#[derive(Subcommand, Debug, Clone)]
pub enum IssueCommand {
List(IssueListArgs),
Create(IssueCreateArgs),
Show(IssueShowArgs),
Edit(IssueEditArgs),
Comment(IssueCommentArgs),
}
#[derive(Args, Debug, Clone)]
pub struct IssueListArgs {
#[arg(long)]
pub repo: Option<String>,
#[arg(long)]
pub state: Option<String>,
#[arg(long)]
pub author: Option<String>,
#[arg(long)]
pub label: Option<String>,
#[arg(long)]
pub assigned: Option<String>,
}
#[derive(Args, Debug, Clone)]
pub struct IssueCreateArgs {
#[arg(long)]
pub repo: Option<String>,
#[arg(long)]
pub title: Option<String>,
#[arg(long)]
pub body: Option<String>,
#[arg(long)]
pub label: Option<Vec<String>>,
#[arg(long, value_name = "HANDLE")]
pub assign: Option<Vec<String>>,
}
#[derive(Args, Debug, Clone)]
pub struct IssueShowArgs {
pub id: String,
#[arg(long, default_value_t = false)]
pub comments: bool,
#[arg(long, default_value_t = false)]
pub json: bool,
}
#[derive(Args, Debug, Clone)]
pub struct IssueEditArgs {
pub id: String,
#[arg(long)]
pub title: Option<String>,
#[arg(long)]
pub body: Option<String>,
#[arg(long)]
pub state: Option<String>,
}
#[derive(Args, Debug, Clone)]
pub struct IssueCommentArgs {
pub id: String,
#[arg(long)]
pub body: Option<String>,
#[arg(long, default_value_t = false)]
pub close: bool,
}
#[derive(Subcommand, Debug, Clone)]
pub enum PullCommand {
List(PullListArgs),
Create(PullCreateArgs),
Show(PullShowArgs),
Review(PullReviewArgs),
Merge(PullMergeArgs),
}
#[derive(Args, Debug, Clone)]
pub struct PullListArgs {
#[arg(long)]
pub repo: Option<String>,
#[arg(long)]
pub state: Option<String>,
#[arg(long)]
pub author: Option<String>,
#[arg(long)]
pub reviewer: Option<String>,
}
#[derive(Args, Debug, Clone)]
pub struct PullCreateArgs {
#[arg(long)]
pub repo: Option<String>,
#[arg(long)]
pub target: Option<String>,
#[arg(long)]
pub base: Option<String>,
#[arg(long)]
pub head: Option<String>,
#[arg(long)]
pub title: Option<String>,
#[arg(long)]
pub body: Option<String>,
#[arg(long = "body-file", short = 'F')]
pub body_file: Option<String>,
#[arg(long, short = 'T')]
pub template: Option<String>,
#[arg(long, short = 'e', default_value_t = false)]
pub editor: bool,
#[arg(long = "no-template", default_value_t = false)]
pub no_template: bool,
#[arg(long, default_value_t = false)]
pub draft: bool,
}
#[derive(Args, Debug, Clone)]
pub struct PullShowArgs {
pub id: String,
#[arg(long, default_value_t = false)]
pub diff: bool,
#[arg(long, default_value_t = false)]
pub comments: bool,
#[arg(long, default_value_t = false)]
pub checks: bool,
}
#[derive(Args, Debug, Clone)]
pub struct PullReviewArgs {
pub id: String,
#[arg(long, default_value_t = false)]
pub approve: bool,
#[arg(long, default_value_t = false)]
pub request_changes: bool,
#[arg(long)]
pub comment: Option<String>,
}
#[derive(Args, Debug, Clone)]
pub struct PullMergeArgs {
pub id: String,
}
#[derive(Subcommand, Debug, Clone)]
pub enum KnotCommand {
Migrate(KnotMigrateArgs),
}
#[derive(Args, Debug, Clone)]
pub struct KnotMigrateArgs {
#[arg(long)]
pub repo: String,
#[arg(long, value_name = "HOST")]
pub to: String,
#[arg(long, default_value_t = true)]
pub https: bool,
#[arg(long, default_value_t = true)]
pub update_record: bool,
}
#[derive(Subcommand, Debug, Clone)]
pub enum SpindleCommand {
List(SpindleListArgs),
Config(SpindleConfigArgs),
Run(SpindleRunArgs),
Logs(SpindleLogsArgs),
#[command(subcommand)]
Secret(SpindleSecretCommand),
}
#[derive(Args, Debug, Clone)]
pub struct SpindleListArgs {
#[arg(long)]
pub repo: Option<String>,
}
#[derive(Args, Debug, Clone)]
pub struct SpindleConfigArgs {
#[arg(long)]
pub repo: Option<String>,
#[arg(long)]
pub url: Option<String>,
#[arg(long, default_value_t = false)]
pub enable: bool,
#[arg(long, default_value_t = false)]
pub disable: bool,
}
#[derive(Args, Debug, Clone)]
pub struct SpindleRunArgs {
#[arg(long)]
pub repo: Option<String>,
#[arg(long)]
pub branch: Option<String>,
#[arg(long, default_value_t = false)]
pub wait: bool,
}
#[derive(Args, Debug, Clone)]
pub struct SpindleLogsArgs {
pub job_id: String,
#[arg(long, default_value_t = false)]
pub follow: bool,
#[arg(long)]
pub lines: Option<usize>,
}
#[derive(Subcommand, Debug, Clone)]
pub enum SpindleSecretCommand {
List(SpindleSecretListArgs),
Add(SpindleSecretAddArgs),
Remove(SpindleSecretRemoveArgs),
}
#[derive(Args, Debug, Clone)]
pub struct SpindleSecretListArgs {
#[arg(long)]
pub repo: String,
}
#[derive(Args, Debug, Clone)]
pub struct SpindleSecretAddArgs {
#[arg(long)]
pub repo: String,
#[arg(long)]
pub key: String,
#[arg(long)]
pub value: String,
}
#[derive(Args, Debug, Clone)]
pub struct SpindleSecretRemoveArgs {
#[arg(long)]
pub repo: String,
#[arg(long)]
pub key: String,
}
#[cfg(test)]
mod tests {
use clap::Parser;
use super::*;
#[test]
fn parses_machine_readable_output_formats() {
let json = Cli::try_parse_from([
"tangled", "repo", "list", "--format", "json",
])
.unwrap();
let yaml = Cli::try_parse_from([
"tangled", "--format", "yml", "issue", "list",
])
.unwrap();
assert_eq!(json.format, OutputFormat::Json);
assert_eq!(yaml.format, OutputFormat::Yaml);
}
#[test]
fn rejects_api_command_tree() {
assert!(
Cli::try_parse_from(["tangled", "api", "auth", "status"]).is_err()
);
}
#[test]
fn parses_convenience_pull_create_without_repo_base_or_head() {
let cli = Cli::try_parse_from(["tangled", "pull", "create"]).unwrap();
assert!(matches!(cli.command, Command::Pull(PullCommand::Create(_))));
}
#[test]
fn parses_pr_alias_for_pull_create() {
let cli = Cli::try_parse_from(["tangled", "pr", "create"]).unwrap();
assert!(matches!(cli.command, Command::Pull(PullCommand::Create(_))));
}
#[test]
fn parses_interactive_repo_create_without_name() {
let cli = Cli::try_parse_from(["tangled", "repo", "create"]).unwrap();
let Command::Repo(RepoCommand::Create(args)) = cli.command else {
panic!("expected repo create");
};
assert!(args.name.is_none());
}
#[test]
fn parses_pull_create_target_and_body_editor_flags() {
let cli = Cli::try_parse_from([
"tangled",
"pull",
"create",
"--target",
"source",
"--template",
"feature",
"--body-file",
"-",
"--editor",
])
.unwrap();
let Command::Pull(PullCommand::Create(args)) = cli.command else {
panic!("expected pull create");
};
assert_eq!(args.target.as_deref(), Some("source"));
assert_eq!(args.template.as_deref(), Some("feature"));
assert_eq!(args.body_file.as_deref(), Some("-"));
assert!(args.editor);
}
#[test]
fn parses_convenience_issue_create_without_repo_or_title() {
let cli = Cli::try_parse_from(["tangled", "issue", "create"]).unwrap();
assert!(matches!(
cli.command,
Command::Issue(IssueCommand::Create(_))
));
}
}