defmodule GitGudWeb.PrLive.Files do
@moduledoc """
The code review screen: a pull request's diff, full width, with the
line comments anchored in it.
Split out of the conversation page so a long diff no longer pushes
the discussion off the bottom, and so reviewing gets the whole
viewport.
"""
use GitGudWeb, :live_view
alias GitGud.Events
alias GitGud.PullRequests
alias GitGud.PullRequests.PrComment
alias GitGud.Repositories
alias GitGud.Repositories.Storage
@impl true
def mount(%{"owner" => owner, "name" => name, "number" => num}, _session, socket) do
repo =
Repositories.get_repository_by_path!(owner, name)
|> GitGud.Repo.preload([:owner, :organization])
pr = PullRequests.get_pull_request!(repo, String.to_integer(num))
{:ok, stats} = Repositories.get_diff_stats(repo, pr.base_sha, pr.head_sha)
diff_files = load_diff(repo, pr)
{:ok,
socket
|> assign(:repo, repo)
|> assign(:handle, Storage.repo_handle(repo))
|> GitGudWeb.RepoLive.Header.assign_chrome(repo)
|> assign(:pr, pr)
|> assign(:stats, stats)
|> assign(:diff_files, diff_files)
|> assign(:loaded_diff_idx, GitGudWeb.DiffComponents.default_loaded(diff_files))
|> assign(:commenting_on, nil)
|> assign(:can_apply?, can_apply?(socket, pr))
|> assign(:page_title, "Files · ##{pr.number} #{pr.title}")}
end
defp load_diff(repo, pr) do
case Repositories.get_diff(repo, pr.base_sha, pr.head_sha) do
{:ok, files} -> files
_ -> []
end
end
defp reload(socket) do
pr = PullRequests.get_pull_request!(socket.assigns.repo, socket.assigns.pr.number)
assign(socket, :pr, pr)
end
defp viewer(socket), do: socket.assigns.current_scope && socket.assigns.current_scope.user
# Applying commits to the PR's source branch, so it needs write
# access there — which for a fork is a different repo than this one.
defp can_apply?(socket, pr) do
case socket.assigns.current_scope && socket.assigns.current_scope.user do
nil ->
false
user ->
source = Repositories.get_repository!(pr.source_repository_id)
GitGud.Organizations.can?(user, source, :write)
end
end
defp line_comment?(%{file_path: p, line: l}) when is_binary(p) and is_integer(l), do: true
defp line_comment?(_), do: false
defp line_comments(pr) do
pr.comments
|> Enum.filter(&line_comment?/1)
|> Enum.reject(&PrComment.deleted?/1)
|> Enum.group_by(&{&1.file_path, &1.line})
end
defp conversation_count(pr), do: Enum.count(pr.comments, &(not line_comment?(&1)))
@impl true
def handle_event("expand_diff_file", params, socket),
do: {:noreply, GitGudWeb.DiffComponents.expand(socket, params)}
def handle_event("comment_on_line", %{"path" => path, "line" => line}, socket) do
if viewer(socket) do
{:noreply, assign(socket, :commenting_on, {path, String.to_integer(line)})}
else
{:noreply, put_flash(socket, :error, "Sign in to comment.")}
end
end
def handle_event("apply_suggestion", %{"id" => id}, socket) do
pr = socket.assigns.pr
with user when not is_nil(user) <- viewer(socket),
comment when not is_nil(comment) <-
Enum.find(pr.comments, &(&1.id == String.to_integer(id))) do
case PullRequests.apply_suggestion(comment, user) do
{:ok, _sha} ->
:ok =
Events.record(pr, "suggestion_applied", user, %{
comment_id: comment.id,
file_path: comment.file_path,
line: comment.line
})
{:noreply,
socket
|> put_flash(:info, "Suggestion applied.")
|> push_navigate(
to:
~p"/r/#{socket.assigns.handle}/#{socket.assigns.repo.name}/pulls/#{pr.number}/files"
)}
{:error, reason} ->
{:noreply, put_flash(socket, :error, apply_error(reason))}
end
else
_ -> {:noreply, socket}
end
end
def handle_event("cancel_line_comment", _params, socket) do
{:noreply, assign(socket, :commenting_on, nil)}
end
def handle_event("save_line_comment", %{"path" => path, "line" => line, "body" => body}, socket) do
pr = socket.assigns.pr
case viewer(socket) do
nil ->
{:noreply, put_flash(socket, :error, "Sign in to comment.")}
user ->
attrs = %{
"body" => String.trim(body),
"file_path" => path,
"line" => String.to_integer(line),
# Pin the head this was written against, so a later push
# doesn't silently re-anchor it to different code.
"commit_sha" => pr.head_sha
}
case PullRequests.add_comment(pr, user, attrs) do
{:ok, comment} ->
:ok =
Events.record(pr, "comment_added", user, %{
comment_id: comment.id,
file_path: path,
line: attrs["line"]
})
{:noreply, socket |> assign(:commenting_on, nil) |> reload()}
{:error, _cs} ->
{:noreply, put_flash(socket, :error, "Could not save that comment.")}
end
end
end
defp apply_error(:forbidden), do: "You don't have write access to the source branch."
defp apply_error(:stale), do: "The branch moved since this suggestion was written."
defp apply_error(:out_of_range), do: "That line no longer exists in the file."
defp apply_error(:closed), do: "This pull request isn't open."
defp apply_error(:no_such_file), do: "That file is no longer in the branch."
defp apply_error(:not_a_suggestion), do: "That comment isn't a suggestion."
defp apply_error(other), do: "Could not apply the suggestion: #{inspect(other)}"
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="space-y-4">
<GitGudWeb.RepoLive.Header.header
repo={@repo}
handle={@handle}
current_scope={@current_scope}
has_packages?={@has_packages?}
can_admin?={@can_admin?}
latest_run={@latest_run}
open_pulls={@open_pulls}
/>
<header>
<h1 class="text-2xl font-semibold">
#{@pr.number} — {@pr.title}
</h1>
<p class="text-xs opacity-60 font-mono mt-1">
{@pr.source_ref} → {@pr.target_ref}
</p>
</header>
<GitGudWeb.PrNav.nav
handle={@handle}
repo={@repo}
pr={@pr}
active={:files}
files_count={length(@diff_files)}
comments_count={conversation_count(@pr)}
events_count={Events.count_for(@pr)}
/>
<section class="flex gap-4 text-sm">
<span><strong>{@stats.files_changed}</strong> files</span>
<span class="text-success">+{@stats.insertions}</span>
<span class="text-error">-{@stats.deletions}</span>
</section>
<p :if={@diff_files == []} class="py-12 text-center text-sm opacity-60">
No changes to show.
</p>
<GitGudWeb.DiffComponents.diff
comments={line_comments(@pr)}
can_comment?={@current_scope != nil and @current_scope.user != nil}
can_apply?={@can_apply?}
commenting_on={@commenting_on}
files={@diff_files}
theme={@editor_theme}
loaded_idx={@loaded_diff_idx}
/>
</div>
</Layouts.app>
"""
end
end
neiam /gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Packages
⭐
Log in to mark this repository.
7.3 KiB · text
History
6280797