Skip to content

Commit 620801b

Browse files
committed
feat: add --only-include flag for replacement include behavior
- Add --only-include CLI flag that replaces source detection entirely - Keep existing --include flag as additive behavior (source files + patterns) - Add validation to prevent both flags being used together + tests
1 parent 98b2074 commit 620801b

File tree

3 files changed

+194
-1
lines changed

3 files changed

+194
-1
lines changed

src/analyzer.rs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,23 @@ fn should_process_file(entry: &ignore::DirEntry, args: &Cli, base_path: &Path) -
106106
return false;
107107
}
108108

109+
// Handle replacement mode with --only-include
110+
if let Some(ref only_includes) = args.only_include {
111+
let matches_only_include = matches_include_patterns(path, only_includes, base_path);
112+
113+
if !matches_only_include {
114+
return false;
115+
}
116+
117+
// Apply excludes if any
118+
if let Some(ref excludes) = args.exclude {
119+
return !matches_exclude_patterns(path, excludes, base_path);
120+
}
121+
122+
return true;
123+
}
124+
125+
// Handle additive mode
109126
// Check if it's a source file
110127
let is_source = source_detection::is_source_file(path);
111128

@@ -357,6 +374,7 @@ mod tests {
357374
paths: vec![dir_path.to_string_lossy().to_string()],
358375
config_path: false,
359376
include: None,
377+
only_include: None,
360378
exclude: None,
361379
max_size: Some(10 * 1024 * 1024), // 10MB
362380
max_depth: Some(10),
@@ -878,4 +896,167 @@ mod tests {
878896

879897
Ok(())
880898
}
899+
900+
#[test]
901+
fn test_only_include_replacement_behavior() -> Result<()> {
902+
let (dir, _files) = setup_test_directory()?;
903+
904+
// Create various test files including non-source files
905+
fs::write(dir.path().join("config.conf"), "key=value")?;
906+
fs::write(dir.path().join("data.toml"), "[section]\nkey = 'value'")?;
907+
fs::write(dir.path().join("template.peb"), "template content")?;
908+
909+
let mut cli = create_test_cli(dir.path());
910+
911+
// Test 1: --only-include should ONLY include specified patterns, no other files
912+
cli.only_include = Some(vec!["*.conf".to_string()]);
913+
let entries = process_entries(&cli)?;
914+
915+
assert_eq!(entries.len(), 1);
916+
assert!(entries[0].path.extension().and_then(|ext| ext.to_str()) == Some("conf"));
917+
918+
// Verify no other files are included
919+
let extensions: Vec<_> = entries
920+
.iter()
921+
.filter_map(|e| e.path.extension().and_then(|ext| ext.to_str()))
922+
.collect();
923+
assert!(!extensions.contains(&"rs"));
924+
assert!(!extensions.contains(&"py"));
925+
assert!(!extensions.contains(&"md"));
926+
assert!(!extensions.contains(&"toml"));
927+
928+
// Test 2: Multiple patterns in --only-include
929+
cli.only_include = Some(vec!["*.conf".to_string(), "*.toml".to_string()]);
930+
let entries = process_entries(&cli)?;
931+
932+
assert_eq!(entries.len(), 2);
933+
let extensions: Vec<_> = entries
934+
.iter()
935+
.filter_map(|e| e.path.extension().and_then(|ext| ext.to_str()))
936+
.collect();
937+
assert!(extensions.contains(&"conf"));
938+
assert!(extensions.contains(&"toml"));
939+
assert!(!extensions.contains(&"rs")); // No other files
940+
assert!(!extensions.contains(&"py"));
941+
942+
// Test 3: --only-include with exclude patterns
943+
cli.only_include = Some(vec![
944+
"*.conf".to_string(),
945+
"*.toml".to_string(),
946+
"*.peb".to_string(),
947+
]);
948+
cli.exclude = Some(vec![Exclude::Pattern("*.toml".to_string())]);
949+
let entries = process_entries(&cli)?;
950+
951+
assert_eq!(entries.len(), 2); // conf and peb, but not toml (excluded)
952+
let extensions: Vec<_> = entries
953+
.iter()
954+
.filter_map(|e| e.path.extension().and_then(|ext| ext.to_str()))
955+
.collect();
956+
assert!(extensions.contains(&"conf"));
957+
assert!(extensions.contains(&"peb"));
958+
assert!(!extensions.contains(&"toml")); // Excluded
959+
assert!(!extensions.contains(&"rs")); // No other files
960+
961+
// Test 4: --only-include with pattern that matches nothing
962+
cli.only_include = Some(vec!["*.nonexistent".to_string()]);
963+
cli.exclude = None;
964+
let entries = process_entries(&cli)?;
965+
966+
assert_eq!(entries.len(), 0); // Should match nothing
967+
968+
Ok(())
969+
}
970+
971+
#[test]
972+
fn test_only_include_vs_include_behavior_difference() -> Result<()> {
973+
let (dir, _files) = setup_test_directory()?;
974+
975+
// Create a non-source file
976+
fs::write(dir.path().join("config.conf"), "key=value")?;
977+
978+
let mut cli = create_test_cli(dir.path());
979+
980+
// Test additive behavior with --include
981+
cli.include = Some(vec!["*.conf".to_string()]);
982+
cli.only_include = None;
983+
let additive_entries = process_entries(&cli)?;
984+
985+
// Should include conf + source files
986+
let additive_extensions: Vec<_> = additive_entries
987+
.iter()
988+
.filter_map(|e| e.path.extension().and_then(|ext| ext.to_str()))
989+
.collect();
990+
assert!(additive_extensions.contains(&"conf")); // Additional pattern
991+
assert!(additive_extensions.contains(&"rs")); // Source files
992+
assert!(additive_extensions.contains(&"py")); // Source files
993+
assert!(additive_extensions.contains(&"md")); // Source files
994+
995+
// Test replacement behavior with --only-include
996+
cli.include = None;
997+
cli.only_include = Some(vec!["*.conf".to_string()]);
998+
let replacement_entries = process_entries(&cli)?;
999+
1000+
// Should include ONLY conf files
1001+
assert_eq!(replacement_entries.len(), 1);
1002+
assert!(
1003+
replacement_entries[0]
1004+
.path
1005+
.extension()
1006+
.and_then(|ext| ext.to_str())
1007+
== Some("conf")
1008+
);
1009+
1010+
let replacement_extensions: Vec<_> = replacement_entries
1011+
.iter()
1012+
.filter_map(|e| e.path.extension().and_then(|ext| ext.to_str()))
1013+
.collect();
1014+
assert!(replacement_extensions.contains(&"conf")); // Only pattern
1015+
assert!(!replacement_extensions.contains(&"rs")); // No source files
1016+
assert!(!replacement_extensions.contains(&"py")); // No source files
1017+
assert!(!replacement_extensions.contains(&"md")); // No source files
1018+
1019+
// Verify the counts are different
1020+
assert!(additive_entries.len() > replacement_entries.len());
1021+
1022+
Ok(())
1023+
}
1024+
1025+
#[test]
1026+
fn test_only_include_single_file_processing() -> Result<()> {
1027+
let (dir, _files) = setup_test_directory()?;
1028+
1029+
// Create and test single file processing with a truly non-source file
1030+
let config_path = dir.path().join("config.conf");
1031+
fs::write(&config_path, "key=value")?;
1032+
1033+
let mut cli = create_test_cli(&config_path);
1034+
cli.paths = vec![config_path.to_string_lossy().to_string()];
1035+
1036+
// Test 1: Single non-source file without --only-include should be rejected
1037+
cli.only_include = None;
1038+
let entries = process_entries(&cli)?;
1039+
assert_eq!(entries.len(), 0);
1040+
1041+
// Test 2: Single non-source file WITH --only-include should be accepted
1042+
cli.only_include = Some(vec!["*.conf".to_string()]);
1043+
let entries = process_entries(&cli)?;
1044+
assert_eq!(entries.len(), 1);
1045+
assert!(entries[0].path.extension().and_then(|ext| ext.to_str()) == Some("conf"));
1046+
1047+
// Test 3: Single source file WITH --only-include that doesn't match should be rejected
1048+
let rs_path = dir.path().join("src/main.rs");
1049+
cli.paths = vec![rs_path.to_string_lossy().to_string()];
1050+
cli.only_include = Some(vec!["*.conf".to_string()]);
1051+
let entries = process_entries(&cli)?;
1052+
assert_eq!(entries.len(), 0);
1053+
1054+
// Test 4: Single source file WITH --only-include that matches should be accepted
1055+
cli.only_include = Some(vec!["*.rs".to_string()]);
1056+
let entries = process_entries(&cli)?;
1057+
assert_eq!(entries.len(), 1);
1058+
assert!(entries[0].path.extension().and_then(|ext| ext.to_str()) == Some("rs"));
1059+
1060+
Ok(())
1061+
}
8811062
}

