ui
e2011b6 · gmorell · 2026-05-22 17:55
Files changed
modified
lib/git_gud/workflows.ex
+25
−0
@@ -188,6 +188,31 @@ defmodule GitGud.Workflows do
| 188 | 188 | |
| 189 | 189 | # ── queries ────────────────────────────────────────────────────────── |
| 190 | 190 | |
| 191 | + @doc """ | |
| 192 | + Return the most-recent commit status row per `context` for a given | |
| 193 | + `(repository, sha)`. Multiple workflows on the same commit produce | |
| 194 | + separate rows; we return them all, ordered by context. | |
| 195 | + | |
| 196 | + `sha` may be either a 20-byte binary OR a hex string — the column | |
| 197 | + is binary, so we coerce. | |
| 198 | + """ | |
| 199 | + def list_commit_statuses(%Repository{id: rid}, sha) when is_binary(sha) do | |
| 200 | + bin = | |
| 201 | + cond do | |
| 202 | + byte_size(sha) == 20 -> sha | |
| 203 | + byte_size(sha) == 40 -> Base.decode16!(sha, case: :mixed) | |
| 204 | + true -> sha | |
| 205 | + end | |
| 206 | + | |
| 207 | + Repo.all( | |
| 208 | + from c in GitGud.Workflows.CommitStatus, | |
| 209 | + where: c.repository_id == ^rid and c.sha == ^bin, | |
| 210 | + order_by: [asc: c.context] | |
| 211 | + ) | |
| 212 | + end | |
| 213 | + | |
| 214 | + def list_commit_statuses(_, _), do: [] | |
| 215 | + | |
| 191 | 216 | def list_runs(%Repository{id: rid}, opts \\ []) do |
| 192 | 217 | limit = Keyword.get(opts, :limit, 50) |
| 193 | 218 |
modified
lib/git_gud_web/components/core_components.ex
+64
−0
@@ -948,4 +948,68 @@ defmodule GitGudWeb.CoreComponents do
| 948 | 948 | </header> |
| 949 | 949 | """ |
| 950 | 950 | end |
| 951 | + | |
| 952 | + @doc """ | |
| 953 | + Renders the stacked pile of per-context commit statuses for a SHA. | |
| 954 | + | |
| 955 | + Loads `GitGud.Workflows.list_commit_statuses/2` for the (repo, sha) | |
| 956 | + pair and renders one pill per row, each linking to its `target_url`. | |
| 957 | + | |
| 958 | + States map to badge color: success → success, failure/error → | |
| 959 | + error, pending → warning. | |
| 960 | + | |
| 961 | + <.commit_status_badges repo={@repo} sha={@commit.sha} /> | |
| 962 | + | |
| 963 | + Renders nothing if there are no statuses for the SHA. | |
| 964 | + """ | |
| 965 | + attr :repo, :map, required: true | |
| 966 | + attr :sha, :any, required: true | |
| 967 | + attr :class, :string, default: "" | |
| 968 | + | |
| 969 | + def commit_status_badges(assigns) do | |
| 970 | + statuses = GitGud.Workflows.list_commit_statuses(assigns.repo, assigns.sha) | |
| 971 | + assigns = assign(assigns, :statuses, statuses) | |
| 972 | + | |
| 973 | + ~H""" | |
| 974 | + <div :if={@statuses != []} class={["flex flex-wrap gap-1 items-center text-xs", @class]}> | |
| 975 | + <%= for s <- @statuses do %> | |
| 976 | + <.link | |
| 977 | + :if={s.target_url} | |
| 978 | + href={s.target_url} | |
| 979 | + class={["badge badge-sm gap-1", status_badge_class(s.state)]} | |
| 980 | + title={s.description || s.context} | |
| 981 | + > | |
| 982 | + <span>{status_glyph(s.state)}</span> | |
| 983 | + <span class="font-mono">{display_context(s.context)}</span> | |
| 984 | + </.link> | |
| 985 | + <span | |
| 986 | + :if={!s.target_url} | |
| 987 | + class={["badge badge-sm gap-1", status_badge_class(s.state)]} | |
| 988 | + title={s.description || s.context} | |
| 989 | + > | |
| 990 | + <span>{status_glyph(s.state)}</span> | |
| 991 | + <span class="font-mono">{display_context(s.context)}</span> | |
| 992 | + </span> | |
| 993 | + <% end %> | |
| 994 | + </div> | |
| 995 | + """ | |
| 996 | + end | |
| 997 | + | |
| 998 | + defp status_badge_class("success"), do: "badge-success" | |
| 999 | + defp status_badge_class("failure"), do: "badge-error" | |
| 1000 | + defp status_badge_class("error"), do: "badge-error" | |
| 1001 | + defp status_badge_class("pending"), do: "badge-warning" | |
| 1002 | + defp status_badge_class(_), do: "badge-ghost" | |
| 1003 | + | |
| 1004 | + defp status_glyph("success"), do: "✓" | |
| 1005 | + defp status_glyph("failure"), do: "✗" | |
| 1006 | + defp status_glyph("error"), do: "!" | |
| 1007 | + defp status_glyph("pending"), do: "⏳" | |
| 1008 | + defp status_glyph(_), do: "·" | |
| 1009 | + | |
| 1010 | + # Strip the `gitgud-actions/` prefix in the badge label so the | |
| 1011 | + # context reads "build" / "lint" instead of the full provider name. | |
| 1012 | + # Hover still shows the full context via `title`. | |
| 1013 | + defp display_context("gitgud-actions/" <> rest), do: rest | |
| 1014 | + defp display_context(other), do: other | |
| 951 | 1015 | end |
modified
lib/git_gud_web/live/pr_live/show.ex
+1
−0
@@ -244,6 +244,7 @@ defmodule GitGudWeb.PrLive.Show do
| 244 | 244 | from {@pr.source_clone_url}:{@pr.source_ref} |
| 245 | 245 | </span> |
| 246 | 246 | </p> |
| 247 | + <.commit_status_badges repo={@repo} sha={@pr.head_sha} class="mt-2" /> | |
| 247 | 248 | </header> |
| 248 | 249 | |
| 249 | 250 | <section class="border border-base-300 rounded p-4"> |
modified
lib/git_gud_web/live/repo_live/commit.ex
+2
−1
@@ -56,13 +56,14 @@ defmodule GitGudWeb.RepoLive.Commit do
| 56 | 56 | can_admin?={@can_admin?} |
| 57 | 57 | latest_run={@latest_run} |
| 58 | 58 | /> |
| 59 | − <header> | |
| 59 | + <header class="space-y-1"> | |
| 60 | 60 | <h1 class="text-xl font-semibold"> |
| 61 | 61 | {commit_title(@commit)} |
| 62 | 62 | </h1> |
| 63 | 63 | <p class="text-xs opacity-60 mt-1 font-mono"> |
| 64 | 64 | {Git.short(@commit.sha)} · {@commit.author_name} · {format_dt(@commit.committed_at)} |
| 65 | 65 | </p> |
| 66 | + <.commit_status_badges repo={@repo} sha={@commit.sha} class="mt-1" /> | |
| 66 | 67 | </header> |
| 67 | 68 | |
| 68 | 69 | <section class="flex gap-4 text-sm"> |
modified
lib/git_gud_web/live/repo_live/log.ex
+1
−0
@@ -84,6 +84,7 @@ defmodule GitGudWeb.RepoLive.Log do
| 84 | 84 | <p class="text-xs opacity-60"> |
| 85 | 85 | {c.author_name} · {format_dt(c.committed_at)} |
| 86 | 86 | </p> |
| 87 | + <.commit_status_badges repo={@repo} sha={c.id} class="mt-1" /> | |
| 87 | 88 | </li> |
| 88 | 89 | </ul> |
| 89 | 90 |
modified
lib/git_gud_web/live/workflow_live/index.ex
+3
−0
@@ -70,6 +70,9 @@ defmodule GitGudWeb.WorkflowLive.Index do
| 70 | 70 | defp status_class("running"), do: "badge-primary" |
| 71 | 71 | defp status_class("queued"), do: "badge-warning" |
| 72 | 72 | defp status_class("failure"), do: "badge-error" |
| 73 | + defp status_class("cancelled"), do: "badge-error" | |
| 74 | + defp status_class("skipped"), do: "badge-ghost" | |
| 75 | + defp status_class("blocked"), do: "badge-ghost" | |
| 73 | 76 | defp status_class(_), do: "badge-neutral" |
| 74 | 77 | |
| 75 | 78 | defp format_dt(%DateTime{} = dt), do: Calendar.strftime(dt, "%Y-%m-%d %H:%M") |
modified
lib/git_gud_web/live/workflow_live/show.ex
+4
−0
@@ -277,7 +277,11 @@ defmodule GitGudWeb.WorkflowLive.Show do
| 277 | 277 | defp status_class("running"), do: "badge-primary" |
| 278 | 278 | defp status_class("queued"), do: "badge-warning" |
| 279 | 279 | defp status_class("pending"), do: "badge-warning" |
| 280 | + # `blocked` = waiting on upstream `needs:`. Distinct from `queued` | |
| 281 | + # so operators can tell pickup-eligible vs gated jobs. | |
| 282 | + defp status_class("blocked"), do: "badge-ghost" | |
| 280 | 283 | defp status_class("failure"), do: "badge-error" |
| 284 | + defp status_class("cancelled"), do: "badge-error" | |
| 281 | 285 | defp status_class("skipped"), do: "badge-ghost" |
| 282 | 286 | defp status_class(_), do: "badge-neutral" |
| 283 | 287 | end |
Parents: 5775192