Skip to content

Commit 6f39c1d

Browse files
authored
feat:Fix Excludes (#14)
* fix excludes * update
1 parent 1a23004 commit 6f39c1d

File tree

4 files changed

+167
-104
lines changed

4 files changed

+167
-104
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "glimpse"
3-
version = "0.7.0"
3+
version = "0.7.1"
44
edition = "2021"
55
description = "A blazingly fast tool for peeking at codebases. Perfect for loading your codebase into an LLM's context."
66
license = "MIT"

flake.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
{
2626
packages.default = pkgs.rustPlatform.buildRustPackage {
2727
pname = "glimpse";
28-
version = "0.7.0";
28+
version = "0.7.1";
2929

3030
src = ./.;
3131

src/analyzer.rs

Lines changed: 164 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::output::{
66
use crate::source_detection;
77
use crate::tokenizer::TokenCounter;
88
use anyhow::Result;
9-
use ignore::WalkBuilder;
9+
use ignore::{overrides::OverrideBuilder, WalkBuilder};
1010
use indicatif::{ProgressBar, ProgressStyle};
1111
use rayon::prelude::*;
1212
use std::fs;
@@ -29,28 +29,35 @@ pub fn process_directory(args: &Cli) -> Result<()> {
2929
);
3030
pb.set_message("Scanning files...");
3131

32-
let max_size = args.max_size.expect("max_size should be set from config");
33-
let max_depth = args.max_depth.expect("max_depth should be set from config");
3432
let output_format = args
3533
.output
3634
.clone()
3735
.expect("output format should be set from config");
36+
let entries = process_entries(&args)?;
37+
pb.finish();
3838

39-
// Build the walker with ignore patterns
40-
let mut builder = WalkBuilder::new(&args.paths[0]);
41-
builder
42-
.max_depth(Some(max_depth))
43-
.hidden(!args.hidden)
44-
.git_ignore(!args.no_ignore)
45-
.ignore(!args.no_ignore);
46-
47-
if let Some(ref includes) = args.include {
48-
for pattern in includes {
49-
builder.add_custom_ignore_filename(pattern);
50-
}
39+
if let Some(pdf_path) = &args.pdf {
40+
let pdf_data = generate_pdf(&entries, args.output.clone().unwrap_or(OutputFormat::Both))?;
41+
fs::write(pdf_path, pdf_data)?;
42+
println!("PDF output written to: {}", pdf_path.display());
43+
} else {
44+
// Handle output (print/copy/save)
45+
let output = generate_output(&entries, output_format)?;
46+
handle_output(output, args)?;
5147
}
5248

53-
// Collect all valid files
49+
if !args.no_tokens {
50+
let counter = create_token_counter(args)?;
51+
display_token_counts(counter, &entries)?;
52+
}
53+
54+
Ok(())
55+
}
56+
57+
pub fn process_entries(args: &Cli) -> Result<Vec<FileEntry>> {
58+
let max_size = args.max_size.expect("max_size should be set from config");
59+
let max_depth = args.max_depth.expect("max_depth should be set from config");
60+
5461
let entries = if args.interactive {
5562
let mut picker = FilePicker::new(
5663
PathBuf::from(&args.paths[0]),
@@ -73,8 +80,8 @@ pub fn process_directory(args: &Cli) -> Result<()> {
7380
.collect::<Vec<FileEntry>>()
7481
} else {
7582
let mut all_entries = Vec::new();
76-
for path in &args.paths {
77-
let path = std::path::Path::new(path);
83+
for path_str in &args.paths {
84+
let path = std::path::Path::new(path_str);
7885
if path.is_dir() {
7986
let mut builder = WalkBuilder::new(path);
8087
builder
@@ -89,14 +96,67 @@ pub fn process_directory(args: &Cli) -> Result<()> {
8996
}
9097
}
9198

99+
let mut override_builder = OverrideBuilder::new(path);
100+
if let Some(ref excludes) = args.exclude {
101+
for exclude in excludes {
102+
match exclude {
103+
Exclude::Pattern(pattern) => {
104+
// Add a '!' prefix if it doesn't already have one
105+
// This makes it a negative pattern (exclude)
106+
let exclude_pattern = if !pattern.starts_with('!') {
107+
format!("!{}", pattern)
108+
} else {
109+
pattern.clone()
110+
};
111+
112+
if let Err(e) = override_builder.add(&exclude_pattern) {
113+
eprintln!(
114+
"Warning: Invalid exclude pattern '{}': {}",
115+
pattern, e
116+
);
117+
}
118+
}
119+
Exclude::File(file_path) => {
120+
// For file excludes, handle differently if:
121+
if file_path.is_absolute() {
122+
// For absolute paths, check if they exist
123+
if file_path.exists() {
124+
// If base_path is part of file_path, make it relative
125+
if let Ok(relative_path) = file_path.strip_prefix(path) {
126+
let pattern = format!("!{}", relative_path.display());
127+
if let Err(e) = override_builder.add(&pattern) {
128+
eprintln!("Warning: Could not add file exclude pattern for '{}': {}", file_path.display(), e);
129+
}
130+
} else {
131+
// This doesn't affect current directory
132+
eprintln!(
133+
"Note: File exclude not under current path: {}",
134+
file_path.display()
135+
);
136+
}
137+
}
138+
} else {
139+
// For relative paths like "src", use as-is with a ! prefix
140+
let pattern = format!("!{}", file_path.display());
141+
if let Err(e) = override_builder.add(&pattern) {
142+
eprintln!(
143+
"Warning: Could not add file exclude pattern '{}': {}",
144+
pattern, e
145+
);
146+
}
147+
}
148+
}
149+
}
150+
}
151+
}
152+
let overrides = override_builder.build()?;
153+
builder.overrides(overrides);
154+
92155
let dir_entries: Vec<FileEntry> = builder
93156
.build()
94157
.par_bridge()
95158
.filter_map(|entry| entry.ok())
96-
.filter(|entry| {
97-
let should_exclude = is_excluded(entry, args);
98-
!should_exclude
99-
})
159+
// No longer need the is_excluded filter here, WalkBuilder handles it
100160
.filter(|entry| {
101161
entry.file_type().map(|ft| ft.is_file()).unwrap_or(false)
102162
&& source_detection::is_source_file(entry.path())
@@ -117,12 +177,44 @@ pub fn process_directory(args: &Cli) -> Result<()> {
117177
.map(|m| m.len() <= max_size)
118178
.unwrap_or(false)
119179
{
120-
let entry = ignore::WalkBuilder::new(path)
121-
.build()
122-
.next()
123-
.and_then(|r| r.ok());
124-
if let Some(entry) = entry {
125-
if !is_excluded(&entry, args) {
180+
// Need to check excludes even for single files explicitly passed
181+
let mut excluded = false;
182+
if let Some(ref excludes) = args.exclude {
183+
let mut override_builder =
184+
OverrideBuilder::new(path.parent().unwrap_or(path)); // Base relative to parent
185+
for exclude in excludes {
186+
match exclude {
187+
Exclude::Pattern(pattern) => {
188+
if let Err(e) = override_builder.add(pattern) {
189+
eprintln!(
190+
"Warning: Invalid exclude pattern '{}': {}",
191+
pattern, e
192+
);
193+
}
194+
}
195+
Exclude::File(file_path) => {
196+
if path == file_path {
197+
excluded = true;
198+
break;
199+
}
200+
}
201+
}
202+
}
203+
if excluded {
204+
continue;
205+
}
206+
let overrides = override_builder.build()?;
207+
if overrides.matched(path, false).is_ignore() {
208+
excluded = true;
209+
}
210+
}
211+
212+
if !excluded {
213+
let entry = ignore::WalkBuilder::new(path)
214+
.build()
215+
.next()
216+
.and_then(|r| r.ok());
217+
if let Some(entry) = entry {
126218
if let Ok(file_entry) = process_file(&entry, path) {
127219
all_entries.push(file_entry);
128220
}
@@ -133,66 +225,11 @@ pub fn process_directory(args: &Cli) -> Result<()> {
133225
}
134226
all_entries
135227
};
136-
pb.finish();
137-
138-
if let Some(pdf_path) = &args.pdf {
139-
let pdf_data = generate_pdf(&entries, args.output.clone().unwrap_or(OutputFormat::Both))?;
140-
fs::write(pdf_path, pdf_data)?;
141-
println!("PDF output written to: {}", pdf_path.display());
142-
} else {
143-
// Handle output (print/copy/save)
144-
let output = generate_output(&entries, output_format)?;
145-
handle_output(output, args)?;
146-
}
147-
148-
if !args.no_tokens {
149-
let counter = create_token_counter(args)?;
150-
display_token_counts(counter, &entries)?;
151-
}
152228

153-
Ok(())
229+
Ok(entries)
154230
}
155231

156-
fn is_excluded(entry: &ignore::DirEntry, args: &Cli) -> bool {
157-
if let Some(excludes) = &args.exclude {
158-
let path_str = entry.path().to_string_lossy();
159-
160-
for exclude in excludes {
161-
match exclude {
162-
Exclude::Pattern(pattern) => {
163-
let pattern = if !pattern.starts_with("./") && !pattern.starts_with("/") {
164-
format!("./{}", pattern)
165-
} else {
166-
pattern.clone()
167-
};
168-
169-
if let Ok(glob) = globset::GlobBuilder::new(&pattern)
170-
.case_insensitive(false)
171-
.build()
172-
{
173-
let matcher = glob.compile_matcher();
174-
let check_path = if !path_str.starts_with("./") {
175-
format!("./{}", path_str)
176-
} else {
177-
path_str.to_string()
178-
};
179-
180-
if matcher.is_match(&check_path) {
181-
return true;
182-
}
183-
}
184-
}
185-
Exclude::File(path) => {
186-
let matches = entry.path().ends_with(path);
187-
if matches {
188-
return true;
189-
}
190-
}
191-
}
192-
}
193-
}
194-
false
195-
}
232+
// Removed the is_excluded function as it's now handled by WalkBuilder overrides
196233

197234
pub fn create_token_counter(args: &Cli) -> Result<TokenCounter> {
198235
match args.tokenizer.as_ref().unwrap_or(&TokenizerType::Tiktoken) {
@@ -296,35 +333,61 @@ mod tests {
296333
}
297334

298335
#[test]
299-
fn test_exclude_patterns() {
300-
let (dir, _files) = setup_test_directory().unwrap();
301-
let entry = ignore::WalkBuilder::new(dir.path())
302-
.build()
303-
.find(|e| e.as_ref().unwrap().path().ends_with("main.rs"))
304-
.unwrap()
305-
.unwrap();
306-
307-
// Test various exclude patterns
336+
fn test_exclude_patterns() -> Result<()> {
337+
let (dir, _files) = setup_test_directory()?;
338+
339+
let main_rs_path = dir.path().join("src/main.rs");
340+
308341
let test_cases = vec![
309342
// Pattern exclusions
310343
(Exclude::Pattern("**/*.rs".to_string()), true),
311344
(Exclude::Pattern("**/*.js".to_string()), false),
312345
(Exclude::Pattern("test/**".to_string()), false),
313346
// File exclusions
314-
(Exclude::File(PathBuf::from("main.rs")), true),
347+
(Exclude::File(main_rs_path.clone()), true),
315348
(Exclude::File(PathBuf::from("nonexistent.rs")), false),
316349
];
317350

318351
for (exclude, should_exclude) in test_cases {
319-
let mut cli = create_test_cli(dir.path());
320-
cli.exclude = Some(vec![exclude]);
352+
let mut override_builder = OverrideBuilder::new(dir.path());
353+
354+
match &exclude {
355+
Exclude::Pattern(pattern) => {
356+
// For patterns that should exclude, we need to add a "!" prefix
357+
// to make them negative patterns (exclusions)
358+
let exclude_pattern = if !pattern.starts_with('!') {
359+
format!("!{}", pattern)
360+
} else {
361+
pattern.clone()
362+
};
363+
override_builder.add(&exclude_pattern).unwrap();
364+
}
365+
Exclude::File(file_path) => {
366+
if file_path.exists() {
367+
// Get the file path relative to the test directory
368+
let rel_path = if file_path.is_absolute() {
369+
file_path.strip_prefix(dir.path()).unwrap_or(file_path)
370+
} else {
371+
file_path
372+
};
373+
// Add as a negative pattern
374+
let pattern = format!("!{}", rel_path.display());
375+
override_builder.add(&pattern).unwrap();
376+
}
377+
}
378+
}
379+
380+
let overrides = override_builder.build()?;
381+
let is_ignored = overrides.matched(&main_rs_path, false).is_ignore();
382+
321383
assert_eq!(
322-
is_excluded(&entry, &cli),
323-
should_exclude,
324-
"Failed for exclude pattern: {:?}",
325-
cli.exclude
384+
is_ignored, should_exclude,
385+
"Failed for exclude: {:?}",
386+
exclude
326387
);
327388
}
389+
390+
Ok(())
328391
}
329392

330393
#[test]

0 commit comments

Comments
 (0)