defmodule GitGudWeb.Api.Mobile.IssueController do
@moduledoc """
Issue viewer, opener and commenter.
Bodies ship twice — `body` (the markdown source, so the phone's
quote-reply can prefix it with `> `) and `body_html` (rendered here,
because reimplementing GFM plus cross-references plus syntax
highlighting in Kotlin is not a thing anyone should do).
"""
use GitGudWeb, :controller
import GitGudWeb.Api.Mobile.Helpers
alias GitGud.Issues
alias GitGud.Labels
alias GitGudWeb.Api.Mobile.Auth
alias GitGudWeb.Api.Mobile.JSON
def index(conn, %{"owner" => owner, "name" => name} = params) do
with {:ok, user} <- Auth.require_scope(conn, "issues:read"),
{:ok, repo} <- fetch_repo(conn, user, owner, name) do
issues =
Issues.list_issues(repo,
state: state_param(params),
search: params["search"],
limit: int_param(params, "limit", 50, 100),
offset: int_param(params, "offset", 0, 10_000)
)
json(conn, %{"issues" => Enum.map(issues, &JSON.issue_summary/1)})
else
{:error, conn} -> conn
end
end
def show(conn, %{"owner" => owner, "name" => name, "number" => number}) do
with {:ok, user} <- Auth.require_scope(conn, "issues:read"),
{:ok, repo} <- fetch_repo(conn, user, owner, name),
{:ok, n} <- number_param(conn, number),
{:ok, issue} <- get_issue(conn, repo, n) do
json(conn, %{
"issue" => JSON.issue(issue, repo),
"comments" => Enum.map(issue.comments, &JSON.comment(&1, repo)),
"can_comment" => GitGud.Organizations.can?(user, repo, :read),
"can_close" => can_close?(user, repo, issue)
})
else
{:error, conn} -> conn
end
end
@doc """
Open an issue: `title`, and optionally `body` and `labels` (names of
the repository's labels, its own or inherited). Labels are applied
for someone with triage access, as on the web — and for a job, whose
`issues: write` is the say-so — and quietly left off otherwise; a
name the repository has no label for is an error.
"""
def create(conn, %{"owner" => owner, "name" => name} = params) do
with {:ok, user} <- Auth.require_scope(conn, "issues:write"),
{:ok, repo} <- fetch_repo(conn, user, owner, name),
{:ok, title} <- require_title(conn, params),
{:ok, labels} <- find_labels(conn, repo, params["labels"]),
:ok <- job_quota(conn, :job_issues) do
attrs = %{"title" => title, "body" => params["body"]}
case Issues.create_issue(repo, user, attrs, job_opts(conn)) do
{:ok, issue} ->
if conn.assigns[:api_job] || GitGud.Organizations.can?(user, repo, :triage),
do: Enum.each(labels, &Issues.attach_label(issue, &1))
:ok = GitGud.Events.record_cross_references(issue, repo, nil, issue.body, user)
conn
|> put_status(201)
|> json(%{"issue" => JSON.issue(Issues.get_issue!(repo, issue.number), repo)})
{:error, :banned} ->
send_error(conn, 403, "you are banned from this repository")
{:error, %Ecto.Changeset{} = cs} ->
send_error(conn, 422, changeset_message(cs))
end
else
{:error, conn} -> conn
end
end
defp require_title(conn, params) do
case String.trim(to_string(params["title"] || "")) do
"" -> {:error, send_error(conn, 422, "title can't be blank")}
title -> {:ok, title}
end
end
defp find_labels(_conn, _repo, nil), do: {:ok, []}
defp find_labels(conn, repo, names) when is_list(names) do
pickable = Labels.list_labels(repo)
Enum.reduce_while(names, {:ok, []}, fn name, {:ok, acc} ->
case Enum.find(pickable, &(&1.name == name)) do
nil -> {:halt, {:error, send_error(conn, 422, "no label named #{name} here")}}
label -> {:cont, {:ok, [label | acc]}}
end
end)
|> case do
{:ok, found} -> {:ok, Enum.reverse(found)}
error -> error
end
end
defp find_labels(conn, _repo, _),
do: {:error, send_error(conn, 422, "labels must be a list of names")}
def comment(conn, %{"owner" => owner, "name" => name, "number" => number} = params) do
with {:ok, user} <- Auth.require_scope(conn, "issues:write"),
{:ok, repo} <- fetch_repo(conn, user, owner, name),
{:ok, n} <- number_param(conn, number),
{:ok, issue} <- get_issue(conn, repo, n),
{:ok, body} <- require_body(conn, params) do
case Issues.add_comment(issue, user, %{"body" => body}) do
{:ok, comment} ->
comment = GitGud.Repo.preload(comment, :author)
conn |> put_status(201) |> json(%{"comment" => JSON.comment(comment, repo)})
{:error, :banned} ->
send_error(conn, 403, "you are banned from this repository")
{:error, %Ecto.Changeset{} = cs} ->
send_error(conn, 422, changeset_message(cs))
end
else
{:error, conn} -> conn
end
end
@doc """
Close or reopen. Gated on `:triage` rather than the issue's author,
plus an explicit author exception — the same rule the web UI applies,
restated here because the context doesn't enforce it.
"""
def set_state(conn, %{"owner" => owner, "name" => name, "number" => number} = params) do
with {:ok, user} <- Auth.require_scope(conn, "issues:write"),
{:ok, repo} <- fetch_repo(conn, user, owner, name),
{:ok, n} <- number_param(conn, number),
{:ok, issue} <- get_issue(conn, repo, n),
{:ok, state} <- require_state(conn, params),
:ok <- authorize_close(conn, user, repo, issue) do
case Issues.set_state(issue, state) do
{:ok, updated} -> json(conn, %{"issue" => JSON.issue(updated, repo)})
{:error, cs} -> send_error(conn, 422, changeset_message(cs))
end
else
{:error, conn} -> conn
end
end
# ── internals ────────────────────────────────────────────────────────
defp get_issue(conn, repo, number) do
{:ok, Issues.get_issue!(repo, number)}
rescue
Ecto.NoResultsError -> {:error, send_error(conn, 404, "no issue ##{number}")}
end
defp authorize_close(conn, user, repo, issue) do
if can_close?(user, repo, issue) do
:ok
else
{:error, send_error(conn, 403, "requires triage access or issue authorship")}
end
end
defp can_close?(user, repo, issue) do
# `can?/3` is nil, not false, for someone with no access at all.
GitGud.Organizations.can?(user, repo, :triage) == true or issue.author_id == user.id
end
defp require_body(conn, params) do
case String.trim(to_string(params["body"] || "")) do
"" -> {:error, send_error(conn, 422, "body can't be blank")}
body -> {:ok, body}
end
end
defp require_state(conn, params) do
case params["state"] do
s when s in ["open", "closed"] -> {:ok, s}
_ -> {:error, send_error(conn, 422, "state must be 'open' or 'closed'")}
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
6.9 KiB · text
History
b868907