Skip to content
Merged
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
11 changes: 3 additions & 8 deletions .github/workflows/_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
BROAD: ${{ inputs.broad_platforms }}
run: |
CORE_UNIX='[
{"os":"ubuntu-latest","cc":"gcc","cxx":"g++","libgit2":true},
{"os":"ubuntu-latest","cc":"gcc","cxx":"g++"},
{"os":"ubuntu-24.04-arm","cc":"gcc","cxx":"g++"},
{"os":"macos-14","cc":"cc","cxx":"c++"},
{"os":"macos-15-intel","cc":"cc","cxx":"c++"}
Expand Down Expand Up @@ -79,15 +79,10 @@ jobs:

- name: Install deps (Ubuntu)
if: startsWith(matrix.os, 'ubuntu')
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev ${{ matrix.libgit2 == true && 'libgit2-dev pkg-config' || '' }}
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev

- name: Test
run: |
if [ "${{ matrix.libgit2 == true }}" = "true" ]; then
echo "REQUIRE_LIBGIT2=1"
pkg-config --modversion libgit2
fi
scripts/test.sh CC=${{ matrix.cc }} CXX=${{ matrix.cxx }} ${{ matrix.libgit2 == true && 'REQUIRE_LIBGIT2=1' || '' }}
run: scripts/test.sh CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
env:
CBM_SKIP_PERF: ${{ inputs.skip_perf && '1' || '' }}

Expand Down
33 changes: 4 additions & 29 deletions Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -34,30 +34,6 @@ TS_INCLUDE = $(CBM_DIR)/vendored/ts_runtime/include
# This ensures we use our vendored copies instead of requiring system libicu-dev.
TS_SRC = $(CBM_DIR)/vendored/ts_runtime/src

# ── Optional libgit2 (faster git history parsing) ────────────────
# Auto-detected via pkg-config. Falls back to popen("git log ...") if absent
# or too old for the git_allocator ABI used by the production allocator bind.
PKG_CONFIG ?= pkg-config
LIBGIT2_MIN_VERSION := 1.7.0
LIBGIT2_AVAILABLE := $(shell $(PKG_CONFIG) --atleast-version=$(LIBGIT2_MIN_VERSION) libgit2 >/dev/null 2>&1 && echo yes || echo no)

ifeq ($(REQUIRE_LIBGIT2),1)
ifneq ($(LIBGIT2_AVAILABLE),yes)
LIBGIT2_VERSION := $(shell $(PKG_CONFIG) --modversion libgit2 2>/dev/null)
$(error libgit2 >= $(LIBGIT2_MIN_VERSION) is required when REQUIRE_LIBGIT2=1; pkg-config found $(if $(LIBGIT2_VERSION),$(LIBGIT2_VERSION),none))
endif
endif

ifeq ($(LIBGIT2_AVAILABLE),yes)
LIBGIT2_CFLAGS := $(shell $(PKG_CONFIG) --cflags libgit2 2>/dev/null)
LIBGIT2_LIBS := $(shell $(PKG_CONFIG) --libs libgit2 2>/dev/null)
LIBGIT2_FLAGS = -DHAVE_LIBGIT2 $(LIBGIT2_CFLAGS)
else
LIBGIT2_CFLAGS =
LIBGIT2_FLAGS =
LIBGIT2_LIBS =
endif

# GCC-only warning suppressions (Clang rejects unknown -Wno-* with -Werror).
# Detect GCC by checking for __GNUC__ without __clang__ — handles all versions.
IS_GCC := $(shell echo | $(CC) -dM -E - 2>/dev/null | grep -q '__GNUC__' && ! echo | $(CC) -dM -E - 2>/dev/null | grep -q '__clang__' && echo yes || echo no)
Expand All @@ -72,8 +48,7 @@ CFLAGS_COMMON = -std=c11 -D_DEFAULT_SOURCE -D_GNU_SOURCE -Wall -Wextra -Werror \
$(GCC_ONLY_FLAGS) \
-Isrc -Ivendored -Ivendored/sqlite3 \
-Ivendored/mimalloc/include \
-I$(CBM_DIR) -I$(TS_INCLUDE) \
$(LIBGIT2_FLAGS)
-I$(CBM_DIR) -I$(TS_INCLUDE)

CXXFLAGS_COMMON = -std=c++14 -Wall -Wextra -Werror \
-Wno-unused-parameter \
Expand Down Expand Up @@ -111,9 +86,9 @@ ifeq ($(STATIC),1)
STATIC_FLAGS := -static
endif

