use clap::{Parser, Subcommand, ValueEnum};
#[derive(Parser, Debug)]
#[command(
name = "gvc",
about = "Gradle Version Catalog - A tool to manage Gradle dependency updates",
version,
author
)]
pub struct Cli {
#[arg(short, long, global = true, default_value = ".")]
pub path: String,
#[arg(long, global = true, value_name = "FILE")]
pub catalog: Option<String>,
#[arg(long, global = true, value_enum, default_value_t = OutputFormat::Text)]
pub format: OutputFormat,
#[arg(long, global = true)]
pub no_color: bool,
#[arg(short = 'q', long, global = true)]
pub quiet: bool,
#[arg(short, long, global = true)]
pub verbose: bool,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Update {
#[arg(short, long)]
interactive: bool,
#[arg(long, value_name = "GLOB", conflicts_with = "target")]
filter: Option<String>,
#[arg(long, value_name = "GLOB")]
target: Option<String>,
#[arg(short, long, action = clap::ArgAction::SetTrue, conflicts_with = "no_stable_only")]
stable_only: bool,
#[arg(long = "no-stable-only", action = clap::ArgAction::SetTrue)]
no_stable_only: bool,
#[arg(long)]
no_git: bool,
#[arg(long, conflicts_with = "apply")]
dry_run: bool,
#[arg(long)]
apply: bool,
},
Check {
#[arg(long)]
include_unstable: bool,
#[arg(long)]
fail_on_updates: bool,
},
Outdated {
#[arg(long)]
include_unstable: bool,
#[arg(long)]
fail_on_updates: bool,
},
List,
Why {
#[arg(value_name = "QUERY")]
query: String,
},
Audit {
#[arg(long)]
fail_on_issues: bool,
},
Doctor {
#[arg(long)]
fail_on_issues: bool,
},
Add {
#[arg(short = 'P', long = "plugin", conflicts_with = "library")]
plugin: bool,
#[arg(short = 'l', long = "library", conflicts_with = "plugin")]
library: bool,
#[arg(value_name = "COORDINATE")]
coordinate: String,
#[arg(long)]
alias: Option<String>,
#[arg(long = "version-alias")]
version_alias: Option<String>,
#[arg(long = "no-stable-only", action = clap::ArgAction::SetFalse, default_value_t = true)]
stable_only: bool,
#[arg(long)]
update_version_alias: bool,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum OutputFormat {
Text,
Json,
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn update_accepts_no_stable_only_flag() {
let cli = Cli::parse_from(["gvc", "update", "--no-stable-only"]);
let Commands::Update { no_stable_only, .. } = cli.command else {
panic!("expected update command");
};
assert!(no_stable_only);
}
#[test]
fn update_accepts_target_and_dry_run_flags() {
let cli = Cli::parse_from(["gvc", "update", "--target", "kotlin", "--dry-run"]);
let Commands::Update {
target, dry_run, ..
} = cli.command
else {
panic!("expected update command");
};
assert_eq!(target.as_deref(), Some("kotlin"));
assert!(dry_run);
}
#[test]
fn update_rejects_filter_with_target() {
let result =
Cli::try_parse_from(["gvc", "update", "--filter", "kotlin", "--target", "ksp"]);
assert!(result.is_err());
}
#[test]
fn update_rejects_apply_with_dry_run() {
let result = Cli::try_parse_from(["gvc", "update", "--dry-run", "--apply"]);
assert!(result.is_err());
}
#[test]
fn accepts_global_agent_output_flags() {
let cli = Cli::parse_from([
"gvc",
"--path",
"/tmp/project",
"--format",
"json",
"--no-color",
"--quiet",
"--catalog",
"gradle/custom.versions.toml",
"list",
]);
assert_eq!(cli.format, OutputFormat::Json);
assert_eq!(cli.path, "/tmp/project");
assert!(cli.no_color);
assert!(cli.quiet);
assert_eq!(cli.catalog.as_deref(), Some("gradle/custom.versions.toml"));
}
#[test]
fn accepts_path_after_subcommand() {
let cli = Cli::parse_from(["gvc", "check", "--path", "/tmp/project"]);
let Commands::Check { .. } = cli.command else {
panic!("expected check command");
};
assert_eq!(cli.path, "/tmp/project");
}
#[test]
fn check_accepts_fail_on_updates_flag() {
let cli = Cli::parse_from(["gvc", "check", "--fail-on-updates"]);
let Commands::Check {
fail_on_updates, ..
} = cli.command
else {
panic!("expected check command");
};
assert!(fail_on_updates);
}
#[test]
fn outdated_accepts_agent_flags() {
let cli = Cli::parse_from(["gvc", "outdated", "--include-unstable", "--fail-on-updates"]);
let Commands::Outdated {
include_unstable,
fail_on_updates,
} = cli.command
else {
panic!("expected outdated command");
};
assert!(include_unstable);
assert!(fail_on_updates);
}
#[test]
fn audit_accepts_fail_on_issues_flag() {
let cli = Cli::parse_from(["gvc", "audit", "--fail-on-issues"]);
let Commands::Audit { fail_on_issues } = cli.command else {
panic!("expected audit command");
};
assert!(fail_on_issues);
}
#[test]
fn why_accepts_query() {
let cli = Cli::parse_from(["gvc", "why", "androidxCore"]);
let Commands::Why { query } = cli.command else {
panic!("expected why command");
};
assert_eq!(query, "androidxCore");
}
#[test]
fn add_accepts_update_version_alias_flag() {
let cli = Cli::parse_from([
"gvc",
"add",
"--update-version-alias",
"com.example:lib:1.0.0",
]);
let Commands::Add {
update_version_alias,
..
} = cli.command
else {
panic!("expected add command");
};
assert!(update_version_alias);
}
#[test]
fn add_accepts_visible_plugin_short_flag() {
let cli = Cli::parse_from(["gvc", "add", "-P", "org.jetbrains.kotlin.jvm:2.0.21"]);
let Commands::Add { plugin, .. } = cli.command else {
panic!("expected add command");
};
assert!(plugin);
}
#[test]
fn add_rejects_legacy_plugin_short_flag() {
let result = Cli::try_parse_from(["gvc", "add", "-p", "org.jetbrains.kotlin.jvm:2.0.21"]);
assert!(result.is_err());
}
#[test]
fn doctor_accepts_fail_on_issues_flag() {
let cli = Cli::parse_from(["gvc", "doctor", "--fail-on-issues"]);
let Commands::Doctor { fail_on_issues } = cli.command else {
panic!("expected doctor command");
};
assert!(fail_on_issues);
}
}