fallow_cli/
runtime_support.rs1use std::path::{Path, PathBuf};
2use std::process::ExitCode;
3
4use fallow_config::{FallowConfig, OutputFormat, ResolvedConfig};
5
6#[derive(Clone, PartialEq, Eq, clap::ValueEnum)]
8pub enum AnalysisKind {
9 #[value(alias = "check")]
10 DeadCode,
11 Dupes,
12 Health,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
17pub enum GroupBy {
18 #[value(alias = "team", alias = "codeowner")]
20 Owner,
21 Directory,
23 #[value(alias = "workspace", alias = "pkg")]
25 Package,
26 #[value(alias = "gl-section")]
30 Section,
31}
32
33pub fn build_ownership_resolver(
38 group_by: Option<GroupBy>,
39 root: &Path,
40 codeowners_path: Option<&str>,
41 output: OutputFormat,
42) -> Result<Option<crate::report::OwnershipResolver>, ExitCode> {
43 let Some(mode) = group_by else {
44 return Ok(None);
45 };
46 match mode {
47 GroupBy::Owner => match crate::codeowners::CodeOwners::load(root, codeowners_path) {
48 Ok(co) => Ok(Some(crate::report::OwnershipResolver::Owner(co))),
49 Err(e) => Err(crate::error::emit_error(&e, 2, output)),
50 },
51 GroupBy::Section => match crate::codeowners::CodeOwners::load(root, codeowners_path) {
52 Ok(co) => {
53 if co.has_sections() {
54 Ok(Some(crate::report::OwnershipResolver::Section(co)))
55 } else {
56 Err(crate::error::emit_error(
57 "--group-by section requires a GitLab-style CODEOWNERS file \
58 with `[Section]` headers. This CODEOWNERS has no sections; \
59 use --group-by owner instead.",
60 2,
61 output,
62 ))
63 }
64 }
65 Err(e) => Err(crate::error::emit_error(&e, 2, output)),
66 },
67 GroupBy::Directory => Ok(Some(crate::report::OwnershipResolver::Directory)),
68 GroupBy::Package => {
69 let workspaces = fallow_config::discover_workspaces(root);
70 if workspaces.is_empty() {
71 Err(crate::error::emit_error(
72 "--group-by package requires a monorepo with workspace packages \
73 (package.json workspaces, pnpm-workspace.yaml, or tsconfig references)",
74 2,
75 output,
76 ))
77 } else {
78 Ok(Some(crate::report::OwnershipResolver::Package(
79 crate::report::grouping::PackageResolver::new(root, &workspaces),
80 )))
81 }
82 }
83 }
84}
85
86fn log_config_loaded(path: &Path, output: OutputFormat, quiet: bool) {
91 if quiet || !matches!(output, OutputFormat::Human) {
92 return;
93 }
94 eprintln!("loaded config: {}", path.display());
95}
96
97#[expect(clippy::ref_option, reason = "&Option matches clap's field type")]
98pub fn load_config(
99 root: &Path,
100 config_path: &Option<PathBuf>,
101 output: OutputFormat,
102 no_cache: bool,
103 threads: usize,
104 production: bool,
105 quiet: bool,
106) -> Result<ResolvedConfig, ExitCode> {
107 let user_config = if let Some(path) = config_path {
108 match FallowConfig::load(path) {
109 Ok(c) => {
110 log_config_loaded(path, output, quiet);
111 Some(c)
112 }
113 Err(e) => {
114 let msg = format!("failed to load config '{}': {e}", path.display());
115 return Err(crate::error::emit_error(&msg, 2, output));
116 }
117 }
118 } else {
119 match FallowConfig::find_and_load(root) {
120 Ok(Some((config, found_path))) => {
121 log_config_loaded(&found_path, output, quiet);
122 Some(config)
123 }
124 Ok(None) => None,
125 Err(e) => {
126 return Err(crate::error::emit_error(&e, 2, output));
127 }
128 }
129 };
130
131 Ok(match user_config {
132 Some(mut config) => {
133 if production {
134 config.production = true;
135 }
136 config.resolve(root.to_path_buf(), output, threads, no_cache, quiet)
137 }
138 None => FallowConfig {
139 production,
140 ..FallowConfig::default()
141 }
142 .resolve(root.to_path_buf(), output, threads, no_cache, quiet),
143 })
144}