lib/openagents_web/live/code_blob_live.ex

main at 436d37facc96 · 6 KB

defmodule OpenAgentsWeb.CodeBlobLive do
  @moduledoc """
  The public file view (#136): a stable, shareable openagents.com URL for
  any file at any ref on a forge repo —
  `/:owner/:repo/blob/:ref/*path`. Branch URLs are living (they follow the
  branch); sha-pinned URLs are permanent.

  Served straight from the bare repo through `OpenAgents.Forge.Browse` (bounded
  git plumbing, WAL-fresh), gated by the repo's disclosure level
  (`OpenAgents.Forge.Visibility`, TRANSPARENCY-001 — files need :l3). Markdown
  renders through `OpenAgents.Markdown`'s bounded MDEx sanitizer; everything else is an
  escaped code block; binaries are named, never rendered. Public posture as
  the leaderboard: read-only, no session required, cannot invoke OpenAgents.
  """

  use OpenAgentsWeb, :live_view

  alias OpenAgents.Forge.Browse
  alias OpenAgentsWeb.OG
  alias OpenAgentsWeb.RepositoryAccess

  @impl true
  def mount(%{"owner" => owner, "repo" => name, "ref" => ref} = params, _session, socket) do
    path = params |> Map.get("path", []) |> Enum.join("/")
    repository = RepositoryAccess.get_visible!(owner, name, socket.assigns.current_user)
    base = RepositoryAccess.base(repository)

    unless OpenAgents.Forge.enabled?() and repository.lifecycle_state == "ready" do
      raise OpenAgentsWeb.PublicNotFoundError
    end

    %{sha: sha, head: head, blob: blob} =
      case Browse.blob_page(repository, ref, path) do
        {:ok, page} -> page
        _ -> raise OpenAgentsWeb.PublicNotFoundError
      end

    # Either the whole repo is browsable (:l3), or this is a published
    # document at the current head. A published path at an older ref is a
    # 404: publishing one document must not publish its history.
    unless RepositoryAccess.allows_file?(
             repository,
             socket.assigns.current_user,
             path,
             sha,
             head
           ) do
      raise OpenAgentsWeb.PublicNotFoundError
    end

    plain = Map.get(params, "plain") == "1"

    {:ok,
     socket
     |> assign(:page_title, "#{Path.basename(path)} · #{repository.name}")
     |> assign(:repository, repository)
     |> assign(:repo, repository.name)
     |> assign(:owner, repository.namespace.slug)
     |> assign(:base, base)
     |> assign(:ref, ref)
     |> assign(:sha, sha)
     |> assign(:path, path)
     |> assign(:blob, blob)
     |> assign(
       :og,
       OG.meta(
         OG.blob(repository.namespace.slug, repository.name, path, %{
           ref: ref,
           size: blob.size,
           lines: if(blob.binary, do: nil, else: blob.content |> String.split("\n") |> length()),
           truncated: blob.truncated
         })
       )
     )
     |> assign(:browsable, RepositoryAccess.full_source?(repository, socket.assigns.current_user))
     |> assign(:markdown?, markdown?(path) and not plain and not blob.binary)}
  rescue
    Ecto.NoResultsError -> raise OpenAgentsWeb.PublicNotFoundError
  end

  defp markdown?(path), do: String.ends_with?(String.downcase(path), [".md", ".markdown"])

  defp short(sha), do: String.slice(sha, 0, 12)

  defp size_text(nil), do: "—"
  defp size_text(bytes) when bytes < 1_024, do: "#{bytes} B"
  defp size_text(bytes) when bytes < 1_048_576, do: "#{div(bytes, 1_024)} KB"
  defp size_text(bytes), do: "#{Float.round(bytes / 1_048_576, 1)} MB"

  @impl true
  def render(assigns) do
    ~H"""
    <Layouts.app
      flash={@flash}
      sidebar_sections={assigns[:sidebar_sections]}
      current_scope={assigns[:current_scope]}
      title="Code"
      full_width
    >
      <main id="code-blob-page" class="app-shell code-shell">
        <section class="code" aria-label="File view">
          <header class="code-heading">
            <div>
              <h1 class="code-path">{@path}</h1>
              <p class="code-meta">
                <code>{@ref}</code>
                <span :if={@ref != short(@sha)}>
                  at
                  <.text_button navigate={"#{@base}/commit/#{short(@sha)}"}>
                    <code>{short(@sha)}</code>
                  </.text_button>
                </span>
                · {size_text(@blob.size)}
                <span :if={@blob.truncated}>· truncated for display</span>
              </p>
            </div>
            <div class="code-actions">
              <.button
                :if={markdown?(@path) and not @blob.binary}
                id="code-plain-toggle"
                variant={:chip}
                size={:xs}
                phx-click={
                  JS.navigate(
                    "#{@base}/blob/#{@ref}/#{@path}" <>
                      if(@markdown?, do: "?plain=1", else: "")
                  )
                }
              >
                {if @markdown?, do: "VIEW SOURCE", else: "VIEW RENDERED"}
              </.button>
            </div>
          </header>

          <.alert :if={@blob.truncated} id="code-truncated" variant={:warning}>
            This file is larger than the display bound; the tail is cut. The
            full file is in the repository itself.
          </.alert>

          <.card id="code-blob">
            <%!-- Authored document, not a message: hard breaks belong to
            typed text, and here they would only reproduce the source file's
            own wrapping. --%>
            <div :if={@markdown?} class="markdown">
              {OpenAgents.Markdown.to_html(@blob.content, hardbreaks: false)}
            </div>
            <pre :if={not @markdown? and not @blob.binary} class="code-source"><code>{@blob.content}</code></pre>
            <.empty :if={@blob.binary} id="code-binary" title="Binary file">
              {size_text(@blob.size)} of binary content — not rendered.
            </.empty>
          </.card>

          <footer class="code-footer">
            <p :if={@browsable}>
              A branch URL follows the branch; this exact revision is
              permanent at
              <.text_button navigate={"#{@base}/blob/#{short(@sha)}/#{@path}"}>
                {@base}/blob/{short(@sha)}/{@path}
              </.text_button>
            </p>
            <p :if={!@browsable}>
              A published document from a private repository. It is served at
              the current revision only — earlier revisions are not public —
              and the rest of the source is not browsable. See
              <.text_button navigate="/changelog">the changelog</.text_button>
              for what has changed and when.
            </p>
          </footer>
        </section>
      </main>
    </Layouts.app>
    """
  end
end