notes · · 3 min

Git Under the Microscope: What 67 AST Tools Found Inside the Core VCS

We benchmarked all 67 prod-code AST tools against git/git: 443,000 lines of C, 4,657 fatal and recoverable error exit points, and multi-file structural codemods.

On this page · 4 sections
  1. Locating Directory Setup in 316 Milliseconds Across 107,000 Declarations
  2. Mapping 4,657 Fatal and Recoverable Error Exits Across C Modules
  3. Detecting Parameterized Filter Dispatch Clones and Test Duplication
  4. Multi-File Structural Codemods with Preserved Metavariables

git/git is the bedrock version control system underlying modern software development, hosting the collaborative workflows of millions of engineering organizations worldwide.

Written primarily in C, the codebase spans 443,306 lines across 986 source files (with 107,779 declarations indexed monorepo-wide). Over two decades of performance tuning, portability shims, and tight POSIX abstractions have produced an intricately wired systems architecture. Navigating, refactoring, and auditing such a foundational C codebase requires semantic tools that understand AST structures, macro expansions, and cross-file translation units.

We evaluated all 67 prod-code AST tools against git/git (tag v2.56.0, commit a018953688) hosted on remote cluster nodes with zero local laptop CPU consumption.


Locating Directory Setup in 316 Milliseconds Across 107,000 Declarations

Bootstrapping any Git command begins with discovering repository boundaries, working trees, and configuration files via setup_git_directory. In a repository with over a thousand C compilation units and header files, locating the exact entry points and caller boundaries across subsystems typically involves noisy text searches.

We ran reciprocal-rank-fusion (RRF) semantic search for setup_git_directory across all indexed declarations:

$ prod-code search "setup_git_directory"
10 hit(s) for `setup_git_directory` in 316 ms (107779 declarations, 1007 files; ranked by words and typed graph and by meaning)

 1. [function] setup_git_directory  scalar.c:517
    setup_git_directory(the_repository);
    attribution [score 0.0310]: lexical: rank 5 (matched setup, git, directory); dense: rank 4 (cosine 0.901)

 2. [function] setup_git_directory  archive.c:789
    setup_git_directory(the_repository);
    attribution [score 0.0307]: lexical: rank 1 (matched setup, git, directory); dense: rank 10 (cosine 0.900)

 3. [function] setup_git_directory  t/helper/test-config.c:105
    setup_git_directory(the_repository);
    attribution [score 0.0304]: lexical: rank 10 (matched setup, git, directory); dense: rank 2 (cosine 0.903)

 4. [function] setup_git_directory  scalar.c:61
    setup_git_directory(the_repository);
    attribution [score 0.0303]: lexical: rank 4 (matched setup, git, directory); dense: rank 8 (cosine 0.900)

 5. [function] setup_git_directory  builtin/merge-file.c:113
    setup_git_directory(the_repository);
    attribution [score 0.0289]: lexical: rank 2 (matched setup, git, directory); dense: rank 18 (cosine 0.899)

In 316 milliseconds, dense semantic embeddings and lexical token ranking isolated the core repository discovery call sites across CLI builtins (builtin/merge-file.c:113), porcelain tools (archive.c:789), and repository orchestration commands (scalar.c:517), scoring cosine similarities between 0.899 and 0.904 without scanning unparsed header noise.


Mapping 4,657 Fatal and Recoverable Error Exits Across C Modules

Git distinguishes sharply between unrecoverable failures that immediately abort execution via die(...) and recoverable diagnostics propagated upward via error(...). Cataloging these exit boundaries across hundreds of translation units is critical when converting CLI commands into re-entrant library functions (such as libgit2 or re-entrant internal subroutines).

We ran structural AST search for fatal terminations matching die($$$):

$ prod-code structural-search 'die($$$)'
3001 match(es) in 327 file(s) (1008 scanned in 7463.23ms)

  • abspath.c:91:4  die("The empty string is not a valid path")
    └─ [$$$ = "The empty string is not a valid path"]
  • abspath.c:146:6  die("More than %d nested symlinks on path '%s'", MAXSYMLINKS, path)
    └─ [$$$ = "More than %d nested symlinks on path '%s'", MAXSYMLINKS, path]
  • apply.c:3634:3  die("unable to read blob object %s", oid_to_hex(result_id))
    └─ [$$$ = "unable to read blob object %s", oid_to_hex(result_id)]
  • archive-tar.c:458:4  die(_("deflate error (%d)"), status)
    └─ [$$$ = _("deflate error (%d)"), status]
  • archive.c:524:3  die(_("not a tree object: %s"), oid_to_hex(&oid))
    └─ [$$$ = _("not a tree object: %s"), oid_to_hex(&oid)]

In 7.46 seconds, the analyzer extracted 3,001 fatal exit paths across 327 files, binding the variable argument lists and format string payloads.

Next, we ran structural AST search for non-fatal diagnostic exits matching error($$$):

$ prod-code structural-search 'error($$$)'
1656 match(es) in 219 file(s) (1008 scanned in 5158.98ms)

  • add-interactive.c:488:10  error(_("could not read index"))
    └─ [$$$ = _("could not read index")]
  • add-patch.c:576:10  error(_("could not parse diff"))
    └─ [$$$ = _("could not parse diff")]
  • advice.c:205:3  error(_("Cherry-picking is not possible because you have unmerged files."))
    └─ [$$$ = _("Cherry-picking is not possible because you have unmerged files.")]
  • commit.c:142:9  error(_("could not parse commit %s"), oid_to_hex(oid))
    └─ [$$$ = _("could not parse commit %s"), oid_to_hex(oid)]

