chore: drop optional libgit2 dependency, keep git log fallback

libgit2 is licensed GPLv2-with-linking-exception, and this project is
deliberately GPL-free (see scripts/license-policy.json). libgit2 was only
an OPTIONAL, faster git-history code path guarded by HAVE_LIBGIT2 and
auto-detected via pkg-config; it always shipped with a popen("git log ...")
fallback that release binaries already used (they were built without
libgit2). Making that popen fallback the SOLE git-history implementation
means there is NO change to shipped behavior.

Changes:
- pass_githistory.c: collapse the HAVE_LIBGIT2 #ifdef so only the popen
  parse_git_log remains; drop the now-unused <git2.h>/<time.h> includes.
- cbm.c / cbm.h / main.c / subprocess.c / index_supervisor.h: remove the
  libgit2 mimalloc allocator bind and its >=1.7.0 version guards; keep the
  tree-sitter + sqlite3 binds; correct the comments.
- Makefile.cbm: remove the optional-libgit2 pkg-config detection block,
  the REQUIRE_LIBGIT2 error gate, and all LIBGIT2 CFLAGS/LIBS/FLAGS.
- .github/workflows/_test.yml: drop the REQUIRE_LIBGIT2 test-matrix leg
  plus its libgit2-dev/pkg-config apt install (lower CI cost, no new
  gating, no trigger change).
- flake.nix: drop libgit2/pkg-config from the dev shell (now-dead dep).

HAVE_LIBGIT2 no longer exists anywhere in the tree.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-07-05 00:37:59 +02:00
1 parent 610531ade3
commit 2e055d771f
10 files changed
+15 -214

No files matched your search

+3 -8
View File
@@ -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++"}
@@ -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' || '' }}
+4 -29
View File
@@ -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)
@@ -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 \
@@ -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 ─────────────────────────────────────────────────
-4
View File
@@ -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 ];
};
});
};
+1 -55
View File
@@ -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>
@@ -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
@@ -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) {
@@ -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 */
}
+2 -2
View File
@@ -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.
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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.
*/
+1 -112
View File
@@ -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;
@@ -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;