neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
Add the F-Droid repository by scanning a QR
9f00bdd · Gabriel Morell · 2026-09-12 18:17
Message
{commit_body(@commit)}
Files changed
modified
lib/git_gud/packages/usage.ex
+3
−10
@@ -404,24 +404,17 @@ defmodule GitGud.Packages.Usage do
| 404 | 404 | %{ |
| 405 | 405 | consume: [ |
| 406 | 406 | step( |
| 407 | − "Add #{ctx.handle} as a repository in F-Droid", | |
| 408 | − "shell", | |
| 409 | − """ | |
| 410 | − #{namespace}?fingerprint=<fingerprint> | |
| 411 | − """ | |
| 412 | − ), | |
| 413 | − step( | |
| 414 | − "Get the fingerprint", | |
| 407 | + "Read the fingerprint, for scripting or to check the one above", | |
| 415 | 408 | "shell", |
| 416 | 409 | """ |
| 417 | 410 | curl -fsSL #{key_url(ctx, "fdroid")} |
| 418 | 411 | """ |
| 419 | 412 | ), |
| 420 | 413 | step( |
| 421 | − "…or add just this repository", | |
| 414 | + "…or add just this repository, with the same fingerprint", | |
| 422 | 415 | "shell", |
| 423 | 416 | """ |
| 424 | − #{url}?fingerprint=<fingerprint> | |
| 417 | + #{url}?fingerprint=$(curl -fsSL #{key_url(ctx, "fdroid")}) | |
| 425 | 418 | """ |
| 426 | 419 | ), |
| 427 | 420 | step( |
modified
lib/git_gud_web/live/repo_live/packages.ex
+87
−1
@@ -79,6 +79,10 @@ defmodule GitGudWeb.RepoLive.Packages do
| 79 | 79 | # common case needs no clicking. |
| 80 | 80 | |> assign(:help_format, default_help_format(groups)) |
| 81 | 81 | |> assign(:help_facet, "manager") |
| 82 | + |> assign(:fdroid_repo, nil) | |
| 83 | + # `default_help_format/1` can land on fdroid when that's what this | |
| 84 | + # repository publishes, and then no tab click ever fires. | |
| 85 | + |> then(&load_fdroid_repo(&1, &1.assigns.help_format)) | |
| 82 | 86 | |> assign(:page_title, "Packages — #{owner}/#{name}")} |
| 83 | 87 | end |
| 84 | 88 |
@@ -166,7 +170,7 @@ defmodule GitGudWeb.RepoLive.Packages do
| 166 | 170 | end |
| 167 | 171 | |
| 168 | 172 | def handle_event("help-format", %{"format" => format}, socket), |
| 169 | − do: {:noreply, assign(socket, :help_format, format)} | |
| 173 | + do: {:noreply, socket |> assign(:help_format, format) |> load_fdroid_repo(format)} | |
| 170 | 174 | |
| 171 | 175 | def handle_event("help-facet", %{"facet" => facet}, socket), |
| 172 | 176 | do: {:noreply, assign(socket, :help_facet, facet)} |
@@ -465,6 +469,7 @@ defmodule GitGudWeb.RepoLive.Packages do
| 465 | 469 | usage={Usage.for_format(@help_format, @usage_ctx)} |
| 466 | 470 | facet={@help_facet} |
| 467 | 471 | format={@help_format} |
| 472 | + fdroid_repo={@fdroid_repo} | |
| 468 | 473 | handle={@handle} |
| 469 | 474 | repo_name={@repo.name} |
| 470 | 475 | /> |
@@ -478,6 +483,7 @@ defmodule GitGudWeb.RepoLive.Packages do
| 478 | 483 | attr :usage, :map, default: nil |
| 479 | 484 | attr :facet, :string, required: true |
| 480 | 485 | attr :format, :string, required: true |
| 486 | + attr :fdroid_repo, :any, default: nil | |
| 481 | 487 | attr :handle, :string, required: true |
| 482 | 488 | attr :repo_name, :string, required: true |
| 483 | 489 |
@@ -510,6 +516,7 @@ defmodule GitGudWeb.RepoLive.Packages do
| 510 | 516 | </p> |
| 511 | 517 | <.usage_steps steps={@usage.ci} /> |
| 512 | 518 | <% else %> |
| 519 | + <.fdroid_qr :if={@format == "fdroid"} repo={@fdroid_repo} /> | |
| 513 | 520 | <.usage_group title="Install" steps={@usage.consume} /> |
| 514 | 521 | <.usage_group title="Publish" steps={@usage.publish} /> |
| 515 | 522 | <p class="text-xs opacity-60"> |
@@ -528,6 +535,48 @@ defmodule GitGudWeb.RepoLive.Packages do
| 528 | 535 | """ |
| 529 | 536 | end |
| 530 | 537 | |
| 538 | + attr :repo, :any, default: nil | |
| 539 | + | |
| 540 | + # F-Droid's "add repository" screen has a scanner. This is the whole | |
| 541 | + # of the setup on a phone: no typing a URL, and no typing a 64-hex | |
| 542 | + # fingerprint, which is the part nobody would get right by hand. | |
| 543 | + defp fdroid_qr(%{repo: nil} = assigns), do: ~H"" | |
| 544 | + | |
| 545 | + defp fdroid_qr(%{repo: :unavailable} = assigns) do | |
| 546 | + ~H""" | |
| 547 | + <p class="text-xs opacity-60"> | |
| 548 | + Couldn't read this namespace's index signing certificate, so there's | |
| 549 | + no QR to show. The instructions below still apply — get the | |
| 550 | + fingerprint from <code class="font-mono">/pkg/_keys/fdroid/…</code> once it's reachable. | |
| 551 | + </p> | |
| 552 | + """ | |
| 553 | + end | |
| 554 | + | |
| 555 | + defp fdroid_qr(assigns) do | |
| 556 | + ~H""" | |
| 557 | + <div class="flex flex-wrap items-start gap-4"> | |
| 558 | + <div class="rounded bg-white p-1.5 leading-none shrink-0"> | |
| 559 | + {Phoenix.HTML.raw(@repo.qr)} | |
| 560 | + </div> | |
| 561 | + <div class="min-w-48 flex-1 space-y-1.5"> | |
| 562 | + <h3 class="text-xs font-semibold uppercase tracking-wide opacity-50">Scan to add</h3> | |
| 563 | + <p class="text-xs opacity-70"> | |
| 564 | + F-Droid → Settings → Repositories → + → scan. One entry covers | |
| 565 | + every Android app this namespace publishes. | |
| 566 | + </p> | |
| 567 | + <pre class="bg-base-200 rounded px-2.5 py-2 overflow-x-auto"><code | |
| 568 | + class="text-[11px] leading-relaxed select-all whitespace-pre-wrap break-all" | |
| 569 | + >{@repo.url}</code></pre> | |
| 570 | + <p class="text-xs opacity-60"> | |
| 571 | + The <code class="font-mono">fingerprint</code> | |
| 572 | + pins the index signing certificate — F-Droid refuses an index signed | |
| 573 | + by anything else. | |
| 574 | + </p> | |
| 575 | + </div> | |
| 576 | + </div> | |
| 577 | + """ | |
| 578 | + end | |
| 579 | + | |
| 531 | 580 | attr :title, :string, required: true |
| 532 | 581 | attr :steps, :list, default: [] |
| 533 | 582 |
@@ -553,6 +602,43 @@ defmodule GitGudWeb.RepoLive.Packages do
| 553 | 602 | """ |
| 554 | 603 | end |
| 555 | 604 | |
| 605 | + # F-Droid adds a repository by scanning a QR, and the URL it scans has | |
| 606 | + # to carry the fingerprint of the index signing certificate — so this | |
| 607 | + # needs the real fingerprint, not the `<fingerprint>` placeholder the | |
| 608 | + # static instructions can offer. | |
| 609 | + # | |
| 610 | + # Loaded when the tab is opened rather than at mount, for two | |
| 611 | + # reasons: asking for the fingerprint *mints the signing key* if the | |
| 612 | + # namespace has none, and under OpenBao it is a network call. Neither | |
| 613 | + # belongs on the render path of every packages page in the forge. | |
| 614 | + defp load_fdroid_repo(socket, "fdroid") do | |
| 615 | + if socket.assigns.fdroid_repo do | |
| 616 | + socket | |
| 617 | + else | |
| 618 | + ctx = socket.assigns.usage_ctx | |
| 619 | + | |
| 620 | + case GitGud.Packages.Formats.Fdroid.fingerprint(socket.assigns.repo) do | |
| 621 | + {:ok, fingerprint} -> | |
| 622 | + url = "#{Usage.namespace_url(ctx, "fdroid")}?fingerprint=#{fingerprint}" | |
| 623 | + assign(socket, :fdroid_repo, %{url: url, fingerprint: fingerprint, qr: qr_svg(url)}) | |
| 624 | + | |
| 625 | + # A sealed or unreachable OpenBao, most likely. The rest of the | |
| 626 | + # instructions are still correct and still useful, so the panel | |
| 627 | + # renders without the QR rather than the page failing. | |
| 628 | + {:error, _reason} -> | |
| 629 | + assign(socket, :fdroid_repo, :unavailable) | |
| 630 | + end | |
| 631 | + end | |
| 632 | + end | |
| 633 | + | |
| 634 | + defp load_fdroid_repo(socket, _format), do: socket | |
| 635 | + | |
| 636 | + defp qr_svg(url) do | |
| 637 | + url | |
| 638 | + |> EQRCode.encode() | |
| 639 | + |> EQRCode.svg(width: 180, background_color: "#ffffff", color: "#000000") | |
| 640 | + end | |
| 641 | + | |
| 556 | 642 | defp format_label(format), do: Map.get(@format_labels, format, format) |
| 557 | 643 | |
| 558 | 644 | # The one-liner that installs a specific version, shown inline with |
modified
test/git_gud_web/package_usage_test.exs
+31
−0
@@ -170,6 +170,37 @@ defmodule GitGudWeb.PackageUsageTest do
| 170 | 170 | end |
| 171 | 171 | end |
| 172 | 172 | |
| 173 | + describe "the F-Droid QR" do | |
| 174 | + setup %{conn: conn, path: path} do | |
| 175 | + {:ok, view, _html} = live(conn, path) | |
| 176 | + %{html: view |> element(~s{button[phx-value-format="fdroid"]}) |> render_click()} | |
| 177 | + end | |
| 178 | + | |
| 179 | + test "renders a scannable code, not a placeholder", %{html: html} do | |
| 180 | + # F-Droid's add-repository screen has a scanner, and the 64-hex | |
| 181 | + # fingerprint is the part nobody would type correctly by hand. | |
| 182 | + assert html =~ "<svg" | |
| 183 | + assert html =~ "Scan to add" | |
| 184 | + | |
| 185 | + # A real fingerprint. The panel owns this URL precisely so no | |
| 186 | + # step anywhere has to print a `<fingerprint>` placeholder for a | |
| 187 | + # value nobody could fill in by hand. | |
| 188 | + assert html =~ ~r/\?fingerprint=[0-9a-f]{64}/ | |
| 189 | + refute html =~ "<fingerprint>" | |
| 190 | + end | |
| 191 | + | |
| 192 | + test "points at the namespace repository, not this one", %{html: html, handle: handle} do | |
| 193 | + # One entry per publisher, not one per Android project. | |
| 194 | + assert html =~ "/pkg/fdroid/#{handle}/_all?fingerprint=" | |
| 195 | + end | |
| 196 | + | |
| 197 | + test "the code encodes the same URL it prints", %{html: html, user: user} do | |
| 198 | + {:ok, fingerprint} = GitGud.Packages.Formats.Fdroid.fingerprint(user) | |
| 199 | + | |
| 200 | + assert html =~ fingerprint | |
| 201 | + end | |
| 202 | + end | |
| 203 | + | |
| 173 | 204 | describe "the packages page" do |
| 174 | 205 | test "documents both facets without anything published yet", %{conn: conn, path: path} do |
| 175 | 206 | {:ok, view, html} = live(conn, path) |
Parents: 316291c