|
1 | 1 | use crate::{ |
2 | 2 | ai::{ProviderKind, read_api_key, resolve_model, resolve_provider}, |
3 | 3 | config::{LoadedConfig, load_config, resolve_config_relative_path}, |
| 4 | + path_glob, |
4 | 5 | tui::{ |
5 | 6 | DashboardEvent, DashboardInit, DashboardItem, DashboardItemStatus, DashboardKind, |
6 | 7 | DashboardLogTone, PlainReporter, ResolvedUiMode, RunReporter, SummaryRow, TuiReporter, |
@@ -413,19 +414,33 @@ fn resolve_config_inputs( |
413 | 414 | cfg: Option<&crate::config::AnnotateConfig>, |
414 | 415 | config_dir: Option<&Path>, |
415 | 416 | ) -> Result<Vec<String>, String> { |
| 417 | + fn has_glob_meta(path: &str) -> bool { |
| 418 | + path.bytes().any(|b| matches!(b, b'*' | b'?' | b'[' | b'{')) |
| 419 | + } |
| 420 | + |
416 | 421 | if let Some(input) = &opts.input { |
417 | 422 | return Ok(vec![input.clone()]); |
418 | 423 | } |
419 | 424 |
|
420 | 425 | if let Some(input) = cfg.and_then(|item| item.input.as_ref()) { |
421 | | - return Ok(vec![resolve_config_relative_path(config_dir, input)]); |
| 426 | + let resolved = vec![resolve_config_relative_path(config_dir, input)]; |
| 427 | + return if resolved.iter().any(|path| has_glob_meta(path)) { |
| 428 | + path_glob::expand_input_globs(&resolved) |
| 429 | + } else { |
| 430 | + Ok(resolved) |
| 431 | + }; |
422 | 432 | } |
423 | 433 |
|
424 | 434 | if let Some(inputs) = cfg.and_then(|item| item.inputs.as_ref()) { |
425 | | - return Ok(inputs |
| 435 | + let resolved = inputs |
426 | 436 | .iter() |
427 | 437 | .map(|input| resolve_config_relative_path(config_dir, input)) |
428 | | - .collect()); |
| 438 | + .collect::<Vec<_>>(); |
| 439 | + return if resolved.iter().any(|path| has_glob_meta(path)) { |
| 440 | + path_glob::expand_input_globs(&resolved) |
| 441 | + } else { |
| 442 | + Ok(resolved) |
| 443 | + }; |
429 | 444 | } |
430 | 445 |
|
431 | 446 | Ok(Vec::new()) |
@@ -2158,6 +2173,77 @@ concurrency = 2 |
2158 | 2173 | ); |
2159 | 2174 | } |
2160 | 2175 |
|
| 2176 | + #[test] |
| 2177 | + fn expand_annotate_invocations_expands_globbed_config_inputs() { |
| 2178 | + let temp_dir = TempDir::new().expect("temp dir"); |
| 2179 | + let project_dir = temp_dir.path().join("project"); |
| 2180 | + let sources_dir = project_dir.join("Sources"); |
| 2181 | + let app_dir = project_dir.join("App").join("Resources"); |
| 2182 | + let module_dir = project_dir.join("Modules").join("Feature"); |
| 2183 | + fs::create_dir_all(&sources_dir).expect("create Sources"); |
| 2184 | + fs::create_dir_all(&app_dir).expect("create app dir"); |
| 2185 | + fs::create_dir_all(&module_dir).expect("create module dir"); |
| 2186 | + |
| 2187 | + let first = app_dir.join("Localizable.xcstrings"); |
| 2188 | + let second = module_dir.join("Localizable.xcstrings"); |
| 2189 | + fs::write( |
| 2190 | + &first, |
| 2191 | + r#"{"sourceLanguage":"en","version":"1.0","strings":{}}"#, |
| 2192 | + ) |
| 2193 | + .expect("write first"); |
| 2194 | + fs::write( |
| 2195 | + &second, |
| 2196 | + r#"{"sourceLanguage":"en","version":"1.0","strings":{}}"#, |
| 2197 | + ) |
| 2198 | + .expect("write second"); |
| 2199 | + |
| 2200 | + let config_path = project_dir.join("langcodec.toml"); |
| 2201 | + fs::write( |
| 2202 | + &config_path, |
| 2203 | + r#"[openai] |
| 2204 | +model = "gpt-5.4" |
| 2205 | +
|
| 2206 | +[annotate] |
| 2207 | +inputs = ["*/**/Localizable.xcstrings"] |
| 2208 | +source_roots = ["Sources"] |
| 2209 | +"#, |
| 2210 | + ) |
| 2211 | + .expect("write config"); |
| 2212 | + |
| 2213 | + let loaded = load_config(Some(config_path.to_str().expect("config path"))) |
| 2214 | + .expect("load config") |
| 2215 | + .expect("config present"); |
| 2216 | + |
| 2217 | + let runs = expand_annotate_invocations( |
| 2218 | + &AnnotateOptions { |
| 2219 | + input: None, |
| 2220 | + source_roots: Vec::new(), |
| 2221 | + output: None, |
| 2222 | + source_lang: None, |
| 2223 | + provider: None, |
| 2224 | + model: None, |
| 2225 | + concurrency: None, |
| 2226 | + config: Some(config_path.to_string_lossy().to_string()), |
| 2227 | + dry_run: false, |
| 2228 | + check: false, |
| 2229 | + ui_mode: UiMode::Plain, |
| 2230 | + }, |
| 2231 | + Some(&loaded), |
| 2232 | + ) |
| 2233 | + .expect("expand annotate invocations"); |
| 2234 | + |
| 2235 | + let mut inputs = runs.into_iter().map(|run| run.input).collect::<Vec<_>>(); |
| 2236 | + inputs.sort(); |
| 2237 | + |
| 2238 | + let mut expected = vec![ |
| 2239 | + first.to_string_lossy().to_string(), |
| 2240 | + second.to_string_lossy().to_string(), |
| 2241 | + ]; |
| 2242 | + expected.sort(); |
| 2243 | + |
| 2244 | + assert_eq!(inputs, expected); |
| 2245 | + } |
| 2246 | + |
2161 | 2247 | #[test] |
2162 | 2248 | fn expand_annotate_invocations_rejects_input_and_inputs_together() { |
2163 | 2249 | let temp_dir = TempDir::new().expect("temp dir"); |
|
0 commit comments