1use std::fs;
2use std::io::IsTerminal;
3use std::path::{Component, Path, PathBuf};
4
5use anyhow::{Context, Result};
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 SchemaArgs {
123 #[arg(value_enum)]
125 contract: SchemaContract,
126 #[arg(long)]
128 output: Option<PathBuf>,
129}
130
131#[derive(Debug, Clone, Copy, ValueEnum)]
132enum SchemaContract {
133 Report,
134 Config,
135 Compare,
136 Explain,
137 Plan,
138 Sarif,
139 Health,
140 Check,
141 Doctor,
142 BuildInfo,
143 List,
144 Show,
145 PromptManifest,
146 Error,
147 FindEstimate,
148 CacheStatus,
149 CachePrune,
150 Prune,
151 CompareNdjson,
152}
153
154#[derive(Debug, Args)]
155struct FindArgs {
156 #[arg(long)]
158 allow_shallow: bool,
159 #[arg(long)]
161 scope: Option<String>,
162 #[arg(long)]
164 allow_empty_scope: bool,
165 #[arg(long)]
167 quiet: bool,
168 #[arg(long)]
170 no_progress: bool,
171 #[arg(long, value_name = "PATH")]
173 state_dir: Option<PathBuf>,
174 #[arg(long, value_name = "PATH")]
176 output_dir: Option<PathBuf>,
177 #[arg(long)]
179 no_cache: bool,
180 #[arg(long)]
182 allow_degraded: bool,
183 #[arg(long, value_name = "RFC3339")]
185 as_of: Option<String>,
186 #[arg(long, value_enum, default_value_t = ReportProfile::Standard)]
188 report_profile: ReportProfile,
189 #[arg(long, value_enum, default_value_t = ReportCompression::None)]
191 compression: ReportCompression,
192 #[arg(long)]
194 estimate_only: bool,
195}
196
197#[derive(Debug, Clone, Copy, ValueEnum)]
198enum ReportProfile {
199 Compact,
200 Standard,
201 FullEvidence,
202}
203
204impl ReportProfile {
205 fn as_str(self) -> &'static str {
206 match self {
207 Self::Compact => "compact",
208 Self::Standard => "standard",
209 Self::FullEvidence => "full_evidence",
210 }
211 }
212}
213
214#[derive(Debug, Clone, Copy, ValueEnum)]
215enum ReportCompression {
216 None,
217 Gzip,
218 Zstd,
219}
220
221impl ReportCompression {
222 fn as_str(self) -> &'static str {
223 match self {
224 Self::None => "none",
225 Self::Gzip => "gzip",
226 Self::Zstd => "zstd",
227 }
228 }
229}
230
231#[derive(Debug, Args)]
232struct BuildInfoArgs {
233 #[arg(long, value_enum, default_value_t = BuildInfoFormat::Json)]
235 format: BuildInfoFormat,
236}
237
238#[derive(Debug, Args)]
239struct InitArgs {
240 #[arg(long)]
242 force: bool,
243}
244
245#[derive(Debug, Args)]
246struct ShowArgs {
247 target_path: String,
249 #[arg(long)]
251 report: Option<PathBuf>,
252 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
254 format: DisplayFormat,
255}
256
257#[derive(Debug, Args)]
258#[command(group(
259 ArgGroup::new("selector")
260 .args(["path", "cluster", "relationship", "top"])
261 .multiple(false)
262))]
263struct ExplainArgs {
264 #[arg(long)]
266 report: Option<PathBuf>,
267 #[arg(long)]
269 path: Option<String>,
270 #[arg(long)]
272 cluster: Option<String>,
273 #[arg(long)]
275 relationship: Option<String>,
276 #[arg(long)]
278 top: Option<i64>,
279 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
281 format: DisplayFormat,
282 #[arg(long)]
284 prompt_pack: Option<PathBuf>,
285 #[arg(long, requires = "prompt_pack")]
287 force: bool,
288 #[arg(long, requires = "prompt_pack")]
290 include_repository_context: bool,
291 #[arg(long, default_value_t = 2048, requires = "include_repository_context")]
293 excerpt_bytes: usize,
294}
295
296#[derive(Debug, Args)]
297#[command(group(
298 ArgGroup::new("selector")
299 .args(["path", "cluster", "relationship"])
300 .required(true)
301 .multiple(false)
302))]
303struct PlanArgs {
304 #[arg(long)]
306 report: Option<PathBuf>,
307 #[arg(long)]
309 path: Option<String>,
310 #[arg(long)]
312 cluster: Option<String>,
313 #[arg(long)]
315 relationship: Option<String>,
316 #[arg(long, default_value_t = 3)]
318 max_slices: i64,
319 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
321 format: DisplayFormat,
322 #[arg(long)]
324 prompt_pack: Option<PathBuf>,
325 #[arg(long, requires = "prompt_pack")]
327 force: bool,
328 #[arg(long, requires = "prompt_pack")]
330 include_repository_context: bool,
331 #[arg(long, default_value_t = 2048, requires = "include_repository_context")]
333 excerpt_bytes: usize,
334}
335
336#[derive(Debug, Args)]
337struct CheckArgs {
338 #[arg(long)]
340 report: Option<PathBuf>,
341 #[arg(long, value_enum)]
343 fail_on_context_band: Option<ContextBand>,
344 #[arg(long, value_enum)]
346 fail_on_slop_band: Option<SlopBand>,
347 #[arg(long, value_enum, default_value_t = CheckFormat::Text)]
349 format: CheckFormat,
350 #[arg(long)]
352 details: bool,
353 #[arg(long)]
355 include_folders: bool,
356 #[arg(long, default_value_t = 0, requires = "details")]
358 offset: usize,
359 #[arg(long, default_value_t = 1000, requires = "details")]
361 limit: usize,
362 #[arg(long)]
364 allow_incomplete_evidence: bool,
365}
366
367#[derive(Debug, Args)]
368struct CompareArgs {
369 #[arg(
371 long,
372 required_unless_present_any = ["base_ref", "baseline"],
373 conflicts_with_all = ["base_ref", "baseline"]
374 )]
375 base: Option<PathBuf>,
376 #[arg(long, conflicts_with_all = ["base", "baseline"])]
378 base_ref: Option<String>,
379 #[arg(long, conflicts_with_all = ["base", "base_ref"])]
381 baseline: Option<String>,
382 #[arg(long, default_value = ".slop/latest/report.json")]
384 head: PathBuf,
385 #[arg(long)]
387 scope: Option<String>,
388 #[arg(long)]
390 allow_shallow: bool,
391 #[arg(long)]
393 allow_incomplete_evidence: bool,
394 #[arg(long, default_value_t = 10)]
396 top: i64,
397 #[arg(long, value_enum, default_value_t = CompareFormat::Text)]
399 format: CompareFormat,
400 #[arg(long, value_enum, default_value_t = CompareDetail::Top)]
402 detail: CompareDetail,
403 #[arg(long, default_value_t = 0)]
405 offset: usize,
406 #[arg(long, default_value_t = 1000)]
408 limit: usize,
409 #[arg(long)]
411 force: bool,
412 #[arg(long)]
414 include_local_paths: bool,
415 #[arg(long, value_enum, default_value_t = PolicySource::Base)]
417 policy_from: PolicySource,
418 #[arg(long)]
420 fail_on_regression: bool,
421}
422
423#[derive(Debug, Args)]
424struct BaselineArgs {
425 #[command(subcommand)]
426 command: BaselineCommand,
427}
428
429#[derive(Debug, Subcommand)]
430enum BaselineCommand {
431 Create {
433 #[arg(long, default_value = "default")]
435 name: String,
436 #[arg(long)]
438 report: Option<PathBuf>,
439 #[arg(long)]
441 force: bool,
442 #[arg(long)]
444 allow_dirty: bool,
445 #[arg(long)]
447 allow_incomplete_evidence: bool,
448 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
450 format: DisplayFormat,
451 },
452 Update {
454 #[arg(long, default_value = "default")]
456 name: String,
457 #[arg(long)]
459 report: Option<PathBuf>,
460 #[arg(long)]
462 allow_dirty: bool,
463 #[arg(long)]
465 allow_incomplete_evidence: bool,
466 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
468 format: DisplayFormat,
469 },
470 List {
472 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
474 format: DisplayFormat,
475 },
476 Inspect {
478 #[arg(long, default_value = "default")]
480 name: String,
481 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
483 format: DisplayFormat,
484 },
485 Validate {
487 #[arg(long, default_value = "default")]
489 name: String,
490 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
492 format: DisplayFormat,
493 },
494 Remove {
496 #[arg(long, default_value = "default")]
498 name: String,
499 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
501 format: DisplayFormat,
502 },
503}
504
505#[derive(Debug, Args)]
506struct ReportArgs {
507 #[command(subcommand)]
508 command: ReportCommand,
509}
510
511#[derive(Debug, Subcommand)]
512enum ReportCommand {
513 Validate {
515 #[arg(value_name = "REPORT_JSON")]
517 path: PathBuf,
518 #[arg(long)]
520 allow_legacy: bool,
521 },
522 Migrate {
524 #[arg(value_name = "REPORT_JSON")]
526 path: PathBuf,
527 #[arg(long, value_name = "PATH")]
529 output: PathBuf,
530 },
531 Schema,
533}
534
535#[derive(Debug, Args)]
536struct SarifArgs {
537 #[arg(long)]
539 report: Option<PathBuf>,
540 #[arg(long)]
542 top: Option<i64>,
543 #[arg(long)]
545 output: Option<PathBuf>,
546}
547
548#[derive(Debug, Args)]
549struct HealthArgs {
550 #[arg(long)]
552 report: Option<PathBuf>,
553 #[arg(long, value_enum, default_value_t = HealthFormat::Text)]
555 format: HealthFormat,
556 #[arg(long, default_value_t = 10)]
558 max_annotations: usize,
559}
560
561#[derive(Debug, Args)]
562struct ConfigArgs {
563 #[command(subcommand)]
564 command: ConfigCommand,
565}
566
567#[derive(Debug, Subcommand)]
568enum ConfigCommand {
569 Show {
571 #[arg(long)]
573 effective: bool,
574 },
575 Validate,
577 DiffDefaults,
579 Migrate,
581 Schema,
583}
584
585#[derive(Debug, Args)]
586struct DoctorArgs {
587 #[arg(long, num_args = 0..=1, default_missing_value = ".slop/diagnostic-bundle.json")]
589 bundle: Option<PathBuf>,
590 #[arg(long, value_enum, default_value_t = DoctorFormat::Text)]
592 format: DoctorFormat,
593 #[arg(long)]
595 scope: Option<String>,
596}
597
598#[derive(Debug, Clone, Copy, ValueEnum)]
599enum DoctorFormat {
600 Text,
601 Json,
602}
603
604#[derive(Debug, Args)]
605struct ListArgs {
606 #[command(subcommand)]
607 command: ListCommand,
608}
609
610#[derive(Debug, Subcommand)]
611enum ListCommand {
612 Findings(ListFilterArgs),
613 Relationships(ListFilterArgs),
614 Clusters(ListFilterArgs),
615 Profiles(ListFilterArgs),
616}
617
618#[derive(Debug, Args)]
619struct ListFilterArgs {
620 #[arg(long)]
622 report: Option<PathBuf>,
623 #[arg(long)]
625 path: Option<String>,
626 #[arg(long)]
628 profile: Option<String>,
629 #[arg(long)]
631 language: Option<String>,
632 #[arg(long, visible_alias = "class")]
634 classification: Option<String>,
635 #[arg(long)]
637 severity: Option<String>,
638 #[arg(long, default_value_t = 50)]
640 top: usize,
641 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
643 format: DisplayFormat,
644 #[arg(long)]
646 wide: bool,
647 #[arg(long)]
649 no_truncate: bool,
650}
651
652#[derive(Debug, Args)]
653struct PruneArgs {
654 #[arg(long)]
656 keep: Option<usize>,
657 #[arg(long)]
659 max_bytes: Option<u64>,
660 #[arg(long)]
662 dry_run: bool,
663 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
665 format: DisplayFormat,
666}
667
668#[derive(Debug, Args)]
669struct CacheArgs {
670 #[arg(long, value_name = "PATH")]
672 state_dir: Option<PathBuf>,
673 #[command(subcommand)]
674 command: CacheCommand,
675}
676
677#[derive(Debug, Subcommand)]
678enum CacheCommand {
679 Status {
680 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
682 format: DisplayFormat,
683 },
684 Prune {
685 #[arg(long, default_value_t = 10_000)]
687 max_entries: usize,
688 #[arg(long, default_value_t = 536_870_912)]
690 max_bytes: u64,
691 #[arg(long)]
693 dry_run: bool,
694 #[arg(long)]
696 compact: bool,
697 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
699 format: DisplayFormat,
700 },
701}
702
703#[derive(Debug, Args)]
704struct CompletionsArgs {
705 shell: CompletionShell,
707}
708
709#[derive(Debug, Args)]
710struct ManArgs {
711 #[arg(long)]
713 output: Option<PathBuf>,
714}
715
716#[derive(Debug, Args)]
717struct ReferenceArgs {
718 #[arg(long)]
720 output: Option<PathBuf>,
721}
722
723#[derive(Debug, Args)]
724struct HtmlArgs {
725 #[arg(long)]
727 report: Option<PathBuf>,
728 #[arg(long)]
730 output: Option<PathBuf>,
731}
732
733#[derive(Debug, Clone, Copy, ValueEnum)]
734enum CompletionShell {
735 Bash,
736 Zsh,
737 Fish,
738 Powershell,
739 Nushell,
740}
741
742#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
743enum DisplayFormat {
744 Text,
745 Json,
746 Yaml,
747}
748
749#[derive(Debug, Clone, Copy, ValueEnum)]
750enum CompareFormat {
751 Text,
752 Json,
753 Yaml,
754 Ndjson,
755}
756
757#[derive(Debug, Clone, Copy, ValueEnum)]
758enum CompareDetail {
759 Summary,
760 Top,
761 Full,
762}
763
764#[derive(Debug, Clone, Copy, ValueEnum)]
765enum HealthFormat {
766 Text,
767 Markdown,
768 Github,
769 Json,
770}
771
772#[derive(Debug, Clone, Copy, ValueEnum)]
773enum BuildInfoFormat {
774 Json,
775}
776
777#[derive(Debug, Clone, Copy, ValueEnum)]
778enum CheckFormat {
779 Text,
780 Json,
781 Github,
782}
783
784#[derive(Debug, Clone, Copy, ValueEnum)]
785enum PolicySource {
786 Base,
787 Head,
788}
789
790impl PolicySource {
791 fn as_str(self) -> &'static str {
792 match self {
793 Self::Base => "base",
794 Self::Head => "head",
795 }
796 }
797}
798
799#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
800enum ErrorFormat {
801 Human,
802 Json,
803}
804
805#[derive(Debug, Clone, Copy, ValueEnum)]
806enum ContextBand {
807 Compact,
808 Healthy,
809 Warning,
810 Critical,
811}
812
813impl ContextBand {
814 fn as_str(self) -> &'static str {
815 match self {
816 Self::Compact => "compact",
817 Self::Healthy => "healthy",
818 Self::Warning => "warning",
819 Self::Critical => "critical",
820 }
821 }
822}
823
824#[derive(Debug, Clone, Copy, ValueEnum)]
825enum SlopBand {
826 Low,
827 Moderate,
828 High,
829 Critical,
830}
831
832impl SlopBand {
833 fn as_str(self) -> &'static str {
834 match self {
835 Self::Low => "low",
836 Self::Moderate => "moderate",
837 Self::High => "high",
838 Self::Critical => "critical",
839 }
840 }
841}
842
843include!("cli/support.rs");
844include!("cli/analysis.rs");
845include!("cli/check.rs");
846include!("cli/baseline_compare.rs");
847include!("cli/reporting.rs");
848include!("cli/listing.rs");
849include!("cli/generation.rs");
850fn execute(repo_root: &Path, command: Command) -> Result<i32> {
851 match command {
852 Command::Init(args) => run_init(repo_root, args),
853 Command::Find(args) => run_find(repo_root, args),
854 Command::Show(args) => run_show(repo_root, args),
855 Command::Explain(args) => run_explain(repo_root, args),
856 Command::Plan(args) => run_plan(repo_root, args),
857 Command::Check(args) => run_check(repo_root, args),
858 Command::Compare(args) => run_compare(repo_root, args),
859 Command::Baseline(args) => run_baseline(repo_root, args),
860 Command::Report(args) => run_report(args),
861 Command::Sarif(args) => run_sarif(repo_root, args),
862 Command::Health(args) => run_health(repo_root, args),
863 Command::Config(args) => run_config(repo_root, args),
864 Command::Doctor(args) => run_doctor(repo_root, args),
865 Command::List(args) => run_list(repo_root, args),
866 Command::Prune(args) => run_prune(repo_root, args),
867 Command::Cache(args) => run_cache(repo_root, args),
868 Command::Completions(args) => run_completions(args),
869 Command::Man(args) => run_man(args),
870 Command::Reference(args) => run_reference(args),
871 Command::Html(args) => run_html(repo_root, args),
872 Command::Version => {
873 println!("{PROJECT_NAME} {VERSION}");
874 Ok(0)
875 }
876 Command::BuildInfo(args) => {
877 match args.format {
878 BuildInfoFormat::Json => {
879 println!("{}", serde_json::to_string_pretty(&build_info::current())?)
880 }
881 }
882 Ok(0)
883 }
884 Command::Schema(args) => {
885 let rendered = match args.contract {
886 SchemaContract::Report => render_json(&report::schema())?,
887 SchemaContract::Config => render_json(&config::schema())?,
888 contract => {
889 let source = match contract {
890 SchemaContract::Compare => include_str!("../schemas/compare-1.json"),
891 SchemaContract::Explain => include_str!("../schemas/explain-2.json"),
892 SchemaContract::Plan => include_str!("../schemas/plan-2.json"),
893 SchemaContract::Sarif => include_str!("../schemas/sarif-1.json"),
894 SchemaContract::Health => include_str!("../schemas/health-1.json"),
895 SchemaContract::Check => include_str!("../schemas/check-1.json"),
896 SchemaContract::Doctor => include_str!("../schemas/doctor-1.json"),
897 SchemaContract::BuildInfo => include_str!("../schemas/build-info-1.json"),
898 SchemaContract::List => include_str!("../schemas/list-1.json"),
899 SchemaContract::Show => include_str!("../schemas/show-1.json"),
900 SchemaContract::PromptManifest => {
901 include_str!("../schemas/prompt-manifest-1.json")
902 }
903 SchemaContract::Error => include_str!("../schemas/error-1.json"),
904 SchemaContract::FindEstimate => {
905 include_str!("../schemas/find-estimate-1.json")
906 }
907 SchemaContract::CacheStatus => {
908 include_str!("../schemas/cache-status-1.json")
909 }
910 SchemaContract::CachePrune => include_str!("../schemas/cache-prune-1.json"),
911 SchemaContract::Prune => include_str!("../schemas/prune-1.json"),
912 SchemaContract::CompareNdjson => {
913 include_str!("../schemas/compare-ndjson-1.json")
914 }
915 SchemaContract::Report | SchemaContract::Config => unreachable!(),
916 };
917 let value: Value = serde_json::from_str(source)?;
918 render_json(&value)?
919 }
920 };
921 write_generated_output(args.output.as_deref(), rendered.as_bytes())?;
922 Ok(0)
923 }
924 }
925}
926
927fn command_requires_repository(command: &Command) -> bool {
928 match command {
929 Command::Completions(_)
930 | Command::Man(_)
931 | Command::Reference(_)
932 | Command::Version
933 | Command::BuildInfo(_)
934 | Command::Schema(_)
935 | Command::Report(_)
936 | Command::Config(ConfigArgs {
937 command: ConfigCommand::Schema,
938 }) => false,
939 Command::Show(args) => args.report.is_none(),
940 Command::Compare(args) => args.base_ref.is_some() || args.baseline.is_some(),
941 Command::Baseline(_) => true,
942 Command::Explain(args) => args.report.is_none() || args.include_repository_context,
943 Command::Plan(args) => args.report.is_none() || args.include_repository_context,
944 Command::Check(args) => args.report.is_none(),
945 Command::Sarif(args) => args.report.is_none(),
946 Command::Health(args) => args.report.is_none(),
947 Command::Html(args) => args.report.is_none(),
948 Command::List(ListArgs {
949 command:
950 ListCommand::Findings(args)
951 | ListCommand::Relationships(args)
952 | ListCommand::Clusters(args)
953 | ListCommand::Profiles(args),
954 }) => args.report.is_none(),
955 _ => true,
956 }
957}
958
959pub fn run() -> i32 {
960 let raw_args = std::env::args_os().collect::<Vec<_>>();
961 let requested_error_format = requested_error_format(&raw_args);
962 let parser_command = parser_command_name(&raw_args);
963 let cli = match Cli::try_parse_from(&raw_args) {
964 Ok(cli) => cli,
965 Err(error) => {
966 let code = error.exit_code();
967 if code == 0 || requested_error_format == ErrorFormat::Human {
968 let _ = error.print();
969 } else {
970 let classified =
971 ClassifiedError::new(ErrorKind::Contract, "parser_error", error.to_string())
972 .at("/arguments")
973 .with_details(json!({"clap_kind": format!("{:?}", error.kind())}));
974 render_runtime_error(
975 requested_error_format,
976 &classified,
977 parser_command.as_deref(),
978 );
979 }
980 return code;
981 }
982 };
983 let error_format = cli.error_format;
984 let command_name = cli.command.name();
985 let repo_root = if command_requires_repository(&cli.command) {
986 match git::resolve_repo_root_from(cli.repo.as_deref()) {
987 Ok(root) => root,
988 Err(error) => {
989 render_runtime_error(
990 error_format,
991 &ClassifiedError::new(
992 ErrorKind::Repository,
993 "repository_not_found",
994 format!("{error:#}"),
995 ),
996 Some(command_name),
997 );
998 return 3;
999 }
1000 }
1001 } else {
1002 PathBuf::new()
1003 };
1004 match execute(&repo_root, cli.command) {
1005 Ok(code) => code,
1006 Err(error) => {
1007 let classified = error.downcast_ref::<ClassifiedError>();
1008 let fallback;
1009 let classified = if let Some(classified) = classified {
1010 classified
1011 } else {
1012 fallback = if error.downcast_ref::<std::io::Error>().is_some() {
1013 ClassifiedError::new(ErrorKind::Io, "io_failure", format!("{error:#}"))
1014 } else {
1015 ClassifiedError::new(
1016 ErrorKind::Repository,
1017 "operation_failed",
1018 format!("{error:#}"),
1019 )
1020 };
1021 &fallback
1022 };
1023 render_runtime_error(error_format, classified, Some(command_name));
1024 classified.kind.exit_code()
1025 }
1026 }
1027}
1028
1029fn requested_error_format(args: &[std::ffi::OsString]) -> ErrorFormat {
1030 args.iter()
1031 .filter_map(|arg| arg.to_str())
1032 .enumerate()
1033 .find_map(|(index, arg)| {
1034 if arg == "--error-format" {
1035 args.get(index + 1).and_then(|value| value.to_str())
1036 } else {
1037 arg.strip_prefix("--error-format=")
1038 }
1039 })
1040 .filter(|value| *value == "json")
1041 .map_or(ErrorFormat::Human, |_| ErrorFormat::Json)
1042}
1043
1044fn parser_command_name(args: &[std::ffi::OsString]) -> Option<String> {
1045 const COMMANDS: &[&str] = &[
1046 "init",
1047 "find",
1048 "show",
1049 "explain",
1050 "plan",
1051 "check",
1052 "compare",
1053 "baseline",
1054 "report",
1055 "sarif",
1056 "health",
1057 "config",
1058 "doctor",
1059 "list",
1060 "prune",
1061 "cache",
1062 "completions",
1063 "man",
1064 "reference",
1065 "html",
1066 "version",
1067 "build-info",
1068 "schema",
1069 ];
1070 args.iter()
1071 .skip(1)
1072 .filter_map(|arg| arg.to_str())
1073 .find(|arg| COMMANDS.contains(arg))
1074 .map(str::to_string)
1075}
1076
1077fn render_runtime_error(format: ErrorFormat, error: &ClassifiedError, command: Option<&str>) {
1078 match format {
1079 ErrorFormat::Human => eprintln!("{}", error.message),
1080 ErrorFormat::Json => eprintln!(
1081 "{}",
1082 serde_json::to_string(&json!({
1083 "schema_version": 1,
1084 "error": {
1085 "kind": error.kind,
1086 "code": error.code,
1087 "pointer": error.pointer,
1088 "message": error.message,
1089 "details": error.details,
1090 "command": command,
1091 "exit_code": error.kind.exit_code()
1092 }
1093 }))
1094 .unwrap_or_else(|_| {
1095 "{\"schema_version\":1,\"error\":{\"code\":\"serialization_failed\"}}".to_string()
1096 })
1097 ),
1098 }
1099}
1100
1101#[cfg(test)]
1102mod tests {
1103 use clap::CommandFactory;
1104
1105 use super::Cli;
1106
1107 fn assert_descriptions(command: &clap::Command, path: &str) {
1108 for argument in command.get_arguments() {
1109 assert!(
1110 argument
1111 .get_help()
1112 .is_some_and(|help| !help.to_string().trim().is_empty()),
1113 "{path} argument {} has no generated reference description",
1114 argument.get_id()
1115 );
1116 }
1117 for subcommand in command.get_subcommands() {
1118 assert_descriptions(subcommand, &format!("{path} {}", subcommand.get_name()));
1119 }
1120 }
1121
1122 #[test]
1123 fn generated_reference_has_no_blank_argument_descriptions() {
1124 assert_descriptions(&Cli::command(), "git-slop");
1125 }
1126}