ui

e2011b6 · gmorell · 2026-05-22 17:55

7 files +100 -1

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 stringthe 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: successsuccess, 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