Skip to main content

git_slop/cli/
entry.rs

1fn execute(repo_root: &Path, command: Command) -> Result<i32> {
2    match command {
3        Command::Init(args) => run_init(repo_root, args),
4        Command::Find(args) => run_find(repo_root, args),
5        Command::Show(args) => run_show(repo_root, args),
6        Command::Explain(args) => run_explain(repo_root, args),
7        Command::Plan(args) => run_plan(repo_root, args),
8        Command::Policy(args) => run_policy(repo_root, args),
9        Command::Advise(args) => run_advise(repo_root, *args),
10        Command::Check(args) => run_check(repo_root, args),
11        Command::Compare(args) => run_compare(repo_root, args),
12        Command::Baseline(args) => run_baseline(repo_root, args),
13        Command::Report(args) => run_report(repo_root, args),
14        Command::Sarif(args) => run_sarif(repo_root, args),
15        Command::Health(args) => run_health(repo_root, args),
16        Command::Config(args) => run_config(repo_root, args),
17        Command::Doctor(args) => run_doctor(repo_root, args),
18        Command::List(args) => run_list(repo_root, args),
19        Command::Prune(args) => run_prune(repo_root, args),
20        Command::Cache(args) => run_cache(repo_root, args),
21        Command::Completions(args) => run_completions(args),
22        Command::Man(args) => run_man(args),
23        Command::Reference(args) => run_reference(args),
24        Command::Html(args) => run_html(repo_root, args),
25        Command::Version => {
26            println!("{PROJECT_NAME} {VERSION}");
27            Ok(0)
28        }
29        Command::BuildInfo(args) => {
30            match args.format {
31                BuildInfoFormat::Json => {
32                    println!("{}", serde_json::to_string_pretty(&build_info::current())?)
33                }
34            }
35            Ok(0)
36        }
37        Command::Schema(args) => {
38            let rendered = match args.contract {
39                SchemaContract::Report => render_json(&report::schema())?,
40                SchemaContract::Config => render_json(&config::schema())?,
41                contract => {
42                    let source = match contract {
43                        SchemaContract::Compare => include_str!("../../schemas/compare-1.json"),
44                        SchemaContract::Init => include_str!("../../schemas/init-1.json"),
45                        SchemaContract::Explain => include_str!("../../schemas/explain-2.json"),
46                        SchemaContract::Plan => include_str!("../../schemas/plan-2.json"),
47                        SchemaContract::Sarif => include_str!("../../schemas/sarif-1.json"),
48                        SchemaContract::Health => include_str!("../../schemas/health-1.json"),
49                        SchemaContract::Check => include_str!("../../schemas/check-1.json"),
50                        SchemaContract::Doctor => include_str!("../../schemas/doctor-1.json"),
51                        SchemaContract::BuildInfo => include_str!("../../schemas/build-info-2.json"),
52                        SchemaContract::ReleaseManifest => {
53                            include_str!("../../schemas/release-manifest-3.json")
54                        }
55                        SchemaContract::List => include_str!("../../schemas/list-1.json"),
56                        SchemaContract::Show => include_str!("../../schemas/show-1.json"),
57                        SchemaContract::PromptManifest => {
58                            include_str!("../../schemas/prompt-manifest-1.json")
59                        }
60                        SchemaContract::Error => include_str!("../../schemas/error-1.json"),
61                        SchemaContract::FindEstimate => {
62                            include_str!("../../schemas/find-estimate-1.json")
63                        }
64                        SchemaContract::CacheStatus => {
65                            include_str!("../../schemas/cache-status-1.json")
66                        }
67                        SchemaContract::CachePrune => {
68                            include_str!("../../schemas/cache-prune-1.json")
69                        }
70                        SchemaContract::Baseline => include_str!("../../schemas/baseline-1.json"),
71                        SchemaContract::Prune => include_str!("../../schemas/prune-1.json"),
72                        SchemaContract::CompareNdjson => {
73                            include_str!("../../schemas/compare-ndjson-1.json")
74                        }
75                        SchemaContract::PolicyPack => {
76                            include_str!("../../schemas/policy-pack-1.json")
77                        }
78                        SchemaContract::PolicyLock => {
79                            include_str!("../../schemas/policy-lock-1.json")
80                        }
81                        SchemaContract::AdviceInput => {
82                            include_str!("../../schemas/advice-input-1.json")
83                        }
84                        SchemaContract::AdviceResponse => {
85                            include_str!("../../schemas/advice-response-1.json")
86                        }
87                        SchemaContract::Advice => include_str!("../../schemas/advice-1.json"),
88                        SchemaContract::AdvisorCorpus => {
89                            include_str!("../../schemas/advisor-corpus-1.json")
90                        }
91                        SchemaContract::AdvisorRatings => {
92                            include_str!("../../schemas/advisor-ratings-1.json")
93                        }
94                        SchemaContract::AdvisorRatingsV2 => {
95                            include_str!("../../schemas/advisor-ratings-2.json")
96                        }
97                        SchemaContract::AdvisorReviewArtifact => {
98                            include_str!("../../schemas/advisor-review-artifact-1.json")
99                        }
100                        SchemaContract::AdvisorReviewManifest => {
101                            include_str!("../../schemas/advisor-review-manifest-1.json")
102                        }
103                        SchemaContract::AdvisorOperationReceipt => {
104                            include_str!("../../schemas/advisor-operation-receipt-1.json")
105                        }
106                        SchemaContract::AdvisorThresholds => {
107                            include_str!("../../schemas/advisor-thresholds-1.json")
108                        }
109                        SchemaContract::AdvisorBenchmark => {
110                            include_str!("../../schemas/advisor-benchmark-1.json")
111                        }
112                        SchemaContract::AdvisorCapacity => {
113                            include_str!("../../schemas/advisor-capacity-1.json")
114                        }
115                        SchemaContract::Report | SchemaContract::Config => unreachable!(),
116                    };
117                    let value: Value = serde_json::from_str(source)?;
118                    render_json(&value)?
119                }
120            };
121            write_generated_output(args.output.as_deref(), rendered.as_bytes())?;
122            Ok(0)
123        }
124    }
125}
126
127fn command_requires_repository(command: &Command) -> bool {
128    match command {
129        Command::Completions(_)
130        | Command::Man(_)
131        | Command::Reference(_)
132        | Command::Version
133        | Command::BuildInfo(_)
134        | Command::Schema(_)
135        | Command::Report(_)
136        | Command::Config(ConfigArgs {
137            command: ConfigCommand::Schema,
138        }) => false,
139        Command::Show(args) => args.report.is_none() || args.require_current,
140        Command::Compare(args) => args.base_ref.is_some() || args.baseline.is_some(),
141        Command::Baseline(_) => true,
142        Command::Policy(_) => true,
143        Command::Advise(_) => true,
144        Command::Explain(args) => {
145            args.report.is_none() || args.include_repository_context || args.require_current
146        }
147        Command::Plan(args) => {
148            args.report.is_none() || args.include_repository_context || args.require_current
149        }
150        Command::Check(args) => args.report.is_none() || args.require_current,
151        Command::Sarif(args) => args.report.is_none() || args.require_current,
152        Command::Health(args) => args.report.is_none() || args.require_current,
153        Command::Html(args) => args.report.is_none() || args.require_current,
154        Command::List(args) => {
155            let output = match &args.command {
156                ListCommand::PolicyFailures(args)
157                | ListCommand::Interventions(args)
158                | ListCommand::Observations(args)
159                | ListCommand::HealthFindings(args)
160                | ListCommand::Findings(args) => &args.output,
161                ListCommand::Relationships(args) => &args.output,
162                ListCommand::Clusters(args) => &args.output,
163                ListCommand::Profiles(args) => &args.output,
164            };
165            output.report.is_none() || output.require_current
166        }
167        _ => true,
168    }
169}
170
171pub fn run() -> i32 {
172    let raw_args = std::env::args_os().collect::<Vec<_>>();
173    if raw_args.len() == 1 {
174        println!(
175            "Git Slop finds repository surfaces that cost too much context.\n\n\
176             QUICK START\n  git slop find                 Run a safe, cacheable first scan\n  \
177             git slop health               Review the current health snapshot\n  \
178             git slop list interventions   Review bounded maintenance candidates\n  \
179             git slop explain --top 5      Understand why candidates surfaced\n\n\
180             INSPECT\n  show · explain · plan · list · html · doctor\n\n\
181             AUTOMATION\n  check · compare · baseline · sarif\n\n\
182             ADOPTION AND STATE\n  init · config · cache · prune\n\n\
183             POLICY-GUIDED ADVICE\n  policy · advise\n\n\
184             GENERATE AND INFO\n  completions · man · reference · schema · version · build-info\n\n\
185             Run `git slop --help` for every command and option."
186        );
187        return 0;
188    }
189    let requested_error_format = requested_error_format(&raw_args);
190    let parser_command = parser_command_name(&raw_args);
191    let cli = match Cli::try_parse_from(&raw_args) {
192        Ok(cli) => cli,
193        Err(error) => {
194            let code = error.exit_code();
195            if code == 0 || requested_error_format == ErrorFormat::Human {
196                let _ = error.print();
197            } else {
198                let classified =
199                    ClassifiedError::new(ErrorKind::Contract, "parser_error", error.to_string())
200                        .at("/arguments")
201                        .with_details(json!({"clap_kind": format!("{:?}", error.kind())}));
202                render_runtime_error(
203                    requested_error_format,
204                    &classified,
205                    parser_command.as_deref(),
206                );
207            }
208            return code;
209        }
210    };
211    let error_format = if cli.error_format == ErrorFormat::Human
212        && matches!(cli.command, Command::Init(InitArgs { format: InitFormat::Json, .. }))
213    {
214        ErrorFormat::Json
215    } else {
216        cli.error_format
217    };
218    let command_name = cli.command.name();
219    let repo_root = if cli.repo.is_some() || command_requires_repository(&cli.command) {
220        match git::resolve_repo_root_from(cli.repo.as_deref()) {
221            Ok(root) => root,
222            Err(error) => {
223                let requested_path = cli.repo.clone().or_else(|| std::env::current_dir().ok());
224                let requested_display = requested_path.as_deref().map_or_else(
225                    || "the current directory".to_string(),
226                    |path| path.display().to_string(),
227                );
228                render_runtime_error(
229                    error_format,
230                    &ClassifiedError::new(
231                        ErrorKind::Repository,
232                        "repository_not_found",
233                        format!(
234                            "Not inside a Git repository: {requested_display}. Run this command from a repository, or pass `--repo <PATH>`."
235                        ),
236                    )
237                    .at("/repo")
238                    .with_details(json!({"path": requested_path, "cause": format!("{error:#}")})),
239                    Some(command_name),
240                );
241                return 3;
242            }
243        }
244    } else {
245        PathBuf::new()
246    };
247    match execute(&repo_root, cli.command) {
248        Ok(code) => code,
249        Err(error) => {
250            let classified = error.downcast_ref::<ClassifiedError>();
251            let fallback;
252            let classified = if let Some(classified) = classified {
253                classified
254            } else {
255                fallback = if error.downcast_ref::<std::io::Error>().is_some() {
256                    ClassifiedError::new(ErrorKind::Io, "io_failure", format!("{error:#}"))
257                } else {
258                    ClassifiedError::new(
259                        ErrorKind::Repository,
260                        "operation_failed",
261                        format!("{error:#}"),
262                    )
263                };
264                &fallback
265            };
266            render_runtime_error(error_format, classified, Some(command_name));
267            classified.kind.exit_code()
268        }
269    }
270}
271
272fn requested_error_format(args: &[std::ffi::OsString]) -> ErrorFormat {
273    let explicit = args.iter()
274        .filter_map(|arg| arg.to_str())
275        .enumerate()
276        .find_map(|(index, arg)| {
277            if arg == "--error-format" {
278                args.get(index + 1).and_then(|value| value.to_str())
279            } else {
280                arg.strip_prefix("--error-format=")
281            }
282        })
283        .is_some_and(|value| value == "json");
284    let init_json = args.iter().filter_map(|arg| arg.to_str()).any(|arg| arg == "init")
285        && args
286            .iter()
287            .filter_map(|arg| arg.to_str())
288            .enumerate()
289            .any(|(index, arg)| {
290                arg == "--format=json"
291                    || (arg == "--format"
292                        && args.get(index + 1).and_then(|value| value.to_str()) == Some("json"))
293            });
294    if explicit || init_json {
295        ErrorFormat::Json
296    } else {
297        ErrorFormat::Human
298    }
299}
300
301fn parser_command_name(args: &[std::ffi::OsString]) -> Option<String> {
302    const COMMANDS: &[&str] = &[
303        "init",
304        "find",
305        "show",
306        "explain",
307        "plan",
308        "policy",
309        "advise",
310        "check",
311        "compare",
312        "baseline",
313        "report",
314        "sarif",
315        "health",
316        "config",
317        "doctor",
318        "list",
319        "prune",
320        "cache",
321        "completions",
322        "man",
323        "reference",
324        "html",
325        "version",
326        "build-info",
327        "schema",
328    ];
329    args.iter()
330        .skip(1)
331        .filter_map(|arg| arg.to_str())
332        .find(|arg| COMMANDS.contains(arg))
333        .map(str::to_string)
334}
335
336fn render_runtime_error(format: ErrorFormat, error: &ClassifiedError, command: Option<&str>) {
337    match format {
338        ErrorFormat::Human => eprintln!("{}", error.message),
339        ErrorFormat::Json => eprintln!(
340            "{}",
341            serde_json::to_string(&json!({
342                "schema_version": 1,
343                "error": {
344                    "kind": error.kind,
345                    "code": error.code,
346                    "pointer": error.pointer,
347                    "message": error.message,
348                    "details": error.details,
349                    "command": command,
350                    "exit_code": error.kind.exit_code()
351                }
352            }))
353            .unwrap_or_else(|_| {
354                "{\"schema_version\":1,\"error\":{\"code\":\"serialization_failed\"}}".to_string()
355            })
356        ),
357    }
358}