From e6a9a449a3d42995baff399a5d7a4797985d5c43 Mon Sep 17 00:00:00 2001 From: ChethanUK Date: Mon, 6 Jul 2026 13:05:43 +0200 Subject: [PATCH] feat(discover): add CBM_EXTRA_SKIP_DIRS env var for site-specific skip dirs Callers can skip extra directory names (comma-separated, whitespace-trimmed) during indexing without a rebuild, checked alongside the hardcoded ALWAYS_SKIP_DIRS in cbm_should_skip_dir. Applies in every mode. The env var is read through cbm_safe_getenv into a local buffer so the value is mt-safe (raw getenv's pointer can be invalidated by a concurrent setenv/putenv) and correct for UTF-8 on Windows. The repro test uses the portable cbm_setenv/cbm_unsetenv wrappers rather than raw POSIX setenv/unsetenv. Covered by a table-driven test that verifies baseline-discovered -> skipped-with-env-set (including a whitespace-padded CSV entry) -> discovered-again-after-unset. Signed-off-by: ChethanUK --- docs/cbmignore.md | 7 ++- src/discover/discover.c | 48 +++++++++++++++++++++ tests/repro/repro_invariant_discovery_fqn.c | 46 ++++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/docs/cbmignore.md b/docs/cbmignore.md index 3d19e61e8..2642a103e 100644 --- a/docs/cbmignore.md +++ b/docs/cbmignore.md @@ -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`** — `/.gitignore` merged with `/info/exclude` (worktree-aware); later patterns win on conflict. Honored even when the indexed directory is not a git repo root. diff --git a/src/discover/discover.c b/src/discover/discover.c index 479dd695b..370e9568b 100644 --- a/src/discover/discover.c +++ b/src/discover/discover.c @@ -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) { @@ -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)) { diff --git a/tests/repro/repro_invariant_discovery_fqn.c b/tests/repro/repro_invariant_discovery_fqn.c index f517de329..7244559ab 100644 --- a/tests/repro/repro_invariant_discovery_fqn.c +++ b/tests/repro/repro_invariant_discovery_fqn.c @@ -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. @@ -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 */