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, 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, value_enum, default_value_t = PolicySource::Base)]
414 policy_from: PolicySource,
415 #[arg(long)]
417 fail_on_regression: bool,
418}
419
420#[derive(Debug, Args)]
421struct BaselineArgs {
422 #[command(subcommand)]
423 command: BaselineCommand,
424}
425
426#[derive(Debug, Subcommand)]
427enum BaselineCommand {
428 Create {
430 #[arg(long, default_value = "default")]
432 name: String,
433 #[arg(long)]
435 report: Option<PathBuf>,
436 #[arg(long)]
438 force: bool,
439 },
440 Update {
442 #[arg(long, default_value = "default")]
444 name: String,
445 #[arg(long)]
447 report: Option<PathBuf>,
448 },
449 Inspect {
451 #[arg(long, default_value = "default")]
453 name: String,
454 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
456 format: DisplayFormat,
457 },
458 Validate {
460 #[arg(long, default_value = "default")]
462 name: String,
463 },
464 Remove {
466 #[arg(long, default_value = "default")]
468 name: String,
469 },
470}
471
472#[derive(Debug, Args)]
473struct ReportArgs {
474 #[command(subcommand)]
475 command: ReportCommand,
476}
477
478#[derive(Debug, Subcommand)]
479enum ReportCommand {
480 Validate {
482 #[arg(value_name = "REPORT_JSON")]
484 path: PathBuf,
485 #[arg(long)]
487 allow_legacy: bool,
488 },
489 Migrate {
491 #[arg(value_name = "REPORT_JSON")]
493 path: PathBuf,
494 #[arg(long, value_name = "PATH")]
496 output: PathBuf,
497 },
498 Schema,
500}
501
502#[derive(Debug, Args)]
503struct SarifArgs {
504 #[arg(long)]
506 report: Option<PathBuf>,
507 #[arg(long)]
509 top: Option<i64>,
510 #[arg(long)]
512 output: Option<PathBuf>,
513}
514
515#[derive(Debug, Args)]
516struct HealthArgs {
517 #[arg(long)]
519 report: Option<PathBuf>,
520 #[arg(long, value_enum, default_value_t = HealthFormat::Text)]
522 format: HealthFormat,
523 #[arg(long, default_value_t = 10)]
525 max_annotations: usize,
526}
527
528#[derive(Debug, Args)]
529struct ConfigArgs {
530 #[command(subcommand)]
531 command: ConfigCommand,
532}
533
534#[derive(Debug, Subcommand)]
535enum ConfigCommand {
536 Show {
538 #[arg(long)]
540 effective: bool,
541 },
542 Validate,
544 DiffDefaults,
546 Migrate,
548 Schema,
550}
551
552#[derive(Debug, Args)]
553struct DoctorArgs {
554 #[arg(long, num_args = 0..=1, default_missing_value = ".slop/diagnostic-bundle.json")]
556 bundle: Option<PathBuf>,
557 #[arg(long, value_enum, default_value_t = DoctorFormat::Text)]
559 format: DoctorFormat,
560 #[arg(long)]
562 scope: Option<String>,
563}
564
565#[derive(Debug, Clone, Copy, ValueEnum)]
566enum DoctorFormat {
567 Text,
568 Json,
569}
570
571#[derive(Debug, Args)]
572struct ListArgs {
573 #[command(subcommand)]
574 command: ListCommand,
575}
576
577#[derive(Debug, Subcommand)]
578enum ListCommand {
579 Findings(ListFilterArgs),
580 Relationships(ListFilterArgs),
581 Clusters(ListFilterArgs),
582 Profiles(ListFilterArgs),
583}
584
585#[derive(Debug, Args)]
586struct ListFilterArgs {
587 #[arg(long)]
589 report: Option<PathBuf>,
590 #[arg(long)]
592 path: Option<String>,
593 #[arg(long)]
595 profile: Option<String>,
596 #[arg(long)]
598 language: Option<String>,
599 #[arg(long, visible_alias = "class")]
601 classification: Option<String>,
602 #[arg(long)]
604 severity: Option<String>,
605 #[arg(long, default_value_t = 50)]
607 top: usize,
608 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
610 format: DisplayFormat,
611 #[arg(long)]
613 wide: bool,
614 #[arg(long)]
616 no_truncate: bool,
617}
618
619#[derive(Debug, Args)]
620struct PruneArgs {
621 #[arg(long)]
623 keep: Option<usize>,
624 #[arg(long)]
626 max_bytes: Option<u64>,
627 #[arg(long)]
629 dry_run: bool,
630 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
632 format: DisplayFormat,
633}
634
635#[derive(Debug, Args)]
636struct CacheArgs {
637 #[arg(long, value_name = "PATH")]
639 state_dir: Option<PathBuf>,
640 #[command(subcommand)]
641 command: CacheCommand,
642}
643
644#[derive(Debug, Subcommand)]
645enum CacheCommand {
646 Status {
647 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
649 format: DisplayFormat,
650 },
651 Prune {
652 #[arg(long, default_value_t = 10_000)]
654 max_entries: usize,
655 #[arg(long, default_value_t = 536_870_912)]
657 max_bytes: u64,
658 #[arg(long)]
660 dry_run: bool,
661 #[arg(long)]
663 compact: bool,
664 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
666 format: DisplayFormat,
667 },
668}
669
670#[derive(Debug, Args)]
671struct CompletionsArgs {
672 shell: CompletionShell,
674}
675
676#[derive(Debug, Args)]
677struct ManArgs {
678 #[arg(long)]
680 output: Option<PathBuf>,
681}
682
683#[derive(Debug, Args)]
684struct ReferenceArgs {
685 #[arg(long)]
687 output: Option<PathBuf>,
688}
689
690#[derive(Debug, Args)]
691struct HtmlArgs {
692 #[arg(long)]
694 report: Option<PathBuf>,
695 #[arg(long)]
697 output: Option<PathBuf>,
698}
699
700#[derive(Debug, Clone, Copy, ValueEnum)]
701enum CompletionShell {
702 Bash,
703 Zsh,
704 Fish,
705 Powershell,
706 Nushell,
707}
708
709#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
710enum DisplayFormat {
711 Text,
712 Json,
713 Yaml,
714}
715
716#[derive(Debug, Clone, Copy, ValueEnum)]
717enum CompareFormat {
718 Text,
719 Json,
720 Yaml,
721 Ndjson,
722}
723
724#[derive(Debug, Clone, Copy, ValueEnum)]
725enum CompareDetail {
726 Summary,
727 Top,
728 Full,
729}
730
731#[derive(Debug, Clone, Copy, ValueEnum)]
732enum HealthFormat {
733 Text,
734 Markdown,
735 Github,
736 Json,
737}
738
739#[derive(Debug, Clone, Copy, ValueEnum)]
740enum BuildInfoFormat {
741 Json,
742}
743
744#[derive(Debug, Clone, Copy, ValueEnum)]
745enum CheckFormat {
746 Text,
747 Json,
748 Github,
749}
750
751#[derive(Debug, Clone, Copy, ValueEnum)]
752enum PolicySource {
753 Base,
754 Head,
755}
756
757impl PolicySource {
758 fn as_str(self) -> &'static str {
759 match self {
760 Self::Base => "base",
761 Self::Head => "head",
762 }
763 }
764}
765
766#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
767enum ErrorFormat {
768 Human,
769 Json,
770}
771
772#[derive(Debug, Clone, Copy, ValueEnum)]
773enum ContextBand {
774 Compact,
775 Healthy,
776 Warning,
777 Critical,
778}
779
780impl ContextBand {
781 fn as_str(self) -> &'static str {
782 match self {
783 Self::Compact => "compact",
784 Self::Healthy => "healthy",
785 Self::Warning => "warning",
786 Self::Critical => "critical",
787 }
788 }
789}
790
791#[derive(Debug, Clone, Copy, ValueEnum)]
792enum SlopBand {
793 Low,
794 Moderate,
795 High,
796 Critical,
797}
798
799impl SlopBand {
800 fn as_str(self) -> &'static str {
801 match self {
802 Self::Low => "low",
803 Self::Moderate => "moderate",
804 Self::High => "high",
805 Self::Critical => "critical",
806 }
807 }
808}
809
810include!("cli/support.rs");
811include!("cli/analysis.rs");
812include!("cli/check.rs");
813include!("cli/baseline_compare.rs");
814include!("cli/reporting.rs");
815include!("cli/listing.rs");
816include!("cli/generation.rs");
817fn execute(repo_root: &Path, command: Command) -> Result<i32> {
818 match command {
819 Command::Init(args) => run_init(repo_root, args),
820 Command::Find(args) => run_find(repo_root, args),
821 Command::Show(args) => run_show(repo_root, args),
822 Command::Explain(args) => run_explain(repo_root, args),
823 Command::Plan(args) => run_plan(repo_root, args),
824 Command::Check(args) => run_check(repo_root, args),
825 Command::Compare(args) => run_compare(repo_root, args),
826 Command::Baseline(args) => run_baseline(repo_root, args),
827 Command::Report(args) => run_report(args),
828 Command::Sarif(args) => run_sarif(repo_root, args),
829 Command::Health(args) => run_health(repo_root, args),
830 Command::Config(args) => run_config(repo_root, args),
831 Command::Doctor(args) => run_doctor(repo_root, args),
832 Command::List(args) => run_list(repo_root, args),
833 Command::Prune(args) => run_prune(repo_root, args),
834 Command::Cache(args) => run_cache(repo_root, args),
835 Command::Completions(args) => run_completions(args),
836 Command::Man(args) => run_man(args),
837 Command::Reference(args) => run_reference(args),
838 Command::Html(args) => run_html(repo_root, args),
839 Command::Version => {
840 println!("{PROJECT_NAME} {VERSION}");
841 Ok(0)
842 }
843 Command::BuildInfo(args) => {
844 match args.format {
845 BuildInfoFormat::Json => {
846 println!("{}", serde_json::to_string_pretty(&build_info::current())?)
847 }
848 }
849 Ok(0)
850 }
851 Command::Schema(args) => {
852 let rendered = match args.contract {
853 SchemaContract::Report => render_json(&report::schema())?,
854 SchemaContract::Config => render_json(&config::schema())?,
855 contract => {
856 let source = match contract {
857 SchemaContract::Compare => include_str!("../schemas/compare-1.json"),
858 SchemaContract::Explain => include_str!("../schemas/explain-2.json"),
859 SchemaContract::Plan => include_str!("../schemas/plan-2.json"),
860 SchemaContract::Sarif => include_str!("../schemas/sarif-1.json"),
861 SchemaContract::Health => include_str!("../schemas/health-1.json"),
862 SchemaContract::Check => include_str!("../schemas/check-1.json"),
863 SchemaContract::Doctor => include_str!("../schemas/doctor-1.json"),
864 SchemaContract::BuildInfo => include_str!("../schemas/build-info-1.json"),
865 SchemaContract::List => include_str!("../schemas/list-1.json"),
866 SchemaContract::Show => include_str!("../schemas/show-1.json"),
867 SchemaContract::PromptManifest => {
868 include_str!("../schemas/prompt-manifest-1.json")
869 }
870 SchemaContract::Error => include_str!("../schemas/error-1.json"),
871 SchemaContract::FindEstimate => {
872 include_str!("../schemas/find-estimate-1.json")
873 }
874 SchemaContract::CacheStatus => {
875 include_str!("../schemas/cache-status-1.json")
876 }
877 SchemaContract::CachePrune => include_str!("../schemas/cache-prune-1.json"),
878 SchemaContract::Prune => include_str!("../schemas/prune-1.json"),
879 SchemaContract::CompareNdjson => {
880 include_str!("../schemas/compare-ndjson-1.json")
881 }
882 SchemaContract::Report | SchemaContract::Config => unreachable!(),
883 };
884 let value: Value = serde_json::from_str(source)?;
885 render_json(&value)?
886 }
887 };
888 write_generated_output(args.output.as_deref(), rendered.as_bytes())?;
889 Ok(0)
890 }
891 }
892}
893
894fn command_requires_repository(command: &Command) -> bool {
895 match command {
896 Command::Completions(_)
897 | Command::Man(_)
898 | Command::Reference(_)
899 | Command::Version
900 | Command::BuildInfo(_)
901 | Command::Schema(_)
902 | Command::Report(_)
903 | Command::Config(ConfigArgs {
904 command: ConfigCommand::Schema,
905 }) => false,
906 Command::Show(args) => args.report.is_none(),
907 Command::Compare(args) => args.base_ref.is_some() || args.baseline.is_some(),
908 Command::Baseline(_) => true,
909 Command::Explain(args) => args.report.is_none() || args.include_repository_context,
910 Command::Plan(args) => args.report.is_none() || args.include_repository_context,
911 Command::Check(args) => args.report.is_none(),
912 Command::Sarif(args) => args.report.is_none(),
913 Command::Health(args) => args.report.is_none(),
914 Command::Html(args) => args.report.is_none(),
915 Command::List(ListArgs {
916 command:
917 ListCommand::Findings(args)
918 | ListCommand::Relationships(args)
919 | ListCommand::Clusters(args)
920 | ListCommand::Profiles(args),
921 }) => args.report.is_none(),
922 _ => true,
923 }
924}
925
926pub fn run() -> i32 {
927 let raw_args = std::env::args_os().collect::<Vec<_>>();
928 let requested_error_format = requested_error_format(&raw_args);
929 let parser_command = parser_command_name(&raw_args);
930 let cli = match Cli::try_parse_from(&raw_args) {
931 Ok(cli) => cli,
932 Err(error) => {
933 let code = error.exit_code();
934 if code == 0 || requested_error_format == ErrorFormat::Human {
935 let _ = error.print();
936 } else {
937 let classified =
938 ClassifiedError::new(ErrorKind::Contract, "parser_error", error.to_string())
939 .at("/arguments")
940 .with_details(json!({"clap_kind": format!("{:?}", error.kind())}));
941 render_runtime_error(
942 requested_error_format,
943 &classified,
944 parser_command.as_deref(),
945 );
946 }
947 return code;
948 }
949 };
950 let error_format = cli.error_format;
951 let command_name = cli.command.name();
952 let repo_root = if command_requires_repository(&cli.command) {
953 match git::resolve_repo_root_from(cli.repo.as_deref()) {
954 Ok(root) => root,
955 Err(error) => {
956 render_runtime_error(
957 error_format,
958 &ClassifiedError::new(
959 ErrorKind::Repository,
960 "repository_not_found",
961 format!("{error:#}"),
962 ),
963 Some(command_name),
964 );
965 return 3;
966 }
967 }
968 } else {
969 PathBuf::new()
970 };
971 match execute(&repo_root, cli.command) {
972 Ok(code) => code,
973 Err(error) => {
974 let classified = error.downcast_ref::<ClassifiedError>();
975 let fallback;
976 let classified = if let Some(classified) = classified {
977 classified
978 } else {
979 fallback = if error.downcast_ref::<std::io::Error>().is_some() {
980 ClassifiedError::new(ErrorKind::Io, "io_failure", format!("{error:#}"))
981 } else {
982 ClassifiedError::new(
983 ErrorKind::Repository,
984 "operation_failed",
985 format!("{error:#}"),
986 )
987 };
988 &fallback
989 };
990 render_runtime_error(error_format, classified, Some(command_name));
991 classified.kind.exit_code()
992 }
993 }
994}
995
996fn requested_error_format(args: &[std::ffi::OsString]) -> ErrorFormat {
997 args.iter()
998 .filter_map(|arg| arg.to_str())
999 .enumerate()
1000 .find_map(|(index, arg)| {
1001 if arg == "--error-format" {
1002 args.get(index + 1).and_then(|value| value.to_str())
1003 } else {
1004 arg.strip_prefix("--error-format=")
1005 }
1006 })
1007 .filter(|value| *value == "json")
1008 .map_or(ErrorFormat::Human, |_| ErrorFormat::Json)
1009}
1010
1011fn parser_command_name(args: &[std::ffi::OsString]) -> Option<String> {
1012 const COMMANDS: &[&str] = &[
1013 "init",
1014 "find",
1015 "show",
1016 "explain",
1017 "plan",
1018 "check",
1019 "compare",
1020 "baseline",
1021 "report",
1022 "sarif",
1023 "health",
1024 "config",
1025 "doctor",
1026 "list",
1027 "prune",
1028 "cache",
1029 "completions",
1030 "man",
1031 "reference",
1032 "html",
1033 "version",
1034 "build-info",
1035 "schema",
1036 ];
1037 args.iter()
1038 .skip(1)
1039 .filter_map(|arg| arg.to_str())
1040 .find(|arg| COMMANDS.contains(arg))
1041 .map(str::to_string)
1042}
1043
1044fn render_runtime_error(format: ErrorFormat, error: &ClassifiedError, command: Option<&str>) {
1045 match format {
1046 ErrorFormat::Human => eprintln!("{}", error.message),
1047 ErrorFormat::Json => eprintln!(
1048 "{}",
1049 serde_json::to_string(&json!({
1050 "schema_version": 1,
1051 "error": {
1052 "kind": error.kind,
1053 "code": error.code,
1054 "pointer": error.pointer,
1055 "message": error.message,
1056 "details": error.details,
1057 "command": command,
1058 "exit_code": error.kind.exit_code()
1059 }
1060 }))
1061 .unwrap_or_else(|_| {
1062 "{\"schema_version\":1,\"error\":{\"code\":\"serialization_failed\"}}".to_string()
1063 })
1064 ),
1065 }
1066}
1067
1068#[cfg(test)]
1069mod tests {
1070 use clap::CommandFactory;
1071
1072 use super::Cli;
1073
1074 fn assert_descriptions(command: &clap::Command, path: &str) {
1075 for argument in command.get_arguments() {
1076 assert!(
1077 argument
1078 .get_help()
1079 .is_some_and(|help| !help.to_string().trim().is_empty()),
1080 "{path} argument {} has no generated reference description",
1081 argument.get_id()
1082 );
1083 }
1084 for subcommand in command.get_subcommands() {
1085 assert_descriptions(subcommand, &format!("{path} {}", subcommand.get_name()));
1086 }
1087 }
1088
1089 #[test]
1090 fn generated_reference_has_no_blank_argument_descriptions() {
1091 assert_descriptions(&Cli::command(), "git-slop");
1092 }
1093}