Fix repository directory navigation

fa4b79283164 · AtlantisPleb · · parent ba7960095b77

Fix repository directory navigation

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • added lib/openagents_web/live/code_tree_live.ex
  • modified lib/openagents_web/route_authority.ex
  • modified lib/openagents_web/router.ex
  • modified test/openagents_web/live/code_live_test.exs

Diff

4 files changed, +173 -1

lib/openagents_web/live/code_tree_live.ex added +120

@@ -0,0 +1,120 @@

1
defmodule OpenAgentsWeb.CodeTreeLive do
2
  @moduledoc """
3
  The public directory view for a repository path at a branch, tag, or commit.
4
5
  The route follows GitHub's `/:owner/:repo/tree/:ref/*path` shape. Directory
6
  reads use the same bounded Git plumbing and repository visibility policy as
7
  the repository home and file view.
8
  """
9
10
  use OpenAgentsWeb, :live_view
11
12
  alias OpenAgents.Forge.Browse
13
  alias OpenAgentsWeb.RepositoryAccess
14
15
  @impl true
16
  def mount(%{"owner" => owner, "repo" => name, "ref" => ref} = params, _session, socket) do
17
    path = params |> Map.get("path", []) |> Enum.join("/")
18
    repository = RepositoryAccess.get_visible!(owner, name, socket.assigns.current_user)
19
20
    unless OpenAgents.Forge.enabled?() and
21
             RepositoryAccess.full_source?(repository, socket.assigns.current_user) do
22
      raise OpenAgentsWeb.PublicNotFoundError
23
    end
24
25
    sha =
26
      case Browse.resolve_commit(repository, ref) do
27
        {:ok, sha} -> sha
28
        _ -> raise OpenAgentsWeb.PublicNotFoundError
29
      end
30
31
    entries =
32
      case Browse.tree(repository, sha, path) do
33
        {:ok, entries} -> entries
34
        _ -> raise OpenAgentsWeb.PublicNotFoundError
35
      end
36
37
    refs = Browse.refs(repository)
38
    base = RepositoryAccess.base(repository)
39
40
    {:ok,
41
     socket
42
     |> assign(:page_title, "#{display_path(path)} · #{repository.name}")
43
     |> assign(:repository, repository)
44
     |> assign(:repo, repository.name)
45
     |> assign(:owner, repository.namespace.slug)
46
     |> assign(:base, base)
47
     |> assign(:ref, ref)
48
     |> assign(:sha, sha)
49
     |> assign(:path, path)
50
     |> assign(:entries, entries)
51
     |> assign(:breadcrumbs, breadcrumbs(base, ref, path))
52
     |> assign(:branch_count, Enum.count(refs, &(&1.kind == :branch)))
53
     |> assign(:tag_count, Enum.count(refs, &(&1.kind == :tag)))}
54
  rescue
55
    Ecto.NoResultsError -> raise OpenAgentsWeb.PublicNotFoundError
56
  end
57
58
  defp display_path(""), do: "Files"
59
  defp display_path(path), do: path
60
61
  defp breadcrumbs(base, ref, path) do
62
    path
63
    |> String.split("/", trim: true)
64
    |> Enum.map_reduce([], fn segment, parents ->
65
      current = parents ++ [segment]
66
      {%{name: segment, href: "#{base}/tree/#{ref}/#{Enum.join(current, "/")}"}, current}
67
    end)
68
    |> elem(0)
69
  end
70
71
  @impl true
72
  def render(assigns) do
