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