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::Check(args) => run_check(repo_root, args),
9        Command::Compare(args) => run_compare(repo_root, args),
10        Command::Baseline(args) => run_baseline(repo_root, args),
11        Command::Report(args) => run_report(args),
12        Command::Sarif(args) => run_sarif(repo_root, args),
13        Command::Health(args) => run_health(repo_root, args),
14        Command::Config(args) => run_config(repo_root, args),
15        Command::Doctor(args) => run_doctor(repo_root, args),
16        Command::List(args) => run_list(repo_root, args),
17        Command::Prune(args) => run_prune(repo_root, args),
18        Command::Cache(args) => run_cache(repo_root, args),
19        Command::Completions(args) => run_completions(args),
20        Command::Man(args) => run_man(args),
21        Command::Reference(args) => run_reference(args),
22        Command::Html(args) => run_html(repo_root, args),
23        Command::Version => {
24            println!("{PROJECT_NAME} {VERSION}");
25            Ok(0)
26        }
27        Command::BuildInfo(args) => {
28            match args.format {
29                BuildInfoFormat::Json => {
30                    println!("{}", serde_json::to_string_pretty(&build_info::current())?)
31                }
32            }
33            Ok(0)
34        }
35        Command::Schema(args) => {
36            let rendered = match args.contract {
37                SchemaContract::Report => render_json(&report::schema())?,
38                SchemaContract::Config => render_json(&config::schema())?,
39                contract => {
40                    let source = match contract {
41                        SchemaContract::Compare => include_str!("../../schemas/compare-1.json"),
42                        SchemaContract::Explain => include_str!("../../schemas/explain-2.json"),
43                        SchemaContract::Plan => include_str!("../../schemas/plan-2.json"),
44                        SchemaContract::Sarif => include_str!("../../schemas/sarif-1.json"),
45                        SchemaContract::Health => include_str!("../../schemas/health-1.json"),
46                        SchemaContract::Check => include_str!("../../schemas/check-1.json"),
47                        SchemaContract::Doctor => include_str!("../../schemas/doctor-1.json"),
48                        SchemaContract::BuildInfo => include_str!("../../schemas/build-info-2.json"),
49                        SchemaContract::ReleaseManifest => {
50                            include_str!("../../schemas/release-manifest-3.json")
51                        }
52                        SchemaContract::List => include_str!("../../schemas/list-1.json"),
53                        SchemaContract::Show => include_str!("../../schemas/show-1.json"),
54                        SchemaContract::PromptManifest => {
55                            include_str!("../../schemas/prompt-manifest-1.json")
56                        }
57                        SchemaContract::Error => include_str!("../../schemas/error-1.json"),
58                        SchemaContract::FindEstimate => {
59                            include_str!("../../schemas/find-estimate-1.json")
60                        }
61                        SchemaContract::CacheStatus => {
62                            include_str!("../../schemas/cache-status-1.json")
63                        }
64                        SchemaContract::CachePrune => {
65                            include_str!("../../schemas/cache-prune-1.json")
66                        }
67                        SchemaContract::Baseline => include_str!("../../schemas/baseline-1.json"),
68                        SchemaContract::Prune => include_str!("../../schemas/prune-1.json"),
69                        SchemaContract::CompareNdjson => {
70                            include_str!("../../schemas/compare-ndjson-1.json")
71                        }
72                        SchemaContract::Report | SchemaContract::Config => unreachable!(),
73                    };
74                    let value: Value = serde_json::from_str(source)?;
75                    render_json(&value)?
76                }
77            };
78            write_generated_output(args.output.as_deref(), rendered.as_bytes())?;
79            Ok(0)
80        }
81    }
82}
83
84fn command_requires_repository(command: &Command) -> bool {
85    match command {
86        Command::Completions(_)
87        | Command::Man(_)
88        | Command::Reference(_)
89        | Command::Version
90        | Command::BuildInfo(_)
91        | Command::Schema(_)
92        | Command::Report(_)
93        | Command::Config(ConfigArgs {
94            command: ConfigCommand::Schema,
95        }) => false,
96        Command::Show(args) => args.report.is_none() || args.require_current,
97        Command::Compare(args) => args.base_ref.is_some() || args.baseline.is_some(),
98        Command::Baseline(_) => true,
99        Command::Explain(args) => {
100            args.report.is_none() || args.include_repository_context || args.require_current
101        }
102        Command::Plan(args) => {
103            args.report.is_none() || args.include_repository_context || args.require_current
104        }
105        Command::Check(args) => args.report.is_none() || args.require_current,
106        Command::Sarif(args) => args.report.is_none() || args.require_current,
107        Command::Health(args) => args.report.is_none() || args.require_current,
108        Command::Html(args) => args.report.is_none() || args.require_current,
109        Command::List(args) => {
110            let output = match &args.command {
111                ListCommand::PolicyFailures(args)
112                | ListCommand::Interventions(args)
113                | ListCommand::Observations(args)
114                | ListCommand::HealthFindings(args)
115                | ListCommand::Findings(args) => &args.output,
116                ListCommand::Relationships(args) => &args.output,
117                ListCommand::Clusters(args) => &args.output,
118                ListCommand::Profiles(args) => &args.output,
119            };
120            output.report.is_none() || output.require_current
121        }
122        _ => true,
123    }
124}
125
126pub fn run() -> i32 {
127    let raw_args = std::env::args_os().collect::<Vec<_>>();
128    if raw_args.len() == 1 {
129        let mut command = Cli::command();
130        let _ = command.print_help();
131        println!();
132        return 0;
133    }
134    let requested_error_format = requested_error_format(&raw_args);
135    let parser_command = parser_command_name(&raw_args);
136    let cli = match Cli::try_parse_from(&raw_args) {
137        Ok(cli) => cli,
138        Err(error) => {
139            let code = error.exit_code();
140            if code == 0 || requested_error_format == ErrorFormat::Human {
141                let _ = error.print();
142            } else {
143                let classified =
144                    ClassifiedError::new(ErrorKind::Contract, "parser_error", error.to_string())
145                        .at("/arguments")
146                        .with_details(json!({"clap_kind": format!("{:?}", error.kind())}));
147                render_runtime_error(
148                    requested_error_format,
149                    &classified,
150                    parser_command.as_deref(),
151                );
152            }
153            return code;
154        }
155    };
156    let error_format = cli.error_format;
157    let command_name = cli.command.name();
158    let repo_root = if command_requires_repository(&cli.command) {
159        match git::resolve_repo_root_from(cli.repo.as_deref()) {
160            Ok(root) => root,
161            Err(error) => {
162                let requested_path = cli.repo.clone().or_else(|| std::env::current_dir().ok());
163                let requested_display = requested_path.as_deref().map_or_else(
164                    || "the current directory".to_string(),
165                    |path| path.display().to_string(),
166                );
167                render_runtime_error(
168                    error_format,
169                    &ClassifiedError::new(
170                        ErrorKind::Repository,
171                        "repository_not_found",
172                        format!(
173                            "Not inside a Git repository: {requested_display}. Run this command from a repository, or pass `--repo <PATH>`."
174                        ),
175                    )
176                    .at("/repo")
177                    .with_details(json!({"path": requested_path, "cause": format!("{error:#}")})),
178                    Some(command_name),
179                );
180                return 3;
181            }
182        }
183    } else {
184        PathBuf::new()
185    };
186    match execute(&repo_root, cli.command) {
187        Ok(code) => code,
188        Err(error) => {
189            let classified = error.downcast_ref::<ClassifiedError>();
190            let fallback;
191            let classified = if let Some(classified) = classified {
192                classified
193            } else {
194                fallback = if error.downcast_ref::<std::io::Error>().is_some() {
195                    ClassifiedError::new(ErrorKind::Io, "io_failure", format!("{error:#}"))
196                } else {
197                    ClassifiedError::new(
198                        ErrorKind::Repository,
199                        "operation_failed",
200                        format!("{error:#}"),
201                    )
202                };
203                &fallback
204            };
205            render_runtime_error(error_format, classified, Some(command_name));
206            classified.kind.exit_code()
207        }
208    }
209}
210
211fn requested_error_format(args: &[std::ffi::OsString]) -> ErrorFormat {
212    args.iter()
213        .filter_map(|arg| arg.to_str())
214        .enumerate()
215        .find_map(|(index, arg)| {
216            if arg == "--error-format" {
217                args.get(index + 1).and_then(|value| value.to_str())
218            } else {
219                arg.strip_prefix("--error-format=")
220            }
221        })
222        .filter(|value| *value == "json")
223        .map_or(ErrorFormat::Human, |_| ErrorFormat::Json)
224}
225
226fn parser_command_name(args: &[std::ffi::OsString]) -> Option<String> {
227    const COMMANDS: &[&str] = &[
228        "init",
229        "find",
230        "show",
231        "explain",
232        "plan",
233        "check",
234        "compare",
235        "baseline",
236        "report",
237        "sarif",
238        "health",
239        "config",
240        "doctor",
241        "list",
242        "prune",
243        "cache",
244        "completions",
245        "man",
246        "reference",
247        "html",
248        "version",
249        "build-info",
250        "schema",
251    ];
252    args.iter()
253        .skip(1)
254        .filter_map(|arg| arg.to_str())
255        .find(|arg| COMMANDS.contains(arg))
256        .map(str::to_string)
257}
258
259fn render_runtime_error(format: ErrorFormat, error: &ClassifiedError, command: Option<&str>) {
260    match format {
261        ErrorFormat::Human => eprintln!("{}", error.message),
262        ErrorFormat::Json => eprintln!(
263            "{}",
264            serde_json::to_string(&json!({
265                "schema_version": 1,
266                "error": {
267                    "kind": error.kind,
268                    "code": error.code,
269                    "pointer": error.pointer,
270                    "message": error.message,
271                    "details": error.details,
272                    "command": command,
273                    "exit_code": error.kind.exit_code()
274                }
275            }))
276            .unwrap_or_else(|_| {
277                "{\"schema_version\":1,\"error\":{\"code\":\"serialization_failed\"}}".to_string()
278            })
279        ),
280    }
281}