src/cli.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,14 @@ pub struct Cli {
3939
#[arg(long)]
4040
pub config_path: bool,
4141

42-
/// Additional patterns to include (e.g. "*.rs,*.go")
42+
/// Additional patterns to include (e.g. "*.rs,*.go") - adds to source file detection
4343
#[arg(short, long, value_delimiter = ',')]
4444
pub include: Option<Vec<String>>,
4545

46+
/// Only include files matching these patterns (e.g. "*.yml,*.toml") - replaces source file detection
47+
#[arg(long, value_delimiter = ',')]
48+
pub only_include: Option<Vec<String>>,
49+
4650
/// Additional patterns to exclude
4751
#[arg(short, long, value_parser = parse_exclude, value_delimiter = ',')]
4852
pub exclude: Option<Vec<Exclude>>,
@@ -168,6 +172,13 @@ impl Cli {
168172
}
169173

170174
pub fn validate_args(&self, is_url: bool) -> anyhow::Result<()> {
175+
// Validate that both include and only_include are not used together
176+
if self.include.is_some() && self.only_include.is_some() {
177+
return Err(anyhow::anyhow!(
178+
"Cannot use both --include and --only-include flags together. Use --include for additive behavior (add to source files) or --only-include for replacement behavior (only specified patterns)."
179+
));
180+
}
181+
171182
if is_url {
172183
return Ok(());
173184
}

src/output.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ mod tests {
449449
config: false,
450450
paths: vec![".".to_string()],
451451
include: None,
452+
only_include: None,
452453
exclude: None,
453454
max_size: Some(1000),
454455
max_depth: Some(10),

0 commit comments

Comments
 (0)