perf(mcp): resolve list_projects branches with one Git child

Previous behavior: src/mcp/mcp.c:3895 called cbm_git_context_resolve for the single branch field. Attached repositories could execute seven Git commands per project, and a rejected status-based replacement took 3,380.410 ms for the isolated 50-project page.

Add src/git/git_context.c:115 resolve_current_branch as the single attached/detached/unborn implementation shared by cbm_git_context_resolve and cbm_git_current_branch. cbm_git_run_first_line_buf drains stdout and reaps symbolic-ref once; cbm_pclose_exit_code normalizes POSIX wait status and Windows GetExitCodeProcess results. Exit 0 returns attached/unborn names, exit 1 returns DETACHED, and non-Git errors remain absent.

Route CBM_ONLY_SUITE=git_context in tests/test_main.c:386 so focused runs execute four tests instead of traversing high-memory fixtures. tests/test_git_context.c:252 covers attached, detached, unborn, non-Git, full-context parity, heap cleanup, and portable command setup; tests/test_security.c:621 covers normalized exit 7.

Verification: Git context ASAN/UBSan 4/4; normalized-exit canary 1/1; MCP ASAN/UBSan 224/224; scripts/check-source-safety.sh passed; staged diff check passed. The -O2 binary returned the isolated default page in 773.848 ms and explicit full inventory in 5,023.220 ms, preserved 41/53 branch fields including the unborn main branch, and reaped both server processes and all reader threads.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-07-16 06:09:34 -04:00
parent d10662a90b
commit aad95af5eb
10 changed files with 186 additions and 8 deletions
+41
View File
@@ -110,6 +110,47 @@ int cbm_git_capture_first_line(const char *repo_path, const char *git_args, char
return *out ? 0 : CBM_NOT_FOUND;
}
int cbm_git_run_first_line_buf(const char *repo_path, const char *git_args,
char *out, size_t out_size, int *out_exit_code) {
if (!out || out_size == 0 || !out_exit_code) {
return CBM_NOT_FOUND;
}
out[0] = '\0';
*out_exit_code = CBM_NOT_FOUND;
char cmd[CBM_GIT_CMD_BUFSZ];
if (!cbm_git_format_command(cmd, sizeof(cmd), repo_path, git_args)) {
return CBM_NOT_FOUND;
}
FILE *fp = cbm_popen(cmd, "r");
if (!fp) {
return CBM_NOT_FOUND;
}
char line[CBM_GIT_OUTPUT_BUFSZ];
bool got_line = fgets(line, (int)sizeof(line), fp) != NULL;
size_t line_len = got_line ? strlen(line) : 0;
bool truncated = got_line && line_len > 0 && line[line_len - 1] != '\n' && !feof(fp);
bool output_fits = true;
if (got_line) {
git_trim_newlines(line);
size_t value_len = strlen(line);
if (value_len >= out_size) {
output_fits = false;
} else {
memcpy(out, line, value_len + 1);
}
}
char drain[CBM_SZ_128];
while (fgets(drain, (int)sizeof(drain), fp)) {
}
*out_exit_code = cbm_pclose_exit_code(fp);
if (truncated || !output_fits) {
out[0] = '\0';
return CBM_NOT_FOUND;
}
return 0;
}
int cbm_git_drain_command(const char *repo_path, const char *git_args) {
char cmd[CBM_GIT_CMD_BUFSZ];
if (!cbm_git_format_command(cmd, sizeof(cmd), repo_path, git_args)) {
+2
View File
@@ -20,6 +20,8 @@ bool cbm_git_format_status_command(char *cmd, size_t cmd_size, const char *repo_
int cbm_git_capture_first_line_buf(const char *repo_path, const char *git_args,
char *out, size_t out_size);
int cbm_git_capture_first_line(const char *repo_path, const char *git_args, char **out);
int cbm_git_run_first_line_buf(const char *repo_path, const char *git_args,
char *out, size_t out_size, int *out_exit_code);
int cbm_git_drain_command(const char *repo_path, const char *git_args);
#endif
+30 -3
View File
@@ -112,6 +112,28 @@ static char *derive_canonical_root(const char *input_path, const char *worktree_
return root;
}
static int resolve_current_branch(const char *path, char **out_branch) {
if (!out_branch) {
return CBM_NOT_FOUND;
}
*out_branch = NULL;
if (!path || !path[0]) {
return CBM_NOT_FOUND;
}
char branch[CBM_GIT_OUTPUT_BUFSZ];
int exit_code = CBM_NOT_FOUND;
if (cbm_git_run_first_line_buf(path, "symbolic-ref --quiet --short HEAD", branch,
sizeof(branch), &exit_code) != 0) {
return CBM_NOT_FOUND;
}
const char *resolved = exit_code == 0 && branch[0] ? branch : exit_code == 1 ? "DETACHED" : NULL;
if (!resolved) {
return CBM_NOT_FOUND;
}
*out_branch = cbm_strdup(resolved);
return *out_branch ? 0 : CBM_NOT_FOUND;
}
static char *slug_from_branch(const char *branch, bool detached) {
const char *fallback = detached ? "detached" : "working-tree";
const char *src = detached ? fallback : (branch && branch[0] ? branch : fallback);
@@ -203,9 +225,10 @@ int cbm_git_context_resolve(const char *path, cbm_git_context_t *out) {
out->head_sha = cbm_strdup("");
}
if (cbm_git_capture_first_line(path, "symbolic-ref --quiet --short HEAD", &out->branch) !=
0) {
out->branch = cbm_strdup("DETACHED");
if (resolve_current_branch(path, &out->branch) != 0) {
out->branch = NULL;
}
if (out->branch && strcmp(out->branch, "DETACHED") == 0) {
out->is_detached = true;
}
@@ -233,6 +256,10 @@ int cbm_git_context_resolve(const char *path, cbm_git_context_t *out) {
return 0;
}
int cbm_git_current_branch(const char *path, char **out_branch) {
return resolve_current_branch(path, out_branch);
}
char *cbm_git_context_branch_qn(const char *project_name, const cbm_git_context_t *ctx) {
const char *project = project_name && project_name[0] ? project_name : "project";
const char *slug = "working-tree";
+3
View File
@@ -21,6 +21,9 @@ typedef struct {
int cbm_git_context_resolve(const char *path, cbm_git_context_t *out);
void cbm_git_context_free(cbm_git_context_t *ctx);
/* Resolve only the current branch for branch-only callers. Returns a heap
* string (including "DETACHED") through out_branch on success. */
int cbm_git_current_branch(const char *path, char **out_branch);
char *cbm_git_context_branch_qn(const char *project_name, const cbm_git_context_t *ctx);
int cbm_git_context_props_json(const cbm_git_context_t *ctx, char *buf, int buf_size);