Make cross-reference backlinks clickable

d2fd801 · Gabriel Morell · 2026-09-10 16:04

2 files +53 -2
Message
{commit_body(@commit)}

Files changed

modified lib/git_gud_web/components/event_components.ex
+28 −2
@@ -251,12 +251,20 @@ defmodule GitGudWeb.EventComponents do
251 251 kind: if(data["from_type"] == "issue", do: "issue", else: "pull request"),
252 252 repo: data["from_repo"],
253 253 number: data["from_number"],
254 title: data["from_title"]
254 + title: data["from_title"],
255 + path: cross_ref_path(data)
255 256 }
256 257
257 258 ~H"""
258 259 referenced this from {@kind}
259 <span class="font-mono text-xs">{@repo}#{@number}</span><span :if={@title}> — {@title}</span>.
260 + <.link :if={@path} navigate={@path} class="link link-hover">
261 + <span class="font-mono text-xs">{@repo}#{@number}</span><span :if={@title}> — {@title}</span>
262 + </.link>
263 + <span :if={is_nil(@path)}>
264 + <span class="font-mono text-xs">{@repo}#{@number}</span><span :if={
265 + @title
266 + }>{@title}</span>
267 + </span>.
260 268 """
261 269 end
262 270
@@ -272,6 +280,24 @@ defmodule GitGudWeb.EventComponents do
272 280 defp describe(%{kind: "unmoderated"}), do: "restored this."
273 281 defp describe(%{kind: kind}), do: kind
274 282
283 + # `from_repo` is stored as "handle/name"; verified routes need the two
284 + # segments apart. Neither a handle nor a repo name may contain a
285 + # slash, so a single split is unambiguous.
286 + defp cross_ref_path(%{"from_repo" => repo, "from_number" => number, "from_type" => type})
287 + when is_binary(repo) and is_integer(number) do
288 + case String.split(repo, "/") do
289 + [handle, name] when handle != "" and name != "" ->
290 + if type == "issue",
291 + do: ~p"/r/#{handle}/#{name}/issues/#{number}",
292 + else: ~p"/r/#{handle}/#{name}/pulls/#{number}"
293 +
294 + _ ->
295 + nil
296 + end
297 + end
298 +
299 + defp cross_ref_path(_data), do: nil
300 +
275 301 # `data` holds hex strings — the sha columns are binary, but an event
276 302 # payload is JSON, so callers encode before recording.
277 303 defp short(hex) when is_binary(hex) and byte_size(hex) >= 7, do: binary_part(hex, 0, 7)
modified test/git_gud_web/live/cross_reference_test.exs
+25 −0
@@ -96,6 +96,9 @@ defmodule GitGudWeb.CrossReferenceTest do
96 96 assert html =~ "referenced this from issue"
97 97 assert html =~ "Mentions it"
98 98 assert html =~ "##{source.number}"
99 +
100 + # And it links back to the source, not just naming it.
101 + assert html =~ ~s(href="/r/#{handle(repo)}/#{repo.name}/issues/#{source.number}")
99 102 end
100 103
101 104 test "a reference already present isn't re-announced on re-save", %{conn: conn} do
@@ -172,6 +175,28 @@ defmodule GitGudWeb.CrossReferenceTest do
172 175 assert Events.list_for(target) == []
173 176 end
174 177
178 + test "a backlink from a PR links to the PR", %{conn: conn} do
179 + {user, repo} = repository_fixture()
180 + target = issue_fixture(repo, user)
181 + pr = pr_with_branches(repo, user, %{"title" => "Source PR", "body" => ""})
182 +
183 + {:ok, lv, _html} =
184 + live(log_in_user(conn, user), ~p"/r/#{handle(repo)}/#{repo.name}/pulls/#{pr.number}")
185 +
186 + _ = lv |> element("button[phx-click=edit_body]") |> render_click()
187 +
188 + _ =
189 + lv
190 + |> form("form[phx-submit=save_body]", %{body: "closes ##{target.number}"})
191 + |> render_submit()
192 +
193 + {:ok, _hist, html} =
194 + live(log_in_user(conn, user), issue_path(repo, target) <> "/history")
195 +
196 + assert html =~ "referenced this from pull request"
197 + assert html =~ ~s(href="/r/#{handle(repo)}/#{repo.name}/pulls/#{pr.number}")
198 + end
199 +
175 200 test "a PR referenced from an issue is backlinked too", %{conn: conn} do
176 201 {user, repo} = repository_fixture()
177 202 pr = pr_with_branches(repo, user, %{"title" => "The PR"})

Parents: 8d95805