4.2 KiB · text 5af55d9
defmodule GitGudWeb.RepoLive.Committers do
use GitGudWeb, :live_view
alias GitGud.Repositories
alias GitGud.Repositories.Storage
@impl true
def mount(%{"owner" => owner, "name" => name}, _session, socket) do
repo =
Repositories.get_repository_by_path!(owner, name)
|> GitGud.Repo.preload([:owner, :organization])
stats = Repositories.committer_stats(repo)
{:ok,
socket
|> assign(:repo, repo)
|> assign(:handle, Storage.repo_handle(repo))
|> GitGudWeb.RepoLive.Header.assign_chrome(repo)
|> assign(:stats, stats)
|> assign(:total_commits, total(stats, :commits))
|> assign(:total_insertions, total(stats, :insertions))
|> assign(:total_deletions, total(stats, :deletions))
|> assign(:page_title, "Committers — #{owner}/#{name}")}
end
defp total(stats, key), do: Enum.reduce(stats, 0, &(Map.get(&1, key) + &2))
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="space-y-4">
<GitGudWeb.RepoLive.Header.header
repo={@repo}
handle={@handle}
current_scope={@current_scope}
has_packages?={@has_packages?}
can_admin?={@can_admin?}
latest_run={@latest_run}
/>
<header>
<h1 class="text-xl font-semibold">Stats</h1>
</header>
<GitGudWeb.RepoLive.StatsTabs.tabs
active={:committers}
handle={@handle}
repo={@repo}
/>
<%= if @stats == [] do %>
<p class="text-sm opacity-70">No commits cached yet for this repository.</p>
<% else %>
<section class="flex flex-wrap gap-4 text-sm">
<span><strong>{length(@stats)}</strong> author(s)</span>
<span><strong>{@total_commits}</strong> commit(s)</span>
<span class="text-success">+{@total_insertions}</span>
<span class="text-error">{@total_deletions}</span>
</section>
<table class="table table-sm">
<thead>
<tr>
<th>Author</th>
<th class="text-right">Commits</th>
<th class="text-right">+/−</th>
<th class="text-right">First</th>
<th class="text-right">Last</th>
</tr>
</thead>
<tbody>
<tr :for={s <- @stats}>
<td>
<div class="flex items-center gap-2">
<span
class="inline-flex size-6 items-center justify-center rounded-full text-xs font-bold text-white"
style={"background-color: " <> identity_color(s.email) <> ";"}
>
{initial(s.name, s.email)}
</span>
<div class="flex flex-col leading-tight">
<span class="font-medium">{s.name || "(unknown)"}</span>
<span class="text-xs opacity-60 font-mono">{s.email}</span>
</div>
</div>
</td>
<td class="text-right font-mono">{s.commits}</td>
<td class="text-right text-xs font-mono whitespace-nowrap">
<span class="text-success">+{s.insertions}</span>
<span class="text-error ml-1">{s.deletions}</span>
</td>
<td class="text-right text-xs opacity-60 font-mono">{format_dt(s.first_at)}</td>
<td class="text-right text-xs opacity-60 font-mono">{format_dt(s.last_at)}</td>
</tr>
</tbody>
</table>
<% end %>
</div>
</Layouts.app>
"""
end
defp initial(name, email) do
source = name || email || "?"
source
|> String.trim()
|> String.first()
|> case do
nil -> "?"
ch -> String.upcase(ch)
end
end
# Same hash-based fallback shape as LanguageColors, so author avatars
# stay consistent across page loads even without an avatar service.
defp identity_color(email) when is_binary(email) do
GitGud.LanguageColors.for("__author_" <> email)
end
defp identity_color(_), do: "#888888"
defp format_dt(nil), do: "—"
defp format_dt(dt), do: Calendar.strftime(dt, "%Y-%m-%d")
end