wip: "full" finder

This commit is contained in:
2026-06-17 10:58:01 -04:00
parent ea0dbab5f7
commit 05ef638d51
4 changed files with 144 additions and 33 deletions

View File

@@ -1,6 +1,8 @@
package findr
import "core:os"
import "core:strings"
import "core:sys/linux"
import "core:testing"
// ============================================================================
@@ -364,3 +366,64 @@ test_no_hidden_skips_dotfiles :: proc(t: ^testing.T) {
{"repo/secrets.env"},
)
}
// ============================================================================
// Special file type tests (SOCK, FIFO, CHR, BLK parity with fd)
// ============================================================================
@(test)
test_fifo_emitted :: proc(t: ^testing.T) {
env := create_test_env()
defer destroy_test_env(&env)
create_git_repo(env, "repo")
create_file(env, "repo/.gitignore", "*.env\n")
fifo_path := join_path(env.temp_dir, "repo/test.fifo")
defer delete(fifo_path)
cpath := strings.clone_to_cstring(fifo_path)
defer delete(cpath)
linux.mknod(cpath, linux.S_IFIFO | linux.Mode{.IRUSR, .IWUSR}, 0)
assert_output(t, env, nil,
{include_hidden = true, ignore_mode = .All},
{"repo/", "repo/.gitignore", "repo/test.fifo"},
)
}
// ============================================================================
// .ignore file support tests (fd respects .ignore in addition to .gitignore)
// ============================================================================
@(test)
test_ignore_file_respected :: proc(t: ^testing.T) {
env := create_test_env()
defer destroy_test_env(&env)
create_git_repo(env, "repo")
create_file(env, "repo/.ignore", "*.tmp\n")
create_file(env, "repo/file.tmp")
create_file(env, "repo/file.txt")
assert_output(t, env, nil,
{include_hidden = true, ignore_mode = .Respected},
{"repo/", "repo/.ignore", "repo/file.txt"},
)
}
@(test)
test_ignore_overrides_gitignore :: proc(t: ^testing.T) {
env := create_test_env()
defer destroy_test_env(&env)
create_git_repo(env, "repo")
create_file(env, "repo/.gitignore", "*.log\n")
create_file(env, "repo/.ignore", "important.log\n")
create_file(env, "repo/debug.log")
create_file(env, "repo/important.log")
assert_output(t, env, nil,
{include_hidden = true, ignore_mode = .Respected},
{"repo/", "repo/.gitignore", "repo/.ignore"},
)
}

View File

@@ -4,15 +4,17 @@ import "core:fmt"
import "core:strings"
import "core:text/regex"
// FIXME: Use a const bit_set[0..<128; u128] here when we start doing optimizations
is_regex_meta :: proc(c: u8) -> bool {
switch c {
case '.', '+', '(', ')', '{', '}', '^', '$', '|':
case '.', '+', '(', ')', '{', '}', '^', '$', '|', '#':
return true
}
return false
}
glob_to_regex :: proc(pattern: string, anchored: bool) -> string {
// TODO: Attempt to pre-allocate the string builder when we start doing optimizations
sb: strings.Builder
strings.builder_init(&sb)
defer strings.builder_destroy(&sb)
@@ -151,17 +153,17 @@ parse :: proc(content: string) -> Gitignore {
delete(regex_str)
if err != nil do continue
append(&gi.rules, Rule{
regex = re,
negated = negated,
dir_only = dir_only,
})
append(&gi.rules, Rule{regex = re, negated = negated, dir_only = dir_only})
}
return gi
}
Match :: enum { None, Ignored, Unignored }
Match :: enum {
None,
Ignored,
Unignored,
}
check_match :: proc(gi: ^Gitignore, path: string, is_dir: bool) -> Match {
result := Match.None
@@ -186,3 +188,4 @@ destroy :: proc(gi: ^Gitignore) {
}
delete(gi.rules)
}

View File

