@@ -730,5 +730,60 @@
end
@doc """
Like `ahead_behind/3`, but for many heads against a single base.
Walks `ancestors(base)` once and reuses it across every head, instead
of re-walking it for each call. For workloads where `head_shas` are
many small offsets from a common base (e.g. a PR-list page where
every PR has `base = main`), this turns
`O(N · |ancestors(base)|)` into `O(|ancestors(base)| + Σ head walks)`.
Returns `{:ok, %{head_sha => %{ahead: N, behind: M}}}` with one entry
per head. Heads not in the graph or whose ref couldn't be resolved
fall back to per-head `ahead_behind/3` (which has its own
cat_object walker fallback). If the graph itself isn't available,
every head goes through the per-head fallback.
Emits `[:ex_git_objectstore, :graph, :query]` telemetry with
`operation: :ahead_behind_many`. `path` is `:graph` when the batched
fast path was used, `:fallback` when nothing was in the graph and
every head went through the per-head walker.
"""
@spec ahead_behind_many(Repo.t(), sha(), [sha()]) ::
{:ok, %{sha() => %{ahead: non_neg_integer(), behind: non_neg_integer()}}}
| {:error, term()}
def ahead_behind_many(%Repo{} = repo, base_sha, head_shas) when is_list(head_shas) do
metadata = %{operation: :ahead_behind_many, repo_id: repo.id}
Telemetry.span([:ex_git_objectstore, :graph, :query], metadata, fn ->
case load_or_fetch_graph(repo) do
{:ok, graph} ->
if Graph.member?(graph, base_sha) do
{:ok, fast} = Graph.ahead_behind_many(graph, base_sha, head_shas)
missing = Enum.reject(head_shas, &Map.has_key?(fast, &1))
merged = fill_per_head(repo, base_sha, missing, fast)
{{:ok, merged}, Map.put(metadata, :path, :graph)}
else
merged = fill_per_head(repo, base_sha, head_shas, %{})
{{:ok, merged}, Map.put(metadata, :path, :fallback)}
end
{:error, _} ->
merged = fill_per_head(repo, base_sha, head_shas, %{})
{{:ok, merged}, Map.put(metadata, :path, :fallback)}
end
end)
end
defp fill_per_head(repo, base_sha, head_shas, acc) do
Enum.reduce(head_shas, acc, fn head_sha, acc ->
case ahead_behind(repo, base_sha, head_sha) do
{:ok, counts} -> Map.put(acc, head_sha, counts)
{:error, _} -> acc
end
end)
end
@doc """
Commits reachable from `head_sha` but not from `base_sha`, newest-first.
Empty when `head_sha` is an ancestor of (or equal to) `base_sha`.