mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
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:
1 parent
610531ade3
commit
2e055d771f
10 files changed
+15
-214
No files matched your search
@@ -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
@@ -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 ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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(¤t, 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(¤t);
|
||||
}
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user