Initial commit on Forgejo
Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
This commit is contained in:
commit
af6077b9c3
455 changed files with 78366 additions and 0 deletions
322
lib/tarakan_web/live/repository_commits_live.ex
Normal file
322
lib/tarakan_web/live/repository_commits_live.ex
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
defmodule TarakanWeb.RepositoryCommitsLive do
|
||||
@moduledoc """
|
||||
Commit history of a repository's branches, plus a per-commit detail view
|
||||
with the full message and patch.
|
||||
|
||||
Every listed commit is reachable from a published branch tip, which is
|
||||
exactly the set `RepositoryCode.authorize_public_commit/2` allows in the
|
||||
code browser - so each commit links straight into the pinned source view.
|
||||
"""
|
||||
|
||||
use TarakanWeb, :live_view
|
||||
|
||||
alias Tarakan.Repositories
|
||||
alias Tarakan.Repositories.Repository
|
||||
alias Tarakan.RepositoryCode
|
||||
alias Tarakan.RepositoryCode.Commit
|
||||
alias TarakanWeb.RepositoryPaths
|
||||
|
||||
@page_size 50
|
||||
@max_diff_lines 5_000
|
||||
@commit_sha_pattern ~r/^[0-9a-f]{40}$/
|
||||
|
||||
@impl true
|
||||
def mount(params, _session, socket) do
|
||||
case repository_from_params(params, socket) do
|
||||
%Repository{} = repository ->
|
||||
if connected?(socket) do
|
||||
Repositories.subscribe()
|
||||
end
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:repository, repository)
|
||||
|> assign(:view_state, :loading)
|
||||
|> assign(:error_kind, nil)
|
||||
|> assign(:limit, @page_size)
|
||||
|> assign(:has_more?, false)
|
||||
|> assign(:branch_options, [])
|
||||
|> assign(:selected_branch, repository.default_branch)
|
||||
|> assign(:commit_sha, nil)
|
||||
|> assign(:commit, nil)
|
||||
|> maybe_load_branch_options()
|
||||
|> stream_configure(:commits, dom_id: &"commit-#{&1.sha}")
|
||||
|> stream(:commits, [])}
|
||||
|
||||
nil ->
|
||||
raise Ecto.NoResultsError, queryable: Repository
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _uri, socket) do
|
||||
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("retry", _params, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:view_state, :loading)
|
||||
|> assign(:error_kind, nil)
|
||||
|> maybe_load()}
|
||||
end
|
||||
|
||||
def handle_event("load_more", _params, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:limit, socket.assigns.limit + @page_size)
|
||||
|> assign(:view_state, :loading)
|
||||
|> maybe_load_commits()}
|
||||
end
|
||||
|
||||
def handle_event("select_branch", %{"branch" => branch}, socket) do
|
||||
branch = String.trim(to_string(branch || ""))
|
||||
|
||||
if socket.assigns.live_action == :index and branch != "" do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:selected_branch, branch)
|
||||
|> assign(:limit, @page_size)
|
||||
|> assign(:view_state, :loading)
|
||||
|> maybe_load_commits()}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_async(:commits, {:ok, result}, socket) do
|
||||
case result do
|
||||
{:ok, %{commits: commits}} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:view_state, :ready)
|
||||
|> assign(:error_kind, nil)
|
||||
|> assign(:has_more?, length(commits) >= socket.assigns.limit)
|
||||
|> stream(:commits, commits, reset: true)}
|
||||
|
||||
{:error, reason} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:view_state, :error)
|
||||
|> assign(:error_kind, reason)
|
||||
|> stream(:commits, [], reset: true)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_async(:commits, {:exit, _reason}, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:view_state, :error)
|
||||
|> assign(:error_kind, :unavailable)}
|
||||
end
|
||||
|
||||
def handle_async(:commit, {:ok, result}, socket) do
|
||||
case result do
|
||||
{:ok, %Commit{} = commit} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:view_state, :ready)
|
||||
|> assign(:error_kind, nil)
|
||||
|> assign(:commit, commit)
|
||||
|> assign(
|
||||
:page_title,
|
||||
"#{commit.subject} · #{socket.assigns.repository.owner}/#{socket.assigns.repository.name}"
|
||||
)}
|
||||
|
||||
{:error, reason} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:view_state, :error)
|
||||
|> assign(:error_kind, reason)
|
||||
|> assign(:commit, nil)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_async(:commit, {:exit, _reason}, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:view_state, :error)
|
||||
|> assign(:error_kind, :unavailable)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(
|
||||
{:repository_record_updated, %Repository{id: repository_id}},
|
||||
%{assigns: %{repository: %Repository{id: repository_id} = repository}} = socket
|
||||
) do
|
||||
if Repositories.get_visible_repository(
|
||||
repository.host,
|
||||
repository.owner,
|
||||
repository.name,
|
||||
socket.assigns.current_scope
|
||||
) do
|
||||
{:noreply, socket}
|
||||
else
|
||||
{:noreply, push_navigate(socket, to: "/")}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({event, %Repository{}}, socket)
|
||||
when event in [:repository_registered, :repository_record_updated],
|
||||
do: {:noreply, socket}
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
socket
|
||||
|> assign(
|
||||
:page_title,
|
||||
"Commits · #{socket.assigns.repository.owner}/#{socket.assigns.repository.name}"
|
||||
)
|
||||
|> assign(:view_state, :loading)
|
||||
|> assign(:error_kind, nil)
|
||||
|> assign(:commit_sha, nil)
|
||||
|> assign(:commit, nil)
|
||||
|> maybe_load_commits()
|
||||
end
|
||||
|
||||
defp apply_action(socket, :show, %{"commit_sha" => commit_sha}) do
|
||||
if Regex.match?(@commit_sha_pattern, commit_sha) do
|
||||
socket
|
||||
|> assign(:view_state, :loading)
|
||||
|> assign(:error_kind, nil)
|
||||
|> assign(:commit_sha, commit_sha)
|
||||
|> assign(:commit, nil)
|
||||
|> stream(:commits, [], reset: true)
|
||||
|> maybe_load_commit()
|
||||
else
|
||||
socket
|
||||
|> assign(:view_state, :error)
|
||||
|> assign(:error_kind, :not_found)
|
||||
|> assign(:commit_sha, nil)
|
||||
|> assign(:commit, nil)
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_load(socket) do
|
||||
case socket.assigns.live_action do
|
||||
:index -> maybe_load_commits(socket)
|
||||
:show -> maybe_load_commit(socket)
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_load_commits(socket) do
|
||||
if connected?(socket) do
|
||||
repository = socket.assigns.repository
|
||||
limit = socket.assigns.limit
|
||||
branch = selected_branch(socket)
|
||||
|
||||
start_async(socket, :commits, fn ->
|
||||
RepositoryCode.list_commits(repository, limit, branch: branch)
|
||||
end)
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_load_commit(socket) do
|
||||
if connected?(socket) and is_binary(socket.assigns.commit_sha) do
|
||||
repository = socket.assigns.repository
|
||||
commit_sha = socket.assigns.commit_sha
|
||||
|
||||
start_async(socket, :commit, fn -> RepositoryCode.show_commit(repository, commit_sha) end)
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
# The default branch goes through the default-tip resolution; any other
|
||||
# picker choice names the branch explicitly.
|
||||
defp selected_branch(socket) do
|
||||
case socket.assigns.selected_branch do
|
||||
branch when is_binary(branch) and branch != "" -> branch
|
||||
_other -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_load_branch_options(socket) do
|
||||
if connected?(socket) do
|
||||
case RepositoryCode.list_branches(socket.assigns.repository) do
|
||||
{:ok, branches} -> assign(socket, :branch_options, branches)
|
||||
{:error, _reason} -> socket
|
||||
end
|
||||
else
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
defp repository_from_params(params, socket) do
|
||||
with {:ok, host, owner, name} <- normalize_params(params),
|
||||
%Repository{} = repository <-
|
||||
Repositories.get_visible_repository(host, owner, name, socket.assigns.current_scope) do
|
||||
repository
|
||||
else
|
||||
_not_visible -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_params(%{"host" => slug, "owner" => owner, "name" => name}) do
|
||||
with {:ok, host} <- Tarakan.Hosts.host_for_slug(slug) do
|
||||
{:ok, host, owner, name}
|
||||
end
|
||||
end
|
||||
|
||||
# Bare GitHub-style route (/owner/name/commits...). A host-like first
|
||||
# segment means the bare pattern swallowed a remote path - a repository
|
||||
# literally named "commits" - so shift the params, same corner as
|
||||
# "code"/"security".
|
||||
defp normalize_params(%{"owner" => owner, "name" => name}) do
|
||||
if Tarakan.Hosts.host_segment?(owner) do
|
||||
normalize_params(%{"host" => owner, "owner" => name, "name" => "commits"})
|
||||
else
|
||||
{:ok, Repository.hosted_host(), owner, name}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_params(_params), do: :error
|
||||
|
||||
defp short_sha(sha) when is_binary(sha), do: binary_part(sha, 0, 7)
|
||||
|
||||
defp commit_datetime(%DateTime{} = datetime),
|
||||
do: Calendar.strftime(datetime, "%Y-%m-%d %H:%M UTC")
|
||||
|
||||
defp commit_datetime(_other), do: "unknown date"
|
||||
|
||||
defp diff_lines(nil), do: []
|
||||
|
||||
defp diff_lines(patch) do
|
||||
patch
|
||||
|> String.split("\n")
|
||||
|> Enum.take(@max_diff_lines)
|
||||
|> Enum.map(&{diff_line_type(&1), &1})
|
||||
end
|
||||
|
||||
defp diff_line_over_limit?(nil), do: false
|
||||
defp diff_line_over_limit?(patch), do: length(String.split(patch, "\n")) > @max_diff_lines
|
||||
|
||||
defp diff_line_type("+++" <> _rest), do: :meta
|
||||
defp diff_line_type("---" <> _rest), do: :meta
|
||||
defp diff_line_type("+" <> _rest), do: :add
|
||||
defp diff_line_type("-" <> _rest), do: :del
|
||||
defp diff_line_type("@@" <> _rest), do: :hunk
|
||||
defp diff_line_type(_line), do: :context
|
||||
|
||||
defp diff_line_class(:add), do: "text-quote"
|
||||
defp diff_line_class(:del), do: "text-signal"
|
||||
defp diff_line_class(:hunk), do: "text-ink-faint"
|
||||
defp diff_line_class(:meta), do: "text-ink-faint"
|
||||
defp diff_line_class(:context), do: "text-ink"
|
||||
|
||||
defp error_title(:empty_repository), do: "No commits yet"
|
||||
defp error_title(:not_found), do: "Commit not found"
|
||||
defp error_title(_other), do: "History unavailable"
|
||||
|
||||
defp error_message(:empty_repository),
|
||||
do: "Nothing has been pushed to this repository yet."
|
||||
|
||||
defp error_message(:not_found),
|
||||
do: "This commit is not reachable from any published branch."
|
||||
|
||||
defp error_message(_other),
|
||||
do: "The commit history could not be read right now. Try again in a moment."
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue