mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-07 08:43:35 +08:00
fix: honor git global core.excludesFile as an ignore source
Implements 2026-06-20-013-fix-global-core-excludesfile-ignore-source. Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
This commit is contained in:
+255
-18
@@ -13,15 +13,19 @@
|
||||
|
||||
#include "foundation/constants.h"
|
||||
#include "foundation/compat_fs.h"
|
||||
#include "foundation/platform.h"
|
||||
#ifdef _WIN32
|
||||
#include "foundation/win_utf8.h"
|
||||
#endif
|
||||
#include <ctype.h>
|
||||
#include <stdint.h> // int64_t
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h> // strdup
|
||||
#include <sys/stat.h>
|
||||
|
||||
int cbm_gitignore_match_result(const cbm_gitignore_t *gi, const char *rel_path, bool is_dir);
|
||||
|
||||
/* ── Hardcoded always-skip directories ──────────────────────────── */
|
||||
|
||||
static const char *ALWAYS_SKIP_DIRS[] = {
|
||||
@@ -131,6 +135,210 @@ static bool str_contains(const char *s, const char *sub) {
|
||||
return strstr(s, sub) != NULL;
|
||||
}
|
||||
|
||||
/* ── Git global excludes resolution ───────────────────────────── */
|
||||
|
||||
enum { GIT_TILDE_PREFIX_LEN = 2 }; /* "~/". */
|
||||
|
||||
static bool ascii_ieq(const char *a, const char *b) {
|
||||
while (*a && *b) {
|
||||
if (tolower((unsigned char)*a) != tolower((unsigned char)*b)) {
|
||||
return false;
|
||||
}
|
||||
a++;
|
||||
b++;
|
||||
}
|
||||
return *a == '\0' && *b == '\0';
|
||||
}
|
||||
|
||||
static char *trim_ws(char *s) {
|
||||
while (*s && isspace((unsigned char)*s)) {
|
||||
s++;
|
||||
}
|
||||
char *end = s + strlen(s);
|
||||
while (end > s && isspace((unsigned char)end[-1])) {
|
||||
end--;
|
||||
}
|
||||
*end = '\0';
|
||||
return s;
|
||||
}
|
||||
|
||||
static void strip_inline_comment(char *s) {
|
||||
bool in_quote = false;
|
||||
char quote = '\0';
|
||||
for (char *p = s; *p; p++) {
|
||||
if ((*p == '"' || *p == '\'') && (p == s || p[-1] != '\\')) {
|
||||
if (!in_quote) {
|
||||
in_quote = true;
|
||||
quote = *p;
|
||||
} else if (*p == quote) {
|
||||
in_quote = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!in_quote && (*p == '#' || *p == ';') && (p == s || isspace((unsigned char)p[-1]))) {
|
||||
*p = '\0';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static char *strip_matching_quotes(char *s) {
|
||||
size_t len = strlen(s);
|
||||
if (len >= CBM_QUOTE_PAIR && ((s[0] == '"' && s[len - SKIP_ONE] == '"') ||
|
||||
(s[0] == '\'' && s[len - SKIP_ONE] == '\''))) {
|
||||
s[len - SKIP_ONE] = '\0';
|
||||
return s + SKIP_ONE;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
static bool has_trailing_sep(const char *path) {
|
||||
size_t len = strlen(path);
|
||||
return len > 0 && (path[len - SKIP_ONE] == '/' || path[len - SKIP_ONE] == '\\');
|
||||
}
|
||||
|
||||
static void path_join(char *out, size_t out_sz, const char *base, const char *rel) {
|
||||
if (!out || out_sz == 0) {
|
||||
return;
|
||||
}
|
||||
if (!base || base[0] == '\0') {
|
||||
snprintf(out, out_sz, "%s", rel ? rel : "");
|
||||
} else if (!rel || rel[0] == '\0') {
|
||||
snprintf(out, out_sz, "%s", base);
|
||||
} else if (has_trailing_sep(base)) {
|
||||
snprintf(out, out_sz, "%s%s", base, rel);
|
||||
} else {
|
||||
snprintf(out, out_sz, "%s/%s", base, rel);
|
||||
}
|
||||
cbm_normalize_path_sep(out);
|
||||
}
|
||||
|
||||
static bool expand_git_path(const char *path, char *out, size_t out_sz) {
|
||||
if (!path || !path[0] || !out || out_sz == 0) {
|
||||
return false;
|
||||
}
|
||||
char normalized[CBM_SZ_4K];
|
||||
snprintf(normalized, sizeof(normalized), "%s", path);
|
||||
cbm_normalize_path_sep(normalized);
|
||||
|
||||
if (normalized[0] != '~') {
|
||||
snprintf(out, out_sz, "%s", normalized);
|
||||
cbm_normalize_path_sep(out);
|
||||
return out[0] != '\0';
|
||||
}
|
||||
|
||||
if (normalized[1] != '\0' && normalized[1] != '/') {
|
||||
return false; /* ~user expansion is intentionally not supported. */
|
||||
}
|
||||
|
||||
const char *home = cbm_get_home_dir();
|
||||
if (!home || home[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
if (normalized[1] == '\0') {
|
||||
snprintf(out, out_sz, "%s", home);
|
||||
cbm_normalize_path_sep(out);
|
||||
} else {
|
||||
path_join(out, out_sz, home, normalized + GIT_TILDE_PREFIX_LEN);
|
||||
}
|
||||
return out[0] != '\0';
|
||||
}
|
||||
|
||||
static bool read_core_excludes_file(const char *config_path, char *out, size_t out_sz) {
|
||||
FILE *f = fopen(config_path, "r");
|
||||
if (!f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool in_core = false;
|
||||
bool found = false;
|
||||
char line[CBM_SZ_4K];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
char *s = trim_ws(line);
|
||||
if (s[0] == '\0' || s[0] == '#' || s[0] == ';') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[0] == '[') {
|
||||
char *end = strchr(s, ']');
|
||||
if (!end) {
|
||||
in_core = false;
|
||||
continue;
|
||||
}
|
||||
*end = '\0';
|
||||
in_core = ascii_ieq(trim_ws(s + SKIP_ONE), "core");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!in_core) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char *eq = strchr(s, '=');
|
||||
if (!eq) {
|
||||
continue;
|
||||
}
|
||||
*eq = '\0';
|
||||
char *key = trim_ws(s);
|
||||
char *value = trim_ws(eq + SKIP_ONE);
|
||||
strip_inline_comment(value);
|
||||
value = strip_matching_quotes(trim_ws(value));
|
||||
|
||||
if (ascii_ieq(key, "excludesfile") && value[0] != '\0' &&
|
||||
expand_git_path(value, out, out_sz)) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
return found;
|
||||
}
|
||||
|
||||
static bool resolve_xdg_git_config_dir(char *out, size_t out_sz) {
|
||||
char env[CBM_SZ_4K];
|
||||
if (cbm_safe_getenv("XDG_CONFIG_HOME", env, sizeof(env), NULL) && env[0] != '\0') {
|
||||
snprintf(out, out_sz, "%s", env);
|
||||
cbm_normalize_path_sep(out);
|
||||
return true;
|
||||
}
|
||||
|
||||
const char *home = cbm_get_home_dir();
|
||||
if (!home || home[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
path_join(out, out_sz, home, ".config");
|
||||
return out[0] != '\0';
|
||||
}
|
||||
|
||||
static bool resolve_global_excludes_path(const char *repo_path, char *out, size_t out_sz) {
|
||||
char config_path[CBM_SZ_4K];
|
||||
|
||||
path_join(config_path, sizeof(config_path), repo_path, ".git/config");
|
||||
if (read_core_excludes_file(config_path, out, out_sz)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const char *home = cbm_get_home_dir();
|
||||
if (home && home[0] != '\0') {
|
||||
path_join(config_path, sizeof(config_path), home, ".gitconfig");
|
||||
if (read_core_excludes_file(config_path, out, out_sz)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
char xdg_config[CBM_SZ_4K];
|
||||
if (resolve_xdg_git_config_dir(xdg_config, sizeof(xdg_config))) {
|
||||
path_join(config_path, sizeof(config_path), xdg_config, "git/config");
|
||||
if (read_core_excludes_file(config_path, out, out_sz)) {
|
||||
return true;
|
||||
}
|
||||
path_join(out, out_sz, xdg_config, "git/ignore");
|
||||
return out[0] != '\0';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ── Public filter functions ─────────────────────── */
|
||||
|
||||
bool cbm_should_skip_dir(const char *dirname, cbm_index_mode_t mode) {
|
||||
@@ -273,6 +481,7 @@ static const char *local_rel_path(const char *rel_path, const char *local_prefix
|
||||
/* Check if a directory entry should be skipped (hardcoded dirs + gitignore). */
|
||||
static bool should_skip_directory(const char *entry_name, const char *rel_path,
|
||||
const cbm_discover_opts_t *opts, const cbm_gitignore_t *gitignore,
|
||||
const cbm_gitignore_t *global_gi,
|
||||
const cbm_gitignore_t *cbmignore, const cbm_gitignore_t *local_gi,
|
||||
const char *local_gi_prefix) {
|
||||
if (cbm_should_skip_dir(entry_name, opts ? opts->mode : CBM_MODE_FULL)) {
|
||||
@@ -281,23 +490,31 @@ static bool should_skip_directory(const char *entry_name, const char *rel_path,
|
||||
if (gitignore && cbm_gitignore_matches(gitignore, rel_path, true)) {
|
||||
return true;
|
||||
}
|
||||
bool global_ignored = global_gi && cbm_gitignore_matches(global_gi, rel_path, true);
|
||||
if (local_gi) {
|
||||
const char *lrel = local_rel_path(rel_path, local_gi_prefix);
|
||||
if (cbm_gitignore_matches(local_gi, lrel, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (cbmignore && cbm_gitignore_matches(cbmignore, rel_path, true)) {
|
||||
return true;
|
||||
if (cbmignore) {
|
||||
int cbm_result = cbm_gitignore_match_result(cbmignore, rel_path, true);
|
||||
if (cbm_result > 0) {
|
||||
return true;
|
||||
}
|
||||
if (cbm_result < 0 && global_ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return global_ignored;
|
||||
}
|
||||
|
||||
/* Check if a regular file should be skipped (filters + gitignore + size). */
|
||||
static bool should_skip_file(const char *entry_name, const char *rel_path,
|
||||
const cbm_discover_opts_t *opts, const cbm_gitignore_t *gitignore,
|
||||
const cbm_gitignore_t *cbmignore, const cbm_gitignore_t *local_gi,
|
||||
const char *local_gi_prefix, off_t file_size) {
|
||||
const cbm_gitignore_t *global_gi, const cbm_gitignore_t *cbmignore,
|
||||
const cbm_gitignore_t *local_gi, const char *local_gi_prefix,
|
||||
off_t file_size) {
|
||||
cbm_index_mode_t mode = opts ? opts->mode : CBM_MODE_FULL;
|
||||
if (cbm_has_ignored_suffix(entry_name, mode)) {
|
||||
return true;
|
||||
@@ -311,19 +528,26 @@ static bool should_skip_file(const char *entry_name, const char *rel_path,
|
||||
if (gitignore && cbm_gitignore_matches(gitignore, rel_path, false)) {
|
||||
return true;
|
||||
}
|
||||
bool global_ignored = global_gi && cbm_gitignore_matches(global_gi, rel_path, false);
|
||||
if (local_gi) {
|
||||
const char *lrel = local_rel_path(rel_path, local_gi_prefix);
|
||||
if (cbm_gitignore_matches(local_gi, lrel, false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (cbmignore && cbm_gitignore_matches(cbmignore, rel_path, false)) {
|
||||
return true;
|
||||
if (cbmignore) {
|
||||
int cbm_result = cbm_gitignore_match_result(cbmignore, rel_path, false);
|
||||
if (cbm_result > 0) {
|
||||
return true;
|
||||
}
|
||||
if (cbm_result < 0 && global_ignored) {
|
||||
global_ignored = false;
|
||||
}
|
||||
}
|
||||
if (opts && opts->max_file_size > 0 && file_size > opts->max_file_size) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return global_ignored;
|
||||
}
|
||||
|
||||
/* Detect language for a file, handling .m disambiguation and JSON filtering. */
|
||||
@@ -384,10 +608,11 @@ static int safe_stat(const char *abs_path, struct stat *st) {
|
||||
/* Process a single regular file entry during directory walk. */
|
||||
static void walk_dir_process_file(const char *abs_path, const char *rel_path, const char *name,
|
||||
const cbm_discover_opts_t *opts, const cbm_gitignore_t *gitignore,
|
||||
const cbm_gitignore_t *global_gi,
|
||||
const cbm_gitignore_t *cbmignore, const cbm_gitignore_t *local_gi,
|
||||
const char *local_gi_prefix, off_t size, file_list_t *out) {
|
||||
if (should_skip_file(name, rel_path, opts, gitignore, cbmignore, local_gi, local_gi_prefix,
|
||||
size)) {
|
||||
if (should_skip_file(name, rel_path, opts, gitignore, global_gi, cbmignore, local_gi,
|
||||
local_gi_prefix, size)) {
|
||||
return;
|
||||
}
|
||||
CBMLanguage lang = detect_file_language(name, abs_path);
|
||||
@@ -435,6 +660,7 @@ static void walk_push_subdir(walk_frame_t *stack, int *top, const char *abs_path
|
||||
static void walk_dir_process_entry(cbm_dirent_t *entry, const walk_frame_t *frame,
|
||||
const cbm_discover_opts_t *opts,
|
||||
const cbm_gitignore_t *gitignore,
|
||||
const cbm_gitignore_t *global_gi,
|
||||
const cbm_gitignore_t *cbmignore, walk_frame_t *stack, int *top,
|
||||
file_list_t *out) {
|
||||
char abs_path[CBM_SZ_4K];
|
||||
@@ -452,7 +678,7 @@ static void walk_dir_process_entry(cbm_dirent_t *entry, const walk_frame_t *fram
|
||||
}
|
||||
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
if (!should_skip_directory(entry->name, rel_path, opts, gitignore, cbmignore,
|
||||
if (!should_skip_directory(entry->name, rel_path, opts, gitignore, global_gi, cbmignore,
|
||||
frame->local_gi, frame->local_gi_prefix)) {
|
||||
walk_push_subdir(stack, top, abs_path, rel_path, frame);
|
||||
} else {
|
||||
@@ -460,16 +686,16 @@ static void walk_dir_process_entry(cbm_dirent_t *entry, const walk_frame_t *fram
|
||||
file_list_add_excluded(out, rel_path);
|
||||
}
|
||||
} else if (S_ISREG(st.st_mode)) {
|
||||
walk_dir_process_file(abs_path, rel_path, entry->name, opts, gitignore, cbmignore,
|
||||
frame->local_gi, frame->local_gi_prefix, st.st_size, out);
|
||||
walk_dir_process_file(abs_path, rel_path, entry->name, opts, gitignore, global_gi,
|
||||
cbmignore, frame->local_gi, frame->local_gi_prefix, st.st_size, out);
|
||||
}
|
||||
}
|
||||
|
||||
enum { GI_OWNED_CAP = 64 };
|
||||
|
||||
static void walk_dir(const char *dir_path, const char *rel_prefix, const cbm_discover_opts_t *opts,
|
||||
const cbm_gitignore_t *gitignore, const cbm_gitignore_t *cbmignore,
|
||||
file_list_t *out) {
|
||||
const cbm_gitignore_t *gitignore, const cbm_gitignore_t *global_gi,
|
||||
const cbm_gitignore_t *cbmignore, file_list_t *out) {
|
||||
walk_frame_t *stack = calloc(WALK_STACK_CAP, sizeof(walk_frame_t));
|
||||
if (!stack) {
|
||||
return;
|
||||
@@ -503,7 +729,8 @@ static void walk_dir(const char *dir_path, const char *rel_prefix, const cbm_dis
|
||||
|
||||
cbm_dirent_t *entry;
|
||||
while ((entry = cbm_readdir(d)) != NULL) {
|
||||
walk_dir_process_entry(entry, &frame, opts, gitignore, cbmignore, stack, &top, out);
|
||||
walk_dir_process_entry(entry, &frame, opts, gitignore, global_gi, cbmignore, stack,
|
||||
&top, out);
|
||||
}
|
||||
cbm_closedir(d);
|
||||
}
|
||||
@@ -546,11 +773,20 @@ int cbm_discover_ex(const char *repo_path, const cbm_discover_opts_t *opts, cbm_
|
||||
char gi_path[CBM_SZ_4K];
|
||||
snprintf(gi_path, sizeof(gi_path), "%s/.git", repo_path);
|
||||
struct stat gi_stat;
|
||||
if (wide_stat(gi_path, &gi_stat) == 0 && S_ISDIR(gi_stat.st_mode)) {
|
||||
bool is_git_repo = wide_stat(gi_path, &gi_stat) == 0 && S_ISDIR(gi_stat.st_mode);
|
||||
bool has_git_config = false;
|
||||
if (is_git_repo) {
|
||||
snprintf(gi_path, sizeof(gi_path), "%s/.git/config", repo_path);
|
||||
has_git_config = wide_stat(gi_path, &gi_stat) == 0 && S_ISREG(gi_stat.st_mode);
|
||||
snprintf(gi_path, sizeof(gi_path), "%s/.gitignore", repo_path);
|
||||
gitignore = cbm_gitignore_load(gi_path);
|
||||
}
|
||||
|
||||
cbm_gitignore_t *global_gi = NULL;
|
||||
if (has_git_config && resolve_global_excludes_path(repo_path, gi_path, sizeof(gi_path))) {
|
||||
global_gi = cbm_gitignore_load(gi_path);
|
||||
}
|
||||
|
||||
/* Load cbmignore if specified or exists at repo root */
|
||||
cbm_gitignore_t *cbmignore = NULL;
|
||||
if (opts && opts->ignore_file) {
|
||||
@@ -562,10 +798,11 @@ int cbm_discover_ex(const char *repo_path, const cbm_discover_opts_t *opts, cbm_
|
||||
|
||||
/* Walk */
|
||||
file_list_t fl = {0};
|
||||
walk_dir(repo_path, "", opts, gitignore, cbmignore, &fl);
|
||||
walk_dir(repo_path, "", opts, gitignore, global_gi, cbmignore, &fl);
|
||||
|
||||
/* Cleanup */
|
||||
cbm_gitignore_free(gitignore);
|
||||
cbm_gitignore_free(global_gi);
|
||||
cbm_gitignore_free(cbmignore);
|
||||
|
||||
*out = fl.files;
|
||||
|
||||
@@ -340,16 +340,16 @@ static bool match_unrooted(const char *pattern, const char *rel_path, const char
|
||||
return false;
|
||||
}
|
||||
|
||||
bool cbm_gitignore_matches(const cbm_gitignore_t *gi, const char *rel_path, bool is_dir) {
|
||||
int cbm_gitignore_match_result(const cbm_gitignore_t *gi, const char *rel_path, bool is_dir) {
|
||||
if (!gi || !rel_path) {
|
||||
return false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Extract the basename for non-rooted pattern matching */
|
||||
const char *basename = strrchr(rel_path, '/');
|
||||
basename = basename ? basename + SKIP_ONE : rel_path;
|
||||
|
||||
bool matched = false;
|
||||
int matched = 0;
|
||||
|
||||
for (int i = 0; i < gi->count; i++) {
|
||||
const gi_pattern_t *p = &gi->patterns[i];
|
||||
@@ -362,13 +362,17 @@ bool cbm_gitignore_matches(const cbm_gitignore_t *gi, const char *rel_path, bool
|
||||
: match_unrooted(p->pattern, rel_path, basename);
|
||||
|
||||
if (this_match) {
|
||||
matched = !p->negated;
|
||||
matched = p->negated ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
bool cbm_gitignore_matches(const cbm_gitignore_t *gi, const char *rel_path, bool is_dir) {
|
||||
return cbm_gitignore_match_result(gi, rel_path, is_dir) > 0;
|
||||
}
|
||||
|
||||
void cbm_gitignore_free(cbm_gitignore_t *gi) {
|
||||
if (!gi) {
|
||||
return;
|
||||
|
||||
@@ -7,6 +7,45 @@
|
||||
#include "test_helpers.h"
|
||||
#include "discover/discover.h"
|
||||
|
||||
typedef struct {
|
||||
char *home;
|
||||
char *xdg_config_home;
|
||||
} git_env_snapshot_t;
|
||||
|
||||
static git_env_snapshot_t save_git_env(void) {
|
||||
git_env_snapshot_t snapshot = {0};
|
||||
const char *home = getenv("HOME");
|
||||
const char *xdg = getenv("XDG_CONFIG_HOME");
|
||||
snapshot.home = home ? cbm_strdup(home) : NULL;
|
||||
snapshot.xdg_config_home = xdg ? cbm_strdup(xdg) : NULL;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
static void restore_git_env(git_env_snapshot_t *snapshot) {
|
||||
if (snapshot->home) {
|
||||
cbm_setenv("HOME", snapshot->home, 1);
|
||||
free(snapshot->home);
|
||||
} else {
|
||||
cbm_unsetenv("HOME");
|
||||
}
|
||||
|
||||
if (snapshot->xdg_config_home) {
|
||||
cbm_setenv("XDG_CONFIG_HOME", snapshot->xdg_config_home, 1);
|
||||
free(snapshot->xdg_config_home);
|
||||
} else {
|
||||
cbm_unsetenv("XDG_CONFIG_HOME");
|
||||
}
|
||||
}
|
||||
|
||||
static bool discover_has_rel_path(const cbm_file_info_t *files, int count, const char *rel_path) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (strcmp(files[i].rel_path, rel_path) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ── Directory skip (always skipped) ───────────────────────────── */
|
||||
|
||||
TEST(skip_git) {
|
||||
@@ -332,6 +371,154 @@ TEST(discover_with_gitignore) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(discover_with_global_xdg_ignore) {
|
||||
git_env_snapshot_t env = save_git_env();
|
||||
char *tmp = th_mktempdir("cbm_disc_global_xdg");
|
||||
ASSERT(tmp != NULL);
|
||||
|
||||
char base[512], repo[512], home[512], xdg[512], xdg_env[512];
|
||||
snprintf(base, sizeof(base), "%s", tmp);
|
||||
snprintf(repo, sizeof(repo), "%s/repo", base);
|
||||
snprintf(home, sizeof(home), "%s/home", base);
|
||||
snprintf(xdg, sizeof(xdg), "%s/xdg", base);
|
||||
snprintf(xdg_env, sizeof(xdg_env), "%s/", xdg);
|
||||
|
||||
cbm_setenv("HOME", home, 1);
|
||||
cbm_setenv("XDG_CONFIG_HOME", xdg_env, 1);
|
||||
|
||||
th_mkdir_p(TH_PATH(repo, ".git"));
|
||||
th_write_file(TH_PATH(repo, ".git/config"), "[core]\n");
|
||||
th_write_file(TH_PATH(xdg, "git/ignore"), "secret.go\nignored_dir/\n");
|
||||
th_write_file(TH_PATH(repo, "main.go"), "package main\n");
|
||||
th_write_file(TH_PATH(repo, "secret.go"), "package secret\n");
|
||||
th_write_file(TH_PATH(repo, "ignored_dir/thing.go"), "package ignored\n");
|
||||
|
||||
cbm_discover_opts_t opts = {0};
|
||||
cbm_file_info_t *files = NULL;
|
||||
int count = 0;
|
||||
|
||||
int rc = cbm_discover(repo, &opts, &files, &count);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(count, 1);
|
||||
ASSERT_TRUE(discover_has_rel_path(files, count, "main.go"));
|
||||
ASSERT_FALSE(discover_has_rel_path(files, count, "secret.go"));
|
||||
ASSERT_FALSE(discover_has_rel_path(files, count, "ignored_dir/thing.go"));
|
||||
|
||||
cbm_discover_free(files, count);
|
||||
restore_git_env(&env);
|
||||
th_cleanup(base);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(discover_global_excludesfile_from_gitconfig_tilde) {
|
||||
git_env_snapshot_t env = save_git_env();
|
||||
char *tmp = th_mktempdir("cbm_disc_global_cfg");
|
||||
ASSERT(tmp != NULL);
|
||||
|
||||
char base[512], repo[512], home[512];
|
||||
snprintf(base, sizeof(base), "%s", tmp);
|
||||
snprintf(repo, sizeof(repo), "%s/repo", base);
|
||||
snprintf(home, sizeof(home), "%s/home", base);
|
||||
|
||||
cbm_setenv("HOME", home, 1);
|
||||
cbm_unsetenv("XDG_CONFIG_HOME");
|
||||
|
||||
th_mkdir_p(TH_PATH(repo, ".git"));
|
||||
th_write_file(TH_PATH(repo, ".git/config"), "[core]\n");
|
||||
th_write_file(TH_PATH(home, ".gitconfig"), "[core]\n excludesFile = ~/custom-ignore\n");
|
||||
th_write_file(TH_PATH(home, "custom-ignore"), "skip-me.go\n");
|
||||
th_write_file(TH_PATH(repo, "keep.go"), "package keep\n");
|
||||
th_write_file(TH_PATH(repo, "skip-me.go"), "package skip\n");
|
||||
|
||||
cbm_discover_opts_t opts = {0};
|
||||
cbm_file_info_t *files = NULL;
|
||||
int count = 0;
|
||||
|
||||
int rc = cbm_discover(repo, &opts, &files, &count);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(count, 1);
|
||||
ASSERT_TRUE(discover_has_rel_path(files, count, "keep.go"));
|
||||
ASSERT_FALSE(discover_has_rel_path(files, count, "skip-me.go"));
|
||||
|
||||
cbm_discover_free(files, count);
|
||||
restore_git_env(&env);
|
||||
th_cleanup(base);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(discover_missing_global_excludes_is_noop) {
|
||||
git_env_snapshot_t env = save_git_env();
|
||||
char *tmp = th_mktempdir("cbm_disc_global_missing");
|
||||
ASSERT(tmp != NULL);
|
||||
|
||||
char base[512], repo[512], home[512];
|
||||
snprintf(base, sizeof(base), "%s", tmp);
|
||||
snprintf(repo, sizeof(repo), "%s/repo", base);
|
||||
snprintf(home, sizeof(home), "%s/home", base);
|
||||
|
||||
cbm_setenv("HOME", home, 1);
|
||||
cbm_unsetenv("XDG_CONFIG_HOME");
|
||||
|
||||
th_mkdir_p(TH_PATH(repo, ".git"));
|
||||
th_write_file(TH_PATH(repo, ".git/config"), "[core]\n");
|
||||
th_write_file(TH_PATH(repo, "main.go"), "package main\n");
|
||||
th_write_file(TH_PATH(repo, "would-be-global.go"), "package global\n");
|
||||
|
||||
cbm_discover_opts_t opts = {0};
|
||||
cbm_file_info_t *files = NULL;
|
||||
int count = 0;
|
||||
|
||||
int rc = cbm_discover(repo, &opts, &files, &count);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(count, 2);
|
||||
ASSERT_TRUE(discover_has_rel_path(files, count, "main.go"));
|
||||
ASSERT_TRUE(discover_has_rel_path(files, count, "would-be-global.go"));
|
||||
|
||||
cbm_discover_free(files, count);
|
||||
restore_git_env(&env);
|
||||
th_cleanup(base);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(discover_cbmignore_negates_global_ignore) {
|
||||
git_env_snapshot_t env = save_git_env();
|
||||
char *tmp = th_mktempdir("cbm_disc_global_neg");
|
||||
ASSERT(tmp != NULL);
|
||||
|
||||
char base[512], repo[512], home[512], xdg[512];
|
||||
snprintf(base, sizeof(base), "%s", tmp);
|
||||
snprintf(repo, sizeof(repo), "%s/repo", base);
|
||||
snprintf(home, sizeof(home), "%s/home", base);
|
||||
snprintf(xdg, sizeof(xdg), "%s/xdg", base);
|
||||
|
||||
cbm_setenv("HOME", home, 1);
|
||||
cbm_setenv("XDG_CONFIG_HOME", xdg, 1);
|
||||
|
||||
th_mkdir_p(TH_PATH(repo, ".git"));
|
||||
th_write_file(TH_PATH(repo, ".git/config"), "[core]\n");
|
||||
th_write_file(TH_PATH(xdg, "git/ignore"), "rescued.go\nblocked.go\n");
|
||||
th_write_file(TH_PATH(repo, ".cbmignore"), "!rescued.go\n");
|
||||
th_write_file(TH_PATH(repo, "main.go"), "package main\n");
|
||||
th_write_file(TH_PATH(repo, "rescued.go"), "package rescued\n");
|
||||
th_write_file(TH_PATH(repo, "blocked.go"), "package blocked\n");
|
||||
|
||||
cbm_discover_opts_t opts = {0};
|
||||
cbm_file_info_t *files = NULL;
|
||||
int count = 0;
|
||||
|
||||
int rc = cbm_discover(repo, &opts, &files, &count);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(count, 2);
|
||||
ASSERT_TRUE(discover_has_rel_path(files, count, "main.go"));
|
||||
ASSERT_TRUE(discover_has_rel_path(files, count, "rescued.go"));
|
||||
ASSERT_FALSE(discover_has_rel_path(files, count, "blocked.go"));
|
||||
|
||||
cbm_discover_free(files, count);
|
||||
restore_git_env(&env);
|
||||
th_cleanup(base);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* issue #234: a directory listed in the root .gitignore (e.g. "vendor/") must
|
||||
* be excluded from discovery even when untracked — Composer/PHP projects rely
|
||||
* on this. */
|
||||
@@ -778,6 +965,10 @@ SUITE(discover) {
|
||||
RUN_TEST(discover_simple);
|
||||
RUN_TEST(discover_skips_git_dir);
|
||||
RUN_TEST(discover_with_gitignore);
|
||||
RUN_TEST(discover_with_global_xdg_ignore);
|
||||
RUN_TEST(discover_global_excludesfile_from_gitconfig_tilde);
|
||||
RUN_TEST(discover_missing_global_excludes_is_noop);
|
||||
RUN_TEST(discover_cbmignore_negates_global_ignore);
|
||||
RUN_TEST(discover_gitignore_dir_excluded_issue234);
|
||||
RUN_TEST(discover_max_file_size);
|
||||
RUN_TEST(discover_null_path);
|
||||
|
||||
Reference in New Issue
Block a user