use std::fs;
use std::io::IsTerminal;
use std::path::{Component, Path, PathBuf};
use anyhow::{Context, Result};
use clap::{ArgGroup, Args, CommandFactory, Parser, Subcommand, ValueEnum};
use clap_complete::{Shell, generate};
use serde_json::{Value, json};
use sha2::Digest;
use crate::build_info;
use crate::config;
use crate::error::{ClassifiedError, ErrorKind};
use crate::health;
use crate::report;
use crate::report_ops::{
ExplainSelector, PlanSelector, PromptPackOptions, compare_payload_with_policy, explain_payload,
failing_records_in, health_json_payload, plan_payload, render_compare_text,
render_explain_text, render_github_annotations, render_json, render_plan_text,
render_show_text, sarif_payload, show_payload, write_prompt_pack,
};
use crate::{PROJECT_NAME, VERSION, analyze, git};
#[derive(Debug, Parser)]
#[command(
name = "git-slop",
about = "Find the files that cost too much context.",
version = VERSION
)]
struct Cli {
#[arg(long, global = true, value_name = "PATH")]
repo: Option<PathBuf>,
#[arg(long, global = true, value_enum, default_value_t = ErrorFormat::Human)]
error_format: ErrorFormat,
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
Init(InitArgs),
Find(FindArgs),
Show(ShowArgs),
Explain(ExplainArgs),
Plan(PlanArgs),
Check(CheckArgs),
Compare(CompareArgs),
Baseline(BaselineArgs),
Report(ReportArgs),
Sarif(SarifArgs),
Health(HealthArgs),
Config(ConfigArgs),
Doctor(DoctorArgs),
List(ListArgs),
Prune(PruneArgs),
Cache(CacheArgs),
Completions(CompletionsArgs),
Man(ManArgs),
Reference(ReferenceArgs),
Html(HtmlArgs),
Version,
BuildInfo(BuildInfoArgs),
Schema(SchemaArgs),
}
impl Command {
fn name(&self) -> &'static str {
match self {
Self::Init(_) => "init",
Self::Find(_) => "find",
Self::Show(_) => "show",
Self::Explain(_) => "explain",
Self::Plan(_) => "plan",
Self::Check(_) => "check",
Self::Compare(_) => "compare",
Self::Baseline(_) => "baseline",
Self::Report(_) => "report",
Self::Sarif(_) => "sarif",
Self::Health(_) => "health",
Self::Config(_) => "config",
Self::Doctor(_) => "doctor",
Self::List(_) => "list",
Self::Prune(_) => "prune",
Self::Cache(_) => "cache",
Self::Completions(_) => "completions",
Self::Man(_) => "man",
Self::Reference(_) => "reference",
Self::Html(_) => "html",
Self::Version => "version",
Self::BuildInfo(_) => "build-info",
Self::Schema(_) => "schema",
}
}
}
#[derive(Debug, Args)]
struct SchemaArgs {
#[arg(value_enum)]
contract: SchemaContract,
#[arg(long)]
output: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum SchemaContract {
Report,
Config,
Compare,
Explain,
Plan,
Sarif,
Health,
Check,
Doctor,
BuildInfo,
List,
Show,
PromptManifest,
Error,
FindEstimate,
CacheStatus,
CachePrune,
Baseline,
Prune,
CompareNdjson,
}
#[derive(Debug, Args)]
struct FindArgs {
#[arg(long)]
allow_shallow: bool,
#[arg(long)]
scope: Option<String>,
#[arg(long)]
allow_empty_scope: bool,
#[arg(long)]
quiet: bool,
#[arg(long)]
no_progress: bool,
#[arg(long, value_name = "PATH")]
state_dir: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
output_dir: Option<PathBuf>,
#[arg(long)]
no_cache: bool,
#[arg(long)]
allow_degraded: bool,
#[arg(long, value_name = "RFC3339")]
as_of: Option<String>,
#[arg(long, value_enum, default_value_t = ReportProfile::Standard)]
report_profile: ReportProfile,
#[arg(long, value_enum, default_value_t = ReportCompression::None)]
compression: ReportCompression,
#[arg(long)]
estimate_only: bool,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum ReportProfile {
Compact,
Standard,
FullEvidence,
}
impl ReportProfile {
fn as_str(self) -> &'static str {
match self {
Self::Compact => "compact",
Self::Standard => "standard",
Self::FullEvidence => "full_evidence",
}
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum ReportCompression {
None,
Gzip,
Zstd,
}
impl ReportCompression {
fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::Gzip => "gzip",
Self::Zstd => "zstd",
}
}
}
#[derive(Debug, Args)]
struct BuildInfoArgs {
#[arg(long, value_enum, default_value_t = BuildInfoFormat::Json)]
format: BuildInfoFormat,
}
#[derive(Debug, Args)]
struct InitArgs {
#[arg(long)]
force: bool,
}
#[derive(Debug, Args)]
struct ShowArgs {
target_path: String,
#[arg(long)]
report: Option<PathBuf>,
#[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
format: DisplayFormat,
}
#[derive(Debug, Args)]
#[command(group(
ArgGroup::new("selector")
.args(["path", "cluster", "relationship", "top"])
.multiple(false)
))]
struct ExplainArgs {
#[arg(long)]
report: Option<PathBuf>,
#[arg(long)]
path: Option<String>,
#[arg(long)]
cluster: Option<String>,
#[arg(long)]
relationship: Option<String>,
#[arg(long)]
top: Option<i64>,
#[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
format: DisplayFormat,
#[arg(long)]
prompt_pack: Option<PathBuf>,
#[arg(long, requires = "prompt_pack")]
force: bool,
#[arg(long, requires = "prompt_pack")]
include_repository_context: bool,
#[arg(long, default_value_t = 2048, requires = "include_repository_context")]
excerpt_bytes: usize,
#[arg(long, requires = "prompt_pack")]
include_local_paths: bool,
}
#[derive(Debug, Args)]
#[command(group(
ArgGroup::new("selector")
.args(["path", "cluster", "relationship"])
.required(true)
.multiple(false)
))]
struct PlanArgs {
#[arg(long)]
report: Option<PathBuf>,
#[arg(long)]
path: Option<String>,
#[arg(long)]
cluster: Option<String>,
#[arg(long)]
relationship: Option<String>,
#[arg(long, default_value_t = 3)]
max_slices: i64,
#[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
format: DisplayFormat,
#[arg(long)]
prompt_pack: Option<PathBuf>,
#[arg(long, requires = "prompt_pack")]
force: bool,
#[arg(long, requires = "prompt_pack")]
include_repository_context: bool,
#[arg(long, default_value_t = 2048, requires = "include_repository_context")]
excerpt_bytes: usize,
#[arg(long, requires = "prompt_pack")]
include_local_paths: bool,
}
#[derive(Debug, Args)]
struct CheckArgs {
#[arg(long)]
report: Option<PathBuf>,
#[arg(long, value_enum)]
fail_on_context_band: Option<ContextBand>,
#[arg(long, value_enum)]
fail_on_slop_band: Option<SlopBand>,
#[arg(long, value_enum, default_value_t = CheckFormat::Text)]
format: CheckFormat,
#[arg(long)]
details: bool,
#[arg(long)]
include_folders: bool,
#[arg(long, default_value_t = 0, requires = "details")]
offset: usize,
#[arg(long, default_value_t = 1000, requires = "details")]
limit: usize,
#[arg(long)]
allow_incomplete_evidence: bool,
#[arg(long)]
evaluate_only: bool,
}
#[derive(Debug, Args)]
struct CompareArgs {
#[arg(
long,
required_unless_present_any = ["base_ref", "baseline"],
conflicts_with_all = ["base_ref", "baseline"]
)]
base: Option<PathBuf>,
#[arg(long, conflicts_with_all = ["base", "baseline"])]
base_ref: Option<String>,
#[arg(long, conflicts_with_all = ["base", "base_ref"])]
baseline: Option<String>,
#[arg(long, default_value = ".slop/latest/report.json")]
head: PathBuf,
#[arg(long)]
scope: Option<String>,
#[arg(long)]
allow_shallow: bool,
#[arg(long)]
allow_incomplete_evidence: bool,
#[arg(long, default_value_t = 10)]
top: i64,
#[arg(long, value_enum, default_value_t = CompareFormat::Text)]
format: CompareFormat,
#[arg(long, value_enum, default_value_t = CompareDetail::Top)]
detail: CompareDetail,
#[arg(long, default_value_t = 0)]
offset: usize,
#[arg(long, default_value_t = 1000)]
limit: usize,
#[arg(long)]
force: bool,
#[arg(long)]
include_local_paths: bool,
#[arg(long)]
include_unchanged: bool,
#[arg(long, value_enum, default_value_t = PolicySource::Base)]
policy_from: PolicySource,
#[arg(long)]
fail_on_regression: bool,
}
#[derive(Debug, Args)]
struct BaselineArgs {
#[command(subcommand)]
command: BaselineCommand,
}
#[derive(Debug, Subcommand)]
enum BaselineCommand {
Ensure {
#[arg(long, default_value = "default")]
name: String,
#[arg(long)]
report: Option<PathBuf>,
#[arg(long)]
replace: bool,
#[arg(long)]
allow_dirty: bool,
#[arg(long)]
allow_incomplete_evidence: bool,
#[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
format: DisplayFormat,
},
Create {
#[arg(long, default_value = "default")]
name: String,
#[arg(long)]
report: Option<PathBuf>,
#[arg(long)]
force: bool,
#[arg(long)]
allow_dirty: bool,
#[arg(long)]
allow_incomplete_evidence: bool,
#[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
format: DisplayFormat,
},
Update {
#[arg(long, default_value = "default")]
name: String,
#[arg(long)]
report: Option<PathBuf>,
#[arg(long)]
allow_dirty: bool,
#[arg(long)]
allow_incomplete_evidence: bool,
#[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
format: DisplayFormat,
},
List {
#[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
format: DisplayFormat,
},
Inspect {
#[arg(long, default_value = "default")]
name: String,
#[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
format: DisplayFormat,
},
Validate {
#[arg(long, default_value = "default")]
name: String,
#[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
format: DisplayFormat,
},
Remove {
#[arg(long, default_value = "default")]
name: String,
#[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
format: DisplayFormat,
},
}
#[derive(Debug, Args)]
struct ReportArgs {
#[command(subcommand)]
command: ReportCommand,
}
#[derive(Debug, Subcommand)]
enum ReportCommand {
Validate {
#[arg(value_name = "REPORT_JSON")]
path: PathBuf,
#[arg(long)]
allow_legacy: bool,
},
Migrate {
#[arg(value_name = "REPORT_JSON")]
path: PathBuf,
#[arg(long, value_name = "PATH")]
output: PathBuf,
},
Schema,
}
#[derive(Debug, Args)]
struct SarifArgs {
#[arg(long)]
report: Option<PathBuf>,
#[arg(long)]
top: Option<i64>,
#[arg(long)]
output: Option<PathBuf>,
#[arg(long)]
include_local_paths: bool,
}
#[derive(Debug, Args)]
struct HealthArgs {
#[arg(long)]
report: Option<PathBuf>,
#[arg(long, value_enum, default_value_t = HealthFormat::Text)]
format: HealthFormat,
#[arg(long, default_value_t = 10)]
max_annotations: usize,
}
#[derive(Debug, Args)]
struct ConfigArgs {
#[command(subcommand)]
command: ConfigCommand,
}
#[derive(Debug, Subcommand)]
enum ConfigCommand {
Show {
#[arg(long)]
effective: bool,
},
Validate,
DiffDefaults,
Migrate,
Schema,
}
#[derive(Debug, Args)]
struct DoctorArgs {
#[arg(long, num_args = 0..=1, default_missing_value = ".slop/diagnostic-bundle.json")]
bundle: Option<PathBuf>,
#[arg(long, value_enum, default_value_t = DoctorFormat::Text)]
format: DoctorFormat,
#[arg(long)]
scope: Option<String>,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum DoctorFormat {
Text,
Json,
}
#[derive(Debug, Args)]
struct ListArgs {
#[command(subcommand)]
command: ListCommand,
}
#[derive(Debug, Subcommand)]
enum ListCommand {
Findings(ListFilterArgs),
Relationships(ListFilterArgs),
Clusters(ListFilterArgs),
Profiles(ListFilterArgs),
}
#[derive(Debug, Args)]
struct ListFilterArgs {
#[arg(long)]
report: Option<PathBuf>,
#[arg(long)]
path: Option<String>,
#[arg(long)]
profile: Option<String>,
#[arg(long)]
language: Option<String>,
#[arg(long, visible_alias = "class")]
classification: Option<String>,
#[arg(long)]
severity: Option<String>,
#[arg(long, default_value_t = 50)]
top: usize,
#[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
format: DisplayFormat,
#[arg(long)]
wide: bool,
#[arg(long)]
no_truncate: bool,
}
#[derive(Debug, Args)]
struct PruneArgs {
#[arg(long)]
keep: Option<usize>,
#[arg(long)]
max_bytes: Option<u64>,
#[arg(long)]
dry_run: bool,
#[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
format: DisplayFormat,
}
#[derive(Debug, Args)]
struct CacheArgs {
#[arg(long, value_name = "PATH", global = true)]
state_dir: Option<PathBuf>,
#[command(subcommand)]
command: CacheCommand,
}
#[derive(Debug, Subcommand)]
enum CacheCommand {
Status {
#[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
format: DisplayFormat,
},
Prune {
#[arg(long, default_value_t = 10_000)]
max_entries: usize,
#[arg(long, default_value_t = 536_870_912)]
max_bytes: u64,
#[arg(long)]
dry_run: bool,
#[arg(long)]
compact: bool,
#[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
format: DisplayFormat,
},
}
#[derive(Debug, Args)]
struct CompletionsArgs {
shell: CompletionShell,
}
#[derive(Debug, Args)]
struct ManArgs {
#[arg(long)]
output: Option<PathBuf>,
}
#[derive(Debug, Args)]
struct ReferenceArgs {
#[arg(long)]
output: Option<PathBuf>,
}
#[derive(Debug, Args)]
struct HtmlArgs {
#[arg(long)]
report: Option<PathBuf>,
#[arg(long)]
output: Option<PathBuf>,
#[arg(long)]
include_local_paths: bool,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum CompletionShell {
Bash,
Zsh,
Fish,
Powershell,
Nushell,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum DisplayFormat {
Text,
Json,
Yaml,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum CompareFormat {
Text,
Json,
Yaml,
Ndjson,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum CompareDetail {
Summary,
Top,
Full,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum HealthFormat {
Text,
Markdown,
Github,
Json,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum BuildInfoFormat {
Json,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum CheckFormat {
Text,
Json,
Github,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum PolicySource {
Base,
Head,
}
impl PolicySource {
fn as_str(self) -> &'static str {
match self {
Self::Base => "base",
Self::Head => "head",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum ErrorFormat {
Human,
Json,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum ContextBand {
Compact,
Healthy,
Warning,
Critical,
}
impl ContextBand {
fn as_str(self) -> &'static str {
match self {
Self::Compact => "compact",
Self::Healthy => "healthy",
Self::Warning => "warning",
Self::Critical => "critical",
}
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum SlopBand {
Low,
Moderate,
High,
Critical,
}
impl SlopBand {
fn as_str(self) -> &'static str {
match self {
Self::Low => "low",
Self::Moderate => "moderate",
Self::High => "high",
Self::Critical => "critical",
}
}
}
include!("cli/support.rs");
include!("cli/analysis.rs");
include!("cli/check.rs");
include!("cli/baseline_compare.rs");
include!("cli/reporting.rs");
include!("cli/listing.rs");
include!("cli/generation.rs");
fn execute(repo_root: &Path, command: Command) -> Result<i32> {
match command {
Command::Init(args) => run_init(repo_root, args),
Command::Find(args) => run_find(repo_root, args),
Command::Show(args) => run_show(repo_root, args),
Command::Explain(args) => run_explain(repo_root, args),
Command::Plan(args) => run_plan(repo_root, args),
Command::Check(args) => run_check(repo_root, args),
Command::Compare(args) => run_compare(repo_root, args),
Command::Baseline(args) => run_baseline(repo_root, args),
Command::Report(args) => run_report(args),
Command::Sarif(args) => run_sarif(repo_root, args),
Command::Health(args) => run_health(repo_root, args),
Command::Config(args) => run_config(repo_root, args),
Command::Doctor(args) => run_doctor(repo_root, args),
Command::List(args) => run_list(repo_root, args),
Command::Prune(args) => run_prune(repo_root, args),
Command::Cache(args) => run_cache(repo_root, args),
Command::Completions(args) => run_completions(args),
Command::Man(args) => run_man(args),
Command::Reference(args) => run_reference(args),
Command::Html(args) => run_html(repo_root, args),
Command::Version => {
println!("{PROJECT_NAME} {VERSION}");
Ok(0)
}
Command::BuildInfo(args) => {
match args.format {
BuildInfoFormat::Json => {
println!("{}", serde_json::to_string_pretty(&build_info::current())?)
}
}
Ok(0)
}
Command::Schema(args) => {
let rendered = match args.contract {
SchemaContract::Report => render_json(&report::schema())?,
SchemaContract::Config => render_json(&config::schema())?,
contract => {
let source = match contract {
SchemaContract::Compare => include_str!("../schemas/compare-1.json"),
SchemaContract::Explain => include_str!("../schemas/explain-2.json"),
SchemaContract::Plan => include_str!("../schemas/plan-2.json"),
SchemaContract::Sarif => include_str!("../schemas/sarif-1.json"),
SchemaContract::Health => include_str!("../schemas/health-1.json"),
SchemaContract::Check => include_str!("../schemas/check-1.json"),
SchemaContract::Doctor => include_str!("../schemas/doctor-1.json"),
SchemaContract::BuildInfo => include_str!("../schemas/build-info-1.json"),
SchemaContract::List => include_str!("../schemas/list-1.json"),
SchemaContract::Show => include_str!("../schemas/show-1.json"),
SchemaContract::PromptManifest => {
include_str!("../schemas/prompt-manifest-1.json")
}
SchemaContract::Error => include_str!("../schemas/error-1.json"),
SchemaContract::FindEstimate => {
include_str!("../schemas/find-estimate-1.json")
}
SchemaContract::CacheStatus => {
include_str!("../schemas/cache-status-1.json")
}
SchemaContract::CachePrune => include_str!("../schemas/cache-prune-1.json"),
SchemaContract::Baseline => include_str!("../schemas/baseline-1.json"),
SchemaContract::Prune => include_str!("../schemas/prune-1.json"),
SchemaContract::CompareNdjson => {
include_str!("../schemas/compare-ndjson-1.json")
}
SchemaContract::Report | SchemaContract::Config => unreachable!(),
};
let value: Value = serde_json::from_str(source)?;
render_json(&value)?
}
};
write_generated_output(args.output.as_deref(), rendered.as_bytes())?;
Ok(0)
}
}
}
fn command_requires_repository(command: &Command) -> bool {
match command {
Command::Completions(_)
| Command::Man(_)
| Command::Reference(_)
| Command::Version
| Command::BuildInfo(_)
| Command::Schema(_)
| Command::Report(_)
| Command::Config(ConfigArgs {
command: ConfigCommand::Schema,
}) => false,
Command::Show(args) => args.report.is_none(),
Command::Compare(args) => args.base_ref.is_some() || args.baseline.is_some(),
Command::Baseline(_) => true,
Command::Explain(args) => args.report.is_none() || args.include_repository_context,
Command::Plan(args) => args.report.is_none() || args.include_repository_context,
Command::Check(args) => args.report.is_none(),
Command::Sarif(args) => args.report.is_none(),
Command::Health(args) => args.report.is_none(),
Command::Html(args) => args.report.is_none(),
Command::List(ListArgs {
command:
ListCommand::Findings(args)
| ListCommand::Relationships(args)
| ListCommand::Clusters(args)
| ListCommand::Profiles(args),
}) => args.report.is_none(),
_ => true,
}
}
pub fn run() -> i32 {
let raw_args = std::env::args_os().collect::<Vec<_>>();
let requested_error_format = requested_error_format(&raw_args);
let parser_command = parser_command_name(&raw_args);
let cli = match Cli::try_parse_from(&raw_args) {
Ok(cli) => cli,
Err(error) => {
let code = error.exit_code();
if code == 0 || requested_error_format == ErrorFormat::Human {
let _ = error.print();
} else {
let classified =
ClassifiedError::new(ErrorKind::Contract, "parser_error", error.to_string())
.at("/arguments")
.with_details(json!({"clap_kind": format!("{:?}", error.kind())}));
render_runtime_error(
requested_error_format,
&classified,
parser_command.as_deref(),
);
}
return code;
}
};
let error_format = cli.error_format;
let command_name = cli.command.name();
let repo_root = if command_requires_repository(&cli.command) {
match git::resolve_repo_root_from(cli.repo.as_deref()) {
Ok(root) => root,
Err(error) => {
render_runtime_error(
error_format,
&ClassifiedError::new(
ErrorKind::Repository,
"repository_not_found",
format!("{error:#}"),
),
Some(command_name),
);
return 3;
}
}
} else {
PathBuf::new()
};
match execute(&repo_root, cli.command) {
Ok(code) => code,
Err(error) => {
let classified = error.downcast_ref::<ClassifiedError>();
let fallback;
let classified = if let Some(classified) = classified {
classified
} else {
fallback = if error.downcast_ref::<std::io::Error>().is_some() {
ClassifiedError::new(ErrorKind::Io, "io_failure", format!("{error:#}"))
} else {
ClassifiedError::new(
ErrorKind::Repository,
"operation_failed",
format!("{error:#}"),
)
};
&fallback
};
render_runtime_error(error_format, classified, Some(command_name));
classified.kind.exit_code()
}
}
}
fn requested_error_format(args: &[std::ffi::OsString]) -> ErrorFormat {
args.iter()
.filter_map(|arg| arg.to_str())
.enumerate()
.find_map(|(index, arg)| {
if arg == "--error-format" {
args.get(index + 1).and_then(|value| value.to_str())
} else {
arg.strip_prefix("--error-format=")
}
})
.filter(|value| *value == "json")
.map_or(ErrorFormat::Human, |_| ErrorFormat::Json)
}
fn parser_command_name(args: &[std::ffi::OsString]) -> Option<String> {
const COMMANDS: &[&str] = &[
"init",
"find",
"show",
"explain",
"plan",
"check",
"compare",
"baseline",
"report",
"sarif",
"health",
"config",
"doctor",
"list",
"prune",
"cache",
"completions",
"man",
"reference",
"html",
"version",
"build-info",
"schema",
];
args.iter()
.skip(1)
.filter_map(|arg| arg.to_str())
.find(|arg| COMMANDS.contains(arg))
.map(str::to_string)
}
fn render_runtime_error(format: ErrorFormat, error: &ClassifiedError, command: Option<&str>) {
match format {
ErrorFormat::Human => eprintln!("{}", error.message),
ErrorFormat::Json => eprintln!(
"{}",
serde_json::to_string(&json!({
"schema_version": 1,
"error": {
"kind": error.kind,
"code": error.code,
"pointer": error.pointer,
"message": error.message,
"details": error.details,
"command": command,
"exit_code": error.kind.exit_code()
}
}))
.unwrap_or_else(|_| {
"{\"schema_version\":1,\"error\":{\"code\":\"serialization_failed\"}}".to_string()
})
),
}
}
#[cfg(test)]
mod tests {
use clap::CommandFactory;
use super::Cli;
fn assert_descriptions(command: &clap::Command, path: &str) {
for argument in command.get_arguments() {
assert!(
argument
.get_help()
.is_some_and(|help| !help.to_string().trim().is_empty()),
"{path} argument {} has no generated reference description",
argument.get_id()
);
}
for subcommand in command.get_subcommands() {
assert_descriptions(subcommand, &format!("{path} {}", subcommand.get_name()));
}
}
#[test]
fn generated_reference_has_no_blank_argument_descriptions() {
assert_descriptions(&Cli::command(), "git-slop");
}
}