In 5.15 seconds, the query surfaced 1,656 recoverable error sites across 219 files. In total, 4,657 error boundaries were mapped across the monorepo with zero regex hallucinations or comment false positives.


Detecting Parameterized Filter Dispatch Clones and Test Duplication

Decades of incremental evolution in C software often result in copy-pasted control-flow structures across related commands or test suites.

We ran AST clone detection across the Git workspace with a 12-line minimum similarity window:

$ prod-code duplicates --min-lines 12
[Clone Group #199] 12 lines | 4 occurrences (Type-2 (Parameterized))
  • Occurrence 1: list-objects-filter.c:82-93
  • Occurrence 2: list-objects-filter.c:183-194
  • Occurrence 3: list-objects-filter.c:287-298
  • Occurrence 4: list-objects-filter.c:401-412
  Preview:
    │ 	default:
    │ 		BUG("unknown filter_situation: %d", filter_situation);
    │ 
    │ 	case LOFS_TAG:
    │ 		assert(obj->type == OBJ_TAG);

[Clone Group #173] 12 lines | 4 occurrences (Type-2 (Parameterized))
  • Occurrence 1: t/unit-tests/u-reftable-block.c:54-65
  • Occurrence 2: t/unit-tests/u-reftable-block.c:150-161
  • Occurrence 3: t/unit-tests/u-reftable-block.c:241-252
  • Occurrence 4: t/unit-tests/u-reftable-block.c:326-337
  Preview:
    │ 	};
    │ 	size_t i = 0;
    │ 	int ret;
    │ 	struct reftable_block block = { 0 };

[Clone Group #60] 12 lines | 3 occurrences (Type-2 (Parameterized))
  • Occurrence 1: commit-graph.c:1245-1256
  • Occurrence 2: commit-graph.c:1276-1287
  • Occurrence 3: commit-graph.c:1420-1431
  Preview:
    │ 					     commit_to_oid);
    │ 
    │ 			if (edge_value >= 0)
    │ 				edge_value += ctx->new_num_commits_in_base;

In list-objects-filter.c, Clone Group #199 matched 4 identical 12-line parameterized switch statements dispatching filter situations. In commit-graph.c, Clone Group #60 surfaced repeated edge valuation logic across commit traversal passes. Surfacing these structural duplicates highlights targeted opportunities to extract shared helpers using code_extract_function.


Multi-File Structural Codemods with Preserved Metavariables

Refactoring systems software often requires applying uniform changes across disjoint subsystems. In Git, internationalization requires wrapping user-facing strings in gettext macros _(). When string formatting parameters are present, regex search-and-replace frequently corrupts argument commas or parenthesis nesting.

We executed a structural AST codemod to locate and internationalize unlocalized blob object read errors:

$ prod-code codemod 'die("unable to read blob object %s", $arg) ==>> die(_("unable to read blob object %s"), $arg)'
`die("unable to read blob object %s", $arg) ==>> die(_("unable to read blob object %s"), $arg)`
6 changed line(s) in 3 file(s)

--- a/apply.c
+++ b/apply.c
@@ -3632,5 +3632,5 @@
 	data = odb_read_object(the_repository->objects, result_id, &type, &size);
 	if (!data || type != OBJ_BLOB)
-		die("unable to read blob object %s", oid_to_hex(result_id));
+		die(_("unable to read blob object %s"), oid_to_hex(result_id));
 	strbuf_attach(&image->buf, data, size, size + 1);

--- a/builtin/unpack-file.c
+++ b/builtin/unpack-file.c
@@ -18,5 +18,5 @@
 	buf = odb_read_object(the_repository->objects, oid, &type, &size);
 	if (!buf || type != OBJ_BLOB)
-		die("unable to read blob object %s", oid_to_hex(oid));
+		die(_("unable to read blob object %s"), oid_to_hex(oid));

 	xsnprintf(path, sizeof(path), ".merge_file_XXXXXX");

--- a/xdiff-interface.c
+++ b/xdiff-interface.c
@@ -191,5 +191,5 @@
 	ptr->ptr = odb_read_object(odb, oid, &type, &size);
 	if (!ptr->ptr || type != OBJ_BLOB)
-		die("unable to read blob object %s", oid_to_hex(oid));
+		die(_("unable to read blob object %s"), oid_to_hex(oid));
 	ptr->size = size;

nothing was written; pass `apply: true` to make these edits

The transformation matched across three completely separate subsystems (apply.c, builtin/unpack-file.c, and xdiff-interface.c). The AST engine correctly bound the $arg metavariable to oid_to_hex(result_id) in apply.c and oid_to_hex(oid) in builtin/unpack-file.c and xdiff-interface.c, producing clean unified diffs while guaranteeing that no unverified modifications touch disk without explicit verification.


In foundational systems software written in C, code intelligence tools cannot treat source trees as flat text files; maintaining stability across decades of evolution requires AST-aware structural search that maps fatal exit boundaries and parameterized codemods that guarantee syntax integrity before touching disk.

Cite this article
Citation
Alexander Panasenko (2026-09-30). Git Under the Microscope: What 67 AST Tools Found Inside the Core VCS. https://prod.codes/blog/git-under-the-microscope-67-ast-tools/