feat: Graph queries — ancestor?, ahead_behind, commits_between #16

merged colechristensen cole.christensen@gmail.com wants to merge feat/graph-queries into main
No CI

PR 2 of 4 for fangorn/ex_git_objectstore#26. Adds the query API that makes the commit-graph useful to Anvil’s PR list and PR show paths (fangorn/anvil#55). Strictly additive — no callers yet.

Queries

  • `Graph.ancestor?/3` — generation-pruned BFS from descendant back. If `gen(ancestor) > gen(descendant)`, return false immediately; otherwise walk parents, skipping any SHA with generation less than `gen(ancestor)`.
  • `Graph.ahead_behind/3` — generation-ordered priority queue. Tag each SHA with `:a` / `:b` / `:both`; commits marked `:both` (and their transitive ancestors) are excluded from both counts.
  • `Graph.commits_between/3` — the head-side set from the ahead_behind walk, sorted newest-first by corrected commit date.

Each returns `{:error, :missing_commit}` if either SHA isn’t in the graph — callers fall back to a reference walker.

Tests

45 graph tests total. New:

  • 5 `ancestor?` (self, parent→child, unrelated, merge parents, missing)
  • 7 `ahead_behind` (identical, linear, diverged, merge-base on both sides, disjoint roots, shared-merge non-double-counting, missing)
  • 4 `commits_between` (empty, linear, excludes base ancestors, merge with ordering)
  • 3 equivalence tests × 10 random DAGs each, comparing every pair against a brute-force `cat_object` walker

Full suite: 681 tests, 0 failures (was 661 on main post-#15). Credo: unchanged from main.

Benchmark

`bench/graph_build.exs` extended. 5000-commit linear chain, in-memory storage:

``` Walker: full ancestry scan (for ahead_behind) 57 ms Graph.ahead_behind (in-memory) 6 ms (9.4x) Graph.commits_between (in-memory) 7 ms ```

On S3, where each `cat_object` is a network round-trip, the walker scales with `commit_count × latency` while `Graph.ahead_behind` is bounded by the in-memory walk length. That is the gap Anvil’s prod is currently paying for.

What’s next

  • PR 3: top-level `ExGitObjectstore.{ahead_behind, commits_between, ancestor?}` with auto-load + fallback to the existing walker when the graph is missing or a SHA isn’t present yet. Plus `ExGitObjectstore.rebuild_graph/1` for explicit seeding.
  • Anvil PR: swap `lib/anvil/git/objectstore.ex` to the new API + a Mix task to seed graphs in dev/prod.
  • (Later) PR 4: incremental `Graph.update/3` wired into the write paths.
Created Apr 18, 2026 at 15:25 UTC | Merged Apr 18, 2026 at 16:52 UTC by colechristensen cole.christensen@gmail.com