Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/cbmignore.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,12 @@ a path wins. For directories:
1. **Built-in skip list** — `.git`, `node_modules`, `dist`, `target`,
`vendor`, tool caches, etc. (60+ names; the fast/moderate index modes add
more, e.g. `docs`, `examples`, `testdata`). Not overridable from any
ignore file today.
ignore file today. The hardcoded list is intentionally fixed across all
repos, but you can extend it per invocation with the `CBM_EXTRA_SKIP_DIRS`
environment variable — a comma-separated list of extra directory names to
skip in every mode (e.g. `CBM_EXTRA_SKIP_DIRS=".dbt,.terraform-state"`).
Useful for site- or repo-specific artifact directories that don't belong
in the universal list. Whitespace around each name is trimmed.
2. **Repo `.gitignore`** — `<repo>/.gitignore` merged with
`<git-common-dir>/info/exclude` (worktree-aware); later patterns win on
conflict. Honored even when the indexed directory is not a git repo root.
Expand Down
48 changes: 48 additions & 0 deletions src/discover/discover.c
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,44 @@ static bool resolve_global_excludes_path(char *out, size_t out_sz) {
return false;
}

/* ── Env-var-configurable extra skip dirs ──────────────────────────
* CBM_EXTRA_SKIP_DIRS: comma-separated directory names to skip, on top of
* the hardcoded ALWAYS_SKIP_DIRS above. Lets a caller add repo-specific or
* site-specific artifact dirs (e.g. ".dbt", a Terraform state dir, a prior
* AI-tooling output dir) per invocation, without a rebuild. Applies in
* every mode, same as ALWAYS_SKIP_DIRS. Not cached across calls (the list
* is short and this keeps the function safe to call from any thread). */
static bool str_in_env_csv(const char *s, const char *csv) {
if (!s || !csv || !csv[0]) {
return false;
}
size_t slen = strlen(s);
const char *p = csv;
while (*p) {
const char *comma = strchr(p, ',');
size_t seg_len = comma ? (size_t)(comma - p) : strlen(p);

const char *seg = p;
size_t len = seg_len;
while (len > 0 && isspace((unsigned char)seg[0])) {
seg++;
len--;
}
while (len > 0 && isspace((unsigned char)seg[len - 1])) {
len--;
}

if (len == slen && strncmp(seg, s, len) == 0) {
return true;
}
if (!comma) {
break;
}
p = comma + 1;
}
return false;
}

/* ── Public filter functions ─────────────────────── */

bool cbm_should_skip_dir(const char *dirname, cbm_index_mode_t mode) {
Expand All @@ -345,6 +383,16 @@ bool cbm_should_skip_dir(const char *dirname, cbm_index_mode_t mode) {
return true;
}

/* Read through cbm_safe_getenv so the value is copied into a caller-owned
* buffer before use — raw getenv() is mt-unsafe (pointer can be invalidated
* by a concurrent setenv/putenv) and mishandles UTF-8 on Windows. */
char extra_skip[CBM_SZ_1K];
if (str_in_env_csv(dirname,
cbm_safe_getenv("CBM_EXTRA_SKIP_DIRS", extra_skip,
sizeof(extra_skip), NULL))) {
return true;
}

/* Fast discovery applies to both MODERATE and FAST — only FULL keeps everything. */
if (mode != CBM_MODE_FULL) {
if (str_in_list(dirname, FAST_SKIP_DIRS)) {
Expand Down
46 changes: 46 additions & 0 deletions tests/repro/repro_invariant_discovery_fqn.c
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,51 @@ TEST(invariant_discovery_always_skip_dirs) {
PASS();
}

/* ── PART A TEST — CBM_EXTRA_SKIP_DIRS env var ─────────────────────────────
*
* Verifies the env-var-configurable extra skip list (discover.c
* cbm_should_skip_dir -> str_in_env_csv): a name NOT in the hardcoded
* ALWAYS_SKIP_DIRS is skipped once listed in CBM_EXTRA_SKIP_DIRS, and goes
* back to being indexed once the env var is cleared — proving the behavior
* is genuinely env-gated, not accidentally hardcoded.
*/
TEST(invariant_discovery_env_extra_skip_dirs) {
const char *custom_dir = "dbt_target_test_fixture";

/* Baseline: not in any hardcoded list, so it must be discovered today. */
cbm_unsetenv("CBM_EXTRA_SKIP_DIRS");
int baseline = check_dir_skipped(custom_dir, CBM_MODE_FULL);
if (baseline < 0) {
printf(" SETUP-ERROR %-32s baseline\n", custom_dir);
ASSERT_EQ(1, 0);
}
if (baseline == 0) {
printf(" UNEXPECTED %-32s already skipped with no env var set\n", custom_dir);
}
ASSERT_GT(baseline, 0);

/* With the env var listing it (alongside noise entries, whitespace-padded
* to also exercise the trimming path), it must now be skipped. */
cbm_setenv("CBM_EXTRA_SKIP_DIRS", ".codegraph, graphify-out ,dbt_target_test_fixture,.wiki", 1);
int with_env = check_dir_skipped(custom_dir, CBM_MODE_FULL);
cbm_unsetenv("CBM_EXTRA_SKIP_DIRS");

if (with_env < 0) {
printf(" SETUP-ERROR %-32s with env var\n", custom_dir);
ASSERT_EQ(1, 0);
}
if (with_env != 0) {
printf(" REGRESSION %-32s not skipped with CBM_EXTRA_SKIP_DIRS set\n", custom_dir);
}
ASSERT_EQ(with_env, 0);

/* And clearing it restores the baseline (not permanently sticky). */
int after_clear = check_dir_skipped(custom_dir, CBM_MODE_FULL);
ASSERT_GT(after_clear, 0);

PASS();
}

/* ── PART A TEST — FAST_SKIP_DIRS table (mode != CBM_MODE_FULL) ────────────
*
* FAST_SKIP_DIRS entries are only skipped when mode != CBM_MODE_FULL.
Expand Down Expand Up @@ -794,6 +839,7 @@ SUITE(repro_invariant_discovery_fqn) {
/* Part A — Discovery hygiene */
RUN_TEST(invariant_discovery_control_always_found);
RUN_TEST(invariant_discovery_always_skip_dirs);
RUN_TEST(invariant_discovery_env_extra_skip_dirs);
RUN_TEST(invariant_discovery_fast_skip_dirs);

/* Part B — FQN same-stem distinctness */
Expand Down
Loading