feat: top-level Graph-aware API (ahead_behind, commits_between, rebuild_graph) #17
feat/top-level-graph-api
into main
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.