LDFLAGS = -lm -lstdc++ -lpthread -lz $(LIBGIT2_LIBS) $(WIN32_LIBS) $(STATIC_FLAGS)
LDFLAGS_TEST = -lm -lstdc++ -lpthread -lz $(SANITIZE) $(LIBGIT2_LIBS) $(WIN32_LIBS)
LDFLAGS_TSAN = -lm -lstdc++ -lpthread -lz -fsanitize=thread $(LIBGIT2_LIBS) $(WIN32_LIBS)
LDFLAGS = -lm -lstdc++ -lpthread -lz $(WIN32_LIBS) $(STATIC_FLAGS)
LDFLAGS_TEST = -lm -lstdc++ -lpthread -lz $(SANITIZE) $(WIN32_LIBS)
LDFLAGS_TSAN = -lm -lstdc++ -lpthread -lz -fsanitize=thread $(WIN32_LIBS)

# ── Source files ─────────────────────────────────────────────────

Expand Down
4 changes: 0 additions & 4 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@
devShells = forAllSystems (pkgs: {
default = pkgs.mkShell {
inputsFrom = [ self.packages.${pkgs.system}.default ];
# libgit2 is an optional dependency auto-detected via pkg-config at
# build time. When present it accelerates git history parsing;
# otherwise the build falls back to shelling out to `git log`.
packages = [ pkgs.pkg-config pkgs.libgit2 ];
};
});
};
Expand Down
56 changes: 1 addition & 55 deletions internal/cbm/cbm.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,6 @@
#include "mimalloc.h" // mi_malloc/mi_calloc/mi_realloc/mi_free/mi_usable_size — bind 3rd-party allocators (#424)
#if defined(CBM_BIND_TS_ALLOCATOR) && CBM_BIND_TS_ALLOCATOR
#include "sqlite3.h" // sqlite3_mem_methods, sqlite3_config, SQLITE_CONFIG_MALLOC — bind sqlite to mimalloc
#if defined(HAVE_LIBGIT2)
#include <git2/version.h>
#if defined(LIBGIT2_VERSION_CHECK)
#if !LIBGIT2_VERSION_CHECK(1, 7, 0)
#error "HAVE_LIBGIT2 requires libgit2 >= 1.7.0 for git_allocator"
#endif
#elif defined(LIBGIT2_VER_MAJOR) && defined(LIBGIT2_VER_MINOR) && defined(LIBGIT2_VER_REVISION)
#if ((LIBGIT2_VER_MAJOR * 1000000) + (LIBGIT2_VER_MINOR * 10000) + (LIBGIT2_VER_REVISION * 100)) < \
1070000
#error "HAVE_LIBGIT2 requires libgit2 >= 1.7.0 for git_allocator"
#endif
#else
#error "HAVE_LIBGIT2 requires known libgit2 version macros for the >= 1.7.0 git_allocator guard"
#endif
#include <git2.h> // git_libgit2_opts, GIT_OPT_SET_ALLOCATOR — bind libgit2 to mimalloc
#include <git2/sys/alloc.h> // git_allocator — not pulled in by <git2.h> (it's a sys/ header)
#endif
#endif
#include <stdint.h> // uint32_t, uint64_t, int64_t
#include <stdlib.h>
Expand Down Expand Up @@ -256,7 +239,7 @@ static TSParser *get_thread_parser(const TSLanguage *ts_lang, CBMLanguage lang)

// --- Allocator binding (defense-in-depth, #424) ---

/* Bind tree-sitter, sqlite3, and libgit2 to mimalloc explicitly so a correct
/* Bind tree-sitter and sqlite3 to mimalloc explicitly so a correct
* binary does NOT depend on the fragile MI_OVERRIDE symbol override. Under
* MI_OVERRIDE=1 — particularly the Windows static-MinGW link with
* --allow-multiple-definition — `malloc`/`free` can resolve to DIFFERENT
Expand Down Expand Up @@ -300,25 +283,6 @@ static int cbm_sqlite_meminit(void *appdata) {
static void cbm_sqlite_memshutdown(void *appdata) {
(void)appdata;
}

#if defined(HAVE_LIBGIT2)
/* libgit2 >= 1.7 git_allocator backed by mimalloc. The struct has exactly
* three members: gmalloc(size_t,file,line), grealloc(ptr,size,file,line),
* gfree(ptr). The file/line args are ignored. */
static void *cbm_git_malloc(size_t n, const char *file, int line) {
(void)file;
(void)line;
return mi_malloc(n);
}
static void *cbm_git_realloc(void *ptr, size_t size, const char *file, int line) {
(void)file;
(void)line;
return mi_realloc(ptr, size);
}
static void cbm_git_free(void *ptr) {
mi_free(ptr);
}
#endif /* HAVE_LIBGIT2 */
#endif /* CBM_BIND_TS_ALLOCATOR */

