1use std::fs;
2use std::io::IsTerminal;
3use std::path::{Component, Path, PathBuf};
4
5use anyhow::{Context, Result, bail};
6use clap::{ArgGroup, Args, CommandFactory, Parser, Subcommand, ValueEnum};
7use clap_complete::{Shell, generate};
8use serde_json::{Value, json};
9use sha2::Digest;
10
11use crate::build_info;
12use crate::config;
13use crate::error::{ClassifiedError, ErrorKind};
14use crate::health;
15use crate::report;
16use crate::report_ops::{
17 ExplainSelector, PlanSelector, PromptPackOptions, compare_payload_with_policy, explain_payload,
18 failing_records_in, health_json_payload, plan_payload, render_compare_text,
19 render_explain_text, render_github_annotations, render_json, render_plan_text,
20 render_show_text, sarif_payload, show_payload, write_prompt_pack,
21};
22use crate::{PROJECT_NAME, VERSION, analyze, git};
23
24#[derive(Debug, Parser)]
25#[command(
26 name = "git-slop",
27 about = "Find the files that cost too much context.",
28 version = VERSION
29)]
30struct Cli {
31 #[arg(long, global = true, value_name = "PATH")]
33 repo: Option<PathBuf>,
34 #[arg(long, global = true, value_enum, default_value_t = ErrorFormat::Human)]
36 error_format: ErrorFormat,
37 #[command(subcommand)]
38 command: Command,
39}
40
41#[derive(Debug, Subcommand)]
42enum Command {
43 Init(InitArgs),
45 Find(FindArgs),
47 Show(ShowArgs),
49 Explain(ExplainArgs),
51 Plan(PlanArgs),
53 Check(CheckArgs),
55 Compare(CompareArgs),
57 Baseline(BaselineArgs),
59 Report(ReportArgs),
61 Sarif(SarifArgs),
63 Health(HealthArgs),
65 Config(ConfigArgs),
67 Doctor(DoctorArgs),
69 List(ListArgs),
71 Prune(PruneArgs),
73 Cache(CacheArgs),
75 Completions(CompletionsArgs),
77 Man(ManArgs),
79 Reference(ReferenceArgs),
81 Html(HtmlArgs),
83 Version,
85 BuildInfo(BuildInfoArgs),
87 Schema(SchemaArgs),
89}
90
91impl Command {
92 fn name(&self) -> &'static str {
93 match self {
94 Self::Init(_) => "init",
95 Self::Find(_) => "find",
96 Self::Show(_) => "show",
97 Self::Explain(_) => "explain",
98 Self::Plan(_) => "plan",
99 Self::Check(_) => "check",
100 Self::Compare(_) => "compare",
101 Self::Baseline(_) => "baseline",
102 Self::Report(_) => "report",
103 Self::Sarif(_) => "sarif",
104 Self::Health(_) => "health",
105 Self::Config(_) => "config",
106 Self::Doctor(_) => "doctor",
107 Self::List(_) => "list",
108 Self::Prune(_) => "prune",
109 Self::Cache(_) => "cache",
110 Self::Completions(_) => "completions",
111 Self::Man(_) => "man",
112 Self::Reference(_) => "reference",
113 Self::Html(_) => "html",
114 Self::Version => "version",
115 Self::BuildInfo(_) => "build-info",
116 Self::Schema(_) => "schema",
117 }
118 }
119}
120
121#[derive(Debug, Args)]
122struct FindArgs {
123 #[arg(long)]
125 allow_shallow: bool,
126 #[arg(long)]
128 scope: Option<String>,
129 #[arg(long)]
131 allow_empty_scope: bool,
132 #[arg(long)]
134 quiet: bool,
135 #[arg(long)]
137 no_progress: bool,
138 #[arg(long, value_name = "PATH")]
140 state_dir: Option<PathBuf>,
141 #[arg(long, value_name = "PATH")]
143 output_dir: Option<PathBuf>,
144 #[arg(long)]
146 no_cache: bool,
147 #[arg(long, conflicts_with_all = ["state_dir", "output_dir"])]
149 ephemeral: bool,
150 #[arg(long)]
152 allow_degraded: bool,
153 #[arg(long, value_name = "RFC3339")]
155 as_of: Option<String>,
156 #[arg(long, value_enum, default_value_t = ReportProfile::Standard)]
158 report_profile: ReportProfile,
159 #[arg(long, value_enum, default_value_t = ReportCompression::None)]
161 compression: ReportCompression,
162 #[arg(long)]
164 estimate_only: bool,
165}
166
167#[derive(Debug, Clone, Copy, ValueEnum)]
168enum ReportProfile {
169 Compact,
170 Standard,
171 FullEvidence,
172}
173
174impl ReportProfile {
175 fn as_str(self) -> &'static str {
176 match self {
177 Self::Compact => "compact",
178 Self::Standard => "standard",
179 Self::FullEvidence => "full_evidence",
180 }
181 }
182}
183
184#[derive(Debug, Clone, Copy, ValueEnum)]
185enum ReportCompression {
186 None,
187 Gzip,
188 Zstd,
189}
190
191impl ReportCompression {
192 fn as_str(self) -> &'static str {
193 match self {
194 Self::None => "none",
195 Self::Gzip => "gzip",
196 Self::Zstd => "zstd",
197 }
198 }
199}
200
201#[derive(Debug, Args)]
202struct BuildInfoArgs {
203 #[arg(long, value_enum, default_value_t = BuildInfoFormat::Json)]
205 format: BuildInfoFormat,
206}
207
208#[derive(Debug, Args)]
209#[command(group(
210 ArgGroup::new("mode")
211 .args(["force", "repair", "check"])
212 .multiple(false)
213))]
214struct InitArgs {
215 #[arg(long)]
217 force: bool,
218 #[arg(long)]
220 repair: bool,
221 #[arg(long)]
223 check: bool,
224 #[arg(long)]
226 gitignore_only: bool,
227}
228
229#[derive(Debug, Args)]
230struct ShowArgs {
231 target_path: String,
233 #[arg(long)]
235 report: Option<PathBuf>,
236 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
238 format: DisplayFormat,
239}
240
241#[derive(Debug, Args)]
242#[command(group(
243 ArgGroup::new("selector")
244 .args(["path", "cluster", "relationship", "top"])
245 .multiple(false)
246))]
247struct ExplainArgs {
248 #[arg(long)]
250 report: Option<PathBuf>,
251 #[arg(long)]
253 path: Option<String>,
254 #[arg(long)]
256 cluster: Option<String>,
257 #[arg(long)]
259 relationship: Option<String>,
260 #[arg(long)]
262 top: Option<i64>,
263 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
265 format: DisplayFormat,
266 #[arg(long)]
268 prompt_pack: Option<PathBuf>,
269 #[arg(long, requires = "prompt_pack")]
271 force: bool,
272 #[arg(long, requires = "prompt_pack")]
274 include_repository_context: bool,
275 #[arg(long, default_value_t = 2048, requires = "include_repository_context")]
277 excerpt_bytes: usize,
278 #[arg(long, requires = "prompt_pack")]
280 include_local_paths: bool,
281}
282
283#[derive(Debug, Args)]
284#[command(group(
285 ArgGroup::new("selector")
286 .args(["path", "cluster", "relationship"])
287 .required(true)
288 .multiple(false)
289))]
290struct PlanArgs {
291 #[arg(long)]
293 report: Option<PathBuf>,
294 #[arg(long)]
296 path: Option<String>,
297 #[arg(long)]
299 cluster: Option<String>,
300 #[arg(long)]
302 relationship: Option<String>,
303 #[arg(long, default_value_t = 3)]
305 max_slices: i64,
306 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
308 format: DisplayFormat,
309 #[arg(long)]
311 prompt_pack: Option<PathBuf>,
312 #[arg(long, requires = "prompt_pack")]
314 force: bool,
315 #[arg(long, requires = "prompt_pack")]
317 include_repository_context: bool,
318 #[arg(long, default_value_t = 2048, requires = "include_repository_context")]
320 excerpt_bytes: usize,
321 #[arg(long, requires = "prompt_pack")]
323 include_local_paths: bool,
324}
325
326#[derive(Debug, Args)]
327struct CheckArgs {
328 #[arg(long)]
330 report: Option<PathBuf>,
331 #[arg(long, value_enum)]
333 fail_on_context_band: Option<ContextBand>,
334 #[arg(long, value_enum)]
336 fail_on_slop_band: Option<SlopBand>,
337 #[arg(long, value_enum, default_value_t = CheckFormat::Text)]
339 format: CheckFormat,
340 #[arg(long)]
342 details: bool,
343 #[arg(long)]
345 include_folders: bool,
346 #[arg(long, default_value_t = 0, requires = "details")]
348 offset: usize,
349 #[arg(long, default_value_t = 1000, requires = "details")]
351 limit: usize,
352 #[arg(long)]
354 allow_incomplete_evidence: bool,
355 #[arg(long)]
357 evaluate_only: bool,
358 #[arg(long)]
360 require_current: bool,
361}
362
363#[derive(Debug, Args)]
364struct CompareArgs {
365 #[arg(long, value_name = "PATH")]
367 state_dir: Option<PathBuf>,
368 #[arg(
370 long,
371 required_unless_present_any = ["base_ref", "baseline"],
372 conflicts_with_all = ["base_ref", "baseline"]
373 )]
374 base: Option<PathBuf>,
375 #[arg(long, conflicts_with_all = ["base", "baseline"])]
377 base_ref: Option<String>,
378 #[arg(long, conflicts_with_all = ["base", "base_ref"])]
380 baseline: Option<String>,
381 #[arg(long, default_value = ".slop/latest/report.json")]
383 head: PathBuf,
384 #[arg(long)]
386 scope: Option<String>,
387 #[arg(long)]
389 allow_shallow: bool,
390 #[arg(long)]
392 allow_incomplete_evidence: bool,
393 #[arg(long, default_value_t = 10)]
395 top: i64,
396 #[arg(long, value_enum, default_value_t = CompareFormat::Text)]
398 format: CompareFormat,
399 #[arg(long, value_enum, default_value_t = CompareDetail::Top)]
401 detail: CompareDetail,
402 #[arg(long, default_value_t = 0)]
404 offset: usize,
405 #[arg(long, default_value_t = 1000)]
407 limit: usize,
408 #[arg(long)]
410 force: bool,
411 #[arg(long)]
413 include_local_paths: bool,
414 #[arg(long)]
416 include_unchanged: bool,
417 #[arg(long, value_enum, default_value_t = PolicySource::Base)]
419 policy_from: PolicySource,
420 #[arg(long)]
422 fail_on_regression: bool,
423}
424
425#[derive(Debug, Args)]
426struct BaselineArgs {
427 #[arg(long, value_name = "PATH", global = true)]
429 state_dir: Option<PathBuf>,
430 #[command(subcommand)]
431 command: BaselineCommand,
432}
433
434#[derive(Debug, Subcommand)]
435enum BaselineCommand {
436 Ensure {
438 #[arg(long, default_value = "default")]
440 name: String,
441 #[arg(long)]
443 report: Option<PathBuf>,
444 #[arg(long)]
446 replace: bool,
447 #[arg(long)]
449 allow_dirty: bool,
450 #[arg(long)]
452 allow_incomplete_evidence: bool,
453 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
455 format: DisplayFormat,
456 },
457 Create {
459 #[arg(long, default_value = "default")]
461 name: String,
462 #[arg(long)]
464 report: Option<PathBuf>,
465 #[arg(long)]
467 force: bool,
468 #[arg(long)]
470 allow_dirty: bool,
471 #[arg(long)]
473 allow_incomplete_evidence: bool,
474 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
476 format: DisplayFormat,
477 },
478 Update {
480 #[arg(long, default_value = "default")]
482 name: String,
483 #[arg(long)]
485 report: Option<PathBuf>,
486 #[arg(long)]
488 allow_dirty: bool,
489 #[arg(long)]
491 allow_incomplete_evidence: bool,
492 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
494 format: DisplayFormat,
495 },
496 List {
498 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
500 format: DisplayFormat,
501 },
502 Inspect {
504 #[arg(long, default_value = "default")]
506 name: String,
507 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
509 format: DisplayFormat,
510 },
511 Validate {
513 #[arg(long, default_value = "default")]
515 name: String,
516 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
518 format: DisplayFormat,
519 },
520 Remove {
522 #[arg(long, default_value = "default")]
524 name: String,
525 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
527 format: DisplayFormat,
528 #[arg(long)]
530 yes: bool,
531 },
532}
533
534#[derive(Debug, Args)]
535struct ReportArgs {
536 #[command(subcommand)]
537 command: ReportCommand,
538}
539
540#[derive(Debug, Subcommand)]
541enum ReportCommand {
542 Validate {
544 #[arg(value_name = "REPORT_JSON", required_unless_present = "report")]
546 path: Option<PathBuf>,
547 #[arg(long, value_name = "REPORT_JSON", required_unless_present = "path")]
549 report: Option<PathBuf>,
550 #[arg(long)]
552 allow_legacy: bool,
553 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
555 format: DisplayFormat,
556 },
557 Migrate {
559 #[arg(value_name = "REPORT_JSON")]
561 path: PathBuf,
562 #[arg(long, value_name = "PATH")]
564 output: PathBuf,
565 },
566 Schema,
568}
569
570#[derive(Debug, Args)]
571struct SarifArgs {
572 #[arg(long)]
574 report: Option<PathBuf>,
575 #[arg(long)]
577 top: Option<i64>,
578 #[arg(long, value_enum, default_value_t = SarifScope::ActionQueue)]
580 scope: SarifScope,
581 #[arg(long)]
583 output: Option<PathBuf>,
584 #[arg(long)]
586 include_local_paths: bool,
587}
588
589#[derive(Debug, Args)]
590struct HealthArgs {
591 #[arg(long)]
593 report: Option<PathBuf>,
594 #[arg(long, value_enum, default_value_t = HealthFormat::Text)]
596 format: HealthFormat,
597 #[arg(long, default_value_t = 10)]
599 max_annotations: usize,
600 #[arg(long)]
602 require_current: bool,
603}
604
605#[derive(Debug, Args)]
606struct ConfigArgs {
607 #[command(subcommand)]
608 command: ConfigCommand,
609}
610
611#[derive(Debug, Subcommand)]
612enum ConfigCommand {
613 Show {
615 #[arg(long)]
617 effective: bool,
618 },
619 Validate,
621 DiffDefaults,
623 Migrate {
625 #[arg(long)]
627 dry_run: bool,
628 #[arg(long)]
630 no_backup: bool,
631 },
632 Schema,
634}
635
636#[derive(Debug, Args)]
637struct DoctorArgs {
638 #[arg(long, num_args = 0..=1, default_missing_value = ".slop/diagnostic-bundle.json")]
640 bundle: Option<PathBuf>,
641 #[arg(long, value_enum, default_value_t = DoctorFormat::Text)]
643 format: DoctorFormat,
644 #[arg(long)]
646 scope: Option<String>,
647 #[arg(long)]
649 require_current: bool,
650}
651
652#[derive(Debug, Clone, Copy, ValueEnum)]
653enum DoctorFormat {
654 Text,
655 Json,
656}
657
658#[derive(Debug, Args)]
659struct ListArgs {
660 #[command(subcommand)]
661 command: ListCommand,
662}
663
664#[derive(Debug, Subcommand)]
665enum ListCommand {
666 Findings(ListFilterArgs),
667 Relationships(ListFilterArgs),
668 Clusters(ListFilterArgs),
669 Profiles(ListFilterArgs),
670}
671
672#[derive(Debug, Args)]
673struct ListFilterArgs {
674 #[arg(long)]
676 report: Option<PathBuf>,
677 #[arg(long)]
679 path: Option<String>,
680 #[arg(long)]
682 profile: Option<String>,
683 #[arg(long)]
685 language: Option<String>,
686 #[arg(long, visible_alias = "class")]
688 classification: Option<String>,
689 #[arg(long)]
691 severity: Option<String>,
692 #[arg(long, default_value_t = 50)]
694 top: usize,
695 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
697 format: DisplayFormat,
698 #[arg(long)]
700 wide: bool,
701 #[arg(long)]
703 no_truncate: bool,
704}
705
706#[derive(Debug, Args)]
707struct PruneArgs {
708 #[arg(long)]
710 keep: Option<usize>,
711 #[arg(long)]
713 max_bytes: Option<u64>,
714 #[arg(long, conflicts_with = "yes")]
716 dry_run: bool,
717 #[arg(long, conflicts_with = "dry_run")]
719 yes: bool,
720 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
722 format: DisplayFormat,
723}
724
725#[derive(Debug, Args)]
726struct CacheArgs {
727 #[arg(long, value_name = "PATH", global = true)]
729 state_dir: Option<PathBuf>,
730 #[command(subcommand)]
731 command: CacheCommand,
732}
733
734#[derive(Debug, Subcommand)]
735enum CacheCommand {
736 Status {
737 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
739 format: DisplayFormat,
740 },
741 Prune {
742 #[arg(long, default_value_t = 10_000)]
744 max_entries: usize,
745 #[arg(long, default_value_t = 536_870_912)]
747 max_bytes: u64,
748 #[arg(long)]
750 dry_run: bool,
751 #[arg(long)]
753 compact: bool,
754 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
756 format: DisplayFormat,
757 },
758}
759
760#[derive(Debug, Args)]
761struct CompletionsArgs {
762 shell: CompletionShell,
764}
765
766#[derive(Debug, Args)]
767struct ManArgs {
768 #[arg(long)]
770 output: Option<PathBuf>,
771}
772
773#[derive(Debug, Args)]
774struct ReferenceArgs {
775 #[arg(long)]
778 output: Option<PathBuf>,
779}
780
781#[derive(Debug, Args)]
782struct HtmlArgs {
783 #[arg(long)]
785 report: Option<PathBuf>,
786 #[arg(long)]
788 output: Option<PathBuf>,
789 #[arg(long)]
791 include_local_paths: bool,
792}
793
794include!("cli/formats.rs");
795include!("cli/support.rs");
796include!("cli/support/validation.rs");
797include!("cli/init.rs");
798include!("cli/analysis.rs");
799include!("cli/analysis_receipt.rs");
800include!("cli/check.rs");
801include!("cli/baseline_compare.rs");
802include!("cli/reporting.rs");
803include!("cli/doctor.rs");
804include!("cli/listing.rs");
805include!("cli/generation.rs");
806include!("cli/generation/artifacts.rs");
807include!("cli/generation/reference.rs");
808include!("cli/generation/reference/bundle.rs");
809fn execute(repo_root: &Path, command: Command) -> Result<i32> {
810 match command {
811 Command::Init(args) => run_init(repo_root, args),
812 Command::Find(args) => run_find(repo_root, args),
813 Command::Show(args) => run_show(repo_root, args),
814 Command::Explain(args) => run_explain(repo_root, args),
815 Command::Plan(args) => run_plan(repo_root, args),
816 Command::Check(args) => run_check(repo_root, args),
817 Command::Compare(args) => run_compare(repo_root, args),
818 Command::Baseline(args) => run_baseline(repo_root, args),
819 Command::Report(args) => run_report(args),
820 Command::Sarif(args) => run_sarif(repo_root, args),
821 Command::Health(args) => run_health(repo_root, args),
822 Command::Config(args) => run_config(repo_root, args),
823 Command::Doctor(args) => run_doctor(repo_root, args),
824 Command::List(args) => run_list(repo_root, args),
825 Command::Prune(args) => run_prune(repo_root, args),
826 Command::Cache(args) => run_cache(repo_root, args),
827 Command::Completions(args) => run_completions(args),
828 Command::Man(args) => run_man(args),
829 Command::Reference(args) => run_reference(args),
830 Command::Html(args) => run_html(repo_root, args),
831 Command::Version => {
832 println!("{PROJECT_NAME} {VERSION}");
833 Ok(0)
834 }
835 Command::BuildInfo(args) => {
836 match args.format {
837 BuildInfoFormat::Json => {
838 println!("{}", serde_json::to_string_pretty(&build_info::current())?)
839 }
840 }
841 Ok(0)
842 }
843 Command::Schema(args) => {
844 let rendered = match args.contract {
845 SchemaContract::Report => render_json(&report::schema())?,
846 SchemaContract::Config => render_json(&config::schema())?,
847 contract => {
848 let source = match contract {
849 SchemaContract::Compare => include_str!("../schemas/compare-1.json"),
850 SchemaContract::Explain => include_str!("../schemas/explain-2.json"),
851 SchemaContract::Plan => include_str!("../schemas/plan-2.json"),
852 SchemaContract::Sarif => include_str!("../schemas/sarif-1.json"),
853 SchemaContract::Health => include_str!("../schemas/health-1.json"),
854 SchemaContract::Check => include_str!("../schemas/check-1.json"),
855 SchemaContract::Doctor => include_str!("../schemas/doctor-1.json"),
856 SchemaContract::BuildInfo => include_str!("../schemas/build-info-2.json"),
857 SchemaContract::ReleaseManifest => {
858 include_str!("../schemas/release-manifest-3.json")
859 }
860 SchemaContract::List => include_str!("../schemas/list-1.json"),
861 SchemaContract::Show => include_str!("../schemas/show-1.json"),
862 SchemaContract::PromptManifest => {
863 include_str!("../schemas/prompt-manifest-1.json")
864 }
865 SchemaContract::Error => include_str!("../schemas/error-1.json"),
866 SchemaContract::FindEstimate => {
867 include_str!("../schemas/find-estimate-1.json")
868 }
869 SchemaContract::CacheStatus => {
870 include_str!("../schemas/cache-status-1.json")
871 }
872 SchemaContract::CachePrune => include_str!("../schemas/cache-prune-1.json"),
873 SchemaContract::Baseline => include_str!("../schemas/baseline-1.json"),
874 SchemaContract::Prune => include_str!("../schemas/prune-1.json"),
875 SchemaContract::CompareNdjson => {
876 include_str!("../schemas/compare-ndjson-1.json")
877 }
878 SchemaContract::Report | SchemaContract::Config => unreachable!(),
879 };
880 let value: Value = serde_json::from_str(source)?;
881 render_json(&value)?
882 }
883 };
884 write_generated_output(args.output.as_deref(), rendered.as_bytes())?;
885 Ok(0)
886 }
887 }
888}
889
890fn command_requires_repository(command: &Command) -> bool {
891 match command {
892 Command::Completions(_)
893 | Command::Man(_)
894 | Command::Reference(_)
895 | Command::Version
896 | Command::BuildInfo(_)
897 | Command::Schema(_)
898 | Command::Report(_)
899 | Command::Config(ConfigArgs {
900 command: ConfigCommand::Schema,
901 }) => false,
902 Command::Show(args) => args.report.is_none(),
903 Command::Compare(args) => args.base_ref.is_some() || args.baseline.is_some(),
904 Command::Baseline(_) => true,
905 Command::Explain(args) => args.report.is_none() || args.include_repository_context,
906 Command::Plan(args) => args.report.is_none() || args.include_repository_context,
907 Command::Check(args) => args.report.is_none() || args.require_current,
908 Command::Sarif(args) => args.report.is_none(),
909 Command::Health(args) => args.report.is_none() || args.require_current,
910 Command::Html(args) => args.report.is_none(),
911 Command::List(ListArgs {
912 command:
913 ListCommand::Findings(args)
914 | ListCommand::Relationships(args)
915 | ListCommand::Clusters(args)
916 | ListCommand::Profiles(args),
917 }) => args.report.is_none(),
918 _ => true,
919 }
920}
921
922pub fn run() -> i32 {
923 let raw_args = std::env::args_os().collect::<Vec<_>>();
924 let requested_error_format = requested_error_format(&raw_args);
925 let parser_command = parser_command_name(&raw_args);
926 let cli = match Cli::try_parse_from(&raw_args) {
927 Ok(cli) => cli,
928 Err(error) => {
929 let code = error.exit_code();
930 if code == 0 || requested_error_format == ErrorFormat::Human {
931 let _ = error.print();
932 } else {
933 let classified =
934 ClassifiedError::new(ErrorKind::Contract, "parser_error", error.to_string())
935 .at("/arguments")
936 .with_details(json!({"clap_kind": format!("{:?}", error.kind())}));
937 render_runtime_error(
938 requested_error_format,
939 &classified,
940 parser_command.as_deref(),
941 );
942 }
943 return code;
944 }
945 };
946 let error_format = cli.error_format;
947 let command_name = cli.command.name();
948 let repo_root = if command_requires_repository(&cli.command) {
949 match git::resolve_repo_root_from(cli.repo.as_deref()) {
950 Ok(root) => root,
951 Err(error) => {
952 render_runtime_error(
953 error_format,
954 &ClassifiedError::new(
955 ErrorKind::Repository,
956 "repository_not_found",
957 format!("{error:#}"),
958 ),
959 Some(command_name),
960 );
961 return 3;
962 }
963 }
964 } else {
965 PathBuf::new()
966 };
967 match execute(&repo_root, cli.command) {
968 Ok(code) => code,
969 Err(error) => {
970 let classified = error.downcast_ref::<ClassifiedError>();
971 let fallback;
972 let classified = if let Some(classified) = classified {
973 classified
974 } else {
975 fallback = if error.downcast_ref::<std::io::Error>().is_some() {
976 ClassifiedError::new(ErrorKind::Io, "io_failure", format!("{error:#}"))
977 } else {
978 ClassifiedError::new(
979 ErrorKind::Repository,
980 "operation_failed",
981 format!("{error:#}"),
982 )
983 };
984 &fallback
985 };
986 render_runtime_error(error_format, classified, Some(command_name));
987 classified.kind.exit_code()
988 }
989 }
990}
991
992fn requested_error_format(args: &[std::ffi::OsString]) -> ErrorFormat {
993 args.iter()
994 .filter_map(|arg| arg.to_str())
995 .enumerate()
996 .find_map(|(index, arg)| {
997 if arg == "--error-format" {
998 args.get(index + 1).and_then(|value| value.to_str())
999 } else {
1000 arg.strip_prefix("--error-format=")
1001 }
1002 })
1003 .filter(|value| *value == "json")
1004 .map_or(ErrorFormat::Human, |_| ErrorFormat::Json)
1005}
1006
1007fn parser_command_name(args: &[std::ffi::OsString]) -> Option<String> {
1008 const COMMANDS: &[&str] = &[
1009 "init",
1010 "find",
1011 "show",
1012 "explain",
1013 "plan",
1014 "check",
1015 "compare",
1016 "baseline",
1017 "report",
1018 "sarif",
1019 "health",
1020 "config",
1021 "doctor",
1022 "list",
1023 "prune",
1024 "cache",
1025 "completions",
1026 "man",
1027 "reference",
1028 "html",
1029 "version",
1030 "build-info",
1031 "schema",
1032 ];
1033 args.iter()
1034 .skip(1)
1035 .filter_map(|arg| arg.to_str())
1036 .find(|arg| COMMANDS.contains(arg))
1037 .map(str::to_string)
1038}
1039
1040fn render_runtime_error(format: ErrorFormat, error: &ClassifiedError, command: Option<&str>) {
1041 match format {
1042 ErrorFormat::Human => eprintln!("{}", error.message),
1043 ErrorFormat::Json => eprintln!(
1044 "{}",
1045 serde_json::to_string(&json!({
1046 "schema_version": 1,
1047 "error": {
1048 "kind": error.kind,
1049 "code": error.code,
1050 "pointer": error.pointer,
1051 "message": error.message,
1052 "details": error.details,
1053 "command": command,
1054 "exit_code": error.kind.exit_code()
1055 }
1056 }))
1057 .unwrap_or_else(|_| {
1058 "{\"schema_version\":1,\"error\":{\"code\":\"serialization_failed\"}}".to_string()
1059 })
1060 ),
1061 }
1062}
1063
1064#[cfg(test)]
1065mod tests {
1066 use clap::CommandFactory;
1067
1068 use super::Cli;
1069
1070 fn assert_descriptions(command: &clap::Command, path: &str) {
1071 for argument in command.get_arguments() {
1072 assert!(
1073 argument
1074 .get_help()
1075 .is_some_and(|help| !help.to_string().trim().is_empty()),
1076 "{path} argument {} has no generated reference description",
1077 argument.get_id()
1078 );
1079 }
1080 for subcommand in command.get_subcommands() {
1081 assert_descriptions(subcommand, &format!("{path} {}", subcommand.get_name()));
1082 }
1083 }
1084
1085 #[test]
1086 fn generated_reference_has_no_blank_argument_descriptions() {
1087 assert_descriptions(&Cli::command(), "git-slop");
1088 }
1089}