feat: top-level Graph-aware API (ahead_behind, commits_between, rebuild_graph) #17

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

PR 3 of 4 for fangorn/ex_git_objectstore#26. This is the entry point Anvil will call. Each query tries the persisted commit-graph first and falls back to the existing `cat_object` walker when the graph isn’t built yet or doesn’t cover one of the query SHAs. No behavior change for existing callers of `ancestor?/3` — only its speed.

Stacked on #16 — rebase-clean when that merges.

New modules

  • `ExGitObjectstore.Graph.Cache` — `:persistent_term`-backed in-process cache keyed by `{storage_module, repo_prefix}`. Reads are lock-free and zero-copy. `put/2` triggers a global GC scan so it’s only called on rebuild or first lazy-load.
  • `ExGitObjectstore.Graph.Fallback` — reference walker implementations of `ahead_behind`, `commits_between`, `ancestor?`. Bounded by `:max_walk` (default 10k) so a pathological history returns `{:error, :walk_limit_exceeded}` instead of running forever.

New public API

```elixir ahead_behind(repo, base, head) :: {:ok, %{ahead, behind}} | {:error, _} commits_between(repo, base, head) :: {:ok, [sha]} | {:error, _} ancestor?(repo, anc, desc) :: {:ok, bool} | {:error, _} # existing, now graph-aware rebuild_graph(repo) :: :ok | {:error, _} ```

All three queries follow the same routing: load-or-fetch-cached graph, verify both SHAs are members, answer from graph. On any failure (missing graph, missing member, corrupt blob), fall back to `Graph.Fallback`. Callers see one stable contract.

`rebuild_graph` builds from refs, persists to storage, and seeds the cache.

Tests

23 new tests on top of PR #16:

  • 6 Cache (hit/miss, overwrite, delete, per-repo keying)
  • 10 Fallback (edge cases, ahead_behind, commits_between ordering, ancestor? semantics, walk-limit ceiling, missing-commit error)
  • 7 integration tests exercising all routing paths:
    • no graph built — fallback
    • after `rebuild_graph` — graph path answers + cache seeded
    • stale graph (commit pushed after build) — falls back because new SHA isn’t in graph

Full suite: 704 tests, 0 failures (was 681 on #16). Credo: unchanged from main.

What’s next

Anvil PR: swap `lib/anvil/git/objectstore.ex` `ahead_behind` / `commits_between` to call `ExGitObjectstore` directly. Add a `mix anvil.graphs.rebuild` task so operators can seed graphs in dev / staging / prod (no push-hook wiring yet — tracked separately).

Deployment notes

  • First query after deploy with no graph: walker path, same perf as today.
  • After one `mix anvil.graphs.rebuild` per repo: graph path on every subsequent query (until a push introduces commits not in the graph, at which point that specific query falls back until the next rebuild).
  • No incremental update in this PR — graph staleness after push is handled by the fallback, not by invalidation.
Created Apr 18, 2026 at 15:31 UTC | Merged Apr 18, 2026 at 18:31 UTC by colechristensen cole.christensen@gmail.com