void cbm_alloc_init(void) {
Expand Down Expand Up @@ -349,24 +313,6 @@ void cbm_alloc_init(void) {
int sqlite_rc = sqlite3_config(SQLITE_CONFIG_MALLOC, &cbm_sqlite_mem);
assert(sqlite_rc == SQLITE_OK && "SQLITE_CONFIG_MALLOC must run before sqlite3_initialize");
(void)sqlite_rc;

#if defined(HAVE_LIBGIT2)
/* libgit2. GIT_OPT_SET_ALLOCATOR MUST be set BEFORE git_libgit2_init():
* libgit2's git_allocator_global_init (run during init) installs the default
* stdalloc only if no custom allocator is set yet
* (`if (git__allocator.gmalloc != git_failalloc_malloc) return 0;`), and the
* pre-init global is the fail-allocator. There is no allocator-reset on
* git_libgit2_shutdown, so binding once here — before pass_githistory's
* per-call git_libgit2_init/shutdown pairs ever run — persists for the whole
* process. git_libgit2_opts(GIT_OPT_SET_ALLOCATOR,...) itself does not
* allocate, so calling it before init is safe. */
static git_allocator cbm_git_alloc = {
cbm_git_malloc, /* gmalloc */
cbm_git_realloc, /* grealloc */
cbm_git_free, /* gfree */
};
git_libgit2_opts(GIT_OPT_SET_ALLOCATOR, &cbm_git_alloc);
#endif /* HAVE_LIBGIT2 */
#endif /* CBM_BIND_TS_ALLOCATOR */
}

Expand Down
4 changes: 2 additions & 2 deletions internal/cbm/cbm.h
Original file line number Diff line number Diff line change
Expand Up @@ -505,11 +505,11 @@ typedef struct {

// --- Public API ---

// Bind third-party allocators (tree-sitter, sqlite3, libgit2) to mimalloc as
// Bind third-party allocators (tree-sitter, sqlite3) to mimalloc as
// defense-in-depth, so they never depend on the fragile MI_OVERRIDE symbol
// override (#424). MUST be called as the very first statement of main(), before
// any sqlite3_open*/sqlite3_initialize (SQLITE_CONFIG_MALLOC returns
// SQLITE_MISUSE once sqlite has initialized) and before any git_libgit2_init.
// SQLITE_MISUSE once sqlite has initialized).
// Idempotent (static guard); intended for single-threaded startup. cbm_init()
// also calls it so non-main entry points (pipeline passes) still get the binds.
// In the test build (no CBM_BIND_TS_ALLOCATOR) this is a no-op.
Expand Down
2 changes: 1 addition & 1 deletion scripts/security-allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ src/mcp/mcp.c:cbm_popen:git ls-files count for auto-index (session_root validate
src/mcp/mcp.c:cbm_popen:update check to api.github.com (hardcoded URL)
src/mcp/mcp.c:popen:via cbm_popen wrapper calls

# ── Pipeline: git history parsing (fallback when libgit2 not available) ────
# ── Pipeline: git history parsing (git log) ────────────────────────────────
src/pipeline/pass_githistory.c:cbm_popen:git log for file history (path validated)
src/pipeline/pass_githistory.c:popen:via cbm_popen wrapper call

Expand Down
2 changes: 1 addition & 1 deletion src/foundation/subprocess.c
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ static int cbm_run_posix(const cbm_proc_opts_t *opts, cbm_proc_result_t *out) {
/* Child: redirect stdout+stderr to the log (or discard), then exec.
* Use open()+dup2() (async-signal-safe, no malloc) rather than freopen():
* the parent may be multithreaded (the MCP server holds worker/watcher/http
* threads plus mimalloc/sqlite/libgit2 global state), and a fork() copies
* threads plus mimalloc/sqlite global state), and a fork() copies
* only the calling thread — a malloc between fork and exec could deadlock on
* a lock another thread held at fork time. open/dup2/execv touch no heap. */
const char *bin = opts->bin;
Expand Down
2 changes: 1 addition & 1 deletion src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ static void setup_signal_handlers(void) {
}

int main(int argc, char **argv) {
/* Defense-in-depth: bind tree-sitter, sqlite3, and libgit2 to mimalloc so a
/* Defense-in-depth: bind tree-sitter and sqlite3 to mimalloc so a
* correct binary does not rely on the fragile MI_OVERRIDE symbol override
* (#424). MUST be the VERY FIRST statement: SQLITE_CONFIG_MALLOC has to run
* before the first sqlite3_open* (cbm_mcp_server_new → cbm_store_open_memory
Expand Down
2 changes: 1 addition & 1 deletion src/mcp/index_supervisor.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* module has no dependency on the response format.
*
* fork+exec only (never fork-and-run-in-child): the server holds persistent
* threads plus mimalloc/sqlite/libgit2 global state with no pthread_atfork, so a
* threads plus mimalloc/sqlite global state with no pthread_atfork, so a
* fork without exec would be a latent deadlock. Recursion is prevented by an argv
* flag (`--index-worker`), never an ambient env var.
*/
Expand Down
113 changes: 1 addition & 112 deletions src/pipeline/pass_githistory.c
Original file line number Diff line number Diff line change
Expand Up @@ -105,116 +105,7 @@ static void commit_free(commit_t *c) {
free(c->files);
}

/* ── libgit2-based git log parsing (preferred) ────────────────────── */

#ifdef HAVE_LIBGIT2
#include <git2.h>
#include <time.h>

static int parse_git_log(const char *repo_path, commit_t **out, int *out_count) {
*out = NULL;
*out_count = 0;

git_libgit2_init();

git_repository *repo = NULL;
if (git_repository_open(&repo, repo_path) != 0) {
git_libgit2_shutdown();
return CBM_NOT_FOUND;
}

/* Walk from HEAD, sorted chronologically */
git_revwalk *walker = NULL;
if (git_revwalk_new(&walker, repo) != 0) {
git_repository_free(repo);
git_libgit2_shutdown();
return CBM_NOT_FOUND;
}
git_revwalk_sorting(walker, GIT_SORT_TIME);
git_revwalk_push_head(walker);

/* 1 year cutoff, max 10k commits */
time_t cutoff = time(NULL) - (365L * 24 * 3600);
int max_commits = 10000;

int cap = CBM_SZ_64;
commit_t *commits = malloc(cap * sizeof(commit_t));
int count = 0;

git_oid oid;
while (git_revwalk_next(&oid, walker) == 0 && count < max_commits) {
git_commit *commit = NULL;
if (git_commit_lookup(&commit, repo, &oid) != 0) {
continue;
}

/* Check if commit is within the 6-month window */
git_time_t ct = git_commit_time(commit);
if ((time_t)ct < cutoff) {
git_commit_free(commit);
break; /* sorted by time — all subsequent commits are older */
}

/* Get commit tree and parent tree for diff */
git_tree *tree = NULL;
git_tree *parent_tree = NULL;
git_commit_tree(&tree, commit);

unsigned int nparents = git_commit_parentcount(commit);
if (nparents > 0) {
git_commit *parent = NULL;
if (git_commit_parent(&parent, commit, 0) == 0) {
git_commit_tree(&parent_tree, parent);
git_commit_free(parent);
}
}

/* Diff parent_tree → tree to find changed files */
git_diff *diff = NULL;
git_diff_options diff_opts;
git_diff_options_init(&diff_opts, GIT_DIFF_OPTIONS_VERSION);
if (git_diff_tree_to_tree(&diff, repo, parent_tree, tree, &diff_opts) == 0) {
commit_t current = {0};

size_t ndeltas = git_diff_num_deltas(diff);
for (size_t d = 0; d < ndeltas; d++) {
const git_diff_delta *delta = git_diff_get_delta(diff, d);
const char *path = delta->new_file.path;
if (path && cbm_is_trackable_file(path)) {
commit_add_file(&current, path);
}
}

if (current.count > 0) {
if (count >= cap) {
cap *= PAIR_LEN;
commits = safe_realloc(commits, cap * sizeof(commit_t));
}
current.timestamp = (long long)ct;
commits[count++] = current;
} else {
commit_free(&current);
}
git_diff_free(diff);
}

if (parent_tree) {
git_tree_free(parent_tree);
}
git_tree_free(tree);
git_commit_free(commit);
}

git_revwalk_free(walker);
git_repository_free(repo);
git_libgit2_shutdown();

*out = commits;
*out_count = count;
return 0;
}

#else /* !HAVE_LIBGIT2 — popen fallback */
/* ── git log parsing (popen "git log") ────────────────────────────── */

static int parse_git_log(const char *repo_path, commit_t **out, int *out_count) {
*out = NULL;
Expand Down Expand Up @@ -297,8 +188,6 @@ static int parse_git_log(const char *repo_path, commit_t **out, int *out_count)
return 0;
}

#endif /* HAVE_LIBGIT2 */

/* Callback to free hash table entries. */
static void free_counter(const char *key, void *val, void *ud) {
(void)ud;
Expand Down
Loading