Skip to content

Commit 5689ea5

Browse files
authored
Merge pull request DeusData#1561 from tmonestudio/codex/bt-240-truthful-index-freshness
feat(mcp): report truthful index freshness from checkout evidence
2 parents 334a408 + c1f30fd commit 5689ea5

12 files changed

Lines changed: 1424 additions & 10 deletions

‎src/git/git_context.c‎

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "foundation/compat_fs.h"
44
#include "foundation/constants.h"
55
#include "foundation/log.h"
6+
#include "foundation/mem_core.h"
67
#include "foundation/platform.h"
78
#include "foundation/str_util.h"
89

@@ -17,6 +18,17 @@ enum {
1718
GIT_OUTPUT_MAX = 4096,
1819
};
1920

21+
/* Bounded worktree-status stream constants.
22+
* STATUS_FIELD_MAX is the largest single NUL-delimited record field we
23+
* buffer; a field at/over the bound is treated as a malformed stream and the
24+
* whole snapshot fails closed. PATH components of real repos are far below
25+
* this even with long paths. */
26+
enum {
27+
STATUS_HEADER_LEN = 3, /* porcelain-v1 record prefix "XY " */
28+
STATUS_CHUNK_MAX = CBM_SZ_4K,
29+
STATUS_FIELD_MAX = CBM_SZ_16K,
30+
};
31+
2032
static char *git_strdup(const char *s) {
2133
if (!s) {
2234
s = "";
@@ -641,3 +653,210 @@ int cbm_git_context_props_json(const cbm_git_context_t *ctx, char *buf, int buf_
641653
}
642654
return off;
643655
}
656+
657+
/* ── Bounded worktree status ──────────────────────────────────────────────
658+
* git status --porcelain=v1 -z --untracked-files=all is parsed record by
659+
* record, NUL-delimited, over a bounded chunk buffer. The full stream is
660+
* counted but only max_samples paths per class (tracked / untracked) are
661+
* ever retained, so a huge dirty tree cannot grow memory. Rename/copy
662+
* records carry a second NUL-terminated path (the source); it is consumed
663+
* without being counted so one rename counts once. */
664+
665+
typedef struct {
666+
cbm_worktree_status_t *out;
667+
int max_samples;
668+
bool expecting_source; /* next field is the source path of an R/C record */
669+
bool parse_ok;
670+
char partial[STATUS_FIELD_MAX];
671+
size_t partial_len;
672+
} status_parser_t;
673+
674+
/* porcelain-v1 status columns (X/Y): documented codes plus the lowercase
675+
* submodule variants and type-change 'T'. Anything else is not a record
676+
* header, so a malformed stream fails closed instead of being misread. */
677+
static bool status_code_ok(char c) {
678+
switch (c) {
679+
case ' ':
680+
case 'M':
681+
case 'A':
682+
case 'D':
683+
case 'R':
684+
case 'C':
685+
case 'U':
686+
case '?':
687+
case '!':
688+
case 'T':
689+
case 'm':
690+
case 'a':
691+
case 'd':
692+
case 'r':
693+
case 'c':
694+
case 'u':
695+
case 't':
696+
return true;
697+
default:
698+
return false;
699+
}
700+
}
701+
702+
/* Retain one bounded sample; the running total always advances so the caller
703+
* sees the true stream count even when samples are capped or OOM. */
704+
static void status_add_sample(int *total, char ***samples, int *sample_count, bool *truncated,
705+
int max_samples, const char *path, size_t path_len) {
706+
(*total)++;
707+
if (*sample_count >= max_samples) {
708+
*truncated = true;
709+
return;
710+
}
711+
char *copy = (char *)cbm_alloc(CBM_MEM_CLASS_OTHER, path_len + 1);
712+
if (!copy) {
713+
*truncated = true;
714+
return;
715+
}
716+
memcpy(copy, path, path_len);
717+
copy[path_len] = '\0';
718+
char **grown = (char **)cbm_realloc(CBM_MEM_CLASS_OTHER, *samples,
719+
(size_t)(*sample_count + 1) * sizeof(char *));
720+
if (!grown) {
721+
cbm_free(CBM_MEM_CLASS_OTHER, copy);
722+
*truncated = true;
723+
return;
724+
}
725+
*samples = grown;
726+
(*samples)[*sample_count] = copy;
727+
(*sample_count)++;
728+
}
729+
730+
/* Parse one complete NUL-delimited field. Returns false on malformed input,
731+
* which fails the whole snapshot closed. */
732+
static bool status_parse_record(cbm_worktree_status_t *out, status_parser_t *parser,
733+
const char *field, size_t field_len) {
734+
if (parser->expecting_source) {
735+
parser->expecting_source = false;
736+
return true; /* second path of a rename/copy record — not counted */
737+
}
738+
if (field_len < STATUS_HEADER_LEN || field[2] != ' ' || !status_code_ok(field[0]) ||
739+
!status_code_ok(field[1])) {
740+
return false;
741+
}
742+
const char *path = field + STATUS_HEADER_LEN;
743+
size_t path_len = field_len - STATUS_HEADER_LEN;
744+
if (field[0] == '?' && field[1] == '?') {
745+
status_add_sample(&out->untracked_count, &out->untracked_paths,
746+
&out->untracked_sample_count, &out->untracked_truncated,
747+
parser->max_samples, path, path_len);
748+
return true;
749+
}
750+
if (field[0] == 'R' || field[0] == 'C' || field[1] == 'R' || field[1] == 'C') {
751+
parser->expecting_source = true;
752+
}
753+
status_add_sample(&out->tracked_count, &out->tracked_paths, &out->tracked_sample_count,
754+
&out->tracked_truncated, parser->max_samples, path, path_len);
755+
return true;
756+
}
757+
758+
/* Feed a chunk of the NUL-delimited stream through the bounded record parser. */
759+
static void status_feed(status_parser_t *parser, const char *data, size_t data_len) {
760+
size_t i = 0;
761+
while (i < data_len) {
762+
if (parser->partial_len >= STATUS_FIELD_MAX) {
763+
parser->parse_ok = false; /* field exceeded the bound: malformed */
764+
return;
765+
}
766+
size_t j = i;
767+
while (j < data_len && data[j] != '\0' &&
768+
parser->partial_len + (j - i) < STATUS_FIELD_MAX) {
769+
j++;
770+
}
771+
size_t take = j - i;
772+
memcpy(parser->partial + parser->partial_len, data + i, take);
773+
parser->partial_len += take;
774+
i += take;
775+
if (i < data_len && data[i] == '\0') {
776+
if (!status_parse_record(parser->out, parser, parser->partial, parser->partial_len)) {
777+
parser->parse_ok = false;
778+
return;
779+
}
780+
parser->partial_len = 0;
781+
i++; /* consume the record terminator */
782+
} else if (i >= data_len) {
783+
return; /* chunk exhausted mid-record */
784+
}
785+
}
786+
}
787+
788+
int cbm_git_worktree_status(const char *validated_root, int max_samples,
789+
cbm_worktree_status_t *out) {
790+
if (!out) {
791+
return CBM_NOT_FOUND;
792+
}
793+
memset(out, 0, sizeof(*out));
794+
if (!validated_root || !validated_root[0] || max_samples < 0) {
795+
return CBM_NOT_FOUND;
796+
}
797+
if (!git_validate_repo_path(validated_root)) {
798+
return CBM_NOT_FOUND;
799+
}
800+
801+
char cmd[GIT_CMD_MAX];
802+
#ifdef _WIN32
803+
const char *null_dev = "NUL";
804+
#else
805+
const char *null_dev = "/dev/null";
806+
#endif
807+
int n = snprintf(cmd, sizeof(cmd),
808+
"git -C \"%s\" status --porcelain=v1 -z --untracked-files=all 2>%s",
809+
validated_root, null_dev);
810+
if (n < 0 || n >= (int)sizeof(cmd)) {
811+
return CBM_NOT_FOUND;
812+
}
813+
814+
/* An inherited GIT_DIR/GIT_WORK_TREE (a git hook, an editor) overrides
815+
* `git -C`, which would report another repository's worktree here. */
816+
FILE *fp = cbm_popen_git(cmd);
817+
if (!fp) {
818+
return 0; /* out is zeroed: available=false */
819+
}
820+
821+
status_parser_t parser;
822+
memset(&parser, 0, sizeof(parser));
823+
parser.out = out;
824+
parser.max_samples = max_samples;
825+
parser.parse_ok = true;
826+
827+
char chunk[STATUS_CHUNK_MAX];
828+
size_t got;
829+
bool io_error = false;
830+
while (parser.parse_ok && (got = fread(chunk, 1, sizeof(chunk), fp)) > 0) {
831+
status_feed(&parser, chunk, got);
832+
}
833+
if (ferror(fp)) {
834+
io_error = true;
835+
}
836+
int rc = cbm_pclose(fp);
837+
838+
if (rc != 0 || !parser.parse_ok || io_error || parser.partial_len > 0) {
839+
/* Fail closed: any deviation from a clean, complete status stream is a
840+
* machine-readable "unavailable", never a clean worktree. */
841+
cbm_git_worktree_status_free(out);
842+
return 0;
843+
}
844+
845+
out->available = true;
846+
return 0;
847+
}
848+
849+
void cbm_git_worktree_status_free(cbm_worktree_status_t *st) {
850+
if (!st) {
851+
return;
852+
}
853+
for (int i = 0; i < st->tracked_sample_count; i++) {
854+
cbm_free(CBM_MEM_CLASS_OTHER, st->tracked_paths[i]);
855+
}
856+
cbm_free(CBM_MEM_CLASS_OTHER, st->tracked_paths);
857+
for (int i = 0; i < st->untracked_sample_count; i++) {
858+
cbm_free(CBM_MEM_CLASS_OTHER, st->untracked_paths[i]);
859+
}
860+
cbm_free(CBM_MEM_CLASS_OTHER, st->untracked_paths);
861+
memset(st, 0, sizeof(*st));
862+
}

‎src/git/git_context.h‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,40 @@ void cbm_git_context_free(cbm_git_context_t *ctx);
5050
char *cbm_git_context_branch_qn(const char *project_name, const cbm_git_context_t *ctx);
5151
int cbm_git_context_props_json(const cbm_git_context_t *ctx, char *buf, int buf_size);
5252

53+
/* Bounded snapshot of the git worktree status for one validated repo root.
54+
*
55+
* available is true ONLY when `git status --porcelain=v1 -z --untracked-files=all`
56+
* exited 0 AND every NUL-delimited record parsed cleanly. Any other outcome —
57+
* shell-unsafe path, spawn failure, nonzero exit, malformed/oversized record,
58+
* trailing partial record — leaves available=false with zero counts and no
59+
* samples. Callers MUST treat !available as "status unavailable", never as a
60+
* clean worktree.
61+
*
62+
* The entire stream is counted, but only max_samples paths per class (tracked
63+
* / untracked) are ever retained, in stream order; exceeding the cap sets the
64+
* matching *_truncated flag. Rename/copy records are counted once (their two
65+
* NUL-separated paths are consumed without counting the second), and the
66+
* sampled path is the record's NEW path. Ignored files never appear because
67+
* --ignored is not requested.
68+
*
69+
* cbm_git_worktree_status returns 0 when *out has been filled (available tells
70+
* whether the snapshot succeeded) and CBM_NOT_FOUND for invalid arguments
71+
* (NULL root/out or max_samples < 0). The caller owns the returned samples via
72+
* cbm_git_worktree_status_free. */
73+
typedef struct {
74+
bool available;
75+
int tracked_count;
76+
int untracked_count;
77+
char **tracked_paths; /* heap-owned samples, tracked_sample_count entries */
78+
int tracked_sample_count;
79+
bool tracked_truncated;
80+
char **untracked_paths;
81+
int untracked_sample_count;
82+
bool untracked_truncated;
83+
} cbm_worktree_status_t;
84+
85+
int cbm_git_worktree_status(const char *validated_root, int max_samples,
86+
cbm_worktree_status_t *out);
87+
void cbm_git_worktree_status_free(cbm_worktree_status_t *st);
88+
5389
#endif

0 commit comments

Comments
 (0)