Files
gg/gg.c
T

679 lines
18 KiB
C
Raw Normal View History

2026-08-21 11:50:24 +03:00
/*
* gg - Grokkin' Grep: a minimal, fast, portable grep-like file finder
*
* Usage: gg SEARCH_TERM [FILE_OR_DIRECTORY]
*
* Created by Luxferre in 2026, released into the public domain
2026-08-21 13:56:57 +03:00
*
2026-08-21 11:50:24 +03:00
*/
2026-08-21 19:26:31 +03:00
#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE)
#define _DARWIN_C_SOURCE
#endif
2026-08-21 11:50:24 +03:00
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#ifndef _DEFAULT_SOURCE
#define _DEFAULT_SOURCE
#endif
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
2026-08-21 13:56:57 +03:00
#include <fnmatch.h>
2026-08-21 11:50:24 +03:00
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
2026-08-21 19:08:45 +03:00
#include <sys/types.h>
/* sysctlbyname() is the portable way to query CPU count on every BSD family
* (FreeBSD, OpenBSD, NetBSD, DragonFly) and on macOS/Darwin. */
2026-08-21 19:26:31 +03:00
#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__) || defined(__APPLE__)
2026-08-21 19:08:45 +03:00
#include <sys/sysctl.h>
#endif
/* On some libc implementations (e.g. FreeBSD) the d_type constants are only
* exposed when BSD visibility is enabled. Provide portable fallbacks matching
* <sys/dirent.h> so the directory-walk classification always compiles. */
#ifndef DT_UNKNOWN
#define DT_UNKNOWN 0
#define DT_FIFO 1
#define DT_CHR 2
#define DT_DIR 4
#define DT_BLK 6
#define DT_REG 8
#define DT_LNK 10
#define DT_SOCK 12
#define DT_WHT 14
#endif
/* memmem() is a BSD/GNU extension, not POSIX. glibc and the BSD/Darwin libcs
* all provide it, but only expose the prototype outside strict conformance
* mode, so declare it ourselves when the system has not already. Any libc that
* genuinely lacks it (rare) gets a small portable fallback. */
2026-08-21 19:26:31 +03:00
#if defined(__GLIBC__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__) || defined(__APPLE__)
2026-08-21 19:08:45 +03:00
#ifndef memmem
void *memmem(const void *haystack, size_t haystacklen,
const void *needle, size_t needlelen);
#endif
#else
static void *gg_memmem(const void *haystack, size_t haystacklen,
const void *needle, size_t needlelen) {
2026-08-21 19:26:31 +03:00
if(needlelen == 0) return (void *)haystack;
if(haystacklen < needlelen) return NULL;
2026-08-21 19:08:45 +03:00
const unsigned char *h = (const unsigned char *)haystack;
const unsigned char *n = (const unsigned char *)needle;
const unsigned char *end = h + haystacklen - needlelen;
2026-08-21 19:26:31 +03:00
for(; h <= end; h++)
if(memcmp(h, n, needlelen) == 0) return (void *)h;
2026-08-21 19:08:45 +03:00
return NULL;
}
#define memmem gg_memmem
#endif
/* Number of online processors, portable across Linux, the BSDs and macOS. */
static long gg_nproc(void) {
#if defined(_SC_NPROCESSORS_ONLN)
long n = sysconf(_SC_NPROCESSORS_ONLN);
2026-08-21 19:26:31 +03:00
if(n > 0) return n;
2026-08-21 19:08:45 +03:00
#elif defined(_SC_NPROCESSORS_CONF)
long n = sysconf(_SC_NPROCESSORS_CONF);
2026-08-21 19:26:31 +03:00
if(n > 0) return n;
2026-08-21 19:08:45 +03:00
#endif
#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__) || defined(__APPLE__)
int ncpu = 1;
size_t len = sizeof(ncpu);
2026-08-21 19:26:31 +03:00
if(sysctlbyname("hw.ncpu", &ncpu, &len, NULL, 0) == 0 && ncpu > 0)
2026-08-21 19:08:45 +03:00
return (long)ncpu;
#endif
return 1;
}
2026-08-21 11:50:24 +03:00
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif
#define IO_BUFSZ (32u * 1024) /* per-thread read buffer */
/* ---------- shared scan state ---------- */
static const char *needle;
static size_t needle_len;
static int found_any = 0;
static pthread_mutex_t found_mx = PTHREAD_MUTEX_INITIALIZER;
2026-08-21 13:56:57 +03:00
static int git_all = 0; /* if set, ignore .gitignore / .git entirely */
/* ---------- .gitignore handling ---------- */
typedef struct Pattern {
int negate; /* pattern began with '!' */
int dir_only; /* pattern ended with '/' (matches dirs only) */
int has_slash; /* pattern contains a '/' (anchored to its .gitignore dir) */
char *pat; /* pattern text (without '!' / trailing '/') */
size_t pat_len;
} Pattern;
typedef struct GitIgnore {
Pattern *pats;
size_t npats;
const char *rel; /* directory this ignore governs, relative to root */
struct GitIgnore *parent;
struct GitIgnore *gnext; /* global list, for cleanup */
} GitIgnore;
static GitIgnore *gi_all = NULL;
2026-08-21 11:50:24 +03:00
/* ---------- directory work queue (parallel walk + scan) ---------- */
typedef struct DirJob {
2026-08-21 13:56:57 +03:00
char *path; /* full path for opendir/open */
char *rel; /* path relative to the search root ("" for root) */
GitIgnore *gi; /* active ignore set for this directory's children */
2026-08-21 11:50:24 +03:00
struct DirJob *next;
} DirJob;
static DirJob *dir_stack = NULL;
static pthread_mutex_t dir_mx = PTHREAD_MUTEX_INITIALIZER;
2026-08-21 13:56:57 +03:00
static void push_dir(const char *path, const char *rel, GitIgnore *gi) {
2026-08-21 11:50:24 +03:00
DirJob *j = malloc(sizeof *j);
2026-08-21 11:56:48 +03:00
if(j == NULL)
2026-08-21 11:50:24 +03:00
return;
j->path = strdup(path);
2026-08-21 11:56:48 +03:00
if(j->path == NULL) {
2026-08-21 11:50:24 +03:00
free(j);
return;
}
2026-08-21 13:56:57 +03:00
j->rel = strdup(rel);
if(j->rel == NULL) {
free(j->path);
free(j);
return;
}
j->gi = gi;
2026-08-21 11:50:24 +03:00
pthread_mutex_lock(&dir_mx);
j->next = dir_stack;
dir_stack = j;
pthread_mutex_unlock(&dir_mx);
}
static void die(const char *msg) {
perror(msg);
exit(2);
}
2026-08-21 13:56:57 +03:00
static void pop_dir(char **path, char **rel, GitIgnore **gi) {
2026-08-21 11:50:24 +03:00
pthread_mutex_lock(&dir_mx);
DirJob *j = dir_stack;
2026-08-21 11:56:48 +03:00
if(j != NULL)
2026-08-21 11:50:24 +03:00
dir_stack = j->next;
pthread_mutex_unlock(&dir_mx);
2026-08-21 13:56:57 +03:00
if(j == NULL) {
*path = NULL;
*rel = NULL;
*gi = NULL;
return;
}
*path = j->path;
*rel = j->rel;
*gi = j->gi;
2026-08-21 11:50:24 +03:00
free(j);
2026-08-21 13:56:57 +03:00
}
/* Read an entire fd into a malloc'd, NUL-terminated buffer. Returns the buffer
* (caller frees) and stores the length in *len, or NULL on error. */
static char *read_all(int fd, size_t *len) {
size_t cap = 4096, n = 0;
char *buf = malloc(cap);
if(buf == NULL)
return NULL;
for(;;) {
if(n == cap) {
cap *= 2;
char *nb = realloc(buf, cap);
if(nb == NULL) {
free(buf);
return NULL;
}
buf = nb;
}
ssize_t r = read(fd, buf + n, cap - n);
if(r < 0) {
if(errno == EINTR)
continue;
free(buf);
return NULL;
}
if(r == 0)
break;
n += (size_t)r;
}
buf = realloc(buf, n + 1);
if(buf == NULL)
return NULL;
buf[n] = '\0';
*len = n;
return buf;
}
/* Parse a .gitignore buffer into patterns, chained under `parent`. Returns the
* new GitIgnore node (sharing `parent` if the file is empty/absent). */
static GitIgnore *load_gitignore(const char *dir, GitIgnore *parent,
const char *rel) {
char path[PATH_MAX];
int n = snprintf(path, sizeof path, "%s/.gitignore", dir);
if(n < 0 || (size_t)n >= sizeof path)
return parent;
int fd = open(path, O_RDONLY | O_CLOEXEC);
if(fd < 0)
return parent;
size_t len = 0;
char *buf = read_all(fd, &len);
close(fd);
if(buf == NULL)
return parent;
Pattern *pa = NULL;
size_t np = 0, cap = 0;
char *p = buf, *end = buf + len;
while(p < end) {
char *nl = memchr(p, '\n', (size_t)(end - p));
size_t llen = (nl != NULL) ? (size_t)(nl - p) : (size_t)(end - p);
if(llen > 0 && p[llen - 1] == '\r')
llen--;
size_t s = 0;
while(s < llen && (p[s] == ' ' || p[s] == '\t'))
s++;
size_t e = llen;
while(e > s && (p[e - 1] == ' ' || p[e - 1] == '\t'))
e--;
if(s >= e || p[s] == '#') {
p = (nl != NULL) ? nl + 1 : end;
continue;
}
char *t = malloc(e - s + 1);
if(t == NULL)
break;
memcpy(t, p + s, e - s);
t[e - s] = '\0';
int negate = 0, dir_only = 0;
if(t[0] == '!') {
negate = 1;
memmove(t, t + 1, strlen(t));
}
size_t tl = strlen(t);
if(tl > 0 && t[tl - 1] == '/') {
dir_only = 1;
t[tl - 1] = '\0';
tl--;
}
int has_slash = (strchr(t, '/') != NULL);
if(np == cap) {
cap = cap ? cap * 2 : 16;
Pattern *na = realloc(pa, cap * sizeof *pa);
if(na == NULL) {
free(t);
break;
}
pa = na;
}
pa[np].negate = negate;
pa[np].dir_only = dir_only;
pa[np].has_slash = has_slash;
pa[np].pat = t;
pa[np].pat_len = tl;
np++;
p = (nl != NULL) ? nl + 1 : end;
}
free(buf);
if(np == 0) {
free(pa);
return parent;
}
GitIgnore *g = malloc(sizeof *g);
if(g == NULL) {
for(size_t i = 0; i < np; i++)
free(pa[i].pat);
free(pa);
return parent;
}
g->pats = pa;
g->npats = np;
g->rel = strdup(rel);
g->parent = parent;
g->gnext = gi_all;
gi_all = g;
return g;
}
/* Match a single path segment `s` (len sl) against a glob `p` (len pl) where
* '*' matches a run of non-'/' chars and '?' matches one char. */
static int seg_eq(const char *p, size_t pl, const char *s, size_t sl) {
size_t i = 0, j = 0;
size_t star = SIZE_MAX, si = 0, sj = 0;
while(i < pl || j < sl) {
if(i < pl && p[i] == '*') {
star = i;
si = i + 1;
sj = j;
i++;
continue;
}
if(i < pl && (p[i] == '?' || (j < sl && p[i] == s[j]))) {
i++;
j++;
continue;
}
if(star != SIZE_MAX) {
/* Extend what the '*' matches by one more char of s, if possible. */
if(sj + 1 > sl)
break;
i = si;
sj++;
j = sj;
continue;
}
return 0;
}
return i == pl && j == sl;
}
/* Match a slash-separated pattern `p` against path `s`, supporting `**`. */
static int match_path(const char *p, const char *s) {
for(;;) {
if(p[0] == '*' && p[1] == '*') {
p += 2;
while(*p == '/')
p++;
if(*p == '\0')
return 1;
const char *sp = s;
for(;;) {
if(match_path(p, sp))
return 1;
while(*sp && *sp != '/')
sp++;
if(*sp == '\0')
break;
sp++;
}
return 0;
}
if(*p == '\0')
return *s == '\0';
if(*p == '/') {
if(*s != '/')
return 0;
p++;
s++;
continue;
}
const char *pe = p;
while(*pe && *pe != '/')
pe++;
const char *se = s;
while(*se && *se != '/')
se++;
if(!seg_eq(p, (size_t)(pe - p), s, (size_t)(se - s)))
return 0;
p = pe;
s = se;
}
}
/* Match a gitignore pattern against a path (path relative to the owning
* .gitignore directory). A leading '/' only marks the pattern as anchored to
* that directory (it is already implied by `path`), so it is stripped before
* matching. Falls back to fnmatch for patterns without `**`. */
static int git_match(const char *pat, const char *path) {
while(*pat == '/')
pat++;
if(*pat == '\0')
return *path == '\0';
if(strstr(pat, "**") == NULL)
return fnmatch(pat, path, FNM_PATHNAME) == 0;
return match_path(pat, path);
}
/* Decide whether an entry named `name` (full relative path `entry_rel`, a
* directory iff `is_dir`) is ignored by the active ignore set `gi`. The
* deepest matching rule wins (git semantics). */
static int is_ignored(const char *entry_rel, const char *name, GitIgnore *gi,
int is_dir) {
GitIgnore *chain[1024];
int n = 0;
for(GitIgnore *g = gi; g != NULL && n < 1024; g = g->parent)
chain[n++] = g;
int ignored = 0;
for(int i = n - 1; i >= 0; i--) { /* root -> child (deepest wins) */
GitIgnore *g = chain[i];
for(size_t p = 0; p < g->npats; p++) {
Pattern *pat = &g->pats[p];
int m;
if(pat->has_slash) {
const char *rp;
if(g->rel[0] == '\0') {
rp = entry_rel;
} else {
size_t rl = strlen(g->rel);
if(strncmp(entry_rel, g->rel, rl) != 0)
continue;
if(entry_rel[rl] != '/')
continue;
rp = entry_rel + rl + 1;
}
m = git_match(pat->pat, rp);
} else {
if(pat->dir_only && !is_dir)
continue;
m = git_match(pat->pat, name);
}
if(m)
ignored = pat->negate ? 0 : 1;
}
}
return ignored;
2026-08-21 11:50:24 +03:00
}
/* ---------- scan one open fd (chunked read + memmem, binary-skipping) ---------- */
typedef struct {
char *buf;
char *carry;
} ScanBufs;
static int scan_fd(int fd, ScanBufs *sb) {
char *buf = sb->buf;
char *carry = sb->carry;
size_t carryn = 0;
int hit = 0;
2026-08-21 11:56:48 +03:00
for(;;) {
2026-08-21 11:50:24 +03:00
ssize_t r = read(fd, buf + carryn, IO_BUFSZ);
2026-08-21 11:56:48 +03:00
if(r < 0) {
if(errno == EINTR)
2026-08-21 11:50:24 +03:00
continue;
break;
}
2026-08-21 11:56:48 +03:00
if(r == 0)
2026-08-21 11:50:24 +03:00
break; /* EOF */
size_t wn = carryn + (size_t)r;
/* Binary detection: a NUL byte marks a binary file, which we skip (like
* ripgrep). Stop reading the moment we see one so we do not waste
* bandwidth scanning the rest of a large binary file. If a needle occurs
* before the first NUL it is still a valid text match. */
void *nul = memchr(buf, '\0', wn);
size_t text_end = (nul != NULL) ? (size_t)((const char *)nul - buf) : wn;
2026-08-21 11:56:48 +03:00
if(text_end >= needle_len) {
if(memmem(buf, text_end, needle, needle_len) != NULL) {
2026-08-21 11:50:24 +03:00
hit = 1;
break;
}
}
2026-08-21 11:56:48 +03:00
if(nul != NULL)
2026-08-21 11:50:24 +03:00
break; /* binary file: no further scanning */
/* No NUL and no match yet: carry the tail across chunks. */
2026-08-21 11:56:48 +03:00
if(wn >= needle_len)
2026-08-21 11:50:24 +03:00
carryn = needle_len - 1;
else
carryn = wn;
memcpy(carry, buf + wn - carryn, carryn);
memcpy(buf, carry, carryn);
}
return hit;
}
static void report_match(const char *path) {
pthread_mutex_lock(&found_mx);
found_any = 1;
puts(path);
pthread_mutex_unlock(&found_mx);
}
static void scan_file(const char *path, ScanBufs *sb) {
int fd = open(path, O_RDONLY | O_CLOEXEC);
2026-08-21 11:56:48 +03:00
if(fd < 0)
2026-08-21 11:50:24 +03:00
return; /* unreadable entries are silently skipped */
2026-08-21 11:56:48 +03:00
if(scan_fd(fd, sb))
2026-08-21 11:50:24 +03:00
report_match(path);
close(fd);
}
/* ---------- worker: pop a directory, scan its files, enqueue subdirs ---------- */
static void *worker(void *arg) {
(void)arg;
ScanBufs sb;
sb.buf = malloc(IO_BUFSZ + needle_len);
sb.carry = malloc(needle_len);
2026-08-21 11:56:48 +03:00
if(sb.buf == NULL || sb.carry == NULL) {
2026-08-21 11:50:24 +03:00
free(sb.buf);
free(sb.carry);
return NULL;
}
char child[PATH_MAX];
2026-08-21 13:56:57 +03:00
char relbuf[PATH_MAX];
2026-08-21 11:56:48 +03:00
for(;;) {
2026-08-21 13:56:57 +03:00
char *dir, *rel;
GitIgnore *parent_gi;
pop_dir(&dir, &rel, &parent_gi);
2026-08-21 11:56:48 +03:00
if(dir == NULL)
2026-08-21 11:50:24 +03:00
break;
2026-08-21 13:56:57 +03:00
GitIgnore *my_gi = parent_gi;
if(!git_all)
my_gi = load_gitignore(dir, parent_gi, rel);
2026-08-21 11:50:24 +03:00
DIR *d = opendir(dir);
2026-08-21 11:56:48 +03:00
if(d != NULL) {
2026-08-21 11:50:24 +03:00
struct dirent *ent;
2026-08-21 11:56:48 +03:00
while((ent = readdir(d)) != NULL) {
if(strcmp(ent->d_name, ".") == 0 ||
2026-08-21 11:50:24 +03:00
strcmp(ent->d_name, "..") == 0)
continue;
2026-08-21 13:56:57 +03:00
const char *name = ent->d_name;
int n = snprintf(child, sizeof child, "%s/%s", dir, name);
2026-08-21 11:56:48 +03:00
if(n < 0 || (size_t)n >= sizeof child)
2026-08-21 11:50:24 +03:00
continue;
2026-08-21 13:56:57 +03:00
n = snprintf(relbuf, sizeof relbuf, "%s%s%s",
rel, (rel[0] == '\0') ? "" : "/", name);
if(n < 0 || (size_t)n >= sizeof relbuf)
continue;
int is_dir = (ent->d_type == DT_DIR);
if(ent->d_type == DT_UNKNOWN) {
struct stat st;
if(lstat(child, &st) == 0)
is_dir = S_ISDIR(st.st_mode);
}
if(!git_all) {
/* Always skip .git directories, just like git. */
if(strcmp(name, ".git") == 0 && is_dir)
continue;
if(is_ignored(relbuf, name, my_gi, is_dir))
continue;
}
2026-08-21 11:56:48 +03:00
if(ent->d_type == DT_REG) {
2026-08-21 11:50:24 +03:00
scan_file(child, &sb);
2026-08-21 11:56:48 +03:00
} else if(ent->d_type == DT_DIR) {
2026-08-21 13:56:57 +03:00
push_dir(child, relbuf, my_gi);
2026-08-21 11:56:48 +03:00
} else if(ent->d_type == DT_LNK) {
2026-08-21 11:50:24 +03:00
continue; /* do not follow symlinks: avoids cycles */
} else {
struct stat st;
2026-08-21 11:56:48 +03:00
if(lstat(child, &st) != 0)
2026-08-21 11:50:24 +03:00
continue;
2026-08-21 11:56:48 +03:00
if(S_ISDIR(st.st_mode))
2026-08-21 13:56:57 +03:00
push_dir(child, relbuf, my_gi);
2026-08-21 11:56:48 +03:00
else if(S_ISREG(st.st_mode))
2026-08-21 11:50:24 +03:00
scan_file(child, &sb);
}
}
closedir(d);
}
free(dir);
2026-08-21 13:56:57 +03:00
free(rel);
2026-08-21 11:50:24 +03:00
}
free(sb.buf);
free(sb.carry);
return NULL;
}
2026-08-21 13:56:57 +03:00
static void free_gitignores(void) {
GitIgnore *g = gi_all;
while(g != NULL) {
GitIgnore *next = g->gnext;
for(size_t i = 0; i < g->npats; i++)
free(g->pats[i].pat);
free(g->pats);
free((void *)g->rel);
free(g);
g = next;
}
gi_all = NULL;
}
2026-08-21 11:50:24 +03:00
int main(int argc, char **argv) {
2026-08-21 11:56:48 +03:00
if(argc < 2 || argc > 3) {
2026-08-21 11:50:24 +03:00
fprintf(stderr, "usage: %s SEARCH_TERM [FILE_OR_DIRECTORY]\n", argv[0]);
return 2;
}
needle = argv[1];
needle_len = strlen(needle);
2026-08-21 11:56:48 +03:00
if(needle_len == 0) {
2026-08-21 11:50:24 +03:00
fprintf(stderr, "gg: empty search term\n");
return 2;
}
2026-08-21 13:56:57 +03:00
git_all = (getenv("GG_GIT_ALL") != NULL);
2026-08-21 11:50:24 +03:00
char cwd[PATH_MAX];
const char *root;
2026-08-21 11:56:48 +03:00
if(argc == 3) {
2026-08-21 11:50:24 +03:00
struct stat st;
2026-08-21 11:56:48 +03:00
if(stat(argv[2], &st) != 0) {
2026-08-21 11:50:24 +03:00
fprintf(stderr, "gg: %s: %s\n", argv[2], strerror(errno));
return 2;
}
2026-08-21 11:56:48 +03:00
if(!S_ISDIR(st.st_mode) && !S_ISREG(st.st_mode)) {
2026-08-21 11:50:24 +03:00
fprintf(stderr, "gg: %s: not a regular file or directory\n", argv[2]);
return 2;
}
root = argv[2];
} else {
2026-08-21 11:56:48 +03:00
if(getcwd(cwd, sizeof cwd) == NULL)
2026-08-21 11:50:24 +03:00
die("getcwd");
root = cwd;
}
struct stat st;
2026-08-21 11:56:48 +03:00
if(stat(root, &st) != 0) {
2026-08-21 11:50:24 +03:00
fprintf(stderr, "gg: %s: %s\n", root, strerror(errno));
return 2;
}
/* Seed the directory work queue, then let the workers walk and scan in
* parallel: each worker pops a directory, scans its regular files inline,
* and pushes any sub-directories it finds. This overlaps the (metadata)
* walk with the (data) scan across all online CPUs. */
2026-08-21 11:56:48 +03:00
if(S_ISREG(st.st_mode)) {
2026-08-21 11:50:24 +03:00
scan_file(root, &(ScanBufs){ malloc(IO_BUFSZ + needle_len),
malloc(needle_len) });
} else {
2026-08-21 13:56:57 +03:00
push_dir(root, "", NULL);
2026-08-21 11:50:24 +03:00
}
2026-08-21 19:08:45 +03:00
long nproc = gg_nproc();
2026-08-21 11:56:48 +03:00
if(nproc < 1)
2026-08-21 11:50:24 +03:00
nproc = 1;
int nth = (int)nproc;
2026-08-21 11:56:48 +03:00
if(nth > 256)
2026-08-21 11:50:24 +03:00
nth = 256;
pthread_t t[256];
int started = 0;
2026-08-21 11:56:48 +03:00
for(int i = 0; i < nth; i++) {
if(pthread_create(&t[i], NULL, worker, NULL) != 0)
2026-08-21 11:50:24 +03:00
break;
started++;
}
2026-08-21 11:56:48 +03:00
for(int i = 0; i < started; i++)
2026-08-21 11:50:24 +03:00
pthread_join(t[i], NULL);
2026-08-21 13:56:57 +03:00
free_gitignores();
2026-08-21 11:50:24 +03:00
return found_any ? 0 : 1;
}