feat: Graph queries — ancestor?, ahead_behind, commits_between #16
feat/graph-queries
into main
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.