@@ -79,6 +79,20 @@ test_glob_backslash_escape :: proc(t: ^testing.T) {
testing.expect_value(t, result, "(^|/)!foo(/.*)?$")
}
@(test)
test_glob_hash_escaped :: proc(t: ^testing.T) {
result := glob_to_regex("#foo", false)
defer delete(result)
testing.expect_value(t, result, "(^|/)\\#foo(/.*)?$")
}
@(test)
test_glob_hash_in_pattern :: proc(t: ^testing.T) {
result := glob_to_regex("#*#", false)
defer delete(result)
testing.expect_value(t, result, "(^|/)\\#[^/]*\\#(/.*)?$")
}
@(test)
test_glob_empty :: proc(t: ^testing.T) {
result := glob_to_regex("", false)
@@ -176,3 +190,15 @@ test_is_ignored_globstar :: proc(t: ^testing.T) {
testing.expect_value(t, is_ignored(&gi, "foo/bar/cache", false), true)
}
@(test)
test_is_ignored_hash_pattern :: proc(t: ^testing.T) {
gi := parse("\\#*\\#\n")
defer destroy(&gi)
testing.expect_value(t, is_ignored(&gi, "#foo#", false), true)
testing.expect_value(t, is_ignored(&gi, "#test#", false), true)
testing.expect_value(t, is_ignored(&gi, "AUTHORS", false), false)
testing.expect_value(t, is_ignored(&gi, "build.zig", false), false)
testing.expect_value(t, is_ignored(&gi, "ChangeLog", false), false)
}

View File

@@ -181,8 +181,7 @@ process_dir :: proc(pool: ^WalkerPool, item: WorkItem) {
rel = ""
}
if has_git || gi_ctx != nil {
gi := load_gitignore(dir_path)
gi := load_ignore_patterns(dir_path, has_git || gi_ctx != nil)
if gi != nil {
new_ctx := new(GIContext)
new_ctx.gi = gi
@@ -197,7 +196,6 @@ process_dir :: proc(pool: ^WalkerPool, item: WorkItem) {
gi_ctx = new_ctx
}
}
rel_buf: [4096]u8
@@ -205,7 +203,7 @@ process_dir :: proc(pool: ^WalkerPool, item: WorkItem) {
if entry.name == ".git" do continue
is_dir := entry.type == .DIR
is_regular := entry.type == .REG || entry.type == .UNKNOWN || entry.type == .LNK
is_nondir := entry.type != .DIR
if pool.exclude_gi != nil && is_ignored(pool.exclude_gi, entry.name, is_dir) {
continue
@@ -241,7 +239,7 @@ process_dir :: proc(pool: ^WalkerPool, item: WorkItem) {
child_path := join_path(dir_path, entry.name)
push_work(pool, WorkItem{path = child_path, rel = child_rel, gi_ctx = gi_ctx})
}
} else if is_regular {
} else if is_nondir {
if should_emit && matches_pattern(pool, entry.name) {
full_path := join_path(dir_path, entry.name)
sync.mutex_lock(&pool.results_mutex)
@@ -345,16 +343,37 @@ free_entries :: proc(entries: ^[dynamic]RawEntry) {
delete(entries^)
}
load_gitignore :: proc(dir_path: string) -> ^Gitignore {
load_ignore_patterns :: proc(dir_path: string, in_repo: bool) -> ^Gitignore {
has_patterns := false
sb: strings.Builder
strings.builder_init(&sb)
defer strings.builder_destroy(&sb)
if in_repo {
gi_path := join_path(dir_path, ".gitignore")
defer delete(gi_path)
data, err := os.read_entire_file_from_path(gi_path, context.allocator)
if err != .NONE do return nil
gi := new(Gitignore)
gi^ = parse(string(data))
delete(gi_path)
if err == .NONE {
fmt.sbprintf(&sb, "%s", string(data))
delete(data)
has_patterns = true
}
}
ig_path := join_path(dir_path, ".ignore")
idata, ierr := os.read_entire_file_from_path(ig_path, context.allocator)
delete(ig_path)
if ierr == .NONE {
fmt.sbprintf(&sb, "%s", string(idata))
delete(idata)
has_patterns = true
}
if !has_patterns do return nil
content := strings.to_string(sb)
gi := new(Gitignore)
gi^ = parse(content)
return gi
}