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::Findings(args) => &args.output,
112                ListCommand::Relationships(args) => &args.output,
113                ListCommand::Clusters(args) => &args.output,
114                ListCommand::Profiles(args) => &args.output,
115            };
116            output.report.is_none() || output.require_current
117        }
118        _ => true,
119    }
120}
121
122pub fn run() -> i32 {
123    let raw_args = std::env::args_os().collect::<Vec<_>>();
124    let requested_error_format = requested_error_format(&raw_args);
125    let parser_command = parser_command_name(&raw_args);
126    let cli = match Cli::try_parse_from(&raw_args) {
127        Ok(cli) => cli,
128        Err(error) => {
129            let code = error.exit_code();
130            if code == 0 || requested_error_format == ErrorFormat::Human {
131                let _ = error.print();
132            } else {
133                let classified =
134                    ClassifiedError::new(ErrorKind::Contract, "parser_error", error.to_string())
135                        .at("/arguments")
136                        .with_details(json!({"clap_kind": format!("{:?}", error.kind())}));
137                render_runtime_error(
138                    requested_error_format,
139                    &classified,
140                    parser_command.as_deref(),
141                );
142            }
143            return code;
144        }
145    };
146    let error_format = cli.error_format;
147    let command_name = cli.command.name();
148    let repo_root = if command_requires_repository(&cli.command) {
149        match git::resolve_repo_root_from(cli.repo.as_deref()) {
150            Ok(root) => root,
151            Err(error) => {
152                let requested_path = cli.repo.clone().or_else(|| std::env::current_dir().ok());
153                let requested_display = requested_path.as_deref().map_or_else(
154                    || "the current directory".to_string(),
155                    |path| path.display().to_string(),
156                );
157                render_runtime_error(
158                    error_format,
159                    &ClassifiedError::new(
160                        ErrorKind::Repository,
161                        "repository_not_found",
162                        format!(
163                            "Not inside a Git repository: {requested_display}. Run this command from a repository, or pass `--repo <PATH>`."
164                        ),
165                    )
166                    .at("/repo")
167                    .with_details(json!({"path": requested_path, "cause": format!("{error:#}")})),
168                    Some(command_name),
169                );
170                return 3;
171            }
172        }
173    } else {
174        PathBuf::new()
175    };
176    match execute(&repo_root, cli.command) {
177        Ok(code) => code,
178        Err(error) => {
179            let classified = error.downcast_ref::<ClassifiedError>();
180            let fallback;
181            let classified = if let Some(classified) = classified {
182                classified
183            } else {
184                fallback = if error.downcast_ref::<std::io::Error>().is_some() {
185                    ClassifiedError::new(ErrorKind::Io, "io_failure", format!("{error:#}"))
186                } else {
187                    ClassifiedError::new(
188                        ErrorKind::Repository,
189                        "operation_failed",
190                        format!("{error:#}"),
191                    )
192                };
193                &fallback
194            };
195            render_runtime_error(error_format, classified, Some(command_name));
196            classified.kind.exit_code()
197        }
198    }
199}
200
201fn requested_error_format(args: &[std::ffi::OsString]) -> ErrorFormat {
202    args.iter()
203        .filter_map(|arg| arg.to_str())
204        .enumerate()
205        .find_map(|(index, arg)| {
206            if arg == "--error-format" {
207                args.get(index + 1).and_then(|value| value.to_str())
208            } else {
209                arg.strip_prefix("--error-format=")
210            }
211        })
212        .filter(|value| *value == "json")
213        .map_or(ErrorFormat::Human, |_| ErrorFormat::Json)
214}
215
216fn parser_command_name(args: &[std::ffi::OsString]) -> Option<String> {
217    const COMMANDS: &[&str] = &[
218        "init",
219        "find",
220        "show",
221        "explain",
222        "plan",
223        "check",
224        "compare",
225        "baseline",
226        "report",
227        "sarif",
228        "health",
229        "config",
230        "doctor",
231        "list",
232        "prune",
233        "cache",
234        "completions",
235        "man",
236        "reference",
237        "html",
238        "version",
239        "build-info",
240        "schema",
241    ];
242    args.iter()
243        .skip(1)
244        .filter_map(|arg| arg.to_str())
245        .find(|arg| COMMANDS.contains(arg))
246        .map(str::to_string)
247}
248
249fn render_runtime_error(format: ErrorFormat, error: &ClassifiedError, command: Option<&str>) {
250    match format {
251        ErrorFormat::Human => eprintln!("{}", error.message),
252        ErrorFormat::Json => eprintln!(
253            "{}",
254            serde_json::to_string(&json!({
255                "schema_version": 1,
256                "error": {
257                    "kind": error.kind,
258                    "code": error.code,
259                    "pointer": error.pointer,
260                    "message": error.message,
261                    "details": error.details,
262                    "command": command,
263                    "exit_code": error.kind.exit_code()
264                }
265            }))
266            .unwrap_or_else(|_| {
267                "{\"schema_version\":1,\"error\":{\"code\":\"serialization_failed\"}}".to_string()
268            })
269        ),
270    }
271}