mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-27 06:53:11 +08:00
Merge pull request #811 from DeusData/distill/793-walk-exclusions
fix(pipeline): honor discovery exclusions in pkgmap/path-alias/envscan walks
This commit is contained in:
@@ -363,16 +363,22 @@ static int scan_env_file(const char *full_path, const char *rel, file_type_t ft,
|
||||
/* Process a single directory entry for env scanning. Returns bindings added. */
|
||||
static int process_env_entry(cbm_dirent_t *ent, const char *dir_path, const char *root_path,
|
||||
cbm_env_binding_t *out, int max_out, char path_stack[][CBM_SZ_512],
|
||||
int *stack_top) {
|
||||
int *stack_top, char **excluded_dirs, int excluded_count) {
|
||||
char full_path[CBM_SZ_512];
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, ent->name);
|
||||
const char *rel = full_path + strlen(root_path);
|
||||
while (*rel == '/') {
|
||||
rel++;
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
if (stat(full_path, &st) != 0) {
|
||||
return 0;
|
||||
}
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
if (!is_ignored_dir(ent->name) && *stack_top < CBM_SZ_256) {
|
||||
if (!is_ignored_dir(ent->name) &&
|
||||
!cbm_pipeline_relpath_is_excluded(rel, excluded_dirs, excluded_count) &&
|
||||
*stack_top < CBM_SZ_256) {
|
||||
strncpy(path_stack[*stack_top], full_path, sizeof(path_stack[0]) - 1);
|
||||
path_stack[*stack_top][sizeof(path_stack[0]) - SKIP_ONE] = '\0';
|
||||
(*stack_top)++;
|
||||
@@ -386,14 +392,11 @@ static int process_env_entry(cbm_dirent_t *ent, const char *dir_path, const char
|
||||
if (ft == FT_UNKNOWN) {
|
||||
return 0;
|
||||
}
|
||||
const char *rel = full_path + strlen(root_path);
|
||||
while (*rel == '/') {
|
||||
rel++;
|
||||
}
|
||||
return scan_env_file(full_path, rel, ft, out, max_out);
|
||||
}
|
||||
|
||||
int cbm_scan_project_env_urls(const char *root_path, cbm_env_binding_t *out, int max_out) {
|
||||
int cbm_scan_project_env_urls_excluded(const char *root_path, cbm_env_binding_t *out, int max_out,
|
||||
char **excluded_dirs, int excluded_count) {
|
||||
if (!root_path || !out || max_out <= 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -418,9 +421,13 @@ int cbm_scan_project_env_urls(const char *root_path, cbm_env_binding_t *out, int
|
||||
cbm_dirent_t *ent;
|
||||
while ((ent = cbm_readdir(d)) && count < max_out) {
|
||||
count += process_env_entry(ent, dir_path, root_path, out + count, max_out - count,
|
||||
path_stack, &stack_top);
|
||||
path_stack, &stack_top, excluded_dirs, excluded_count);
|
||||
}
|
||||
cbm_closedir(d);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int cbm_scan_project_env_urls(const char *root_path, cbm_env_binding_t *out, int max_out) {
|
||||
return cbm_scan_project_env_urls_excluded(root_path, out, max_out, NULL, 0);
|
||||
}
|
||||
|
||||
@@ -818,7 +818,7 @@ static void merge_pkg_entries(cbm_pipeline_ctx_t *ctx, cbm_pkg_entries_t *pkg_en
|
||||
* by the main discoverer (package.json, composer.json — in
|
||||
* IGNORED_JSON_FILES) still feed pkgmap. Append into worker 0's
|
||||
* array so the existing merge below sees them. */
|
||||
cbm_pkgmap_scan_repo(ctx->repo_path, &pkg_entries[0]);
|
||||
cbm_pkgmap_scan_repo(ctx->repo_path, &pkg_entries[0], ctx->excluded_dirs, ctx->excluded_count);
|
||||
cbm_pipeline_set_pkgmap(cbm_pkgmap_build(pkg_entries, worker_count, ctx->project_name));
|
||||
for (int i = 0; i < worker_count; i++) {
|
||||
cbm_pkg_entries_free(&pkg_entries[i]);
|
||||
|
||||
@@ -845,7 +845,7 @@ static bool pkgmap_is_reparse_point(const char *abs_path) {
|
||||
* it hang. On Windows we additionally skip reparse points before
|
||||
* descending as a best-effort early-out. */
|
||||
static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_entries_t *entries,
|
||||
int depth) {
|
||||
int depth, char **excluded_dirs, int excluded_count) {
|
||||
if (depth >= PKGMAP_WALK_MAX_DEPTH) {
|
||||
cbm_log_info("pkgmap.walk", "depth_cap", rel_dir && rel_dir[0] ? rel_dir : ".");
|
||||
return 0;
|
||||
@@ -874,7 +874,8 @@ static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_ent
|
||||
continue;
|
||||
}
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
if (cbm_should_skip_dir(name, CBM_MODE_FULL)) {
|
||||
if (cbm_should_skip_dir(name, CBM_MODE_FULL) ||
|
||||
cbm_pipeline_relpath_is_excluded(rel_path, excluded_dirs, excluded_count)) {
|
||||
continue;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
@@ -886,7 +887,8 @@ static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_ent
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
parsed += pkgmap_walk_dir(abs_path, rel_path, entries, depth + 1);
|
||||
parsed += pkgmap_walk_dir(abs_path, rel_path, entries, depth + 1, excluded_dirs,
|
||||
excluded_count);
|
||||
continue;
|
||||
}
|
||||
if (!S_ISREG(st.st_mode)) {
|
||||
@@ -920,11 +922,12 @@ static int pkgmap_walk_dir(const char *abs_dir, const char *rel_dir, cbm_pkg_ent
|
||||
* Windows reparse points, so it cannot hang on directory junctions.
|
||||
* This is what lets bare workspace imports (e.g. "@org/pkg" declared in
|
||||
* an ignored package.json) resolve on Windows as well as POSIX. */
|
||||
int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries) {
|
||||
int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries, char **excluded_dirs,
|
||||
int excluded_count) {
|
||||
if (!repo_path || !entries) {
|
||||
return 0;
|
||||
}
|
||||
int parsed = pkgmap_walk_dir(repo_path, "", entries, 0);
|
||||
int parsed = pkgmap_walk_dir(repo_path, "", entries, 0, excluded_dirs, excluded_count);
|
||||
cbm_log_info("pkgmap.scan_repo", "manifests", pkgmap_itoa(parsed));
|
||||
return parsed;
|
||||
}
|
||||
@@ -961,7 +964,8 @@ CBMHashTable *cbm_pkgmap_build_from_files(const cbm_file_info_t *files, int file
|
||||
* (the canonical case: package.json, which is in IGNORED_JSON_FILES).
|
||||
* Falls back to the files[]-only behaviour if repo_path is NULL. */
|
||||
CBMHashTable *cbm_pkgmap_build_from_repo(const char *repo_path, const cbm_file_info_t *files,
|
||||
int file_count, const char *project_name) {
|
||||
int file_count, const char *project_name,
|
||||
char **excluded_dirs, int excluded_count) {
|
||||
cbm_pkg_entries_t entries;
|
||||
cbm_pkg_entries_init(&entries);
|
||||
|
||||
@@ -985,7 +989,7 @@ CBMHashTable *cbm_pkgmap_build_from_repo(const char *repo_path, const cbm_file_i
|
||||
free(source);
|
||||
}
|
||||
|
||||
int from_walk = cbm_pkgmap_scan_repo(repo_path, &entries);
|
||||
int from_walk = cbm_pkgmap_scan_repo(repo_path, &entries, excluded_dirs, excluded_count);
|
||||
cbm_log_info("pkgmap.scan", "manifests_from_files", pkgmap_itoa(from_files),
|
||||
"manifests_from_walk", pkgmap_itoa(from_walk), "entries",
|
||||
pkgmap_itoa(entries.count));
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
#include "pipeline/path_alias.h"
|
||||
|
||||
#include "pipeline/pipeline_internal.h"
|
||||
|
||||
#include "foundation/compat.h"
|
||||
#include "foundation/compat_fs.h"
|
||||
#include "foundation/constants.h"
|
||||
@@ -380,7 +382,8 @@ static const char *const TS_CONFIG_NAMES[] = {"tsconfig.json", "jsconfig.json"};
|
||||
enum { TS_CONFIG_NAMES_COUNT = 2 };
|
||||
|
||||
static void find_alias_files(const char *abs_dir, const char *rel_dir, alias_config_hit_t *out,
|
||||
int *count, int max_count, int depth) {
|
||||
int *count, int max_count, int depth, char **excluded_dirs,
|
||||
int excluded_count) {
|
||||
if (*count >= max_count || depth > CBM_PATH_ALIAS_MAX_DEPTH) {
|
||||
return;
|
||||
}
|
||||
@@ -422,12 +425,18 @@ static void find_alias_files(const char *abs_dir, const char *rel_dir, alias_con
|
||||
} else {
|
||||
snprintf(child_rel, sizeof(child_rel), "%s/%s", rel_dir, name);
|
||||
}
|
||||
find_alias_files(child_abs, child_rel, out, count, max_count, depth + 1);
|
||||
if (cbm_pipeline_relpath_is_excluded(child_rel, excluded_dirs, excluded_count)) {
|
||||
continue;
|
||||
}
|
||||
find_alias_files(child_abs, child_rel, out, count, max_count, depth + 1, excluded_dirs,
|
||||
excluded_count);
|
||||
}
|
||||
cbm_closedir(d);
|
||||
}
|
||||
|
||||
cbm_path_alias_collection_t *cbm_load_path_aliases(const char *repo_path) {
|
||||
cbm_path_alias_collection_t *cbm_load_path_aliases_excluded(const char *repo_path,
|
||||
char **excluded_dirs,
|
||||
int excluded_count) {
|
||||
if (!repo_path) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -436,7 +445,8 @@ cbm_path_alias_collection_t *cbm_load_path_aliases(const char *repo_path) {
|
||||
return NULL;
|
||||
}
|
||||
int count = 0;
|
||||
find_alias_files(repo_path, "", hits, &count, CBM_PATH_ALIAS_MAX_FILES, 0);
|
||||
find_alias_files(repo_path, "", hits, &count, CBM_PATH_ALIAS_MAX_FILES, 0, excluded_dirs,
|
||||
excluded_count);
|
||||
if (count >= CBM_PATH_ALIAS_MAX_FILES) {
|
||||
cbm_log_warn("path_alias.files.cap_hit", "repo", repo_path, "kept", "256_of_more");
|
||||
}
|
||||
@@ -479,6 +489,10 @@ cbm_path_alias_collection_t *cbm_load_path_aliases(const char *repo_path) {
|
||||
return coll;
|
||||
}
|
||||
|
||||
cbm_path_alias_collection_t *cbm_load_path_aliases(const char *repo_path) {
|
||||
return cbm_load_path_aliases_excluded(repo_path, NULL, 0);
|
||||
}
|
||||
|
||||
const cbm_path_alias_map_t *cbm_path_alias_find_for_file(const cbm_path_alias_collection_t *coll,
|
||||
const char *rel_path) {
|
||||
if (!coll || !rel_path) {
|
||||
|
||||
@@ -72,6 +72,9 @@ typedef struct {
|
||||
* are found (also NULL on out-of-memory). Caller frees with
|
||||
* cbm_path_alias_collection_free. */
|
||||
cbm_path_alias_collection_t *cbm_load_path_aliases(const char *repo_path);
|
||||
cbm_path_alias_collection_t *cbm_load_path_aliases_excluded(const char *repo_path,
|
||||
char **excluded_dirs,
|
||||
int excluded_count);
|
||||
|
||||
/* Free a collection produced by cbm_load_path_aliases. NULL-safe. */
|
||||
void cbm_path_alias_collection_free(cbm_path_alias_collection_t *coll);
|
||||
|
||||
@@ -737,8 +737,9 @@ static int run_sequential_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
|
||||
* Use the repo-walking variant so manifests filtered out by the main
|
||||
* discoverer (package.json, composer.json) still feed pkgmap and let
|
||||
* workspace imports like `@my/pkg` resolve to their target Module. */
|
||||
cbm_pipeline_set_pkgmap(
|
||||
cbm_pkgmap_build_from_repo(ctx->repo_path, files, file_count, ctx->project_name));
|
||||
cbm_pipeline_set_pkgmap(cbm_pkgmap_build_from_repo(ctx->repo_path, files, file_count,
|
||||
ctx->project_name, ctx->excluded_dirs,
|
||||
ctx->excluded_count));
|
||||
|
||||
CBMFileResult **seq_cache = (CBMFileResult **)calloc(file_count, sizeof(CBMFileResult *));
|
||||
if (seq_cache) {
|
||||
@@ -1261,7 +1262,8 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
|
||||
|
||||
/* Phase 2b: Load build-tool path aliases (tsconfig/jsconfig today). NULL
|
||||
* when no usable configs are found — non-TS projects pay nothing. */
|
||||
path_aliases = cbm_load_path_aliases(p->repo_path);
|
||||
path_aliases =
|
||||
cbm_load_path_aliases_excluded(p->repo_path, p->excluded_dirs, p->excluded_count);
|
||||
|
||||
/* Build shared context for pass functions */
|
||||
cbm_pipeline_ctx_t ctx = {
|
||||
@@ -1273,6 +1275,8 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
|
||||
.pipeline = p, /* so passes can record per-file skips (Track B) */
|
||||
.mode = (int)p->mode,
|
||||
.path_aliases = path_aliases,
|
||||
.excluded_dirs = p->excluded_dirs,
|
||||
.excluded_count = p->excluded_count,
|
||||
};
|
||||
|
||||
rc = run_extraction_phase(p, &ctx, files, file_count);
|
||||
|
||||
@@ -806,7 +806,16 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
|
||||
cbm_log_info("incremental.registry_seed", "symbols", itoa_buf(cbm_registry_size(registry)),
|
||||
"elapsed_ms", itoa_buf((int)elapsed_ms(t)));
|
||||
|
||||
cbm_path_alias_collection_t *path_aliases = cbm_load_path_aliases(cbm_pipeline_repo_path(p));
|
||||
/* Discovery exclusions (gitignore + skip dirs) captured by the run that
|
||||
* routed here. Borrowed from the pipeline so the auxiliary repo walks
|
||||
* (pkgmap via merge_pkg_entries, path aliases) skip excluded subtrees on
|
||||
* incremental runs too — same borrow as the full path (#792/#804). */
|
||||
char **excluded_dirs = NULL;
|
||||
int excluded_count = 0;
|
||||
cbm_pipeline_get_excluded(p, &excluded_dirs, &excluded_count);
|
||||
|
||||
cbm_path_alias_collection_t *path_aliases =
|
||||
cbm_load_path_aliases_excluded(cbm_pipeline_repo_path(p), excluded_dirs, excluded_count);
|
||||
|
||||
cbm_pipeline_ctx_t ctx = {
|
||||
.project_name = project,
|
||||
@@ -817,6 +826,8 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
|
||||
.pipeline = p, /* so passes can record per-file skips (Track B) */
|
||||
.mode = cbm_pipeline_get_mode(p),
|
||||
.path_aliases = path_aliases,
|
||||
.excluded_dirs = excluded_dirs,
|
||||
.excluded_count = excluded_count,
|
||||
};
|
||||
|
||||
for (int i = 0; i < ci; i++) {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "cbm.h"
|
||||
#include "lsp/go_lsp.h" /* CBMLSPDef for cbm_parallel_resolve cross-LSP inputs */
|
||||
#include <stdatomic.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ── Shared pipeline constants ─────────────────────────────────── */
|
||||
|
||||
@@ -81,8 +82,30 @@ typedef struct {
|
||||
* configs are an easy follow-on). NULL when no usable configs were found.
|
||||
* Owned by pipeline.c / pipeline_incremental.c. */
|
||||
const cbm_path_alias_collection_t *path_aliases;
|
||||
|
||||
/* Directory subtrees excluded during discovery. Borrowed from pipeline.c. */
|
||||
char **excluded_dirs;
|
||||
int excluded_count;
|
||||
} cbm_pipeline_ctx_t;
|
||||
|
||||
static inline int cbm_pipeline_relpath_is_excluded(const char *rel_path, char *const *excluded_dirs,
|
||||
int excluded_count) {
|
||||
if (!rel_path || rel_path[0] == '\0' || !excluded_dirs || excluded_count <= 0) {
|
||||
return 0;
|
||||
}
|
||||
for (int i = 0; i < excluded_count; i++) {
|
||||
const char *excluded = excluded_dirs[i];
|
||||
if (!excluded || excluded[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
size_t n = strlen(excluded);
|
||||
if (strncmp(rel_path, excluded, n) == 0 && (rel_path[n] == '\0' || rel_path[n] == '/')) {
|
||||
return SKIP_ONE;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Get the current pipeline's package map (NULL if none). */
|
||||
CBMHashTable *cbm_pipeline_get_pkgmap(void);
|
||||
void cbm_pipeline_set_pkgmap(CBMHashTable *map);
|
||||
@@ -131,9 +154,11 @@ CBMHashTable *cbm_pkgmap_build(cbm_pkg_entries_t *worker_entries, int worker_cou
|
||||
const char *project_name);
|
||||
|
||||
/* Build pkgmap by reading manifest files from the files array (sequential path). */
|
||||
int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries);
|
||||
int cbm_pkgmap_scan_repo(const char *repo_path, cbm_pkg_entries_t *entries, char **excluded_dirs,
|
||||
int excluded_count);
|
||||
CBMHashTable *cbm_pkgmap_build_from_repo(const char *repo_path, const cbm_file_info_t *files,
|
||||
int file_count, const char *project_name);
|
||||
int file_count, const char *project_name,
|
||||
char **excluded_dirs, int excluded_count);
|
||||
CBMHashTable *cbm_pkgmap_build_from_files(const cbm_file_info_t *files, int file_count,
|
||||
const char *project_name);
|
||||
|
||||
@@ -530,8 +555,14 @@ typedef struct {
|
||||
/* Scan a project directory for environment variable assignments with URL values.
|
||||
* Walks the filesystem, scans Dockerfiles, shell scripts, .env, YAML, TOML,
|
||||
* Terraform, and .properties files. Filters out secrets.
|
||||
* Returns number of bindings written to out (up to max_out). */
|
||||
* Returns number of bindings written to out (up to max_out).
|
||||
* NOTE: this walker currently has no production callers — it is exercised
|
||||
* only by tests. The _excluded variant honors discovery exclusions for
|
||||
* consistency with the pkgmap/path-alias walks (#792); the plain variant
|
||||
* scans unexcluded (NULL exclusion list). */
|
||||
int cbm_scan_project_env_urls(const char *root_path, cbm_env_binding_t *out, int max_out);
|
||||
int cbm_scan_project_env_urls_excluded(const char *root_path, cbm_env_binding_t *out, int max_out,
|
||||
char **excluded_dirs, int excluded_count);
|
||||
|
||||
/* ── Incremental pipeline (pipeline_incremental.c) ───────────────── */
|
||||
|
||||
|
||||
@@ -336,6 +336,62 @@ TEST(path_alias_loader_monorepo_dotdot_climb) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Loader honors discovery exclusions (#792) ─────────────────── */
|
||||
|
||||
/* find_alias_files must not descend into discovery-excluded subtrees.
|
||||
* Control run first (no exclusions → both configs collected) so the
|
||||
* exclusion assertion below cannot pass vacuously. */
|
||||
TEST(path_alias_loader_honors_discovery_exclusions) {
|
||||
char tmpl[256];
|
||||
snprintf(tmpl, sizeof(tmpl), "/tmp/cbm_palias_excl_XXXXXX");
|
||||
char *root = cbm_mkdtemp(tmpl);
|
||||
ASSERT_NOT_NULL(root);
|
||||
|
||||
char sub[512];
|
||||
snprintf(sub, sizeof(sub), "%s/big_generated", root);
|
||||
cbm_mkdir(sub);
|
||||
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s/tsconfig.json", root);
|
||||
ASSERT_EQ(write_file(path,
|
||||
"{\n \"compilerOptions\": {\n \"paths\": {\n"
|
||||
" \"@root/*\": [\"shared/*\"]\n }\n }\n}\n"),
|
||||
0);
|
||||
snprintf(path, sizeof(path), "%s/big_generated/tsconfig.json", root);
|
||||
ASSERT_EQ(write_file(path,
|
||||
"{\n \"compilerOptions\": {\n \"paths\": {\n"
|
||||
" \"@gen/*\": [\"./src/*\"]\n }\n }\n}\n"),
|
||||
0);
|
||||
|
||||
/* Control: the unexcluded loader collects BOTH configs. */
|
||||
cbm_path_alias_collection_t *coll = cbm_load_path_aliases(root);
|
||||
ASSERT_NOT_NULL(coll);
|
||||
ASSERT_EQ(coll->count, 2);
|
||||
cbm_path_alias_collection_free(coll);
|
||||
|
||||
/* Excluding big_generated drops its config; the root one survives. */
|
||||
char *excluded[] = {(char *)"big_generated"};
|
||||
coll = cbm_load_path_aliases_excluded(root, excluded, 1);
|
||||
ASSERT_NOT_NULL(coll);
|
||||
ASSERT_EQ(coll->count, 1);
|
||||
const cbm_path_alias_map_t *m = cbm_path_alias_find_for_file(coll, "src/x.ts");
|
||||
ASSERT_NOT_NULL(m);
|
||||
char *r = cbm_path_alias_resolve(m, "@root/utils");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_STR_EQ(r, "shared/utils");
|
||||
free(r);
|
||||
cbm_path_alias_collection_free(coll);
|
||||
|
||||
snprintf(path, sizeof(path), "%s/big_generated/tsconfig.json", root);
|
||||
unlink(path);
|
||||
snprintf(path, sizeof(path), "%s/tsconfig.json", root);
|
||||
unlink(path);
|
||||
snprintf(path, sizeof(path), "%s/big_generated", root);
|
||||
rmdir(path);
|
||||
rmdir(root);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Loader returns NULL when no configs found ─────────────────── */
|
||||
|
||||
TEST(path_alias_loader_no_configs) {
|
||||
@@ -363,5 +419,6 @@ void suite_path_alias(void) {
|
||||
RUN_TEST(path_alias_find_for_file_nearest_ancestor);
|
||||
RUN_TEST(path_alias_loader_monorepo);
|
||||
RUN_TEST(path_alias_loader_monorepo_dotdot_climb);
|
||||
RUN_TEST(path_alias_loader_honors_discovery_exclusions);
|
||||
RUN_TEST(path_alias_loader_no_configs);
|
||||
}
|
||||
|
||||
@@ -4845,6 +4845,129 @@ TEST(envscan_non_url_values_skipped) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Discovery-exclusion plumbing in auxiliary repo walks (#792) ── */
|
||||
|
||||
/* Boundary semantics of the shared exclusion predicate: anchored at the
|
||||
* repo root, matches the excluded dir itself and its subtree, but never
|
||||
* sibling names sharing a prefix. Regression guard for issue #792. */
|
||||
TEST(pipeline_relpath_excluded_boundary) {
|
||||
char *excluded[] = {(char *)"vendor_big", (char *)"packages/big"};
|
||||
|
||||
/* Exact match and subtree paths are excluded. */
|
||||
ASSERT_TRUE(cbm_pipeline_relpath_is_excluded("vendor_big", excluded, 2));
|
||||
ASSERT_TRUE(cbm_pipeline_relpath_is_excluded("vendor_big/lib/package.json", excluded, 2));
|
||||
ASSERT_TRUE(cbm_pipeline_relpath_is_excluded("packages/big", excluded, 2));
|
||||
ASSERT_TRUE(cbm_pipeline_relpath_is_excluded("packages/big/src/x.ts", excluded, 2));
|
||||
|
||||
/* Sibling names sharing the prefix are NOT excluded ('/'-boundary). */
|
||||
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor_bigger", excluded, 2));
|
||||
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor", excluded, 2));
|
||||
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("packages/bigger/x.ts", excluded, 2));
|
||||
|
||||
/* Exclusions are root-anchored prefixes, not substring matches. */
|
||||
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("src/vendor_big/x.c", excluded, 2));
|
||||
|
||||
/* NULL / empty safety. */
|
||||
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded(NULL, excluded, 2));
|
||||
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("", excluded, 2));
|
||||
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor_big", NULL, 0));
|
||||
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor_big", excluded, 0));
|
||||
char *with_empty[] = {(char *)""};
|
||||
ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor_big", with_empty, 1));
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Helper: does the entries array contain a package with this name? */
|
||||
static int pkg_entries_has_name(const cbm_pkg_entries_t *e, const char *name) {
|
||||
for (int i = 0; i < e->count; i++) {
|
||||
if (e->items[i].pkg_name && strcmp(e->items[i].pkg_name, name) == 0)
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The pkgmap repo walk must honor discovery exclusions (issue #792: a
|
||||
* gitignored huge subtree kept the pkgmap walk busy for 15 minutes).
|
||||
* Control run first (no exclusions → BOTH manifests parsed) so the
|
||||
* exclusion assertion below cannot pass vacuously. */
|
||||
TEST(pkgmap_scan_repo_honors_discovery_exclusions) {
|
||||
char tmpdir[256];
|
||||
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_pkgmap_excl_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmpdir))
|
||||
FAIL("tmpdir");
|
||||
|
||||
char dir[512];
|
||||
snprintf(dir, sizeof(dir), "%s/packages", tmpdir);
|
||||
cbm_mkdir(dir);
|
||||
snprintf(dir, sizeof(dir), "%s/packages/app", tmpdir);
|
||||
cbm_mkdir(dir);
|
||||
snprintf(dir, sizeof(dir), "%s/vendor_big", tmpdir);
|
||||
cbm_mkdir(dir);
|
||||
snprintf(dir, sizeof(dir), "%s/vendor_big/lib", tmpdir);
|
||||
cbm_mkdir(dir);
|
||||
|
||||
write_temp_file(tmpdir, "packages/app/package.json",
|
||||
"{\"name\":\"@org/app\",\"main\":\"index.js\"}\n");
|
||||
write_temp_file(tmpdir, "vendor_big/lib/package.json",
|
||||
"{\"name\":\"@org/vendored\",\"main\":\"index.js\"}\n");
|
||||
|
||||
/* Control: NULL exclusion list — the walk reaches and parses BOTH
|
||||
* manifests (proves the excluded one is reachable + parseable). */
|
||||
cbm_pkg_entries_t control;
|
||||
cbm_pkg_entries_init(&control);
|
||||
cbm_pkgmap_scan_repo(tmpdir, &control, NULL, 0);
|
||||
ASSERT_TRUE(pkg_entries_has_name(&control, "@org/app"));
|
||||
ASSERT_TRUE(pkg_entries_has_name(&control, "@org/vendored"));
|
||||
cbm_pkg_entries_free(&control);
|
||||
|
||||
/* With vendor_big excluded (as discovery reports for a gitignored
|
||||
* subtree): the walk must not descend into it. */
|
||||
char *excluded[] = {(char *)"vendor_big"};
|
||||
cbm_pkg_entries_t entries;
|
||||
cbm_pkg_entries_init(&entries);
|
||||
cbm_pkgmap_scan_repo(tmpdir, &entries, excluded, 1);
|
||||
ASSERT_TRUE(pkg_entries_has_name(&entries, "@org/app"));
|
||||
ASSERT_FALSE(pkg_entries_has_name(&entries, "@org/vendored"));
|
||||
cbm_pkg_entries_free(&entries);
|
||||
|
||||
th_rmtree(tmpdir);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The env-URL walk must honor discovery exclusions the same way (#792).
|
||||
* Control run first via the NULL-exclusion wrapper so the exclusion
|
||||
* assertion cannot pass vacuously. */
|
||||
TEST(envscan_walk_honors_discovery_exclusions) {
|
||||
char tmpdir[256];
|
||||
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_envscan_excl_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmpdir))
|
||||
FAIL("tmpdir");
|
||||
|
||||
char dir[512];
|
||||
snprintf(dir, sizeof(dir), "%s/big_generated", tmpdir);
|
||||
cbm_mkdir(dir);
|
||||
|
||||
write_temp_file(tmpdir, "deploy.sh",
|
||||
"#!/bin/bash\nexport CONTROL_URL=\"https://api.example.com/v1\"\n");
|
||||
write_temp_file(tmpdir, "big_generated/env.sh",
|
||||
"#!/bin/bash\nexport EXCLUDED_URL=\"https://excluded.example.com/v1\"\n");
|
||||
|
||||
/* Control: the NULL-exclusion wrapper sees both bindings. */
|
||||
cbm_env_binding_t bindings[32];
|
||||
int count = cbm_scan_project_env_urls(tmpdir, bindings, 32);
|
||||
ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "CONTROL_URL"));
|
||||
ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "EXCLUDED_URL"));
|
||||
|
||||
/* With big_generated excluded, its binding must disappear. */
|
||||
char *excluded[] = {(char *)"big_generated"};
|
||||
count = cbm_scan_project_env_urls_excluded(tmpdir, bindings, 32, excluded, 1);
|
||||
ASSERT_NOT_NULL(find_binding_by_key(bindings, count, "CONTROL_URL"));
|
||||
ASSERT_TRUE(find_binding_by_key(bindings, count, "EXCLUDED_URL") == NULL);
|
||||
|
||||
th_rmtree(tmpdir);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Git history tests (port of githistory_test.go) ────────────── */
|
||||
|
||||
/* Port of Go TestIsTrackableFile from githistory_test.go */
|
||||
@@ -6364,6 +6487,10 @@ SUITE(pipeline) {
|
||||
RUN_TEST(envscan_secret_file_exclusion);
|
||||
RUN_TEST(envscan_skips_ignored_dirs);
|
||||
RUN_TEST(envscan_non_url_values_skipped);
|
||||
/* Discovery-exclusion plumbing in auxiliary repo walks (#792) */
|
||||
RUN_TEST(pipeline_relpath_excluded_boundary);
|
||||
RUN_TEST(pkgmap_scan_repo_honors_discovery_exclusions);
|
||||
RUN_TEST(envscan_walk_honors_discovery_exclusions);
|
||||
/* Function registry / resolver */
|
||||
RUN_TEST(registry_resolve_single_candidate);
|
||||
RUN_TEST(registry_fuzzy_nonexistent);
|
||||
|
||||
Reference in New Issue
Block a user