Show repository tags on /repositories, and filter by them

83a7db3 · Gabriel Morell · 2026-09-11 01:43

3 files +80 -5
Message
{commit_body(@commit)}

Files changed

modified lib/git_gud/tags.ex
+15 −0
@@ -48,6 +48,21 @@ defmodule GitGud.Tags do
48 48 @doc "Just the names, for rendering."
49 49 def tag_names(%Repository{} = repo), do: repo |> list_tags() |> Enum.map(& &1.name)
50 50
51 + @doc """
52 + Tag names for many repositories at once, keyed by repo id.
53 +
54 + One query for a whole listing rather than one per row.
55 + """
56 + def names_by_repository(repo_ids) when is_list(repo_ids) do
57 + from(t in RepositoryTag,
58 + where: t.repository_id in ^repo_ids,
59 + order_by: [asc: t.name],
60 + select: {t.repository_id, t.name}
61 + )
62 + |> Repo.all()
63 + |> Enum.group_by(fn {rid, _} -> rid end, fn {_, name} -> name end)
64 + end
65 +
51 66 @doc """
52 67 Replace a repository's tags with `names`.
53 68
modified lib/git_gud_web/live/repo_live/index.ex
+17 −5
@@ -29,6 +29,7 @@ defmodule GitGudWeb.RepoLive.Index do
29 29 |> assign(:page_title, "Repositories")
30 30 |> assign(:repos, repos)
31 31 |> assign(:languages, Repositories.top_languages_for(ids))
32 + |> assign(:tags, GitGud.Tags.names_by_repository(ids))
32 33 |> assign(:ci_status, Workflows.latest_run_status_by_repo(ids))
33 34 |> assign(:query, "")
34 35 |> assign(:scope_filter, "all")
@@ -67,18 +68,19 @@ defmodule GitGudWeb.RepoLive.Index do
67 68 # add round-trips.
68 69 defp visible(assigns) do
69 70 assigns.repos
70 |> Enum.filter(&matches?(&1, assigns.query))
71 + |> Enum.filter(&matches?(&1, assigns.query, assigns.tags))
71 72 |> Enum.filter(&in_scope?(&1, assigns.scope_filter, assigns.current_scope))
72 73 |> sort_by(assigns.sort)
73 74 end
74 75
75 defp matches?(_repo, ""), do: true
76 + defp matches?(_repo, "", _tags), do: true
76 77
77 defp matches?(repo, query) do
78 + defp matches?(repo, query, tags) do
78 79 needle = String.downcase(query)
79 80
80 81 String.contains?(String.downcase("#{repo.handle}/#{repo.name}"), needle) or
81 String.contains?(String.downcase(repo.description || ""), needle)
82 + String.contains?(String.downcase(repo.description || ""), needle) or
83 + Enum.any?(Map.get(tags, repo.id, []), &String.contains?(&1, needle))
82 84 end
83 85
84 86 defp in_scope?(_repo, "all", _scope), do: true
@@ -157,7 +159,7 @@ defmodule GitGudWeb.RepoLive.Index do
157 159 type="search"
158 160 name="query"
159 161 value={@query}
160 placeholder="Filter by name or description…"
162 + placeholder="Filter by name, description or tag…"
161 163 class="input input-bordered input-sm flex-1 min-w-48"
162 164 />
163 165 <select name="sort" class="select select-bordered select-sm">
@@ -249,6 +251,16 @@ defmodule GitGudWeb.RepoLive.Index do
249 251 {repo.description}
250 252 </p>
251 253
254 + <div :if={@tags[repo.id]} class="flex flex-wrap gap-1 mt-1">
255 + <.link
256 + :for={tag <- @tags[repo.id]}
257 + navigate={~p"/tags/#{tag}"}
258 + class="badge badge-xs badge-outline hover:badge-primary"
259 + >
260 + {tag}
261 + </.link>
262 + </div>
263 +
252 264 <div class="text-xs opacity-60 mt-2 flex items-center gap-3 flex-wrap">
253 265 <span :if={@languages[repo.id]} class="flex items-center gap-1.5">
254 266 <span
modified test/git_gud_web/live/repo_live/index_test.exs
+48 −0
@@ -128,4 +128,52 @@ defmodule GitGudWeb.RepoLive.IndexTest do
128 128
129 129 assert html =~ "1 of 2 shown"
130 130 end
131 +
132 + describe "tags" do
133 + test "a repo's tags show on its card and link to the listing", %{conn: conn, user: user} do
134 + {_user, repo} = repository_fixture(%{owner: user, name: "tagged-repo"})
135 + {:ok, _} = GitGud.Tags.set_tags(repo, ["elixir", "git"])
136 +
137 + {:ok, _lv, html} = live(log_in_user(conn, user), ~p"/repositories")
138 +
139 + assert html =~ "tagged-repo"
140 + assert html =~ ~s(href="/tags/elixir")
141 + assert html =~ ~s(href="/tags/git")
142 + end
143 +
144 + test "an untagged repo shows no tag row", %{conn: conn, user: user} do
145 + {_user, _repo} = repository_fixture(%{owner: user, name: "plain-repo"})
146 +
147 + {:ok, _lv, html} = live(log_in_user(conn, user), ~p"/repositories")
148 +
149 + assert html =~ "plain-repo"
150 + refute html =~ ~s(href="/tags/)
151 + end
152 +
153 + test "the filter matches on tag as well as name", %{conn: conn, user: user} do
154 + {_user, tagged} = repository_fixture(%{owner: user, name: "alpha"})
155 + {_user, _other} = repository_fixture(%{owner: user, name: "beta"})
156 + {:ok, _} = GitGud.Tags.set_tags(tagged, ["elixir"])
157 +
158 + {:ok, lv, _html} = live(log_in_user(conn, user), ~p"/repositories")
159 +
160 + html = lv |> form("form[phx-change=filter]", %{query: "elixir"}) |> render_change()
161 +
162 + assert html =~ "alpha"
163 + refute html =~ "beta"
164 + end
165 +
166 + test "filtering by name still works alongside", %{conn: conn, user: user} do
167 + {_user, tagged} = repository_fixture(%{owner: user, name: "alpha"})
168 + {_user, _other} = repository_fixture(%{owner: user, name: "beta"})
169 + {:ok, _} = GitGud.Tags.set_tags(tagged, ["elixir"])
170 +
171 + {:ok, lv, _html} = live(log_in_user(conn, user), ~p"/repositories")
172 +
173 + html = lv |> form("form[phx-change=filter]", %{query: "beta"}) |> render_change()
174 +
175 + assert html =~ "beta"
176 + refute html =~ ">alpha<"
177 + end
178 + end
131 179 end

Parents: 5c4496e