/* Independent local-object fixtures for mcp/test_impact_git.h. * No implementation helpers or command-count assumptions are used. */ #include "test_framework.h" #include "test_helpers.h" #include #include #include #include #include #include #include enum { GF_PATH = 4096, GF_SMALL = 512 }; static const unsigned char gf_binary[] = {'A', 0, '\r', '\n', '\n', 'Z'}; static const unsigned char gf_new_binary[] = {'N', 0, 'B'}; static const char gf_old_text[] = "before first\ncontext\nbefore last\n"; static const char gf_head_text[] = "after first\ncontext\nafter last\n"; static const char gf_base_text[] = "base branch only\n"; static const char *gf_runner_binary; typedef struct { char home[GF_PATH], repo[GF_PATH], git[GF_PATH]; char capture[GF_PATH], log[GF_PATH], object_file[GF_PATH]; char a[65], p[65], q[65], head[65], base[65]; char binary[65], newer[65], old_text[65], head_text[65], base_text[65], attrs[65], link[65]; unsigned oid_length; } gf_fixture_t; static bool gf_path(char *out, const char *root, const char *leaf) { int n = snprintf(out, GF_PATH, "%s/%s", root, leaf); return n > 0 && n < GF_PATH; } static bool gf_absolute(const char *path) { #ifdef _WIN32 return path && ((path[0] && path[1] == ':' && (path[2] == '/' || path[2] == '\\')) || (path[0] == '\\' && path[1] == '\\')); #else return path && path[0] == '/'; #endif } static bool gf_write(const char *path, const void *bytes, size_t length) { FILE *file = cbm_fopen(path, "wb"); if (!file) return false; bool ok = fwrite(bytes, 1, length, file) == length; if (fclose(file) != 0) ok = false; return ok; } /* Normal harness entry (test_main.c) keeps the runner image for a private * executable copy. */ void tf_test_impact_git_set_binary(const char *path); void tf_test_impact_git_set_binary(const char *path) { gf_runner_binary = path; } /* Runner role (test_main.c's role table): the native executable fixture for * Git's optional diff/textconv command. Extra arguments added by Git are * deliberately ignored; the environment is left unchanged. */ int tf_role_git_facts_diff_probe(int argc, char **argv); int tf_role_git_facts_diff_probe(int argc, char **argv) { return argc >= 3 && gf_write(argv[2], "invoked\n", 8) ? 0 : 96; } /* Runner role: only this isolated child mutates its environment, before * harness threads start, and returns -1 so main re-enters the existing * exact-diff test; the parent process remains untouched. An inherited * CBM_TEST_ONLY_FILE (a narrowed CI run) is cleared: the runner unions it with * CBM_TEST_ONLY, and one naming this suite would re-run the spawning test, * which spawns again. An inherited CBM_TEST_COVERAGE_DIR (a coverage-map run) * is cleared too: with it the child would start a coverage run of its own, * which refuses the per-test LLVM_PROFILE_FILE it inherits. Without it the * child's profile goes to that file, credited to the spawning test like any * other child process. */ int tf_role_git_facts_hostile_diff_probe(int argc, char **argv); int tf_role_git_facts_hostile_diff_probe(int argc, char **argv) { if (argc != 2) { return -1; } if (cbm_setenv("GIT_DIFF_OPTS", "--unified=999", 1) != 0 || cbm_setenv("CBM_TEST_ONLY_FILE", "", 1) != 0 || cbm_unsetenv("CBM_TEST_COVERAGE_DIR") != 0 || cbm_setenv( "CBM_TEST_ONLY", "test_impact_git:test_git_facts_diff_uses_merge_base_zero_context_and_nul_metadata", 1) != 0) { return 96; } argv[1] = "test_impact_git"; return -1; } /* Git's configured external-driver values are shell command strings. Quote * only fixture-owned paths; Git queries themselves always use native argv. */ static bool gf_driver_command(char *out, size_t capacity, const char *binary, const char *marker) { const char *parts[] = {binary, "__cbm_git_facts_diff_probe", marker}; size_t used = 0; for (size_t part = 0; part < 3; part++) { if (used + 2 >= capacity) return false; if (part) out[used++] = ' '; out[used++] = '\''; for (const char *p = parts[part]; *p; p++) { const char *escaped = *p == '\'' ? "'\\''" : NULL; size_t length = escaped ? 4 : 1; if (used + length + 2 >= capacity) return false; if (escaped) memcpy(out + used, escaped, length); else out[used] = *p; used += length; } out[used++] = '\''; } if (used >= capacity) return false; out[used] = '\0'; return true; } static bool gf_git_exit(gf_fixture_t *fx, const char *const *tail, int expected_exit, char *output, size_t capacity) { const char *argv[64] = { fx->git, "-C", fx->repo, "-c", "commit.gpgSign=false", "-c", "core.autocrlf=false"}; size_t n = 7; for (size_t i = 0; tail[i]; i++) { if (n + 1 >= sizeof(argv) / sizeof(argv[0])) return false; argv[n++] = tail[i]; } argv[n] = NULL; /* No quiet timeout: the fixture waits for Git to finish, and the suite's * own ceiling is the backstop. A diff-driver control runs a fresh copy of * the test runner under `git diff`; on a loaded or Windows host that copy * took over 10 s to start and finish, and a 10 s quiet timeout killed Git * (outcome HANG) -- the timeout, not the assertion, decided the test. */ cbm_proc_opts_t opts = {.bin = fx->git, .argv = argv, .stdout_file = fx->capture, .log_file = fx->log, .strip_git_repo_env = true}; cbm_proc_result_t result = {0}; cbm_proc_outcome_t expected_outcome = expected_exit == 0 ? CBM_PROC_CLEAN : CBM_PROC_EXIT_NONZERO; if (cbm_subprocess_run(&opts, &result) != 0 || result.outcome != expected_outcome || result.exit_code != expected_exit || !result.tree_quiesced) return false; if (!output) return true; FILE *file = cbm_fopen(fx->capture, "rb"); if (!file || capacity == 0) { if (file) (void)fclose(file); return false; } size_t used = fread(output, 1, capacity - 1, file); int extra = fgetc(file); bool ok = extra == EOF && !ferror(file); if (fclose(file) != 0) ok = false; output[used] = '\0'; return ok; } static bool gf_git(gf_fixture_t *fx, const char *const *tail, char *output, size_t capacity) { return gf_git_exit(fx, tail, 0, output, capacity); } static bool gf_oid(gf_fixture_t *fx, const char *const *argv, char out[65]) { char text[80]; if (!gf_git(fx, argv, text, sizeof(text))) return false; size_t n = strlen(text); if (n && text[n - 1] == '\n') text[--n] = '\0'; if (n && text[n - 1] == '\r') text[--n] = '\0'; if (n != fx->oid_length) return false; for (size_t i = 0; i < n; i++) { if (!((text[i] >= '0' && text[i] <= '9') || (text[i] >= 'a' && text[i] <= 'f'))) return false; } memcpy(out, text, n + 1); return true; } static bool gf_object(gf_fixture_t *fx, const char *type, const void *bytes, size_t length, char out[65]) { const char *argv[] = {"hash-object", "-w", "--no-filters", "-t", type, "--", fx->object_file, NULL}; return gf_write(fx->object_file, bytes, length) && gf_oid(fx, argv, out); } static bool gf_index(gf_fixture_t *fx, const char *mode, const char *oid, const char *path) { char entry[GF_SMALL]; int n = snprintf(entry, sizeof(entry), "%s,%s,%s", mode, oid, path); if (n <= 0 || (size_t)n >= sizeof(entry)) return false; const char *argv[] = {"update-index", "--add", "--cacheinfo", entry, NULL}; return gf_git(fx, argv, NULL, 0); } static bool gf_tree(gf_fixture_t *fx, int version, char out[65]) { const char *empty[] = {"read-tree", "--empty", NULL}; const char *write[] = {"write-tree", NULL}; if (!gf_git(fx, empty, NULL, 0)) return false; const char *regular[] = {"binary.bin", ":colon.bin", "-dash.bin", "[glob]*.bin", "dir/file.bin"}; for (size_t i = 0; i < sizeof(regular) / sizeof(regular[0]); i++) { if (!gf_index(fx, "100644", fx->binary, regular[i])) return false; } const char *text = version == 1 ? fx->head_text : version == 2 ? fx->base_text : fx->old_text; if (!gf_index(fx, "100644", text, "p.txt") || !gf_index(fx, "100644", fx->attrs, ".gitattributes") || !gf_index(fx, "120000", fx->link, "link")) return false; if (version == 1) { if (!gf_index(fx, "100644", fx->newer, "new.bin") || !gf_index(fx, "160000", fx->a, "submodule")) return false; } else if (!gf_index(fx, "100644", fx->binary, "deleted.bin")) return false; return gf_oid(fx, write, out); } static bool gf_commit(gf_fixture_t *fx, const char *tree, const char *parent_one, const char *parent_two, const char *label, char out[65]) { char commit[1024]; int n = snprintf( commit, sizeof(commit), "tree %s\n%s%s%s%s%s%s" "author Git Facts Fixture 946684800 +0000\n" "committer Git Facts Fixture 946684800 +0000\n\n%s\n", tree, parent_one ? "parent " : "", parent_one ? parent_one : "", parent_one ? "\n" : "", parent_two ? "parent " : "", parent_two ? parent_two : "", parent_two ? "\n" : "", label); return n > 0 && (size_t)n < sizeof(commit) && gf_object(fx, "commit", commit, (size_t)n, out); } static bool gf_ref(gf_fixture_t *fx, const char *name, const char *oid) { const char *argv[] = {"update-ref", name, oid, NULL}; return gf_git(fx, argv, NULL, 0); } static bool gf_config(gf_fixture_t *fx, const char *key, const char *value) { const char *argv[] = {"config", "--local", key, value, NULL}; return gf_git(fx, argv, NULL, 0); } /* A--P---M(topic/HEAD) * \ / * Q--+--B(base) * merge-base(M,B)=Q, which deliberately differs from first-parent(M)=P. * Raw commit objects give fixed timestamps/identities without changing env. */ static bool gf_fixture_open(gf_fixture_t *fx, bool sha256) { memset(fx, 0, sizeof(*fx)); const char *resolved = cbm_find_cli("git", cbm_get_home_dir()); if (!resolved || !gf_absolute(resolved) || strlen(resolved) >= sizeof(fx->git)) return false; memcpy(fx->git, resolved, strlen(resolved) + 1); const char *home = th_mktempdir("cbm-git-facts"); if (!home || strlen(home) >= sizeof(fx->home)) return false; memcpy(fx->home, home, strlen(home) + 1); bool ok = gf_path(fx->repo, home, "repo") && gf_path(fx->capture, home, "stdout.bin") && gf_path(fx->log, home, "stderr.log") && gf_path(fx->object_file, home, "object.bin"); fx->oid_length = sha256 ? 64U : 40U; if (ok) ok = th_mkdir_p(fx->repo) == 0; const char *init[] = {"-c", "init.templateDir=", "init", "--quiet", sha256 ? "--object-format=sha256" : "--object-format=sha1", "--initial-branch=topic", NULL}; const char attrs[] = "p.txt diff=fixturetrap\n"; if (ok) ok = gf_git(fx, init, NULL, 0) && gf_config(fx, "core.protectNTFS", "false") && gf_config(fx, "core.protectHFS", "false") && gf_config(fx, "core.autocrlf", "false") && gf_config(fx, "core.worktree", fx->repo) && gf_object(fx, "blob", gf_binary, sizeof(gf_binary), fx->binary) && gf_object(fx, "blob", gf_new_binary, sizeof(gf_new_binary), fx->newer) && gf_object(fx, "blob", gf_old_text, sizeof(gf_old_text) - 1, fx->old_text) && gf_object(fx, "blob", gf_head_text, sizeof(gf_head_text) - 1, fx->head_text) && gf_object(fx, "blob", gf_base_text, sizeof(gf_base_text) - 1, fx->base_text) && gf_object(fx, "blob", attrs, sizeof(attrs) - 1, fx->attrs) && gf_object(fx, "blob", "p.txt", 5, fx->link); char tree_a[65], tree_h[65], tree_b[65]; if (ok) ok = gf_tree(fx, 0, tree_a) && gf_commit(fx, tree_a, NULL, NULL, "A", fx->a) && gf_commit(fx, tree_a, fx->a, NULL, "P", fx->p) && gf_commit(fx, tree_a, fx->a, NULL, "Q", fx->q) && gf_tree(fx, 1, tree_h) && gf_commit(fx, tree_h, fx->p, fx->q, "M", fx->head) && gf_tree(fx, 2, tree_b) && gf_commit(fx, tree_b, fx->q, NULL, "B", fx->base) && gf_ref(fx, "refs/heads/topic", fx->head) && gf_ref(fx, "refs/heads/base", fx->base); if (!ok) { fprintf(stderr, "Git facts fixture setup failed (%s, native Git: %s)\n", sha256 ? "required SHA-256" : "SHA-1", fx->git); (void)th_rmtree(fx->home); } return ok; } static cbm_git_facts_options_t gf_options(gf_fixture_t *fx) { return (cbm_git_facts_options_t){.root = fx->repo, .base_ref = "refs/heads/base", .git_executable = fx->git, .expected_head = fx->head, .deadline_ms = cbm_now_ms() + 60000U, .command_limit = 256, .stdout_limit = 1024 * 1024, .stderr_limit = 65536, .total_output_limit = 4 * 1024 * 1024}; } static bool gf_bytes_equal(cbm_git_bytes_t bytes, const void *expected, size_t length) { return bytes.length == length && (length == 0 || (bytes.data && memcmp(bytes.data, expected, length) == 0)); } static bool gf_contains(cbm_git_bytes_t bytes, const char *needle) { size_t length = strlen(needle); if (!bytes.data || length > bytes.length) return false; for (size_t i = 0; i <= bytes.length - length; i++) { if (memcmp(bytes.data + i, needle, length) == 0) return true; } return false; } static bool gf_blob_cleared(const cbm_git_blob_t *blob) { return !blob->bytes.data && blob->bytes.length == 0 && blob->oid[0] == '\0' && blob->mode == 0; } TEST(test_git_facts_pins_actual_merge_base_and_both_refs) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); const cbm_git_facts_identity_t *id = facts ? cbm_git_facts_identity(facts) : NULL; bool matched = id && id->oid_hex_length == 40 && gf_absolute(id->root) && id->git_dir && id->common_dir && strcmp(id->head, fx.head) == 0 && strcmp(id->base, fx.base) == 0 && strcmp(id->merge_base, fx.q) == 0 && strcmp(id->merge_base, fx.p) != 0; cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_EQ(error.status, CBM_GIT_FACTS_OK); ASSERT_TRUE(matched); PASS(); } TEST(test_git_facts_ref_index_and_worktree_mutation_cannot_change_snapshot) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); cbm_git_diff_t before = {0}, after = {0}; bool before_ok = facts && cbm_git_facts_diff(facts, &before, &error); unsigned char patch[16384], names[1024]; bool copied = before_ok && before.patch.data && before.patch.length > 0 && before.name_status.data && before.name_status.length > 0 && before.patch.length <= sizeof(patch) && before.name_status.length <= sizeof(names); size_t patch_length = copied ? before.patch.length : 0; size_t names_length = copied ? before.name_status.length : 0; if (copied) { memcpy(patch, before.patch.data, patch_length); memcpy(names, before.name_status.data, names_length); } char path[GF_PATH]; bool mutated = gf_path(path, fx.repo, "p.txt") && gf_write(path, "dirty\n", 6) && gf_index(&fx, "100644", fx.base_text, "p.txt") && gf_ref(&fx, "refs/heads/topic", fx.a) && gf_ref(&fx, "refs/heads/base", fx.p); bool after_ok = facts && cbm_git_facts_diff(facts, &after, &error); cbm_git_blob_t blob = {0}; cbm_git_blob_status_t read = facts ? cbm_git_facts_read_blob(facts, CBM_GIT_REV_HEAD, "p.txt", 5, &blob, &error) : CBM_GIT_BLOB_ERROR; bool same = copied && after_ok && gf_bytes_equal(after.patch, patch, patch_length) && gf_bytes_equal(after.name_status, names, names_length) && read == CBM_GIT_BLOB_FOUND && gf_bytes_equal(blob.bytes, gf_head_text, sizeof(gf_head_text) - 1); cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(before_ok); ASSERT_TRUE(mutated); ASSERT_TRUE(same); PASS(); } TEST(test_git_facts_reads_exact_binary_literal_paths_and_all_revisions) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); const char *paths[] = {"binary.bin", ":colon.bin", "-dash.bin", "[glob]*.bin", "dir/file.bin"}; bool exact = facts != NULL; for (size_t i = 0; facts && i < sizeof(paths) / sizeof(paths[0]); i++) { cbm_git_blob_t blob = {0}; exact = cbm_git_facts_read_blob(facts, CBM_GIT_REV_HEAD, paths[i], strlen(paths[i]), &blob, &error) == CBM_GIT_BLOB_FOUND && exact; exact = gf_bytes_equal(blob.bytes, gf_binary, sizeof(gf_binary)) && blob.mode == 0100644 && strcmp(blob.oid, fx.binary) == 0 && exact; } const char *texts[] = {gf_head_text, gf_base_text, gf_old_text}; for (int i = 0; facts && i < 3; i++) { const char path[] = {'p', '.', 't', 'x', 't', '!', '!'}; /* explicit length, no terminator */ cbm_git_blob_t blob = {0}; exact = cbm_git_facts_read_blob(facts, (cbm_git_revision_t)i, path, 5, &blob, &error) == CBM_GIT_BLOB_FOUND && gf_bytes_equal(blob.bytes, texts[i], strlen(texts[i])) && exact; } cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(exact); PASS(); } TEST(test_git_facts_distinguishes_absent_invalid_and_nonregular_paths) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); cbm_git_blob_t blob; memset(&blob, 0xa5, sizeof(blob)); cbm_git_blob_status_t absent = facts ? cbm_git_facts_read_blob(facts, CBM_GIT_REV_HEAD, "missing.bin", 11, &blob, &error) : CBM_GIT_BLOB_ERROR; bool absent_clear = gf_blob_cleared(&blob); const char *invalid[] = {"", "/p.txt", "./p.txt", "dir/../p.txt", "p\0.txt", "link", "dir", "submodule"}; const size_t lengths[] = {0, 6, 7, 12, 6, 4, 3, 9}; bool rejected = facts != NULL; for (size_t i = 0; facts && i < sizeof(invalid) / sizeof(invalid[0]); i++) { memset(&blob, 0xa5, sizeof(blob)); rejected = cbm_git_facts_read_blob(facts, CBM_GIT_REV_HEAD, invalid[i], lengths[i], &blob, &error) == CBM_GIT_BLOB_ERROR && gf_blob_cleared(&blob) && rejected; } cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_EQ(absent, CBM_GIT_BLOB_ABSENT); ASSERT_TRUE(absent_clear); ASSERT_TRUE(rejected); PASS(); } TEST(test_git_facts_ancestry_distinguishes_no_from_invalid_and_missing_objects) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); char missing[65]; memset(missing, 'f', fx.oid_length); missing[fx.oid_length] = '\0'; cbm_git_ancestry_t yes = facts ? cbm_git_facts_is_ancestor(facts, fx.q, fx.head, &error) : -1; cbm_git_ancestry_t no = facts ? cbm_git_facts_is_ancestor(facts, fx.base, fx.head, &error) : -1; cbm_git_ancestry_t invalid = facts ? cbm_git_facts_is_ancestor(facts, "HEAD", fx.head, &error) : 0; cbm_git_ancestry_t absent = facts ? cbm_git_facts_is_ancestor(facts, missing, fx.head, &error) : 0; cbm_git_ancestry_t noncommit = facts ? cbm_git_facts_is_ancestor(facts, fx.binary, fx.head, &error) : 0; cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_EQ(yes, CBM_GIT_ANCESTRY_YES); ASSERT_EQ(no, CBM_GIT_ANCESTRY_NO); ASSERT_EQ(invalid, CBM_GIT_ANCESTRY_ERROR); ASSERT_EQ(absent, CBM_GIT_ANCESTRY_ERROR); ASSERT_EQ(noncommit, CBM_GIT_ANCESTRY_ERROR); PASS(); } TEST(test_git_facts_diff_uses_merge_base_zero_context_and_nul_metadata) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); bool configured = gf_config(&fx, "diff.interHunkContext", "100") && gf_config(&fx, "diff.renames", "true"); cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); cbm_git_diff_t diff = {0}; bool diff_ok = facts && cbm_git_facts_diff(facts, &diff, &error); const char names[] = "D\0deleted.bin\0A\0new.bin\0M\0p.txt\0A\0submodule\0"; bool metadata = diff_ok && gf_bytes_equal(diff.name_status, names, sizeof(names) - 1); bool patch = diff_ok && gf_contains(diff.patch, "@@ -1 +1 @@") && gf_contains(diff.patch, "@@ -3 +3 @@") && gf_contains(diff.patch, "-before first\n") && gf_contains(diff.patch, "+after first\n") && !gf_contains(diff.patch, "base branch only") && !gf_contains(diff.patch, "\n context\n"); cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(configured); ASSERT_TRUE(diff_ok); ASSERT_TRUE(metadata); ASSERT_TRUE(patch); PASS(); } TEST(test_git_facts_rejects_malformed_options_and_expected_head_mismatch) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); cbm_git_facts_options_t good = gf_options(&fx); bool invalid = true; for (int i = 0; i < 9; i++) { cbm_git_facts_options_t opts = good; if (i == 0) opts.root = NULL; if (i == 1) opts.root = "relative"; if (i == 2) opts.base_ref = NULL; if (i == 3) opts.git_executable = "git"; if (i == 4) opts.deadline_ms = 0; if (i == 5) opts.command_limit = 0; if (i == 6) opts.stdout_limit = 0; if (i == 7) opts.stderr_limit = 0; if (i == 8) opts.total_output_limit = 0; cbm_git_facts_error_t error; memset(&error, 0xa5, sizeof(error)); cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); invalid = !facts && error.status == CBM_GIT_FACTS_INVALID && memchr(error.diagnostic, '\0', sizeof(error.diagnostic)) && invalid; cbm_git_facts_free(facts); } good.expected_head = fx.a; cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&good, &error); bool mismatch = !facts && error.status == CBM_GIT_FACTS_IDENTITY_MISMATCH; cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(invalid); ASSERT_TRUE(mismatch); PASS(); } static bool gf_cancelled(void *context) { return *(const bool *)context; } TEST(test_git_facts_precancel_and_expired_deadline_are_deterministic) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); cbm_git_facts_options_t opts = gf_options(&fx); bool cancelled = true; opts.cancelled = gf_cancelled; opts.cancel_context = &cancelled; cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); bool cancel_ok = !facts && error.status == CBM_GIT_FACTS_CANCELLED; cbm_git_facts_free(facts); opts.cancelled = NULL; opts.deadline_ms = 1; /* fixed past absolute deadline, not a measured race */ facts = cbm_git_facts_open(&opts, &error); bool deadline_ok = !facts && error.status == CBM_GIT_FACTS_DEADLINE; cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(cancel_ok); ASSERT_TRUE(deadline_ok); PASS(); } TEST(test_git_facts_command_and_output_caps_fail_closed) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); bool limited = true; for (int i = 0; i < 3; i++) { cbm_git_facts_options_t opts = gf_options(&fx); if (i == 0) opts.command_limit = 1; if (i == 1) opts.stdout_limit = 1; if (i == 2) opts.total_output_limit = 1; cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); limited = !facts && error.status == CBM_GIT_FACTS_LIMIT && limited; cbm_git_facts_free(facts); } int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(limited); PASS(); } TEST(test_git_facts_required_sha256_objects_and_identity) { gf_fixture_t fx; /* Required fixture: unsupported installed Git fails setup explicitly. */ ASSERT_TRUE(gf_fixture_open(&fx, true)); cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); const cbm_git_facts_identity_t *id = facts ? cbm_git_facts_identity(facts) : NULL; bool identity = id && id->oid_hex_length == 64 && strlen(id->head) == 64 && strcmp(id->head, fx.head) == 0 && strcmp(id->base, fx.base) == 0 && strcmp(id->merge_base, fx.q) == 0; cbm_git_blob_t blob = {0}; bool exact = facts && cbm_git_facts_read_blob(facts, CBM_GIT_REV_HEAD, ":colon.bin", 10, &blob, &error) == CBM_GIT_BLOB_FOUND && strlen(blob.oid) == 64 && gf_bytes_equal(blob.bytes, gf_binary, sizeof(gf_binary)); cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(identity); ASSERT_TRUE(exact); PASS(); } TEST(test_git_facts_never_runs_external_diff_or_textconv) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); char binary[GF_PATH], marker[GF_PATH], command[GF_PATH * 3]; bool prepared = gf_path(binary, fx.home, #ifdef _WIN32 "diff-probe.exe" #else "diff-probe" #endif ) && gf_path(marker, fx.home, "driver-invoked") && gf_runner_binary; if (prepared) { cbm_normalize_path_sep(binary); cbm_normalize_path_sep(marker); prepared = cbm_copy_file(gf_runner_binary, binary) == 0; } if (prepared) th_make_executable(binary); char attributes_path[GF_PATH]; static const char attributes[] = "p.txt diff=fixturetrap\n"; prepared = prepared && gf_path(attributes_path, fx.repo, ".gitattributes") && gf_write(attributes_path, attributes, sizeof(attributes) - 1) && gf_driver_command(command, sizeof(command), binary, marker); bool control[2] = {false}, isolated[2] = {false}; for (int i = 0; prepared && i < 2; i++) { const char *key = i == 0 ? "diff.external" : "diff.fixturetrap.textconv"; bool configured = gf_config(&fx, key, command); const char *probe[] = { "diff", i == 0 ? "--ext-diff" : "--textconv", fx.q, fx.head, "--", "p.txt", NULL}; bool probe_ok = configured && gf_git(&fx, probe, NULL, 0); bool marker_exists = cbm_file_exists(marker); control[i] = probe_ok && marker_exists; if (!control[i]) { fprintf(stderr, "Git driver control %d: configured=%d command=%d marker=%d\n", i, configured, probe_ok, marker_exists); FILE *diagnostic = cbm_fopen(fx.log, "rb"); if (diagnostic) { char detail[512]; size_t length = fread(detail, 1, sizeof(detail) - 1, diagnostic); detail[length] = '\0'; fprintf(stderr, "Git driver fixture stderr: %s\n", detail); (void)fclose(diagnostic); } } bool reset = cbm_unlink(marker) == 0; cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); cbm_git_diff_t diff = {0}; isolated[i] = reset && facts && cbm_git_facts_diff(facts, &diff, &error) && gf_contains(diff.patch, "+after first\n") && !cbm_file_exists(marker); cbm_git_facts_free(facts); const char *unset[] = {"config", "--local", "--unset", key, NULL}; prepared = gf_git(&fx, unset, NULL, 0) && prepared; } int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(prepared); ASSERT_TRUE(control[0]); ASSERT_TRUE(control[1]); ASSERT_TRUE(isolated[0]); ASSERT_TRUE(isolated[1]); PASS(); } TEST(test_git_facts_rejects_local_grafts_that_change_ancestry) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); char info[GF_PATH], path[GF_PATH], graft[160]; int length = snprintf(graft, sizeof(graft), "%s %s\n", fx.head, fx.a); bool written = length > 0 && (size_t)length < sizeof(graft) && gf_path(info, fx.repo, ".git/info") && th_mkdir_p(info) == 0 && gf_path(path, info, "grafts") && gf_write(path, graft, (size_t)length); const char *merge[] = {"merge-base", "--all", fx.head, fx.base, NULL}; char altered[65]; bool graft_effective = written && gf_oid(&fx, merge, altered) && strcmp(altered, fx.a) == 0 && strcmp(altered, fx.q) != 0; cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); bool rejected = !facts && error.status == CBM_GIT_FACTS_UNSUPPORTED; cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(graft_effective); ASSERT_TRUE(rejected); PASS(); } TEST(test_git_facts_rejects_no_common_ancestor) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); char tree_spec[80], tree[65], orphan[65], control_output[8]; int n = snprintf(tree_spec, sizeof(tree_spec), "%s^{tree}", fx.a); const char *resolve_tree[] = {"rev-parse", "--verify", tree_spec, NULL}; bool created = n > 0 && (size_t)n < sizeof(tree_spec) && gf_oid(&fx, resolve_tree, tree) && gf_commit(&fx, tree, NULL, NULL, "unrelated-root", orphan) && gf_ref(&fx, "refs/heads/base", orphan); const char *merge[] = {"merge-base", "--all", fx.head, orphan, NULL}; bool unrelated = created && gf_git_exit(&fx, merge, 1, control_output, sizeof(control_output)) && control_output[0] == '\0'; cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); bool rejected = !facts && error.status == CBM_GIT_FACTS_NO_MERGE_BASE; cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(unrelated); ASSERT_TRUE(rejected); PASS(); } TEST(test_git_facts_rejects_multiple_criss_cross_merge_bases) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); char tree_spec[80], tree[65], other_merge[65], control_output[160]; int n = snprintf(tree_spec, sizeof(tree_spec), "%s^{tree}", fx.head); const char *resolve_tree[] = {"rev-parse", "--verify", tree_spec, NULL}; /* Existing HEAD merges P,Q. A separate Q,P merge makes P and Q exactly * the two best common ancestors; neither is an ancestor of the other. */ bool created = n > 0 && (size_t)n < sizeof(tree_spec) && gf_oid(&fx, resolve_tree, tree) && gf_commit(&fx, tree, fx.q, fx.p, "independent-reverse-merge", other_merge) && gf_ref(&fx, "refs/heads/base", other_merge); const char *merge[] = {"merge-base", "--all", fx.head, other_merge, NULL}; char first[65], second[65], extra; bool two = created && gf_git(&fx, merge, control_output, sizeof(control_output)) && sscanf(control_output, "%64s %64s %c", first, second, &extra) == 2 && ((strcmp(first, fx.p) == 0 && strcmp(second, fx.q) == 0) || (strcmp(first, fx.q) == 0 && strcmp(second, fx.p) == 0)); cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); bool rejected = !facts && error.status == CBM_GIT_FACTS_AMBIGUOUS_MERGE_BASE; cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(two); ASSERT_TRUE(rejected); PASS(); } TEST(test_git_facts_rejects_local_shallow_state) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); char path[GF_PATH], contents[80], control_output[16]; int n = snprintf(contents, sizeof(contents), "%s\n", fx.q); bool written = n > 0 && (size_t)n < sizeof(contents) && gf_path(path, fx.repo, ".git/shallow") && gf_write(path, contents, (size_t)n); const char *probe[] = {"rev-parse", "--is-shallow-repository", NULL}; bool shallow = written && gf_git(&fx, probe, control_output, sizeof(control_output)) && (strcmp(control_output, "true\n") == 0 || strcmp(control_output, "true\r\n") == 0); cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); bool rejected = !facts && error.status == CBM_GIT_FACTS_UNSUPPORTED; cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(shallow); ASSERT_TRUE(rejected); PASS(); } TEST(test_git_facts_rejects_history_override_added_after_open) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); bool opened = facts && error.status == CBM_GIT_FACTS_OK; bool before = facts && cbm_git_facts_is_ancestor(facts, fx.q, fx.head, &error) == CBM_GIT_ANCESTRY_YES; char info[GF_PATH], path[GF_PATH], graft[160], altered[65]; int n = snprintf(graft, sizeof(graft), "%s %s\n", fx.head, fx.a); bool written = opened && n > 0 && (size_t)n < sizeof(graft) && gf_path(info, fx.repo, ".git/info") && th_mkdir_p(info) == 0 && gf_path(path, info, "grafts") && gf_write(path, graft, (size_t)n); const char *merge[] = {"merge-base", "--all", fx.head, fx.base, NULL}; bool override_active = written && gf_oid(&fx, merge, altered) && strcmp(altered, fx.a) == 0; cbm_git_ancestry_t after = facts ? cbm_git_facts_is_ancestor(facts, fx.q, fx.head, &error) : CBM_GIT_ANCESTRY_NO; bool rejected = after == CBM_GIT_ANCESTRY_ERROR && error.status == CBM_GIT_FACTS_UNSUPPORTED; cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(opened); ASSERT_TRUE(before); ASSERT_TRUE(override_active); ASSERT_TRUE(rejected); PASS(); } TEST(test_git_facts_cancellation_after_open_is_latched) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); bool cancelled = false; cbm_git_facts_options_t opts = gf_options(&fx); opts.cancelled = gf_cancelled; opts.cancel_context = &cancelled; cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); bool opened = facts && error.status == CBM_GIT_FACTS_OK; bool before = facts && cbm_git_facts_is_ancestor(facts, fx.q, fx.head, &error) == CBM_GIT_ANCESTRY_YES; cancelled = true; cbm_git_diff_t diff; memset(&diff, 0xa5, sizeof(diff)); bool diff_ok = facts && cbm_git_facts_diff(facts, &diff, &error); bool first = facts && !diff_ok && error.status == CBM_GIT_FACTS_CANCELLED && !diff.patch.data && diff.patch.length == 0 && !diff.name_status.data && diff.name_status.length == 0; cancelled = false; /* the same cancelled handle must not resume */ cbm_git_blob_t blob; memset(&blob, 0xa5, sizeof(blob)); cbm_git_blob_status_t read = facts ? cbm_git_facts_read_blob(facts, CBM_GIT_REV_HEAD, "p.txt", 5, &blob, &error) : CBM_GIT_BLOB_FOUND; bool latched = read == CBM_GIT_BLOB_ERROR && error.status == CBM_GIT_FACTS_CANCELLED && gf_blob_cleared(&blob); cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(opened); ASSERT_TRUE(before); ASSERT_TRUE(first); ASSERT_TRUE(latched); PASS(); } /* Insert before SUITE(test_impact_git), then register this test in that suite. * Reuses the existing independent fixture helpers; no new implementation seam. */ TEST(test_git_facts_gitfile_redirection_cannot_certify_false_ancestry) { gf_fixture_t original, redirected; ASSERT_TRUE(gf_fixture_open(&original, false)); bool second_ready = gf_fixture_open(&redirected, false); if (!second_ready) { int cleanup = th_rmtree(original.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(second_ready); } /* Objects are generated from identical fixed bytes in both native repos. * Their identities therefore stay the same while their topology differs. */ bool same_objects = strcmp(original.head, redirected.head) == 0 && strcmp(original.base, redirected.base) == 0 && strcmp(original.q, redirected.q) == 0; char original_git[GF_PATH]; bool path_ready = gf_path(original_git, original.home, "original-git"); const char *relocate[] = { "-c", "init.templateDir=", "init", "--quiet", "--separate-git-dir", original_git, NULL}; bool relocated = same_objects && path_ready && gf_git(&original, relocate, NULL, 0); cbm_git_facts_options_t opts = gf_options(&original); cbm_git_facts_error_t error = {0}; cbm_git_facts_t *facts = relocated ? cbm_git_facts_open(&opts, &error) : NULL; bool opened = facts && error.status == CBM_GIT_FACTS_OK; bool originally_no = facts && cbm_git_facts_is_ancestor(facts, original.base, original.head, &error) == CBM_GIT_ANCESTRY_NO && error.status == CBM_GIT_FACTS_OK; char target_git[GF_PATH], info[GF_PATH], graft_path[GF_PATH], gitfile_path[GF_PATH]; char graft[160], gitfile[GF_PATH + 32]; int graft_length = snprintf(graft, sizeof(graft), "%s %s\n", original.head, original.base); bool target_ready = opened && gf_path(target_git, redirected.repo, ".git") && gf_path(info, target_git, "info") && th_mkdir_p(info) == 0 && gf_path(graft_path, info, "grafts") && graft_length > 0 && (size_t)graft_length < sizeof(graft) && gf_write(graft_path, graft, (size_t)graft_length); int gitfile_length = target_ready ? snprintf(gitfile, sizeof(gitfile), "gitdir: %s\n", target_git) : -1; bool repointed = target_ready && gitfile_length > 0 && (size_t)gitfile_length < sizeof(gitfile) && gf_path(gitfile_path, original.repo, ".git") && gf_write(gitfile_path, gitfile, (size_t)gitfile_length); const char *control[] = {"--no-replace-objects", "merge-base", "--is-ancestor", original.base, original.head, NULL}; /* -C discovery now returns YES through B's graft, even with the adapter's * no-replace flag. The original separate Git directory is left intact. */ bool redirected_yes = repointed && gf_git(&original, control, NULL, 0); cbm_git_ancestry_t after = facts ? cbm_git_facts_is_ancestor(facts, original.base, original.head, &error) : CBM_GIT_ANCESTRY_YES; bool retained_or_rejected = (after == CBM_GIT_ANCESTRY_NO && error.status == CBM_GIT_FACTS_OK) || (after == CBM_GIT_ANCESTRY_ERROR && error.status != CBM_GIT_FACTS_OK); cbm_git_facts_free(facts); int cleanup_original = th_rmtree(original.home); int cleanup_redirected = th_rmtree(redirected.home); ASSERT_EQ(cleanup_original, 0); ASSERT_EQ(cleanup_redirected, 0); ASSERT_TRUE(same_objects); ASSERT_TRUE(relocated); ASSERT_TRUE(opened); ASSERT_TRUE(originally_no); ASSERT_TRUE(redirected_yes); ASSERT_TRUE(retained_or_rejected); PASS(); } TEST(test_git_facts_inherited_diff_options_cannot_override_zero_context) { const char *home = th_mktempdir("cbm-git-diff-env"); ASSERT_NOT_NULL(home); char log[GF_PATH]; bool prepared = gf_runner_binary && gf_path(log, home, "child.log"); const char *argv[] = {gf_runner_binary, "__cbm_git_facts_hostile_diff_probe", NULL}; cbm_proc_opts_t options = { .bin = gf_runner_binary, .argv = argv, .log_file = log, .quiet_timeout_ms = 60000}; cbm_proc_result_t result = {0}; int launched = prepared ? cbm_subprocess_run(&options, &result) : -1; bool passed = launched == 0 && result.outcome == CBM_PROC_CLEAN && result.exit_code == 0 && result.tree_quiesced; if (!passed && prepared) { FILE *file = cbm_fopen(log, "rb"); if (file) { char detail[4096]; size_t n = fread(detail, 1, sizeof(detail) - 1, file); detail[n] = '\0'; fprintf(stderr, "Isolated hostile-diff child: %s\n", detail); (void)fclose(file); } } int cleanup = th_rmtree(home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(prepared); ASSERT_EQ(launched, 0); ASSERT_TRUE(passed); PASS(); } /* A narrowed CI run hands every runner process a CBM_TEST_ONLY_FILE. The * hostile-diff child must still run exactly its own test: the runner unions * the file with CBM_TEST_ONLY, so an inherited file would add its tests, and * one naming this suite would make the child re-run the spawning test. */ #define GF_OTHER_TEST "test_git_facts_rejects_malformed_options_and_expected_head_mismatch" TEST(test_git_facts_hostile_probe_ignores_an_inherited_selection_file) { const char *home = th_mktempdir("cbm-git-diff-only"); ASSERT_NOT_NULL(home); char log[GF_PATH]; char only[GF_PATH]; static const char token[] = "test_impact_git:" GF_OTHER_TEST "\n"; bool prepared = gf_runner_binary && gf_path(log, home, "child.log") && gf_path(only, home, "only.txt") && gf_write(only, token, sizeof(token) - 1); const char *inherited = getenv("CBM_TEST_ONLY_FILE"); char *saved = inherited ? cbm_strdup(inherited) : NULL; bool set = prepared && (!inherited || saved) && cbm_setenv("CBM_TEST_ONLY_FILE", only, 1) == 0; const char *argv[] = {gf_runner_binary, "__cbm_git_facts_hostile_diff_probe", NULL}; cbm_proc_opts_t options = { .bin = gf_runner_binary, .argv = argv, .log_file = log, .quiet_timeout_ms = 60000}; cbm_proc_result_t result = {0}; int launched = set ? cbm_subprocess_run(&options, &result) : -1; bool restored = saved ? cbm_setenv("CBM_TEST_ONLY_FILE", saved, 1) == 0 : cbm_unsetenv("CBM_TEST_ONLY_FILE") == 0; free(saved); char detail[16384] = ""; FILE *file = launched == 0 ? cbm_fopen(log, "rb") : NULL; if (file) { size_t n = fread(detail, 1, sizeof(detail) - 1, file); detail[n] = '\0'; (void)fclose(file); } bool own_test_only = launched == 0 && result.outcome == CBM_PROC_CLEAN && result.exit_code == 0 && strstr(detail, "test_git_facts_diff_uses_merge_base_zero_context") && !strstr(detail, GF_OTHER_TEST); if (!own_test_only && launched == 0) { fprintf(stderr, "Isolated hostile-diff child: %s\n", detail); } int cleanup = th_rmtree(home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(set); ASSERT_TRUE(restored); ASSERT_EQ(launched, 0); ASSERT_TRUE(own_test_only); PASS(); } #undef GF_OTHER_TEST /* Independent D1 inventory tests. Reuse the native local-object gf_* fixture * helpers above; construct byte-only names in Git objects, never on disk. */ typedef struct { const char *path; size_t length; uint32_t mode; cbm_git_tree_object_type_t type; const char *oid; } gfi_expected_t; static bool gfi_matches(const cbm_git_tree_inventory_t *inventory, const gfi_expected_t *expected, size_t count) { if (inventory->count != count || (count && !inventory->entries)) return false; if (!count) return inventory->entries == NULL; for (size_t i = 0; i < count; i++) { const cbm_git_tree_entry_t *entry = &inventory->entries[i]; size_t oid_length = strlen(expected[i].oid); if (!entry->path || entry->path_length != expected[i].length || memcmp(entry->path, expected[i].path, expected[i].length) != 0 || entry->path[entry->path_length] != '\0' || entry->mode != expected[i].mode || entry->object_type != expected[i].type || oid_length >= sizeof(entry->oid) || entry->oid[oid_length] != '\0' || memcmp(entry->oid, expected[i].oid, oid_length) != 0) return false; } return true; } static bool gfi_cleared(const cbm_git_tree_inventory_t *inventory) { return inventory->entries == NULL && inventory->count == 0; } static cbm_git_tree_inventory_t gfi_poisoned(void) { static const cbm_git_tree_entry_t sentinel = {.path = "old-prefix", .path_length = 10}; return (cbm_git_tree_inventory_t){.entries = &sentinel, .count = 1}; } /* Tree object encoding is Git's fixture format: ASCII octal mode, space, * basename bytes, NUL, raw object ID. Records supplied here are preordered in * native Git tree order; the inventory oracle below is independently literal. */ typedef struct { const char *mode; const char *name; size_t length; const char *oid; } gfi_raw_entry_t; static int gfi_hex(unsigned char ch) { if (ch >= '0' && ch <= '9') return ch - '0'; if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; return -1; } static bool gfi_raw_tree_format(gf_fixture_t *fx, const gfi_raw_entry_t *rows, size_t count, bool validate_format, char out[65]) { size_t bytes = 0, oid_bytes = fx->oid_length / 2; for (size_t i = 0; i < count; i++) { size_t mode = strlen(rows[i].mode); if (!rows[i].length || rows[i].length > SIZE_MAX - mode - oid_bytes - 2 || bytes > SIZE_MAX - (mode + rows[i].length + oid_bytes + 2) || strlen(rows[i].oid) != fx->oid_length) return false; bytes += mode + rows[i].length + oid_bytes + 2; } CBMArena arena; cbm_arena_init_lazy(&arena, 4096); unsigned char *buffer = cbm_arena_alloc(&arena, bytes ? bytes : 1); bool ok = buffer != NULL; size_t used = 0; for (size_t i = 0; i < count && ok; i++) { size_t mode = strlen(rows[i].mode); memcpy(buffer + used, rows[i].mode, mode); used += mode; buffer[used++] = ' '; memcpy(buffer + used, rows[i].name, rows[i].length); used += rows[i].length; buffer[used++] = 0; for (size_t j = 0; j < oid_bytes; j++) { int high = gfi_hex((unsigned char)rows[i].oid[2 * j]); int low = gfi_hex((unsigned char)rows[i].oid[2 * j + 1]); if (high < 0 || low < 0) { ok = false; break; } buffer[used++] = (unsigned char)((high << 4) | low); } } ok = ok && used == bytes; if (ok && validate_format) { ok = gf_object(fx, "tree", buffer, bytes, out); } else if (ok) { /* One synthetic output-parser fixture intentionally exceeds Git fsck's * pathname recommendation. Keep its raw tree bytes; never materialize * that name in the filesystem or relax normal fixture validation. */ const char *argv[] = {"hash-object", "-w", "--no-filters", "--literally", "-t", "tree", "--", fx->object_file, NULL}; ok = gf_write(fx->object_file, buffer, bytes) && gf_oid(fx, argv, out); } cbm_arena_destroy(&arena); return ok; } static bool gfi_raw_tree(gf_fixture_t *fx, const gfi_raw_entry_t *rows, size_t count, char out[65]) { return gfi_raw_tree_format(fx, rows, count, true, out); } static bool gfi_head_tree(gf_fixture_t *fx, const char *tree) { char head[65]; if (!gf_commit(fx, tree, fx->head, NULL, "inventory head", head) || !gf_ref(fx, "refs/heads/topic", head)) return false; memcpy(fx->head, head, fx->oid_length + 1); return true; } #define GFI_BLOB(path_, oid_) {path_, sizeof(path_) - 1, 0100644, CBM_GIT_TREE_BLOB, oid_} TEST(test_git_facts_inventory_pins_complete_head_and_actual_merge_base) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); const char *read_head[] = {"read-tree", fx.head, NULL}; const char *write_tree[] = {"write-tree", NULL}; char tree[65]; bool configured = gf_git(&fx, read_head, NULL, 0) && gf_index(&fx, "100755", fx.binary, "run.sh") && gf_oid(&fx, write_tree, tree) && gfi_head_tree(&fx, tree); const gfi_expected_t head_rows[] = { GFI_BLOB("-dash.bin", fx.binary), GFI_BLOB(".gitattributes", fx.attrs), GFI_BLOB(":colon.bin", fx.binary), GFI_BLOB("[glob]*.bin", fx.binary), GFI_BLOB("binary.bin", fx.binary), GFI_BLOB("dir/file.bin", fx.binary), {"link", 4, 0120000, CBM_GIT_TREE_BLOB, fx.link}, GFI_BLOB("new.bin", fx.newer), GFI_BLOB("p.txt", fx.head_text), {"run.sh", 6, 0100755, CBM_GIT_TREE_BLOB, fx.binary}, {"submodule", 9, 0160000, CBM_GIT_TREE_COMMIT, fx.a} }; const gfi_expected_t base_rows[] = { GFI_BLOB("-dash.bin", fx.binary), GFI_BLOB(".gitattributes", fx.attrs), GFI_BLOB(":colon.bin", fx.binary), GFI_BLOB("[glob]*.bin", fx.binary), GFI_BLOB("binary.bin", fx.binary), GFI_BLOB("deleted.bin", fx.binary), GFI_BLOB("dir/file.bin", fx.binary), {"link", 4, 0120000, CBM_GIT_TREE_BLOB, fx.link}, GFI_BLOB("p.txt", fx.old_text) }; cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = configured ? cbm_git_facts_open(&opts, &error) : NULL; const cbm_git_facts_identity_t *id = facts ? cbm_git_facts_identity(facts) : NULL; bool opened = id && strcmp(id->head, fx.head) == 0 && strcmp(id->merge_base, fx.q) == 0 && strcmp(id->merge_base, fx.p) != 0; char product[GF_PATH], untracked[GF_PATH]; bool missing = gf_path(product, fx.repo, "p.txt") && !cbm_file_exists(product); cbm_git_tree_inventory_t head = gfi_poisoned(), base = gfi_poisoned(); bool first = facts && cbm_git_facts_inventory(facts, CBM_GIT_REV_HEAD, &head, &error) && error.status == CBM_GIT_FACTS_OK && gfi_matches(&head, head_rows, 11); const char *empty_index[] = {"read-tree", "--empty", NULL}; bool mutated = missing && gf_git(&fx, empty_index, NULL, 0) && gf_write(product, "ambient edit\n", 13) && gf_path(untracked, fx.repo, "untracked.c") && gf_write(untracked, "not in commit\n", 14) && gf_ref(&fx, "refs/heads/topic", fx.a) && gf_ref(&fx, "refs/heads/base", fx.a); bool merge_base = facts && cbm_git_facts_inventory(facts, CBM_GIT_REV_MERGE_BASE, &base, &error) && error.status == CBM_GIT_FACTS_OK && gfi_matches(&base, base_rows, 9); cbm_git_tree_inventory_t again = gfi_poisoned(); bool pinned = facts && cbm_git_facts_inventory(facts, CBM_GIT_REV_HEAD, &again, &error) && error.status == CBM_GIT_FACTS_OK && gfi_matches(&again, head_rows, 11); /* Earlier views remain owned by the same handle after later operations. */ bool retained = first && merge_base && gfi_matches(&head, head_rows, 11) && gfi_matches(&base, base_rows, 9); cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(configured); ASSERT_TRUE(opened); ASSERT_TRUE(missing); ASSERT_TRUE(mutated); ASSERT_TRUE(first); ASSERT_TRUE(merge_base); ASSERT_TRUE(pinned); ASSERT_TRUE(retained); PASS(); } TEST(test_git_facts_inventory_preserves_literal_bytes_and_sha256) { for (int algorithm = 0; algorithm < 2; algorithm++) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, algorithm != 0)); /* SHA-256 is required, never skipped. */ char nested[65], tree[65]; const gfi_raw_entry_t child[] = {{"100644", "leaf\nname", 9, fx.binary}}; bool configured = gfi_raw_tree(&fx, child, 1, nested); const gfi_raw_entry_t raw[] = { {"100644", "a", 1, fx.binary}, {"100644", "aa", 2, fx.newer}, {"40000", "dir", 3, nested}, {"100644", "line\nname", 9, fx.binary}, {"100644", "tab\tname", 8, fx.binary}, {"100644", "z", 1, fx.binary}, {"100644", "\x80" "high", 5, fx.binary}, {"100644", "\xff" "tail", 5, fx.newer} }; configured = configured && gfi_raw_tree(&fx, raw, 8, tree) && gfi_head_tree(&fx, tree); const gfi_expected_t expected[] = { GFI_BLOB("a", fx.binary), GFI_BLOB("aa", fx.newer), GFI_BLOB("dir/leaf\nname", fx.binary), GFI_BLOB("line\nname", fx.binary), GFI_BLOB("tab\tname", fx.binary), GFI_BLOB("z", fx.binary), GFI_BLOB("\x80" "high", fx.binary), GFI_BLOB("\xff" "tail", fx.newer) }; cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = configured ? cbm_git_facts_open(&opts, &error) : NULL; const cbm_git_facts_identity_t *id = facts ? cbm_git_facts_identity(facts) : NULL; bool opened = id && id->oid_hex_length == (algorithm ? 64U : 40U); cbm_git_tree_inventory_t inventory = gfi_poisoned(); bool exact = facts && cbm_git_facts_inventory(facts, CBM_GIT_REV_HEAD, &inventory, &error) && error.status == CBM_GIT_FACTS_OK && gfi_matches(&inventory, expected, 8); cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); if (!configured || !opened || !exact) fprintf(stderr, "Git inventory byte-path fixture failed (%s)\n", algorithm ? "SHA-256" : "SHA-1"); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(configured); ASSERT_TRUE(opened); ASSERT_TRUE(exact); } PASS(); } TEST(test_git_facts_inventory_empty_and_invalid_revision_clear_output) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); const char *empty_index[] = {"read-tree", "--empty", NULL}; const char *write_tree[] = {"write-tree", NULL}; char tree[65]; bool configured = gf_git(&fx, empty_index, NULL, 0) && gf_oid(&fx, write_tree, tree) && gfi_head_tree(&fx, tree) && gf_ref(&fx, "refs/heads/base", fx.head); cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = configured ? cbm_git_facts_open(&opts, &error) : NULL; const cbm_git_facts_identity_t *id = facts ? cbm_git_facts_identity(facts) : NULL; bool opened = id && strcmp(id->merge_base, fx.head) == 0; cbm_git_tree_inventory_t inventory = gfi_poisoned(); bool head_empty = facts && cbm_git_facts_inventory(facts, CBM_GIT_REV_HEAD, &inventory, &error) && error.status == CBM_GIT_FACTS_OK && gfi_cleared(&inventory); inventory = gfi_poisoned(); bool invalid = facts && !cbm_git_facts_inventory(facts, CBM_GIT_REV_BASE, &inventory, &error) && error.status == CBM_GIT_FACTS_INVALID && gfi_cleared(&inventory); inventory = gfi_poisoned(); bool base_empty = facts && cbm_git_facts_inventory(facts, CBM_GIT_REV_MERGE_BASE, &inventory, &error) && error.status == CBM_GIT_FACTS_OK && gfi_cleared(&inventory); cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(configured); ASSERT_TRUE(opened); ASSERT_TRUE(head_empty); ASSERT_TRUE(invalid); ASSERT_TRUE(base_empty); PASS(); } TEST(test_git_facts_inventory_cancellation_clears_prefix_and_latches) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); bool cancelled = false; cbm_git_facts_options_t opts = gf_options(&fx); opts.cancelled = gf_cancelled; opts.cancel_context = &cancelled; cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); bool opened = facts && error.status == CBM_GIT_FACTS_OK; cbm_git_blob_t blob; bool control = facts && cbm_git_facts_read_blob(facts, CBM_GIT_REV_HEAD, "binary.bin", 10, &blob, &error) == CBM_GIT_BLOB_FOUND && gf_bytes_equal(blob.bytes, gf_binary, sizeof(gf_binary)); cbm_git_tree_inventory_t before = gfi_poisoned(); bool inventoried = facts && cbm_git_facts_inventory(facts, CBM_GIT_REV_HEAD, &before, &error) && error.status == CBM_GIT_FACTS_OK && before.count == 10 && before.entries; cancelled = true; cbm_git_tree_inventory_t rejected = gfi_poisoned(); bool first = facts && !cbm_git_facts_inventory(facts, CBM_GIT_REV_HEAD, &rejected, &error) && error.status == CBM_GIT_FACTS_CANCELLED && gfi_cleared(&rejected); cancelled = false; /* Explicit state transition; no timing window or racing thread. */ rejected = gfi_poisoned(); bool latched = facts && !cbm_git_facts_inventory(facts, CBM_GIT_REV_MERGE_BASE, &rejected, &error) && error.status == CBM_GIT_FACTS_CANCELLED && gfi_cleared(&rejected); memset(&blob, 0xa5, sizeof(blob)); bool existing_latch = facts && cbm_git_facts_read_blob(facts, CBM_GIT_REV_HEAD, "binary.bin", 10, &blob, &error) == CBM_GIT_BLOB_ERROR && error.status == CBM_GIT_FACTS_CANCELLED && gf_blob_cleared(&blob); cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(opened); ASSERT_TRUE(control); ASSERT_TRUE(inventoried); ASSERT_TRUE(first); ASSERT_TRUE(latched); ASSERT_TRUE(existing_latch); PASS(); } TEST(test_git_facts_inventory_output_limit_has_no_prefix_and_latches) { enum { ITEMS = 512, NAME_LENGTH = 80, QUERY_CAP = 32768 }; gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); char names[ITEMS][NAME_LENGTH + 1]; gfi_raw_entry_t rows[ITEMS]; gfi_expected_t expected[ITEMS]; bool configured = true; for (int i = 0; i < ITEMS; i++) { int n = snprintf(names[i], sizeof(names[i]), "entry-%04d-", i); if (n <= 0 || n > NAME_LENGTH) { configured = false; break; } memset(names[i] + n, 'x', (size_t)(NAME_LENGTH - n)); names[i][NAME_LENGTH] = '\0'; rows[i] = (gfi_raw_entry_t){"100644", names[i], NAME_LENGTH, fx.binary}; expected[i] = (gfi_expected_t){names[i], NAME_LENGTH, 0100644, CBM_GIT_TREE_BLOB, fx.binary}; } char tree[65]; configured = configured && (size_t)ITEMS * NAME_LENGTH > QUERY_CAP && gfi_raw_tree(&fx, rows, ITEMS, tree) && gfi_head_tree(&fx, tree); cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *ample = configured ? cbm_git_facts_open(&opts, &error) : NULL; bool ample_open = ample && error.status == CBM_GIT_FACTS_OK; cbm_git_tree_inventory_t complete = gfi_poisoned(); bool control = ample && cbm_git_facts_inventory(ample, CBM_GIT_REV_HEAD, &complete, &error) && error.status == CBM_GIT_FACTS_OK && gfi_matches(&complete, expected, ITEMS); cbm_git_facts_free(ample); /* Aggregate byte budget: independent of how many native commands the API * uses. The literal path bytes alone exceed this cap. Open must succeed. */ opts = gf_options(&fx); opts.total_output_limit = QUERY_CAP; cbm_git_facts_t *limited = configured ? cbm_git_facts_open(&opts, &error) : NULL; bool limited_open = limited && error.status == CBM_GIT_FACTS_OK; cbm_git_tree_inventory_t inventory = gfi_poisoned(); bool limit = limited && !cbm_git_facts_inventory(limited, CBM_GIT_REV_HEAD, &inventory, &error) && error.status == CBM_GIT_FACTS_LIMIT && gfi_cleared(&inventory); inventory = gfi_poisoned(); bool latched = limited && !cbm_git_facts_inventory(limited, CBM_GIT_REV_MERGE_BASE, &inventory, &error) && error.status == CBM_GIT_FACTS_LIMIT && gfi_cleared(&inventory); cbm_git_blob_t blob; memset(&blob, 0xa5, sizeof(blob)); bool existing_latch = limited && cbm_git_facts_read_blob(limited, CBM_GIT_REV_HEAD, "entry-0000-", 11, &blob, &error) == CBM_GIT_BLOB_ERROR && error.status == CBM_GIT_FACTS_LIMIT && gf_blob_cleared(&blob); cbm_git_facts_free(limited); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(configured); ASSERT_TRUE(ample_open); ASSERT_TRUE(control); ASSERT_TRUE(limited_open); ASSERT_TRUE(limit); ASSERT_TRUE(latched); ASSERT_TRUE(existing_latch); PASS(); } /* Independent D1b tests. Native objects/argv helpers above remain unchanged. */ static const unsigned char gfa_old_names[] = "D\0deleted.bin\0A\0new.bin\0M\0p.txt\0A\0submodule\0"; static const unsigned char gfa_renamed_names[] = "A\0new.c\0D\0old.c\0D\0removed.c\0"; static const unsigned char gfa_merge_names[] = "M\0new.c\0M\0revert.c\0"; static cbm_git_bytes_t gfa_poisoned(void) { static const unsigned char stale[] = "stale"; return (cbm_git_bytes_t){stale, sizeof(stale)}; } static bool gfa_clear(cbm_git_bytes_t out) { return out.data == NULL && out.length == 0; } static bool gfa_clean_error(const cbm_git_facts_error_t *e) { return e->status == CBM_GIT_FACTS_OK && e->exit_code == -1 && e->diagnostic[0] == '\0'; } static bool gfa_linear(gf_fixture_t *fx, const char *tree_a, const char *tree_m, const char *tree_h) { char a[65], m[65], h[65]; bool ok = gf_commit(fx, tree_a, NULL, NULL, "ancestor A", a) && gf_commit(fx, tree_m, a, NULL, "ancestor M", m) && gf_commit(fx, tree_h, m, NULL, "ancestor H", h) && gf_ref(fx, "refs/heads/topic", h) && gf_ref(fx, "refs/heads/base", m); if (ok) { memcpy(fx->a, a, fx->oid_length + 1); memcpy(fx->q, m, fx->oid_length + 1); memcpy(fx->base, m, fx->oid_length + 1); memcpy(fx->head, h, fx->oid_length + 1); } return ok; } static bool gfa_rename_history(gf_fixture_t *fx) { const gfi_raw_entry_t a[] = { {"100644", "old.c", 5, fx->old_text}, {"100644", "removed.c", 9, fx->binary}, {"100644", "revert.c", 8, fx->old_text} }; const gfi_raw_entry_t m[] = { {"100644", "new.c", 5, fx->old_text}, {"100644", "revert.c", 8, fx->head_text} }; const gfi_raw_entry_t h[] = { {"100644", "new.c", 5, fx->head_text}, {"100644", "revert.c", 8, fx->old_text} }; char ta[65], tm[65], th[65]; return gfi_raw_tree(fx, a, 3, ta) && gfi_raw_tree(fx, m, 2, tm) && gfi_raw_tree(fx, h, 2, th) && gfa_linear(fx, ta, tm, th) && gf_config(fx, "diff.renames", "true"); } TEST(test_git_ancestor_changes_requires_ancestor_of_actual_merge_base) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); const cbm_git_facts_identity_t *id = facts ? cbm_git_facts_identity(facts) : NULL; bool identity = id && strcmp(id->merge_base, fx.q) == 0 && strcmp(id->merge_base, fx.p) != 0; bool controls = facts && cbm_git_facts_is_ancestor(facts, fx.p, fx.head, &error) == CBM_GIT_ANCESTRY_YES && cbm_git_facts_is_ancestor(facts, fx.p, fx.q, &error) == CBM_GIT_ANCESTRY_NO; cbm_git_bytes_t out = gfa_poisoned(); memset(&error, 0xa5, sizeof(error)); int no = facts ? cbm_git_facts_ancestor_changes(facts, fx.p, fx.oid_length, &out, &error) : -9; bool normal_no = no == CBM_GIT_ANCESTOR_CHANGES_NOT_ANCESTOR && gfa_clear(out) && gfa_clean_error(&error); out = gfa_poisoned(); memset(&error, 0xa5, sizeof(error)); int head = facts ? cbm_git_facts_ancestor_changes(facts, fx.head, fx.oid_length, &out, &error) : -9; bool head_no = head == CBM_GIT_ANCESTOR_CHANGES_NOT_ANCESTOR && gfa_clear(out) && gfa_clean_error(&error); out = gfa_poisoned(); memset(&error, 0xa5, sizeof(error)); int yes = facts ? cbm_git_facts_ancestor_changes(facts, fx.a, fx.oid_length, &out, &error) : -9; bool reused = yes == CBM_GIT_ANCESTOR_CHANGES_OK && gfa_clean_error(&error) && gf_bytes_equal(out, gfa_old_names, sizeof(gfa_old_names)-1); cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(identity); ASSERT_TRUE(controls); ASSERT_TRUE(normal_no); ASSERT_TRUE(head_no); ASSERT_TRUE(reused); PASS(); } TEST(test_git_ancestor_changes_preserves_earlier_paths_reversion_and_pinned_head) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); bool configured = gfa_rename_history(&fx); cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = configured ? cbm_git_facts_open(&opts, &error) : NULL; const cbm_git_facts_identity_t *id = facts ? cbm_git_facts_identity(facts) : NULL; bool identity = id && strcmp(id->merge_base, fx.q) == 0 && strcmp(id->head, fx.head) == 0; cbm_git_diff_t old = {0}; bool legacy = facts && cbm_git_facts_diff(facts, &old, &error) && gf_bytes_equal(old.name_status, gfa_merge_names, sizeof(gfa_merge_names)-1); char path[GF_PATH]; const char *empty[] = {"read-tree", "--empty", NULL}; bool moved = legacy && gf_ref(&fx, "refs/heads/topic", fx.a) && gf_ref(&fx, "refs/heads/base", fx.a) && gf_git(&fx, empty, NULL, 0) && gf_index(&fx, "100644", fx.binary, "ambient.c") && gf_path(path, fx.repo, "new.c") && gf_write(path, "ambient\n", 8); cbm_git_bytes_t out = gfa_poisoned(); int rc = facts ? cbm_git_facts_ancestor_changes(facts, fx.a, fx.oid_length, &out, &error) : -9; bool exact = rc == CBM_GIT_ANCESTOR_CHANGES_OK && gfa_clean_error(&error) && gf_bytes_equal(out, gfa_renamed_names, sizeof(gfa_renamed_names)-1); cbm_git_diff_t again = {0}; bool unchanged = facts && cbm_git_facts_diff(facts, &again, &error) && gf_bytes_equal(again.name_status, gfa_merge_names, sizeof(gfa_merge_names)-1); cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(configured); ASSERT_TRUE(identity); ASSERT_TRUE(legacy); ASSERT_TRUE(moved); ASSERT_TRUE(exact); ASSERT_TRUE(unchanged); PASS(); } TEST(test_git_ancestor_changes_equalities_and_empty_reversion_are_distinct) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = cbm_git_facts_open(&opts, &error); cbm_git_diff_t old = {0}; bool legacy = facts && cbm_git_facts_diff(facts, &old, &error); cbm_git_bytes_t out = gfa_poisoned(); bool equal_m = facts && cbm_git_facts_ancestor_changes(facts, fx.q, fx.oid_length, &out, &error) == CBM_GIT_ANCESTOR_CHANGES_OK && gfa_clean_error(&error) && legacy && gf_bytes_equal(out, old.name_status.data, old.name_status.length); cbm_git_facts_free(facts); bool base_at_head = gf_ref(&fx, "refs/heads/base", fx.head); opts = gf_options(&fx); facts = base_at_head ? cbm_git_facts_open(&opts, &error) : NULL; const cbm_git_facts_identity_t *id = facts ? cbm_git_facts_identity(facts) : NULL; bool identity = id && strcmp(id->head, id->merge_base) == 0; out = gfa_poisoned(); bool all_equal = facts && cbm_git_facts_ancestor_changes(facts, fx.head, fx.oid_length, &out, &error) == CBM_GIT_ANCESTOR_CHANGES_OK && gfa_clean_error(&error) && out.length == 0; cbm_git_facts_free(facts); /* Empty A->H with nonempty M->H: empty output cannot clear has_changes. */ const gfi_raw_entry_t a[] = {{"100644", "revert.c", 8, fx.old_text}}; const gfi_raw_entry_t m[] = {{"100644", "revert.c", 8, fx.head_text}}; char ta[65], tm[65]; bool history = gfi_raw_tree(&fx, a, 1, ta) && gfi_raw_tree(&fx, m, 1, tm) && gfa_linear(&fx, ta, tm, ta); opts = gf_options(&fx); facts = history ? cbm_git_facts_open(&opts, &error) : NULL; old = (cbm_git_diff_t){0}; const char changed[] = "M\0revert.c\0"; bool nonempty = facts && cbm_git_facts_diff(facts, &old, &error) && gf_bytes_equal(old.name_status, changed, sizeof(changed)-1); out = gfa_poisoned(); bool reverted = facts && cbm_git_facts_ancestor_changes(facts, fx.a, fx.oid_length, &out, &error) == CBM_GIT_ANCESTOR_CHANGES_OK && gfa_clean_error(&error) && out.length == 0; cbm_git_facts_free(facts); int cleanup = th_rmtree(fx.home); ASSERT_EQ(cleanup, 0); ASSERT_TRUE(legacy); ASSERT_TRUE(equal_m); ASSERT_TRUE(base_at_head); ASSERT_TRUE(identity); ASSERT_TRUE(all_equal); ASSERT_TRUE(history); ASSERT_TRUE(nonempty); ASSERT_TRUE(reverted); PASS(); } TEST(test_git_ancestor_changes_validates_full_commit_object_and_argument_span) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx, false)); char expression[96], tree[65], tag[65], tag_bytes[512], missing[65]; int n = snprintf(expression, sizeof(expression), "%s^{tree}", fx.head); const char *tree_args[] = {"rev-parse", "--verify", expression, NULL}; bool tree_ok = n > 0 && (size_t)n < sizeof(expression) && gf_oid(&fx, tree_args, tree); n = snprintf(tag_bytes, sizeof(tag_bytes), "object %s\ntype commit\ntag fixture\ntagger Git Facts Fixture 946684800 +0000\n\nfixture tag\n", fx.a); bool tag_ok = n > 0 && (size_t)n < sizeof(tag_bytes) && gf_object(&fx, "tag", tag_bytes, (size_t)n, tag); memset(missing, '0', fx.oid_length); missing[fx.oid_length] = '\0'; const char *missing_type[] = {"cat-file", "-t", missing, NULL}; bool absent_control = gf_git_exit(&fx, missing_type, 128, NULL, 0); cbm_git_facts_options_t opts = gf_options(&fx); cbm_git_facts_error_t error; cbm_git_facts_t *facts = tree_ok && tag_ok && absent_control ? cbm_git_facts_open(&opts, &error) : NULL; bool opened = facts != NULL, invalid = opened; for (unsigned i=0; facts && i<12; i++) { char input[65]; memset(input, 'a', sizeof(input)); memcpy(input, fx.a, fx.oid_length); const char *arg = input; size_t len = fx.oid_length; if (i==0) arg=NULL; if (i==1) len=0; if (i==2) len=fx.oid_length-1; if (i==3) len=64; /* SHA-256 width is invalid for this SHA-1 owner. */ if (i==4) input[4]='\0'; if (i==5) input[4]='g'; if (i==6) input[4]=' '; if (i==7) input[4]='^'; if (i==8) { arg="HEAD"; len=4; } if (i==9) input[0]='-'; cbm_git_bytes_t out=gfa_poisoned(); memset(&error,0xa5,sizeof(error)); int rc=cbm_git_facts_ancestor_changes(i==10?NULL:facts,arg,len,i==11?NULL:&out,&error); invalid = rc==CBM_GIT_ANCESTOR_CHANGES_ERROR && error.status==CBM_GIT_FACTS_INVALID && (i==11 || gfa_clear(out)) && invalid; } const char *wrong_type[] = {tree, fx.binary, tag}; bool rejected=opened; for (size_t i=0; facts && i<3; i++) { cbm_git_bytes_t out=gfa_poisoned(); memset(&error,0xa5,sizeof(error)); int rc=cbm_git_facts_ancestor_changes(facts,wrong_type[i],fx.oid_length,&out,&error); rejected = rc==CBM_GIT_ANCESTOR_CHANGES_ERROR && error.status==CBM_GIT_FACTS_INVALID && gfa_clear(out) && rejected; } cbm_git_bytes_t out=gfa_poisoned(); int absent=facts?cbm_git_facts_ancestor_changes(facts,missing,fx.oid_length,&out,&error):-9; bool missing_error=absent==CBM_GIT_ANCESTOR_CHANGES_ERROR && error.status==CBM_GIT_FACTS_COMMAND && gfa_clear(out); out=gfa_poisoned(); bool reusable=facts && cbm_git_facts_ancestor_changes(facts,fx.a,fx.oid_length,&out,NULL)==CBM_GIT_ANCESTOR_CHANGES_OK && gf_bytes_equal(out,gfa_old_names,sizeof(gfa_old_names)-1); cbm_git_facts_free(facts); int cleanup=th_rmtree(fx.home); ASSERT_EQ(cleanup,0); ASSERT_TRUE(tree_ok); ASSERT_TRUE(tag_ok); ASSERT_TRUE(absent_control); ASSERT_TRUE(opened); ASSERT_TRUE(invalid); ASSERT_TRUE(rejected); ASSERT_TRUE(missing_error); ASSERT_TRUE(reusable); PASS(); } typedef struct { char *input; size_t length; bool armed, overwritten; } gfa_borrow_t; /* Prompt and non-reentrant: poisons only the caller's borrowed bytes at the first gate. */ static bool gfa_poison_borrowed_oid(void *context) { gfa_borrow_t *c=context; if(c->armed && !c->overwritten) { memset(c->input,'#',c->length);c->overwritten=true; } return false; } TEST(test_git_ancestor_changes_sha256_raw_paths_and_local_oid_copy) { enum { LONG_PATH=65537, EXPECTED_CAP=LONG_PATH+1024 }; for(int algorithm=0;algorithm<2;algorithm++) { gf_fixture_t fx; ASSERT_TRUE(gf_fixture_open(&fx,algorithm!=0)); char long_name[LONG_PATH+1];memset(long_name,'z',LONG_PATH);long_name[LONG_PATH]='\0'; const gfi_raw_entry_t a[] = { {"100644","binary",6,fx.binary},{"100644","deleted",7,fx.binary}, {"160000","gitlink",7,fx.a},{"100644","mode",4,fx.binary},{"100644","type",4,fx.binary} }; const gfi_raw_entry_t h[] = { {"100644","-dash",5,fx.binary},{"100644",":colon",6,fx.binary},{"100644","[glob]*",7,fx.binary}, {"100644","back\\slash",10,fx.binary},{"100644","binary",6,fx.newer}, {"160000","gitlink",7,fx.p},{"100644","line\nname",9,fx.binary},{"100755","mode",4,fx.binary}, {"100644","tab\tname",8,fx.binary},{"120000","type",4,fx.link}, {"100644",long_name,LONG_PATH,fx.binary},{"100644","\x80" "high",5,fx.binary} }; char ta[65],th[65];bool history=gfi_raw_tree(&fx,a,5,ta) && gfi_raw_tree_format(&fx,h,12,false,th) && gfa_linear(&fx,ta,ta,th); const char prefix[]="A\0-dash\0A\0:colon\0A\0[glob]*\0A\0back\\slash\0M\0binary\0D\0deleted\0M\0gitlink\0A\0line\nname\0M\0mode\0A\0tab\tname\0T\0type\0A\0"; const char suffix[]="A\0\x80" "high\0"; unsigned char expected[EXPECTED_CAP];size_t used=0; memcpy(expected+used,prefix,sizeof(prefix)-1);used+=sizeof(prefix)-1; memcpy(expected+used,long_name,LONG_PATH);used+=LONG_PATH;expected[used++]=0; memcpy(expected+used,suffix,sizeof(suffix)-1);used+=sizeof(suffix)-1; char caller_oid[64];memset(caller_oid,'?',sizeof(caller_oid));memcpy(caller_oid,fx.a,fx.oid_length); unsigned uppercase=0; for(unsigned i=0;i='a' && caller_oid[i]<='f'){caller_oid[i]=(char)(caller_oid[i]-'a'+'A');uppercase++;} gfa_borrow_t borrow={.input=caller_oid,.length=fx.oid_length}; cbm_git_facts_options_t opts=gf_options(&fx);opts.cancelled=gfa_poison_borrowed_oid;opts.cancel_context=&borrow; cbm_git_facts_error_t error;cbm_git_facts_t *facts=history?cbm_git_facts_open(&opts,&error):NULL; const cbm_git_facts_identity_t *id=facts?cbm_git_facts_identity(facts):NULL; bool identity=id && id->oid_hex_length==(algorithm?64U:40U) && strcmp(id->merge_base,fx.q)==0; cbm_git_bytes_t out=gfa_poisoned();borrow.armed=true; int rc=facts?cbm_git_facts_ancestor_changes(facts,caller_oid,fx.oid_length,&out,&error):-9; borrow.armed=false; bool exact=rc==CBM_GIT_ANCESTOR_CHANGES_OK && gfa_clean_error(&error) && gf_bytes_equal(out,expected,used); memset(caller_oid,'!',sizeof(caller_oid));bool retained=exact && gf_bytes_equal(out,expected,used); cbm_git_bytes_t again=gfa_poisoned(); bool equal_m=facts && cbm_git_facts_ancestor_changes(facts,fx.q,fx.oid_length,&again,&error)==CBM_GIT_ANCESTOR_CHANGES_OK && gf_bytes_equal(again,expected,used); bool wrong_width=true; if(algorithm) { again=gfa_poisoned();int bad=cbm_git_facts_ancestor_changes(facts,fx.a,40,&again,&error);wrong_width=bad==CBM_GIT_ANCESTOR_CHANGES_ERROR && error.status==CBM_GIT_FACTS_INVALID && gfa_clear(again); } cbm_git_facts_free(facts);int cleanup=th_rmtree(fx.home); ASSERT_EQ(cleanup,0);ASSERT_TRUE(history);ASSERT_TRUE(identity);ASSERT_TRUE(uppercase>0);ASSERT_TRUE(borrow.overwritten); ASSERT_TRUE(exact);ASSERT_TRUE(retained);ASSERT_TRUE(equal_m);ASSERT_TRUE(wrong_width); } PASS(); } TEST(test_git_ancestor_changes_repeats_preserve_all_previous_owner_buffers) { gf_fixture_t fx;ASSERT_TRUE(gf_fixture_open(&fx,false));bool configured=gfa_rename_history(&fx); cbm_git_facts_options_t opts=gf_options(&fx);cbm_git_facts_error_t error; cbm_git_facts_t *facts=configured?cbm_git_facts_open(&opts,&error):NULL;cbm_git_diff_t legacy={0}; bool old=facts && cbm_git_facts_diff(facts,&legacy,&error); unsigned char patch[16384],names[1024];bool copied=old && legacy.patch.length>0 && legacy.patch.length<=sizeof(patch) && legacy.name_status.length<=sizeof(names); if(copied){memcpy(patch,legacy.patch.data,legacy.patch.length);memcpy(names,legacy.name_status.data,legacy.name_status.length);} const unsigned char *old_patch=legacy.patch.data,*old_names=legacy.name_status.data; cbm_git_bytes_t output=gfa_poisoned();bool first=facts && cbm_git_facts_ancestor_changes(facts,fx.a,fx.oid_length,&output,&error)==CBM_GIT_ANCESTOR_CHANGES_OK; cbm_git_bytes_t alias_a=output; bool second=facts && cbm_git_facts_ancestor_changes(facts,fx.q,fx.oid_length,&output,&error)==CBM_GIT_ANCESTOR_CHANGES_OK; cbm_git_bytes_t alias_m=output; bool no=facts && cbm_git_facts_ancestor_changes(facts,fx.head,fx.oid_length,&output,&error)==CBM_GIT_ANCESTOR_CHANGES_NOT_ANCESTOR && gfa_clear(output) && gfa_clean_error(&error); bool invalid=facts && cbm_git_facts_ancestor_changes(facts,"HEAD",4,&output,&error)==CBM_GIT_ANCESTOR_CHANGES_ERROR && error.status==CBM_GIT_FACTS_INVALID && gfa_clear(output); cbm_git_diff_t later={0};bool repeated=facts && cbm_git_facts_diff(facts,&later,&error); bool retained=copied && first && second && legacy.patch.data==old_patch && legacy.name_status.data==old_names && gf_bytes_equal(legacy.patch,patch,legacy.patch.length) && gf_bytes_equal(legacy.name_status,names,legacy.name_status.length) && gf_bytes_equal(alias_a,gfa_renamed_names,sizeof(gfa_renamed_names)-1) && gf_bytes_equal(alias_m,gfa_merge_names,sizeof(gfa_merge_names)-1) && repeated && gf_bytes_equal(later.patch,patch,legacy.patch.length) && gf_bytes_equal(later.name_status,names,legacy.name_status.length); cbm_git_facts_free(facts);int cleanup=th_rmtree(fx.home); ASSERT_EQ(cleanup,0);ASSERT_TRUE(configured);ASSERT_TRUE(old);ASSERT_TRUE(copied);ASSERT_TRUE(first);ASSERT_TRUE(second); ASSERT_TRUE(no);ASSERT_TRUE(invalid);ASSERT_TRUE(repeated);ASSERT_TRUE(retained);PASS(); } TEST(test_git_ancestor_changes_cancellation_is_shared_terminal_and_preserves_views) { gf_fixture_t fx;ASSERT_TRUE(gf_fixture_open(&fx,false));bool cancelled=false; cbm_git_facts_options_t opts=gf_options(&fx);opts.cancelled=gf_cancelled;opts.cancel_context=&cancelled; cbm_git_facts_error_t error;cbm_git_facts_t *facts=cbm_git_facts_open(&opts,&error); cbm_git_bytes_t old=gfa_poisoned();bool control=facts && cbm_git_facts_ancestor_changes(facts,fx.q,fx.oid_length,&old,&error)==CBM_GIT_ANCESTOR_CHANGES_OK && gf_bytes_equal(old,gfa_old_names,sizeof(gfa_old_names)-1); cancelled=true;cbm_git_bytes_t out=gfa_poisoned(); bool rejected=facts && cbm_git_facts_ancestor_changes(facts,fx.a,fx.oid_length,&out,&error)==CBM_GIT_ANCESTOR_CHANGES_ERROR && error.status==CBM_GIT_FACTS_CANCELLED && gfa_clear(out); cancelled=false;out=gfa_poisoned(); bool sticky=facts && cbm_git_facts_ancestor_changes(facts,fx.q,fx.oid_length,&out,&error)==CBM_GIT_ANCESTOR_CHANGES_ERROR && error.status==CBM_GIT_FACTS_CANCELLED && gfa_clear(out); cbm_git_diff_t diff;memset(&diff,0xa5,sizeof(diff)); bool shared=facts && !cbm_git_facts_diff(facts,&diff,&error) && error.status==CBM_GIT_FACTS_CANCELLED && gfa_clear(diff.patch) && gfa_clear(diff.name_status); bool retained=control && gf_bytes_equal(old,gfa_old_names,sizeof(gfa_old_names)-1); cbm_git_facts_free(facts);int cleanup=th_rmtree(fx.home); ASSERT_EQ(cleanup,0);ASSERT_TRUE(control);ASSERT_TRUE(rejected);ASSERT_TRUE(sticky);ASSERT_TRUE(shared);ASSERT_TRUE(retained);PASS(); } TEST(test_git_ancestor_changes_output_and_cumulative_limits_publish_no_prefix) { enum { ITEMS=512, LENGTH=80, CAP=32768 }; gf_fixture_t fx;ASSERT_TRUE(gf_fixture_open(&fx,false)); char names[ITEMS][LENGTH+1];gfi_raw_entry_t rows[ITEMS];bool configured=true; for(int i=0;iLENGTH){configured=false;break;} memset(names[i]+n,'x',(size_t)(LENGTH-n));names[i][LENGTH]='\0'; rows[i]=(gfi_raw_entry_t){"100644",names[i],LENGTH,fx.binary}; } char tree[65];configured=configured && gfi_raw_tree(&fx,rows,ITEMS,tree) && gfi_head_tree(&fx,tree); cbm_git_facts_options_t opts=gf_options(&fx);cbm_git_facts_error_t error; cbm_git_facts_t *ample=configured?cbm_git_facts_open(&opts,&error):NULL;cbm_git_bytes_t complete=gfa_poisoned(); bool control=ample && cbm_git_facts_ancestor_changes(ample,fx.q,fx.oid_length,&complete,&error)==CBM_GIT_ANCESTOR_CHANGES_OK && gfa_clean_error(&error) && complete.length>CAP && complete.data; bool all_entries=control; for(int i=0;i0 && gf_bytes_equal(first,gfa_old_names,sizeof(gfa_old_names)-1) && gf_bytes_equal(kept.bytes,gf_binary,sizeof(gf_binary)); cbm_git_facts_free(facts);int cleanup=th_rmtree(fx.home); ASSERT_EQ(cleanup,0);ASSERT_TRUE(opened);ASSERT_TRUE(prior);ASSERT_TRUE(successes>0);ASSERT_TRUE(!unexpected);ASSERT_TRUE(limit);ASSERT_TRUE(sticky);ASSERT_TRUE(shared);ASSERT_TRUE(retained);PASS(); } static bool gfa_topology_stays_pinned(void) { gf_fixture_t original,redirected; if(!gf_fixture_open(&original,false))return false; if(!gf_fixture_open(&redirected,false)){(void)th_rmtree(original.home);return false;} bool same=strcmp(original.q,redirected.q)==0 && strcmp(original.p,redirected.p)==0; char original_git[GF_PATH];bool named=gf_path(original_git,original.home,"ancestor-original-git"); const char *relocate[]={"-c","init.templateDir=","init","--quiet","--separate-git-dir",original_git,NULL}; bool relocated=same && named && gf_git(&original,relocate,NULL,0); cbm_git_facts_options_t opts=gf_options(&original);cbm_git_facts_error_t error; cbm_git_facts_t *facts=relocated?cbm_git_facts_open(&opts,&error):NULL; cbm_git_bytes_t out=gfa_poisoned(); bool before=facts && cbm_git_facts_ancestor_changes(facts,original.p,original.oid_length,&out,&error)==CBM_GIT_ANCESTOR_CHANGES_NOT_ANCESTOR && gfa_clear(out) && gfa_clean_error(&error); char target[GF_PATH],info[GF_PATH],graft_path[GF_PATH],gitfile_path[GF_PATH]; char graft[160],gitfile[GF_PATH+32];int n=snprintf(graft,sizeof(graft),"%s %s\n",original.q,original.p); bool grafted=n>0 && (size_t)n0 && (size_t)n0 && (size_t)narena, 4096); if (!gf_fixture_open(&f->git, sha256)) return false; bool ok = gf_object(&f->git, "blob", "", 0, f->empty_oid) && gf_object(&f->git, "blob", gfb_binary, sizeof(gfb_binary), f->binary_oid) && gf_object(&f->git, "blob", gfb_header_body, sizeof(gfb_header_body)-1, f->header_oid); for (int j = 0; ok && j < 3; j++) { f->large[j] = cbm_arena_alloc(&f->arena, GFB_BODY); ok = f->large[j] != NULL; for (size_t i = 0; ok && i < GFB_BODY; i++) f->large[j][i] = (unsigned char)((i + (size_t)j * 31) % 251); if (ok) ok = gf_object(&f->git, "blob", f->large[j], GFB_BODY, f->large_oid[j]); } const char *paths[] = {":colon", "[g]*", "back\\slash", "empty", "header", "large-a", "large-b", "large-c", "line\nbreak", "link", "p.txt", "submodule", "tab\tname", gfb_utf}; const char *oids[] = {f->binary_oid, f->binary_oid, f->binary_oid, f->empty_oid, f->header_oid, f->large_oid[0], f->large_oid[1], f->large_oid[2], f->binary_oid, f->git.link, f->git.head_text, f->git.a, f->binary_oid, f->binary_oid}; gfi_raw_entry_t rows[GFB_COUNT]; for (size_t i = 0; i < GFB_COUNT; i++) { uint32_t mode = i == GFB_EXEC ? 0100755 : i == GFB_LINK ? 0120000 : i == GFB_SUBMODULE ? 0160000 : 0100644; const char *mode_text = i == GFB_EXEC ? "100755" : i == GFB_LINK ? "120000" : i == GFB_SUBMODULE ? "160000" : "100644"; size_t length = i == GFB_UTF ? sizeof(gfb_utf) : strlen(paths[i]); f->expected[i] = (gfi_expected_t){paths[i], length, mode, i == GFB_SUBMODULE ? CBM_GIT_TREE_COMMIT : CBM_GIT_TREE_BLOB, oids[i]}; rows[i] = (gfi_raw_entry_t){mode_text, paths[i], length, oids[i]}; } char th[65], tm[65], q[65], h[65], b[65]; ok = ok && gfi_raw_tree(&f->git, rows, GFB_COUNT, th); rows[GFB_TEXT].oid = f->git.old_text; ok = ok && gfi_raw_tree(&f->git, rows, GFB_COUNT, tm) && gf_commit(&f->git, tm, f->git.a, NULL, "batch Q", q) && gf_commit(&f->git, th, f->git.p, q, "batch H", h) && gf_commit(&f->git, tm, q, NULL, "batch B", b) && gf_ref(&f->git, "refs/heads/topic", h) && gf_ref(&f->git, "refs/heads/base", b); if (ok) { memcpy(f->git.q, q, f->git.oid_length+1); memcpy(f->git.head, h, f->git.oid_length+1); memcpy(f->git.base, b, f->git.oid_length+1); } return ok; } static int gfb_close(gfb_fixture_t *f) { cbm_arena_destroy(&f->arena); return f->git.home[0] ? th_rmtree(f->git.home) : 0; } static cbm_git_blob_batch_limits_t gfb_limits(void) { return (cbm_git_blob_batch_limits_t){.max_entries=1024, .max_input_bytes=1024*1024, .max_arena_bytes=32U*1024U*1024U}; } static cbm_git_blob_batch_t gfb_poisoned(void) { static const cbm_git_blob_batch_item_t old = {.inventory_index=123}; return (cbm_git_blob_batch_t){.revision=CBM_GIT_REV_MERGE_BASE, .commit="old", .items=&old, .count=1}; } static bool gfb_cleared(cbm_git_blob_batch_t out) { return out.revision == 0 && out.commit == NULL && out.items == NULL && out.count == 0; } static cbm_git_bytes_t gfb_expected_bytes(gfb_fixture_t *f, cbm_git_revision_t rev, size_t index) { if (index == GFB_EMPTY) return (cbm_git_bytes_t){NULL,0}; if (index == GFB_HEADER) return (cbm_git_bytes_t){(const unsigned char *)gfb_header_body, sizeof(gfb_header_body)-1}; if (index >= GFB_LARGE_A && index <= GFB_LARGE_C) return (cbm_git_bytes_t){f->large[index-GFB_LARGE_A],GFB_BODY}; if (index == GFB_TEXT) { const char *text = rev == CBM_GIT_REV_HEAD ? gf_head_text : gf_old_text; return (cbm_git_bytes_t){(const unsigned char *)text,strlen(text)}; } return (cbm_git_bytes_t){gfb_binary,sizeof(gfb_binary)}; } static bool gfb_matches(gfb_fixture_t *f, cbm_git_revision_t rev, const size_t *indices, size_t count, const cbm_git_blob_batch_t *out) { const char *commit = rev == CBM_GIT_REV_HEAD ? f->git.head : f->git.q; if (out->revision != rev || !out->commit || strcmp(out->commit,commit) || out->count != count || (count ? !out->items : out->items != NULL)) return false; for (size_t i=0;i=GFB_COUNT || index==GFB_LINK || index==GFB_SUBMODULE) return false; const cbm_git_blob_batch_item_t *item=&out->items[i]; const cbm_git_tree_entry_t *e=item->entry; const gfi_expected_t *want=&f->expected[index]; const char *oid=index==GFB_TEXT && rev==CBM_GIT_REV_MERGE_BASE ? f->git.old_text : want->oid; cbm_git_bytes_t bytes=gfb_expected_bytes(f,rev,index); if (item->inventory_index!=index || !e || !e->path || e->path_length!=want->length || memcmp(e->path,want->path,want->length) || e->path[e->path_length]!=0 || e->mode!=want->mode || e->object_type!=CBM_GIT_TREE_BLOB || strcmp(e->oid,oid) || !gf_bytes_equal(item->bytes,bytes.data,bytes.length)) return false; } return true; } /* A failed Git facts call names its stage and error: these fixtures fail * intermittently on macos-15-intel CI only, and a bare ASSERT left nothing to * attribute. */ static void gfb_report(const char *stage, const cbm_git_facts_error_t *e) { fprintf(stderr, "Git facts %s failed: status=%d exit=%d diagnostic=%.300s\n", stage, e->status, e->exit_code, e->diagnostic); } static bool gfb_inventory_control(gfb_fixture_t *f, cbm_git_facts_t *facts) { cbm_git_facts_error_t error; memset(&error, 0, sizeof(error)); cbm_git_tree_inventory_t inventory={0}; const cbm_git_facts_identity_t *id=cbm_git_facts_identity(facts); if (!id || strcmp(id->merge_base,f->git.q)!=0 || strcmp(id->merge_base,f->git.p)==0) { fprintf(stderr, "Git facts identity control failed: identity=%d\n", id != NULL); return false; } if (!cbm_git_facts_inventory(facts,CBM_GIT_REV_HEAD,&inventory,&error)) { gfb_report("inventory", &error); return false; } return gfi_matches(&inventory,f->expected,GFB_COUNT); } static bool gfb_good(gfb_fixture_t *f, cbm_git_facts_t *facts, cbm_git_revision_t rev, const size_t *indices, size_t count, const cbm_git_blob_batch_limits_t *limits, cbm_git_blob_batch_t *keep) { cbm_git_blob_batch_request_t request={rev,indices,count}; cbm_git_blob_batch_t out=gfb_poisoned(); cbm_git_facts_error_t error; memset(&error,0xa5,sizeof(error)); bool read=cbm_git_facts_read_blob_batch(facts,&request,limits,&out,&error); if (!read || !gfa_clean_error(&error)) gfb_report(read ? "blob batch (unclean error)" : "blob batch", &error); bool ok=read && gfa_clean_error(&error) && gfb_matches(f,rev,indices,count,&out); if (keep) *keep=out; return ok; } static bool gfb_error(cbm_git_facts_t *facts, const cbm_git_blob_batch_request_t *request, const cbm_git_blob_batch_limits_t *limits, cbm_git_facts_status_t expected) { cbm_git_blob_batch_t out=gfb_poisoned(); cbm_git_facts_error_t error; memset(&error,0xa5,sizeof(error)); return !cbm_git_facts_read_blob_batch(facts,request,limits,&out,&error) && error.status==expected && gfb_cleared(out); } TEST(test_git_blob_batch_native_bytes_and_both_object_formats) { bool setups=true, exact=true, clean=true; for (int algorithm=0;algorithm<2;algorithm++) { gfb_fixture_t f; bool configured=gfb_open(&f,algorithm!=0); cbm_git_facts_options_t opts=gf_options(&f.git); cbm_git_facts_error_t error; cbm_git_facts_t *facts=configured?cbm_git_facts_open(&opts,&error):NULL; bool control=facts && gfb_inventory_control(&f,facts); const size_t head[]={GFB_EMPTY,GFB_COLON,GFB_HEADER,GFB_LARGE_A,GFB_EXEC,GFB_TEXT}; const size_t merge[]={GFB_TEXT,GFB_LARGE_B,GFB_EMPTY}; cbm_git_blob_batch_limits_t limits=gfb_limits(); exact=control && gfb_good(&f,facts,CBM_GIT_REV_HEAD,head,6,&limits,NULL) && gfb_good(&f,facts,CBM_GIT_REV_MERGE_BASE,merge,3,&limits,NULL) && exact; setups=configured && control && setups; cbm_git_facts_free(facts); clean=gfb_close(&f)==0 && clean; } ASSERT_TRUE(clean); ASSERT_TRUE(setups); ASSERT_TRUE(exact); PASS(); } TEST(test_git_blob_batch_keeps_request_order_duplicates_and_raw_paths) { gfb_fixture_t f; bool configured=gfb_open(&f,true); cbm_git_facts_options_t opts=gf_options(&f.git); cbm_git_facts_error_t error; cbm_git_facts_t *facts=configured?cbm_git_facts_open(&opts,&error):NULL; bool control=facts && gfb_inventory_control(&f,facts); const size_t indices[]={GFB_UTF,GFB_EXEC,GFB_COLON,GFB_TAB,GFB_LINE,GFB_GLOB,GFB_EXEC,GFB_UTF}; cbm_git_blob_batch_limits_t limits=gfb_limits(); cbm_git_blob_batch_t out={0}; bool exact=control && gfb_good(&f,facts,CBM_GIT_REV_HEAD,indices,8,&limits,&out); bool distinct=exact && out.items[1].entry->mode==0100755 && out.items[2].entry->mode==0100644 && strcmp(out.items[1].entry->oid,out.items[2].entry->oid)==0 && out.items[1].entry->path_length!=out.items[2].entry->path_length; cbm_git_facts_free(facts); int cleanup=gfb_close(&f); ASSERT_EQ(cleanup,0); ASSERT_TRUE(configured); ASSERT_TRUE(control); ASSERT_TRUE(exact); ASSERT_TRUE(distinct); PASS(); } TEST(test_git_blob_batch_pinned_revisions_and_cache_history_guards) { gfb_fixture_t f; bool configured=gfb_open(&f,false); cbm_git_facts_options_t opts=gf_options(&f.git); cbm_git_facts_error_t error; cbm_git_facts_t *facts=configured?cbm_git_facts_open(&opts,&error):NULL; const size_t index=GFB_TEXT; cbm_git_blob_batch_limits_t limits=gfb_limits(); bool control=facts && gfb_inventory_control(&f,facts); bool before=control && gfb_good(&f,facts,CBM_GIT_REV_HEAD,&index,1,&limits,NULL); char path[GF_PATH]; const char *empty_index[]={"read-tree","--empty",NULL}; bool mutation=configured && gf_path(path,f.git.repo,"p.txt") && gf_write(path,"ambient\n",8) && gf_git(&f.git,empty_index,NULL,0) && gf_ref(&f.git,"refs/heads/topic",f.git.a) && gf_ref(&f.git,"refs/heads/base",f.git.p); bool pinned=before && mutation && gfb_good(&f,facts,CBM_GIT_REV_HEAD,&index,1,&limits,NULL) && gfb_good(&f,facts,CBM_GIT_REV_MERGE_BASE,&index,1,&limits,NULL); char graft[160],info[GF_PATH];int n=snprintf(graft,sizeof(graft),"%s %s\n",f.git.head,f.git.a); bool written=control && n>0 && (size_t)n0 && (size_t)n0 && (size_t)n=10;n/=10)digits++; return width+1+4+1+digits+1+size+1; } TEST(test_git_blob_batch_exact_frame_caps_and_native_split_batches) { gfb_fixture_t f;bool configured=gfb_open(&f,false); const size_t indices[]={GFB_LARGE_C,GFB_LARGE_A,GFB_LARGE_B}; cbm_git_blob_batch_limits_t limits=gfb_limits(); size_t exact=gfb_frame_size(f.git.oid_length,GFB_BODY); bool controls=configured, outcomes=true; for(int variant=0;configured && variant<3;variant++) { cbm_git_facts_options_t opts=gf_options(&f.git); opts.stdout_limit=variant==2?exact-1:exact; cbm_git_facts_error_t error;cbm_git_facts_t *facts=cbm_git_facts_open(&opts,&error); if (!facts) gfb_report("open (budget variant)", &error); bool opened=facts && gfb_inventory_control(&f,facts); controls=opened && controls; if(variant==0)outcomes=opened && gfb_good(&f,facts,CBM_GIT_REV_HEAD,indices,1,&limits,NULL) && outcomes; if(variant==1)outcomes=opened && gfb_good(&f,facts,CBM_GIT_REV_HEAD,indices,3,&limits,NULL) && outcomes; if(variant==2) { const size_t mixed[]={GFB_EMPTY,GFB_COLON,GFB_LARGE_C}; cbm_git_blob_batch_request_t request={CBM_GIT_REV_HEAD,mixed,3}; bool limited=opened && gfb_error(facts,&request,&limits,CBM_GIT_FACTS_LIMIT); cbm_git_blob_batch_request_t empty={CBM_GIT_REV_HEAD,NULL,0}; outcomes=limited && gfb_error(facts,&empty,&limits,CBM_GIT_FACTS_LIMIT) && outcomes; } cbm_git_facts_free(facts); } int cleanup=gfb_close(&f); ASSERT_EQ(cleanup,0);ASSERT_TRUE(configured);ASSERT_TRUE(controls);ASSERT_TRUE(outcomes);PASS(); } TEST(test_git_blob_batch_per_call_and_existing_shared_budgets) { gfb_fixture_t f;bool configured=gfb_open(&f,false); const size_t repeated[]={GFB_COLON,GFB_EXEC,GFB_HEADER,GFB_COLON}; /* Four entries, but exactly two distinct OIDs and two physical phases. */ size_t exact_input=2U*2U*(f.git.oid_length+1U); cbm_git_blob_batch_request_t request={CBM_GIT_REV_HEAD,repeated,4}; cbm_git_blob_batch_limits_t ample=gfb_limits();bool controls=configured, outcomes=true; for(int variant=0;configured && variant<4;variant++) { cbm_git_facts_options_t opts=gf_options(&f.git);cbm_git_facts_error_t error; cbm_git_facts_t *facts=cbm_git_facts_open(&opts,&error); cbm_git_blob_t retained={0}; bool before=facts && gfb_inventory_control(&f,facts) && cbm_git_facts_read_blob(facts,CBM_GIT_REV_HEAD,"p.txt",5,&retained,&error)==CBM_GIT_BLOB_FOUND && gf_bytes_equal(retained.bytes,gf_head_text,sizeof(gf_head_text)-1); controls=before && controls; cbm_git_blob_batch_limits_t limit=ample; if(variant==0) { limit.max_entries=4;limit.max_input_bytes=exact_input; outcomes=before && gfb_good(&f,facts,CBM_GIT_REV_HEAD,repeated,4,&limit,NULL) && gfb_good(&f,facts,CBM_GIT_REV_HEAD,repeated,4,&limit,NULL) && outcomes; } else { cbm_git_blob_batch_request_t chosen=request; if(variant==1)limit.max_entries=3; if(variant==2)limit.max_input_bytes=exact_input-1; if(variant==3) { /* No selected objects/items: guard/helper allocations still count. */ chosen=(cbm_git_blob_batch_request_t){CBM_GIT_REV_HEAD,NULL,0}; bool empty_control=before && gfb_good(&f,facts,CBM_GIT_REV_HEAD,NULL,0,&le,NULL); outcomes=empty_control && outcomes; limit.max_arena_bytes=1; } bool limited=before && gfb_error(facts,&chosen,&limit,CBM_GIT_FACTS_LIMIT); outcomes=limited && gfb_error(facts,&request,&le,CBM_GIT_FACTS_LIMIT) && outcomes; } outcomes=gf_bytes_equal(retained.bytes,gf_head_text,sizeof(gf_head_text)-1) && outcomes; cbm_git_facts_free(facts); } /* Repeated successful calls cannot reset shared native-command/output caps. * Bounds below use public limits and a minimum of one real command/body per * success, never an exact internal command count or phase callback number. */ for(int budget=0;configured && budget<2;budget++) { cbm_git_facts_options_t opts=gf_options(&f.git); if(budget==0)opts.command_limit=64; else opts.total_output_limit=3U*GFB_BODY+32768U; cbm_git_facts_error_t error;cbm_git_facts_t *facts=cbm_git_facts_open(&opts,&error); cbm_git_blob_t retained={0}; bool before=facts && cbm_git_facts_read_blob(facts,CBM_GIT_REV_HEAD,"p.txt",5,&retained,&error)==CBM_GIT_BLOB_FOUND && gf_bytes_equal(retained.bytes,gf_head_text,sizeof(gf_head_text)-1); controls=before && controls; size_t index=budget==0?GFB_HEADER:GFB_LARGE_A; cbm_git_blob_batch_request_t one={CBM_GIT_REV_HEAD,&index,1}; cbm_git_blob_batch_t first={0};unsigned successes=0; bool limited=false, unexpected=false; unsigned bound=budget==0?opts.command_limit+1U:5U; for(unsigned i=0;before && i0 && gfb_matches(&f,CBM_GIT_REV_HEAD,&index,1,&first) && gf_bytes_equal(retained.bytes,gf_head_text,sizeof(gf_head_text)-1); outcomes=before && successes>0 && !unexpected && limited && sticky && retained_ok && outcomes; cbm_git_facts_free(facts); } int cleanup=gfb_close(&f); ASSERT_EQ(cleanup,0);ASSERT_TRUE(configured);ASSERT_TRUE(controls);ASSERT_TRUE(outcomes);PASS(); } static bool gfb_header_error(cbm_git_bytes_t bytes,const char *oid,size_t width, cbm_git_facts_status_t expected) { size_t size=123, header=456;cbm_git_facts_error_t error; memset(&error,0xa5,sizeof(error)); return !cbm_git_facts_test_batch_header(bytes,oid,width,&size,&header,&error) && error.status==expected && size==0 && header==0; } TEST(test_git_blob_batch_strict_headers_and_native_missing_object) { bool grammar=true; for(size_t width=40;width<=64;width+=24) { char oid[65];memset(oid,'a',width);oid[width]=0; char capture[256];int n=snprintf(capture,sizeof(capture),"%s blob 3\nXYZ\n",oid); size_t size=123,header=456;cbm_git_facts_error_t error; bool positive=n>0 && (size_t)n0 && (size_t)n0 && (size_t)n0 && (size_t)n0 && (size_t)narmed)return false; c->calls++; return c->cancel_at && c->calls>=c->cancel_at; } TEST(test_git_blob_batch_cancellation_is_sticky_and_owner_local) { gfb_fixture_t f;bool configured=gfb_open(&f,false); const size_t index=GFB_LARGE_A;cbm_git_blob_batch_limits_t limits=gfb_limits(); cbm_git_blob_batch_request_t request={CBM_GIT_REV_HEAD,&index,1}; gfb_cancel_t observed={0};cbm_git_facts_options_t opts=gf_options(&f.git); opts.cancelled=gfb_cancel;opts.cancel_context=&observed; cbm_git_facts_error_t error;cbm_git_facts_t *healthy=configured?cbm_git_facts_open(&opts,&error):NULL; bool control=healthy && gfb_inventory_control(&f,healthy); observed.armed=true; bool positive=control && gfb_good(&f,healthy,CBM_GIT_REV_HEAD,&index,1,&limits,NULL) && observed.calls>=2; observed.armed=false; bool stopped=true; /* Entry and final publication require at least two polls on any successful * nonempty call. These two early thresholds do not identify a phase or rely * on the process-supervision loop's variable callback count. */ for(size_t threshold=1;configured && threshold<=2;threshold++) { gfb_cancel_t cancel={0};opts=gf_options(&f.git);opts.cancelled=gfb_cancel;opts.cancel_context=&cancel; cbm_git_facts_t *facts=cbm_git_facts_open(&opts,&error); const size_t small=GFB_COLON;cbm_git_blob_batch_t kept={0}; bool before=facts && gfb_good(&f,facts,CBM_GIT_REV_HEAD,&small,1,&limits,&kept); cancel.armed=true;cancel.cancel_at=threshold; bool rejected=before && gfb_error(facts,&request,&limits,CBM_GIT_FACTS_CANCELLED); cancel.armed=false; cbm_git_blob_batch_request_t empty={CBM_GIT_REV_HEAD,NULL,0}; bool latched=rejected && gfb_error(facts,&empty,&limits,CBM_GIT_FACTS_CANCELLED) && gfb_error(facts,&request,&limits,CBM_GIT_FACTS_CANCELLED); bool retained=before && gfb_matches(&f,CBM_GIT_REV_HEAD,&small,1,&kept); stopped=before && rejected && latched && retained && stopped; cbm_git_facts_free(facts); } bool independent=control && gfb_good(&f,healthy,CBM_GIT_REV_HEAD,&index,1,&limits,NULL); cbm_git_facts_free(healthy); /* Existing deterministic expired-open boundary; no live-clock race. */ opts=gf_options(&f.git);opts.deadline_ms=1; cbm_git_facts_t *expired=configured?cbm_git_facts_open(&opts,&error):NULL; bool deadline=configured && !expired && error.status==CBM_GIT_FACTS_DEADLINE; cbm_git_facts_free(expired);int cleanup=gfb_close(&f); ASSERT_EQ(cleanup,0);ASSERT_TRUE(configured);ASSERT_TRUE(control);ASSERT_TRUE(positive); ASSERT_TRUE(stopped);ASSERT_TRUE(independent);ASSERT_TRUE(deadline);PASS(); } static cbm_git_bytes_t gfb_frame(CBMArena *arena,const char *oid,const void *body,size_t length, bool payload) { char header[128];int n=snprintf(header,sizeof(header),"%s blob %zu\n",oid,length); if(n<=0 || (size_t)n>=sizeof(header) || length>SIZE_MAX-(size_t)n-1) return (cbm_git_bytes_t){NULL,0}; size_t total=(size_t)n+(payload?length+1:0); unsigned char *bytes=cbm_arena_alloc(arena,total); if(!bytes)return (cbm_git_bytes_t){NULL,0}; memcpy(bytes,header,(size_t)n); if(payload) { if(length)memcpy(bytes+n,body,length); bytes[total-1]='\n'; } return (cbm_git_bytes_t){bytes,total}; } static cbm_git_bytes_t gfb_concat(CBMArena *arena,cbm_git_bytes_t a,cbm_git_bytes_t b) { if(a.length>SIZE_MAX-b.length)return (cbm_git_bytes_t){NULL,0}; size_t size=a.length+b.length;unsigned char *bytes=cbm_arena_alloc(arena,size?size:1); if(!bytes)return (cbm_git_bytes_t){NULL,0}; if(a.length)memcpy(bytes,a.data,a.length); if(b.length)memcpy(bytes+a.length,b.data,b.length); return (cbm_git_bytes_t){bytes,size}; } static bool gfb_capture_error(cbm_git_bytes_t capture,size_t width,const char *const *oids, const size_t *sizes,size_t count,bool payload, cbm_git_facts_status_t expected) { cbm_git_facts_error_t error;memset(&error,0xa5,sizeof(error)); return !cbm_git_facts_test_batch_capture(capture,width,oids,sizes,count,payload,&error) && error.status==expected; } TEST(test_git_blob_batch_exact_payload_frames_and_no_prefix_acceptance) { bool controls=true, outcomes=true; for(size_t width=40;width<=64;width+=24) { CBMArena arena;cbm_arena_init_lazy(&arena,4096); char a_oid[65],b_oid[65];memset(a_oid,'a',width);memset(b_oid,'b',width); a_oid[width]=b_oid[width]=0; const char *oids[]={a_oid,b_oid}; const size_t sizes[]={sizeof(gfb_binary),sizeof(gfb_header_body)-1}; cbm_git_bytes_t a=gfb_frame(&arena,a_oid,gfb_binary,sizes[0],true); cbm_git_bytes_t b=gfb_frame(&arena,b_oid,gfb_header_body,sizes[1],true); bool allocated=a.data && b.data; cbm_git_bytes_t complete=allocated?gfb_concat(&arena,a,b):(cbm_git_bytes_t){NULL,0}; cbm_git_facts_error_t error;memset(&error,0xa5,sizeof(error)); bool positive=complete.data && cbm_git_facts_test_batch_capture(complete,width,oids,sizes,2,true,&error) && gfa_clean_error(&error); controls=positive && controls; if(complete.data) { for(size_t cut=0;cut0; if(copied)memcpy(patch,diff.patch.data,diff.patch.length); CBMArena input;cbm_arena_init_lazy(&input,4096); size_t *indices=cbm_arena_alloc(&input,3*sizeof(*indices)); static const size_t wanted[]={GFB_LARGE_A,GFB_UTF,GFB_EMPTY}; if(indices)memcpy(indices,wanted,sizeof(wanted)); cbm_git_blob_batch_limits_t limits=gfb_limits();cbm_git_blob_batch_t first={0},second={0}; bool one=legacy && indices && gfb_good(&f,facts,CBM_GIT_REV_HEAD,indices,3,&limits,&first); if(indices)for(size_t i=0;i<3;i++)indices[i]=SIZE_MAX; cbm_arena_destroy(&input); const size_t merge[]={GFB_TEXT,GFB_HEADER}; bool two=legacy && gfb_good(&f,facts,CBM_GIT_REV_MERGE_BASE,merge,2,&limits,&second); size_t invalid=GFB_COUNT;cbm_git_blob_batch_request_t request={CBM_GIT_REV_HEAD,&invalid,1}; bool ordinary=legacy && gfb_error(facts,&request,&limits,CBM_GIT_FACTS_INVALID); const size_t next=GFB_EXEC;request=(cbm_git_blob_batch_request_t){CBM_GIT_REV_HEAD,&next,1}; cbm_git_blob_batch_t optional=gfb_poisoned(); bool again=legacy && cbm_git_facts_read_blob_batch(facts,&request,&limits,&optional,NULL) && gfb_matches(&f,CBM_GIT_REV_HEAD,&next,1,&optional); cbm_git_blob_batch_limits_t limited=limits;limited.max_entries=1; request=(cbm_git_blob_batch_request_t){CBM_GIT_REV_HEAD,wanted,3}; bool stopped=legacy && gfb_error(facts,&request,&limited,CBM_GIT_FACTS_LIMIT); bool stable=one && two && copied && gfb_matches(&f,CBM_GIT_REV_HEAD,wanted,3,&first) && gfb_matches(&f,CBM_GIT_REV_MERGE_BASE,merge,2,&second) && gfi_matches(&inventory,f.expected,GFB_COUNT) && gf_bytes_equal(blob.bytes,gf_head_text,sizeof(gf_head_text)-1) && gf_bytes_equal(diff.patch,patch,diff.patch.length) && gf_bytes_equal(diff.name_status,changed,sizeof(changed)-1) && gf_bytes_equal(ancestor,changed,sizeof(changed)-1); cbm_git_facts_free(facts);int cleanup=gfb_close(&f); ASSERT_EQ(cleanup,0);ASSERT_TRUE(configured);ASSERT_TRUE(legacy);ASSERT_TRUE(copied); ASSERT_TRUE(one);ASSERT_TRUE(two);ASSERT_TRUE(ordinary);ASSERT_TRUE(again);ASSERT_TRUE(stopped); ASSERT_TRUE(stable);PASS(); } SUITE(test_impact_git) { RUN_TEST(test_git_facts_inventory_pins_complete_head_and_actual_merge_base); RUN_TEST(test_git_facts_inventory_preserves_literal_bytes_and_sha256); RUN_TEST(test_git_facts_inventory_empty_and_invalid_revision_clear_output); RUN_TEST(test_git_facts_inventory_cancellation_clears_prefix_and_latches); RUN_TEST(test_git_facts_inventory_output_limit_has_no_prefix_and_latches); RUN_TEST(test_git_facts_inherited_diff_options_cannot_override_zero_context); RUN_TEST(test_git_facts_hostile_probe_ignores_an_inherited_selection_file); RUN_TEST(test_git_facts_gitfile_redirection_cannot_certify_false_ancestry); RUN_TEST(test_git_facts_pins_actual_merge_base_and_both_refs); RUN_TEST(test_git_facts_ref_index_and_worktree_mutation_cannot_change_snapshot); RUN_TEST(test_git_facts_reads_exact_binary_literal_paths_and_all_revisions); RUN_TEST(test_git_facts_distinguishes_absent_invalid_and_nonregular_paths); RUN_TEST(test_git_facts_ancestry_distinguishes_no_from_invalid_and_missing_objects); RUN_TEST(test_git_facts_diff_uses_merge_base_zero_context_and_nul_metadata); RUN_TEST(test_git_facts_rejects_malformed_options_and_expected_head_mismatch); RUN_TEST(test_git_facts_precancel_and_expired_deadline_are_deterministic); RUN_TEST(test_git_facts_command_and_output_caps_fail_closed); RUN_TEST(test_git_facts_required_sha256_objects_and_identity); RUN_TEST(test_git_facts_never_runs_external_diff_or_textconv); RUN_TEST(test_git_facts_rejects_local_grafts_that_change_ancestry); RUN_TEST(test_git_facts_rejects_no_common_ancestor); RUN_TEST(test_git_facts_rejects_multiple_criss_cross_merge_bases); RUN_TEST(test_git_facts_rejects_local_shallow_state); RUN_TEST(test_git_facts_rejects_history_override_added_after_open); RUN_TEST(test_git_facts_cancellation_after_open_is_latched); RUN_TEST(test_git_ancestor_changes_requires_ancestor_of_actual_merge_base); RUN_TEST(test_git_ancestor_changes_preserves_earlier_paths_reversion_and_pinned_head); RUN_TEST(test_git_ancestor_changes_equalities_and_empty_reversion_are_distinct); RUN_TEST(test_git_ancestor_changes_validates_full_commit_object_and_argument_span); RUN_TEST(test_git_ancestor_changes_sha256_raw_paths_and_local_oid_copy); RUN_TEST(test_git_ancestor_changes_repeats_preserve_all_previous_owner_buffers); RUN_TEST(test_git_ancestor_changes_cancellation_is_shared_terminal_and_preserves_views); RUN_TEST(test_git_ancestor_changes_output_and_cumulative_limits_publish_no_prefix); RUN_TEST(test_git_ancestor_changes_repeated_calls_share_existing_command_budget); RUN_TEST(test_git_ancestor_changes_history_and_external_driver_guards_apply); RUN_TEST(test_git_blob_batch_native_bytes_and_both_object_formats); RUN_TEST(test_git_blob_batch_keeps_request_order_duplicates_and_raw_paths); RUN_TEST(test_git_blob_batch_pinned_revisions_and_cache_history_guards); RUN_TEST(test_git_blob_batch_validates_full_selection_and_empty_requests); RUN_TEST(test_git_blob_batch_exact_frame_caps_and_native_split_batches); RUN_TEST(test_git_blob_batch_per_call_and_existing_shared_budgets); RUN_TEST(test_git_blob_batch_strict_headers_and_native_missing_object); RUN_TEST(test_git_blob_batch_cancellation_is_sticky_and_owner_local); RUN_TEST(test_git_blob_batch_exact_payload_frames_and_no_prefix_acceptance); RUN_TEST(test_git_blob_batch_owner_lifetimes_and_legacy_views_survive); }