@@ -68,6 +68,59 @@
end
end
@doc """
Paginated walk that returns a cursor for efficient continuation.
Returns `{:ok, commits, cursor}` where `cursor` is a list of SHAs
representing the walk frontier. Pass this cursor to `log_continue/3`
for the next page. An empty cursor means no more commits.
Each page is O(page_size) regardless of depth — no re-walking from HEAD.
## Options
* `:max_count` — page size (default: 20)
"""
@spec log_page(Repo.t(), String.t(), keyword()) ::
{:ok, [{String.t(), Commit.t()}], [String.t()]} | {:error, term()}
def log_page(%Repo{} = repo, start_sha, opts \\ []) do
max_count = Keyword.get(opts, :max_count, 20)
case seed_queue(repo, start_sha) do
{:ok, queue, seen} ->
{commits, remaining} = walk_page(repo, queue, seen, [], 0, max_count)
{:ok, commits, queue_to_cursor(remaining)}
{:error, _} = err ->
err
end
end
@doc """
Continue a paginated walk from a cursor returned by `log_page/3` or
a previous `log_continue/3` call.
Returns `{:ok, [], []}` if the cursor is empty (no more commits).
## Options
* `:max_count` — page size (default: 20)
"""
@spec log_continue(Repo.t(), [String.t()], keyword()) ::
{:ok, [{String.t(), Commit.t()}], [String.t()]} | {:error, term()}
def log_continue(%Repo{} = _repo, [], _opts), do: {:ok, [], []}
def log_continue(%Repo{} = repo, cursor_shas, opts) when is_list(cursor_shas) do
max_count = Keyword.get(opts, :max_count, 20)
case cursor_to_queue(repo, cursor_shas) do
{:ok, queue, seen} ->
{commits, remaining} = walk_page(repo, queue, seen, [], 0, max_count)
{:ok, commits, queue_to_cursor(remaining)}
{:error, _} = err ->
err
end
end
defp apply_skip_and_limit(commits, 0, :infinity), do: commits
defp apply_skip_and_limit(commits, skip, max_count) do
@@ -94,6 +147,27 @@
err
end
end
# Rebuild a walk queue from serialized cursor SHAs.
defp cursor_to_queue(repo, shas) do
{queue, seen} =
Enum.reduce(shas, {[], MapSet.new()}, fn sha, {q, s} ->
case ObjectResolver.read(repo, sha) do
{:ok, %Commit{} = commit} ->
ts = parse_timestamp(commit.committer)
{insert_sorted(q, {ts, sha, commit}), MapSet.put(s, sha)}
_ ->
{q, s}
end
end)
{:ok, queue, seen}
end
defp queue_to_cursor(queue) do
Enum.map(queue, fn {_ts, sha, _commit} -> sha end)
end
@doc """
Find the merge base (lowest common ancestor) of two commits.
@@ -156,6 +230,21 @@
defp walk(repo, [{_ts, sha, commit} | rest], seen, acc, count, limit) do
{queue, seen} = enqueue_parents(repo, commit.parents, rest, seen)
walk(repo, queue, seen, [{sha, commit} | acc], count + 1, limit)
end
# Like walk/6 but returns {commits, remaining_queue} for cursor pagination.
defp walk_page(_repo, [], _seen, acc, _count, _limit) do
{Enum.reverse(acc), []}
end
defp walk_page(_repo, queue, _seen, acc, count, limit)
when is_integer(limit) and count >= limit do
{Enum.reverse(acc), queue}
end
defp walk_page(repo, [{_ts, sha, commit} | rest], seen, acc, count, limit) do
{queue, seen} = enqueue_parents(repo, commit.parents, rest, seen)
walk_page(repo, queue, seen, [{sha, commit} | acc], count + 1, limit)
end
defp enqueue_parents(_repo, [], queue, seen), do: {queue, seen}