73
    ~H"""
74
    <Layouts.app
75
      flash={@flash}
76
      sidebar_sections={assigns[:sidebar_sections]}
77
      current_scope={@current_scope}
78
      title="Code"
79
    >
80
      <main id="code-tree-page" class="app-shell code-shell">
81
        <.repo_view
82
          owner={@owner}
83
          repo={@repo}
84
          visibility={if @repository.visibility == "public", do: :public, else: :private}
85
        >
86
          <:tabs>
87
            <.repo_tabs>
88
              <:tab icon="code" navigate={@base} current>Code</:tab>
89
              <:tab icon="empty-circle" navigate={"#{@base}/issues"}>Issues</:tab>
90
              <:tab icon="cube" navigate={"#{@base}/projects"}>Projects</:tab>
91
            </.repo_tabs>
92
          </:tabs>
93
94
          <nav class="code-heading" aria-label="Repository path">
95
            <h1 class="code-path">
96
              <.text_button navigate={@base}>{@repo}</.text_button>
97
              <span :for={crumb <- @breadcrumbs}>
98
                / <.text_button navigate={crumb.href}>{crumb.name}</.text_button>
99
              </span>
100
            </h1>
101
            <p class="code-meta">
102
              <code>{@ref}</code> at <code>{String.slice(@sha, 0, 12)}</code>
103
            </p>
104
          </nav>
105
106
          <.file_table
107
            owner={@owner}
108
            repo={@repo}
109
            ref={@ref}
110
            path={@path}
111
            entries={@entries}
112
            branches={@branch_count}
113
            tags={@tag_count}
114
          />
115
        </.repo_view>
116
      </main>
117
    </Layouts.app>
118
    """
119
  end
120
end
lib/openagents_web/route_authority.ex modified +5 -1

@@ -269,7 +269,11 @@ defmodule OpenAgentsWeb.RouteAuthority do

269 269
270 270
  defp repository_browser_path?(path) do
271 271
    path == "/:owner/:repo" or
272
      String.starts_with?(path, ["/:owner/:repo/commit/", "/:owner/:repo/blob/"])
272
      String.starts_with?(path, [
273
        "/:owner/:repo/commit/",
274
        "/:owner/:repo/tree/",
275
        "/:owner/:repo/blob/"
276
      ])
273 277
  end
274 278
275 279
  defp mutation_verb?(_path), do: true
lib/openagents_web/router.ex modified +1

@@ -322,6 +322,7 @@ defmodule OpenAgentsWeb.Router do

322 322
      on_mount: [{OpenAgentsWeb.UserAuth, :mount_current_user}] do
323 323
      live "/:owner/:repo", CodeRepoLive, :index
324 324
      live "/:owner/:repo/commit/:sha", CodeCommitLive, :index
325
      live "/:owner/:repo/tree/:ref/*path", CodeTreeLive, :index
325 326
      live "/:owner/:repo/blob/:ref/*path", CodeBlobLive, :index
326 327
    end
327 328
  end
test/openagents_web/live/code_live_test.exs modified +47

@@ -477,6 +477,53 @@ defmodule OpenAgentsWeb.CodeLiveTest do

477 477
    end
478 478
  end
479 479
480
  describe "/code/:repo/tree/:ref/*path" do
481
    test "renders a repository directory and links its children", %{conn: conn} do
482
      browsable()
483
484
      {:ok, view, _html} =
485
        live(conn, "/OpenAgentsInc/openagents.com/tree/main/docs")
486
487
      assert has_element?(view, "#code-tree-page")
488
489
      assert has_element?(
490
               view,
491
               ~s(a[href="/OpenAgentsInc/openagents.com/blob/main/docs/audit.md"])
492
             )
493
    end
494
495
    test "follows a directory link emitted by the repository home", %{conn: conn} do
496
      browsable()
497
      {:ok, home, _html} = live(conn, "/OpenAgentsInc/openagents.com")
498
499
      assert has_element?(
500
               home,
501
               ~s(a[href="/OpenAgentsInc/openagents.com/tree/main/docs"])
502
             )
503
504
      {:ok, tree, _html} =
505
        live(conn, "/OpenAgentsInc/openagents.com/tree/main/docs")
506
507
      assert has_element?(tree, ~s([data-kind="blob"]), "audit.md")
508
    end
509
510
    test "a directory remains concealed when the repository source is not browsable", %{
511
      conn: conn
512
    } do
513
      assert_raise OpenAgentsWeb.PublicNotFoundError, fn ->
514
        live(conn, "/OpenAgentsInc/openagents.com/tree/main/docs")
515
      end
516
    end
517
518
    test "a missing directory 404s", %{conn: conn} do
519
      browsable()
520
521
      assert_raise OpenAgentsWeb.PublicNotFoundError, fn ->
522
        live(conn, "/OpenAgentsInc/openagents.com/tree/main/missing")
523
      end
524
    end
525
  end
526
480 527
  describe "/code/:repo/commit/:sha" do
481 528
    test "renders subject, trailers, and changed files — but no diff below :l3", %{
482 529
      conn: conn,

This page updates live while a promote is in flight · changelog