defmodule GitGudWeb.RepoLive.Languages do
use GitGudWeb, :live_view
alias GitGud.Repositories
alias GitGud.Repositories.Storage
@impl true
def mount(%{"owner" => owner, "name" => name} = params, _session, socket) do
repo =
Repositories.get_repository_by_path!(owner, name)
|> GitGud.Repo.preload([:owner, :organization])
ref = params["ref"] || repo.default_branch
{sha, stats, status} = resolve_stats(repo, ref)
{:ok,
socket
|> assign(:repo, repo)
|> assign(:handle, Storage.repo_handle(repo))
|> GitGudWeb.RepoLive.Header.assign_chrome(repo)
|> assign(:current_ref, ref)
|> assign(:head_sha, sha)
|> assign(:stats, stats)
|> assign(:status, status)
|> assign(:page_title, "Languages — #{owner}/#{name}")}
end
defp resolve_stats(repo, ref) do
case Repositories.resolve(repo, ref) do
{:ok, sha} ->
case Repositories.get_language_stats(repo, sha) do
{:ok, stats} ->
{sha, stats, :ready}
:not_computed ->
_ =
GitGud.Repositories.Workers.LanguageStats.new_job(%{
repository_id: repo.id,
sha: sha
})
|> Oban.insert()
{sha, nil, :pending}
end
_ ->
{nil, nil, :empty}
end
end
@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={:languages}
handle={@handle}
repo={@repo}
/>
<p class="text-xs opacity-60 font-mono">
on {@current_ref}<%= if @head_sha do %>
· {GitGud.Git.short(@head_sha)}
<% end %>
</p>
<%= case @status do %>
<% :empty -> %>
<p class="text-sm opacity-70">This branch or repository is empty.</p>
<% :pending -> %>
<div class="rounded-box border border-base-300 bg-base-200 p-4 text-sm space-y-2">
<p>Language stats are still being computed.</p>
<p class="text-xs opacity-70">
Refresh in a moment — a background job is walking the tree and tallying
bytes per detected language.
</p>
</div>
<% :ready -> %>
<.bar stats={@stats} />
<table class="table table-sm">
<thead>
<tr>
<th class="w-6"></th>
<th>Language</th>
<th class="text-right">Percent</th>
<th class="text-right">Bytes</th>
<th class="text-right">Files</th>
</tr>
</thead>
<tbody>
<tr :for={s <- @stats.languages}>
<td>
<span
class="inline-block size-3 rounded-full"
style={"background-color: " <> GitGud.LanguageColors.for(s.language) <> ";"}
/>
</td>
<td class="font-mono">{s.language}</td>
<td class="text-right">{percent(s.bytes, @stats.total_bytes)}%</td>
<td class="text-right font-mono opacity-70">{format_bytes(s.bytes)}</td>
<td class="text-right opacity-70">{s.files}</td>
</tr>
</tbody>
<tfoot>
<tr class="text-xs opacity-60">
<td colspan="2">Total</td>
<td class="text-right">100%</td>
<td class="text-right font-mono">{format_bytes(@stats.total_bytes)}</td>
<td class="text-right">{@stats.file_count}</td>
</tr>
</tfoot>
</table>
<p :if={@stats[:computed_at]} class="text-xs opacity-50">
Computed {Calendar.strftime(@stats.computed_at, "%Y-%m-%d %H:%M UTC")}
</p>
<% end %>
</div>
</Layouts.app>
"""
end
attr :stats, :map, required: true
defp bar(assigns) do
~H"""
<div class="flex w-full overflow-hidden rounded bg-base-300" style="height: 1em;">
<div
:for={s <- @stats.languages}
class="h-full"
style={
"width: #{percent(s.bytes, @stats.total_bytes)}%; background-color: " <>
GitGud.LanguageColors.for(s.language) <> ";"
}
title={"#{s.language} · #{percent(s.bytes, @stats.total_bytes)}%"}
/>
</div>
"""
end
defp percent(_b, 0), do: 0.0
defp percent(b, total), do: Float.round(b * 100 / total, 2)
defp format_bytes(b) when b < 1024, do: "#{b} B"
defp format_bytes(b) when b < 1_000_000, do: "#{Float.round(b / 1024, 1)} KiB"
defp format_bytes(b), do: "#{Float.round(b / 1_048_576, 2)} MiB"
end
5.1 KiB · text
5af55d9