neiam /gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Packages
⭐
Log in to mark this repository.
Edit issue and PR descriptions in place, recorded in history
4d15a64 · Gabriel Morell · 2026-09-10 14:24
Message
{commit_body(@commit)}
Files changed
modified
lib/git_gud/events/event.ex
+1
−1
@@ -8,7 +8,7 @@ defmodule GitGud.Events.Event do
| 8 | 8 | |
| 9 | 9 | @valid_kinds ~w( |
| 10 | 10 | opened closed reopened merged |
| 11 | − title_changed | |
| 11 | + title_changed description_changed | |
| 12 | 12 | pushed |
| 13 | 13 | labeled unlabeled |
| 14 | 14 | moderated unmoderated |
modified
lib/git_gud_web/components/event_components.ex
+5
−2
@@ -122,8 +122,8 @@ defmodule GitGudWeb.EventComponents do
| 122 | 122 | """ |
| 123 | 123 | end |
| 124 | 124 | |
| 125 | − defp diffable?(%{kind: "comment_edited", data: %{"from" => f, "to" => t}}) | |
| 126 | − when is_binary(f) and is_binary(t), | |
| 125 | + defp diffable?(%{kind: kind, data: %{"from" => f, "to" => t}}) | |
| 126 | + when kind in ["comment_edited", "description_changed"] and is_binary(f) and is_binary(t), | |
| 127 | 127 | do: true |
| 128 | 128 | |
| 129 | 129 | defp diffable?(_), do: false |
@@ -148,6 +148,7 @@ defmodule GitGudWeb.EventComponents do
| 148 | 148 | defp review_word("commented"), do: "commented" |
| 149 | 149 | defp review_word(_), do: nil |
| 150 | 150 | |
| 151 | + defp dot_class("description_changed"), do: "bg-base-300" | |
| 151 | 152 | defp dot_class("reviewed"), do: "bg-accent" |
| 152 | 153 | defp dot_class(k) when k in ["comment_added", "comment_edited"], do: "bg-info" |
| 153 | 154 | defp dot_class("comment_deleted"), do: "bg-error" |
@@ -181,6 +182,8 @@ defmodule GitGudWeb.EventComponents do
| 181 | 182 | |
| 182 | 183 | defp describe(%{kind: "title_changed"}), do: "changed the title." |
| 183 | 184 | |
| 185 | + defp describe(%{kind: "description_changed"}), do: "edited the description." | |
| 186 | + | |
| 184 | 187 | defp describe(%{kind: "pushed", data: data}) do |
| 185 | 188 | assigns = %{ |
| 186 | 189 | from: short(data["from"]), |
modified
lib/git_gud_web/live/issue_live/show.ex
+85
−11
@@ -30,6 +30,7 @@ defmodule GitGudWeb.IssueLive.Show do
| 30 | 30 | |> assign(:editing_title?, false) |
| 31 | 31 | |> assign(:editing_comment_id, nil) |
| 32 | 32 | |> assign(:title_error, nil) |
| 33 | + |> assign(:editing_body?, false) | |
| 33 | 34 | |> assign(:event_count, Events.count_for(issue)) |
| 34 | 35 | |> assign(:page_title, "##{issue.number} #{issue.title}")} |
| 35 | 36 | end |
@@ -44,10 +45,10 @@ defmodule GitGudWeb.IssueLive.Show do
| 44 | 45 | # |
| 45 | 46 | # The author can fix their own title; repo owners and org admins can |
| 46 | 47 | # fix anyone's. `can_admin?` comes from `assign_chrome/2`. |
| 47 | − defp can_edit_title?(%{current_scope: %{user: %{id: uid}}} = assigns), | |
| 48 | + defp can_edit?(%{current_scope: %{user: %{id: uid}}} = assigns), | |
| 48 | 49 | do: assigns.can_admin? || assigns.issue.author_id == uid |
| 49 | 50 | |
| 50 | − defp can_edit_title?(_assigns), do: false | |
| 51 | + defp can_edit?(_assigns), do: false | |
| 51 | 52 | |
| 52 | 53 | # Same rule as the title: the comment's author, or a repo admin. A |
| 53 | 54 | # deleted comment is nobody's to edit. |
@@ -75,7 +76,7 @@ defmodule GitGudWeb.IssueLive.Show do
| 75 | 76 | end |
| 76 | 77 | |
| 77 | 78 | def handle_event("edit_title", _params, socket) do |
| 78 | − if can_edit_title?(socket.assigns) do | |
| 79 | + if can_edit?(socket.assigns) do | |
| 79 | 80 | {:noreply, socket |> assign(:editing_title?, true) |> assign(:title_error, nil)} |
| 80 | 81 | else |
| 81 | 82 | {:noreply, socket} |
@@ -90,7 +91,7 @@ defmodule GitGudWeb.IssueLive.Show do
| 90 | 91 | issue = socket.assigns.issue |
| 91 | 92 | |
| 92 | 93 | cond do |
| 93 | − not can_edit_title?(socket.assigns) -> | |
| 94 | + not can_edit?(socket.assigns) -> | |
| 94 | 95 | {:noreply, put_flash(socket, :error, "You can't rename this issue.")} |
| 95 | 96 | |
| 96 | 97 | String.trim(title) == issue.title -> |
@@ -110,6 +111,7 @@ defmodule GitGudWeb.IssueLive.Show do
| 110 | 111 | |> assign(:editing_title?, false) |
| 111 | 112 | |> assign(:editing_comment_id, nil) |
| 112 | 113 | |> assign(:title_error, nil) |
| 114 | + |> assign(:editing_body?, false) | |
| 113 | 115 | |> assign(:event_count, Events.count_for(issue)) |
| 114 | 116 | |> reload()} |
| 115 | 117 |
@@ -263,6 +265,50 @@ defmodule GitGudWeb.IssueLive.Show do
| 263 | 265 | end |
| 264 | 266 | end |
| 265 | 267 | |
| 268 | + def handle_event("edit_body", _params, socket) do | |
| 269 | + if can_edit?(socket.assigns) do | |
| 270 | + {:noreply, assign(socket, :editing_body?, true)} | |
| 271 | + else | |
| 272 | + {:noreply, socket} | |
| 273 | + end | |
| 274 | + end | |
| 275 | + | |
| 276 | + def handle_event("cancel_edit_body", _params, socket) do | |
| 277 | + {:noreply, assign(socket, :editing_body?, false)} | |
| 278 | + end | |
| 279 | + | |
| 280 | + def handle_event("save_body", %{"body" => body}, socket) do | |
| 281 | + target = socket.assigns.issue | |
| 282 | + trimmed = String.trim(body) | |
| 283 | + | |
| 284 | + cond do | |
| 285 | + not can_edit?(socket.assigns) -> | |
| 286 | + {:noreply, put_flash(socket, :error, "You can't edit this description.")} | |
| 287 | + | |
| 288 | + trimmed == (target.body || "") -> | |
| 289 | + {:noreply, assign(socket, :editing_body?, false)} | |
| 290 | + | |
| 291 | + true -> | |
| 292 | + case Issues.update_issue(target, %{"body" => trimmed}) do | |
| 293 | + {:ok, updated} -> | |
| 294 | + :ok = | |
| 295 | + Events.record(target, "description_changed", viewer(socket), %{ | |
| 296 | + from: target.body || "", | |
| 297 | + to: updated.body || "" | |
| 298 | + }) | |
| 299 | + | |
| 300 | + {:noreply, | |
| 301 | + socket | |
| 302 | + |> assign(:editing_body?, false) | |
| 303 | + |> assign(:event_count, Events.count_for(target)) | |
| 304 | + |> reload()} | |
| 305 | + | |
| 306 | + {:error, _cs} -> | |
| 307 | + {:noreply, put_flash(socket, :error, "Could not save that description.")} | |
| 308 | + end | |
| 309 | + end | |
| 310 | + end | |
| 311 | + | |
| 266 | 312 | defp changeset_error(cs, field) do |
| 267 | 313 | case cs.errors[field] do |
| 268 | 314 | {msg, _opts} -> msg |
@@ -298,7 +344,7 @@ defmodule GitGudWeb.IssueLive.Show do
| 298 | 344 | number={@issue.number} |
| 299 | 345 | title={@issue.title} |
| 300 | 346 | editing?={@editing_title?} |
| 301 | − can_edit?={can_edit_title?(assigns)} | |
| 347 | + can_edit?={can_edit?(assigns)} | |
| 302 | 348 | error={@title_error} |
| 303 | 349 | /> |
| 304 | 350 | <p class="text-xs opacity-60"> |
@@ -314,12 +360,40 @@ defmodule GitGudWeb.IssueLive.Show do
| 314 | 360 | </header> |
| 315 | 361 | |
| 316 | 362 | <section class="border border-base-300 rounded p-4"> |
| 317 | − <GitGudWeb.ModerationComponents.moderated_body | |
| 318 | − target={@issue} | |
| 319 | − viewer={@current_scope && @current_scope.user} | |
| 320 | − repo={@repo} | |
| 321 | − theme={@editor_theme} | |
| 322 | − /> | |
| 363 | + <%= if @editing_body? do %> | |
| 364 | + <form phx-submit="save_body" class="space-y-2"> | |
| 365 | + <textarea | |
| 366 | + name="body" | |
| 367 | + rows="10" | |
| 368 | + aria-label="Description" | |
| 369 | + class="textarea textarea-bordered w-full font-mono text-sm" | |
| 370 | + >{@issue.body}</textarea> | |
| 371 | + <div class="flex justify-end gap-2"> | |
| 372 | + <button type="button" phx-click="cancel_edit_body" class="btn btn-xs btn-ghost"> | |
| 373 | + Cancel | |
| 374 | + </button> | |
| 375 | + <button type="submit" class="btn btn-xs btn-primary">Save</button> | |
| 376 | + </div> | |
| 377 | + </form> | |
| 378 | + <% else %> | |
| 379 | + <div class="flex justify-end"> | |
| 380 | + <button | |
| 381 | + :if={can_edit?(assigns)} | |
| 382 | + type="button" | |
| 383 | + phx-click="edit_body" | |
| 384 | + class="btn btn-xs btn-ghost opacity-60 hover:opacity-100" | |
| 385 | + title="Edit description" | |
| 386 | + > | |
| 387 | + <.icon name="hero-pencil-square" class="size-3" /> Edit | |
| 388 | + </button> | |
| 389 | + </div> | |
| 390 | + <GitGudWeb.ModerationComponents.moderated_body | |
| 391 | + target={@issue} | |
| 392 | + viewer={@current_scope && @current_scope.user} | |
| 393 | + repo={@repo} | |
| 394 | + theme={@editor_theme} | |
| 395 | + /> | |
| 396 | + <% end %> | |
| 323 | 397 | </section> |
| 324 | 398 | |
| 325 | 399 | <section :if={@all_labels != []}> |
modified
lib/git_gud_web/live/pr_live/show.ex
+85
−11
@@ -62,6 +62,7 @@ defmodule GitGudWeb.PrLive.Show do
| 62 | 62 | |> assign(:editing_comment_id, nil) |
| 63 | 63 | |> assign(:commenting_on, nil) |
| 64 | 64 | |> assign(:title_error, nil) |
| 65 | + |> assign(:editing_body?, false) | |
| 65 | 66 | |> assign(:event_count, Events.count_for(pr)) |
| 66 | 67 | |> assign(:page_title, "##{pr.number} #{pr.title}")} |
| 67 | 68 | end |
@@ -70,10 +71,10 @@ defmodule GitGudWeb.PrLive.Show do
| 70 | 71 | |
| 71 | 72 | # Takes assigns rather than the socket so `render/1` and the event |
| 72 | 73 | # handlers can share it. Author or repo admin, same rule as issues. |
| 73 | − defp can_edit_title?(%{current_scope: %{user: %{id: uid}}} = assigns), | |
| 74 | + defp can_edit?(%{current_scope: %{user: %{id: uid}}} = assigns), | |
| 74 | 75 | do: assigns.can_admin? || assigns.pr.author_id == uid |
| 75 | 76 | |
| 76 | − defp can_edit_title?(_assigns), do: false | |
| 77 | + defp can_edit?(_assigns), do: false | |
| 77 | 78 | |
| 78 | 79 | # Closing and reopening are the author's or a maintainer's to do. |
| 79 | 80 | # `can_admin?` already covers repo owner and org admin. |
@@ -211,7 +212,7 @@ defmodule GitGudWeb.PrLive.Show do
| 211 | 212 | end |
| 212 | 213 | |
| 213 | 214 | def handle_event("edit_title", _params, socket) do |
| 214 | − if can_edit_title?(socket.assigns) do | |
| 215 | + if can_edit?(socket.assigns) do | |
| 215 | 216 | {:noreply, socket |> assign(:editing_title?, true) |> assign(:title_error, nil)} |
| 216 | 217 | else |
| 217 | 218 | {:noreply, socket} |
@@ -226,7 +227,7 @@ defmodule GitGudWeb.PrLive.Show do
| 226 | 227 | pr = socket.assigns.pr |
| 227 | 228 | |
| 228 | 229 | cond do |
| 229 | − not can_edit_title?(socket.assigns) -> | |
| 230 | + not can_edit?(socket.assigns) -> | |
| 230 | 231 | {:noreply, put_flash(socket, :error, "You can't rename this pull request.")} |
| 231 | 232 | |
| 232 | 233 | String.trim(title) == pr.title -> |
@@ -247,6 +248,7 @@ defmodule GitGudWeb.PrLive.Show do
| 247 | 248 | |> assign(:editing_comment_id, nil) |
| 248 | 249 | |> assign(:commenting_on, nil) |
| 249 | 250 | |> assign(:title_error, nil) |
| 251 | + |> assign(:editing_body?, false) | |
| 250 | 252 | |> assign(:event_count, Events.count_for(pr)) |
| 251 | 253 | |> reload()} |
| 252 | 254 |
@@ -477,6 +479,50 @@ defmodule GitGudWeb.PrLive.Show do
| 477 | 479 | end |
| 478 | 480 | end |
| 479 | 481 | |
| 482 | + def handle_event("edit_body", _params, socket) do | |
| 483 | + if can_edit?(socket.assigns) do | |
| 484 | + {:noreply, assign(socket, :editing_body?, true)} | |
| 485 | + else | |
| 486 | + {:noreply, socket} | |
| 487 | + end | |
| 488 | + end | |
| 489 | + | |
| 490 | + def handle_event("cancel_edit_body", _params, socket) do | |
| 491 | + {:noreply, assign(socket, :editing_body?, false)} | |
| 492 | + end | |
| 493 | + | |
| 494 | + def handle_event("save_body", %{"body" => body}, socket) do | |
| 495 | + target = socket.assigns.pr | |
| 496 | + trimmed = String.trim(body) | |
| 497 | + | |
| 498 | + cond do | |
| 499 | + not can_edit?(socket.assigns) -> | |
| 500 | + {:noreply, put_flash(socket, :error, "You can't edit this description.")} | |
| 501 | + | |
| 502 | + trimmed == (target.body || "") -> | |
| 503 | + {:noreply, assign(socket, :editing_body?, false)} | |
| 504 | + | |
| 505 | + true -> | |
| 506 | + case PullRequests.update_pull_request(target, %{"body" => trimmed}) do | |
| 507 | + {:ok, updated} -> | |
| 508 | + :ok = | |
| 509 | + Events.record(target, "description_changed", viewer(socket), %{ | |
| 510 | + from: target.body || "", | |
| 511 | + to: updated.body || "" | |
| 512 | + }) | |
| 513 | + | |
| 514 | + {:noreply, | |
| 515 | + socket | |
| 516 | + |> assign(:editing_body?, false) | |
| 517 | + |> assign(:event_count, Events.count_for(target)) | |
| 518 | + |> reload()} | |
| 519 | + | |
| 520 | + {:error, _cs} -> | |
| 521 | + {:noreply, put_flash(socket, :error, "Could not save that description.")} | |
| 522 | + end | |
| 523 | + end | |
| 524 | + end | |
| 525 | + | |
| 480 | 526 | defp changeset_error(cs, field) do |
| 481 | 527 | case cs.errors[field] do |
| 482 | 528 | {msg, _opts} -> msg |
@@ -515,7 +561,7 @@ defmodule GitGudWeb.PrLive.Show do
| 515 | 561 | number={@pr.number} |
| 516 | 562 | title={@pr.title} |
| 517 | 563 | editing?={@editing_title?} |
| 518 | − can_edit?={can_edit_title?(assigns)} | |
| 564 | + can_edit?={can_edit?(assigns)} | |
| 519 | 565 | error={@title_error} |
| 520 | 566 | /> |
| 521 | 567 | </div> |
@@ -566,12 +612,40 @@ defmodule GitGudWeb.PrLive.Show do
| 566 | 612 | </header> |
| 567 | 613 | |
| 568 | 614 | <section class="border border-base-300 rounded p-4"> |
| 569 | − <GitGudWeb.ModerationComponents.moderated_body | |
| 570 | − target={@pr} | |
| 571 | − viewer={@current_scope && @current_scope.user} | |
| 572 | − repo={@repo} | |
| 573 | − theme={@editor_theme} | |
| 574 | − /> | |
| 615 | + <%= if @editing_body? do %> | |
| 616 | + <form phx-submit="save_body" class="space-y-2"> | |
| 617 | + <textarea | |
| 618 | + name="body" | |
| 619 | + rows="10" | |
| 620 | + aria-label="Description" | |
| 621 | + class="textarea textarea-bordered w-full font-mono text-sm" | |
| 622 | + >{@pr.body}</textarea> | |
| 623 | + <div class="flex justify-end gap-2"> | |
| 624 | + <button type="button" phx-click="cancel_edit_body" class="btn btn-xs btn-ghost"> | |
| 625 | + Cancel | |
| 626 | + </button> | |
| 627 | + <button type="submit" class="btn btn-xs btn-primary">Save</button> | |
| 628 | + </div> | |
| 629 | + </form> | |
| 630 | + <% else %> | |
| 631 | + <div class="flex justify-end"> | |
| 632 | + <button | |
| 633 | + :if={can_edit?(assigns)} | |
| 634 | + type="button" | |
| 635 | + phx-click="edit_body" | |
| 636 | + class="btn btn-xs btn-ghost opacity-60 hover:opacity-100" | |
| 637 | + title="Edit description" | |
| 638 | + > | |
| 639 | + <.icon name="hero-pencil-square" class="size-3" /> Edit | |
| 640 | + </button> | |
| 641 | + </div> | |
| 642 | + <GitGudWeb.ModerationComponents.moderated_body | |
| 643 | + target={@pr} | |
| 644 | + viewer={@current_scope && @current_scope.user} | |
| 645 | + repo={@repo} | |
| 646 | + theme={@editor_theme} | |
| 647 | + /> | |
| 648 | + <% end %> | |
| 575 | 649 | </section> |
| 576 | 650 | |
| 577 | 651 | <section class="flex gap-4 text-sm"> |
added
test/git_gud_web/live/description_edit_test.exs
+154
−0
@@ -0,0 +1,154 @@
| 1 | +defmodule GitGudWeb.DescriptionEditTest do | |
| 2 | + @moduledoc """ | |
| 3 | + Editing an issue or PR description in place, the same way titles are | |
| 4 | + edited, with the change recorded as a collapsed diff in history. | |
| 5 | + """ | |
| 6 | + | |
| 7 | + use GitGudWeb.ConnCase, async: false | |
| 8 | + | |
| 9 | + import Phoenix.LiveViewTest | |
| 10 | + import GitGud.AccountsFixtures | |
| 11 | + import GitGud.ForgeFixtures | |
| 12 | + | |
| 13 | + alias GitGud.Events | |
| 14 | + alias GitGud.Issues | |
| 15 | + alias GitGud.PullRequests | |
| 16 | + alias GitGud.Repositories | |
| 17 | + | |
| 18 | + defp handle(repo), | |
| 19 | + do: Repositories.Storage.repo_handle(GitGud.Repo.preload(repo, [:owner, :organization])) | |
| 20 | + | |
| 21 | + defp issue_path(repo, issue), | |
| 22 | + do: ~p"/r/#{handle(repo)}/#{repo.name}/issues/#{issue.number}" | |
| 23 | + | |
| 24 | + defp pr_path(repo, pr), | |
| 25 | + do: ~p"/r/#{handle(repo)}/#{repo.name}/pulls/#{pr.number}" | |
| 26 | + | |
| 27 | + describe "issues" do | |
| 28 | + test "the author can edit the description in place", %{conn: conn} do | |
| 29 | + {user, repo} = repository_fixture() | |
| 30 | + issue = issue_fixture(repo, user, %{"body" => "Original description"}) | |
| 31 | + | |
| 32 | + {:ok, lv, _html} = live(log_in_user(conn, user), issue_path(repo, issue)) | |
| 33 | + | |
| 34 | + _ = lv |> element("button[phx-click=edit_body]") |> render_click() | |
| 35 | + html = lv |> form("form[phx-submit=save_body]", %{body: "Rewritten"}) |> render_submit() | |
| 36 | + | |
| 37 | + assert html =~ "Rewritten" | |
| 38 | + assert Issues.get_issue!(repo, issue.number).body == "Rewritten" | |
| 39 | + end | |
| 40 | + | |
| 41 | + test "an unrelated user gets no edit affordance", %{conn: conn} do | |
| 42 | + {_owner, repo} = repository_fixture(%{visibility: "public"}) | |
| 43 | + author = user_fixture() | |
| 44 | + issue = issue_fixture(repo, author, %{"body" => "Theirs"}) | |
| 45 | + | |
| 46 | + outsider = user_fixture() | |
| 47 | + {:ok, lv, _html} = live(log_in_user(conn, outsider), issue_path(repo, issue)) | |
| 48 | + | |
| 49 | + refute has_element?(lv, "button[phx-click=edit_body]") | |
| 50 | + end | |
| 51 | + | |
| 52 | + test "an unrelated user can't edit by sending the event", %{conn: conn} do | |
| 53 | + {_owner, repo} = repository_fixture(%{visibility: "public"}) | |
| 54 | + author = user_fixture() | |
| 55 | + issue = issue_fixture(repo, author, %{"body" => "Untouchable"}) | |
| 56 | + | |
| 57 | + outsider = user_fixture() | |
| 58 | + {:ok, lv, _html} = live(log_in_user(conn, outsider), issue_path(repo, issue)) | |
| 59 | + | |
| 60 | + render_hook(lv, "save_body", %{"body" => "Hijacked"}) | |
| 61 | + | |
| 62 | + assert Issues.get_issue!(repo, issue.number).body == "Untouchable" | |
| 63 | + end | |
| 64 | + | |
| 65 | + test "a repo admin can edit someone else's description", %{conn: conn} do | |
| 66 | + {owner, repo} = repository_fixture(%{visibility: "public"}) | |
| 67 | + author = user_fixture() | |
| 68 | + issue = issue_fixture(repo, author, %{"body" => "Needs work"}) | |
| 69 | + | |
| 70 | + {:ok, lv, _html} = live(log_in_user(conn, owner), issue_path(repo, issue)) | |
| 71 | + | |
| 72 | + _ = lv |> element("button[phx-click=edit_body]") |> render_click() | |
| 73 | + html = lv |> form("form[phx-submit=save_body]", %{body: "Tidied"}) |> render_submit() | |
| 74 | + | |
| 75 | + assert html =~ "Tidied" | |
| 76 | + end | |
| 77 | + | |
| 78 | + test "an unchanged description records nothing", %{conn: conn} do | |
| 79 | + {user, repo} = repository_fixture() | |
| 80 | + issue = issue_fixture(repo, user, %{"body" => "Same"}) | |
| 81 | + | |
| 82 | + {:ok, lv, _html} = live(log_in_user(conn, user), issue_path(repo, issue)) | |
| 83 | + _ = lv |> element("button[phx-click=edit_body]") |> render_click() | |
| 84 | + _ = lv |> form("form[phx-submit=save_body]", %{body: "Same"}) |> render_submit() | |
| 85 | + | |
| 86 | + assert Events.list_for(issue) == [] | |
| 87 | + end | |
| 88 | + | |
| 89 | + test "the change shows in history as a collapsed diff", %{conn: conn} do | |
| 90 | + {user, repo} = repository_fixture() | |
| 91 | + issue = issue_fixture(repo, user, %{"body" => "line one\nline two"}) | |
| 92 | + | |
| 93 | + {:ok, lv, _html} = live(log_in_user(conn, user), issue_path(repo, issue)) | |
| 94 | + _ = lv |> element("button[phx-click=edit_body]") |> render_click() | |
| 95 | + | |
| 96 | + _ = | |
| 97 | + lv | |
| 98 | + |> form("form[phx-submit=save_body]", %{body: "line one\nline three"}) | |
| 99 | + |> render_submit() | |
| 100 | + | |
| 101 | + {:ok, _hist, html} = live(log_in_user(conn, user), issue_path(repo, issue) <> "/history") | |
| 102 | + | |
| 103 | + assert html =~ "edited the description." | |
| 104 | + assert html =~ "Show what changed" | |
| 105 | + refute html =~ "<details open" | |
| 106 | + assert html =~ "line two" | |
| 107 | + assert html =~ "line three" | |
| 108 | + end | |
| 109 | + end | |
| 110 | + | |
| 111 | + describe "pull requests" do | |
| 112 | + test "the author can edit the description in place", %{conn: conn} do | |
| 113 | + {user, repo} = repository_fixture() | |
| 114 | + pr = pr_with_branches(repo, user, %{"body" => "PR description"}) | |
| 115 | + | |
| 116 | + {:ok, lv, _html} = live(log_in_user(conn, user), pr_path(repo, pr)) | |
| 117 | + | |
| 118 | + _ = lv |> element("button[phx-click=edit_body]") |> render_click() | |
| 119 | + | |
| 120 | + html = | |
| 121 | + lv |> form("form[phx-submit=save_body]", %{body: "Better description"}) |> render_submit() | |
| 122 | + | |
| 123 | + assert html =~ "Better description" | |
| 124 | + assert PullRequests.get_pull_request!(repo, pr.number).body == "Better description" | |
| 125 | + end | |
| 126 | + | |
| 127 | + test "the edit is recorded", %{conn: conn} do | |
| 128 | + {user, repo} = repository_fixture() | |
| 129 | + pr = pr_with_branches(repo, user, %{"body" => "before"}) | |
| 130 | + | |
| 131 | + {:ok, lv, _html} = live(log_in_user(conn, user), pr_path(repo, pr)) | |
| 132 | + _ = lv |> element("button[phx-click=edit_body]") |> render_click() | |
| 133 | + _ = lv |> form("form[phx-submit=save_body]", %{body: "after"}) |> render_submit() | |
| 134 | + | |
| 135 | + assert [%{kind: "description_changed", data: data}] = Events.list_for(pr) | |
| 136 | + assert data["from"] == "before" | |
| 137 | + assert data["to"] == "after" | |
| 138 | + end | |
| 139 | + | |
| 140 | + test "an unrelated user can't edit it", %{conn: conn} do | |
| 141 | + {_owner, repo} = repository_fixture(%{visibility: "public"}) | |
| 142 | + author = user_fixture() | |
| 143 | + pr = pr_with_branches(repo, author, %{"body" => "Hands off"}) | |
| 144 | + | |
| 145 | + outsider = user_fixture() | |
| 146 | + {:ok, lv, _html} = live(log_in_user(conn, outsider), pr_path(repo, pr)) | |
| 147 | + | |
| 148 | + refute has_element?(lv, "button[phx-click=edit_body]") | |
| 149 | + render_hook(lv, "save_body", %{"body" => "Hijacked"}) | |
| 150 | + | |
| 151 | + assert PullRequests.get_pull_request!(repo, pr.number).body == "Hands off" | |
| 152 | + end | |
| 153 | + end | |
| 154 | +end |
modified
test/git_gud_web/live/issue_live/title_history_test.exs
+7
−7
@@ -27,7 +27,7 @@ defmodule GitGudWeb.IssueLive.TitleHistoryTest do
| 27 | 27 | |
| 28 | 28 | {:ok, lv, _html} = live(log_in_user(conn, user), issue_path(repo, issue)) |
| 29 | 29 | |
| 30 | − html = lv |> element("button", "Edit") |> render_click() | |
| 30 | + html = lv |> element("button[phx-click=edit_title]") |> render_click() | |
| 31 | 31 | assert html =~ ~s(name="title") |
| 32 | 32 | |
| 33 | 33 | html = |
@@ -45,7 +45,7 @@ defmodule GitGudWeb.IssueLive.TitleHistoryTest do
| 45 | 45 | |
| 46 | 46 | {:ok, lv, _html} = live(log_in_user(conn, owner), issue_path(repo, issue)) |
| 47 | 47 | |
| 48 | − _ = lv |> element("button", "Edit") |> render_click() | |
| 48 | + _ = lv |> element("button[phx-click=edit_title]") |> render_click() | |
| 49 | 49 | |
| 50 | 50 | html = |
| 51 | 51 | lv |> form("form[phx-submit=save_title]", %{title: "Fixed by admin"}) |> render_submit() |
@@ -92,7 +92,7 @@ defmodule GitGudWeb.IssueLive.TitleHistoryTest do
| 92 | 92 | issue = issue_fixture(repo, user, %{"title" => "Keep me"}) |
| 93 | 93 | |
| 94 | 94 | {:ok, lv, _html} = live(log_in_user(conn, user), issue_path(repo, issue)) |
| 95 | − _ = lv |> element("button", "Edit") |> render_click() | |
| 95 | + _ = lv |> element("button[phx-click=edit_title]") |> render_click() | |
| 96 | 96 | |
| 97 | 97 | render_hook(lv, "save_title", %{"title" => " "}) |
| 98 | 98 |
@@ -104,7 +104,7 @@ defmodule GitGudWeb.IssueLive.TitleHistoryTest do
| 104 | 104 | issue = issue_fixture(repo, user, %{"title" => "Unchanged"}) |
| 105 | 105 | |
| 106 | 106 | {:ok, lv, _html} = live(log_in_user(conn, user), issue_path(repo, issue)) |
| 107 | − _ = lv |> element("button", "Edit") |> render_click() | |
| 107 | + _ = lv |> element("button[phx-click=edit_title]") |> render_click() | |
| 108 | 108 | _ = lv |> form("form[phx-submit=save_title]", %{title: "Unchanged"}) |> render_submit() |
| 109 | 109 | |
| 110 | 110 | assert Events.list_for(issue) == [] |
@@ -115,8 +115,8 @@ defmodule GitGudWeb.IssueLive.TitleHistoryTest do
| 115 | 115 | issue = issue_fixture(repo, user, %{"title" => "Still here"}) |
| 116 | 116 | |
| 117 | 117 | {:ok, lv, _html} = live(log_in_user(conn, user), issue_path(repo, issue)) |
| 118 | − _ = lv |> element("button", "Edit") |> render_click() | |
| 119 | − html = lv |> element("button", "Cancel") |> render_click() | |
| 118 | + _ = lv |> element("button[phx-click=edit_title]") |> render_click() | |
| 119 | + html = lv |> element("button[phx-click=cancel_edit_title]") |> render_click() | |
| 120 | 120 | |
| 121 | 121 | assert html =~ "Still here" |
| 122 | 122 | assert Events.list_for(issue) == [] |
@@ -129,7 +129,7 @@ defmodule GitGudWeb.IssueLive.TitleHistoryTest do
| 129 | 129 | issue = issue_fixture(repo, user, %{"title" => "Before"}) |
| 130 | 130 | |
| 131 | 131 | {:ok, lv, _html} = live(log_in_user(conn, user), issue_path(repo, issue)) |
| 132 | − _ = lv |> element("button", "Edit") |> render_click() | |
| 132 | + _ = lv |> element("button[phx-click=edit_title]") |> render_click() | |
| 133 | 133 | _ = lv |> form("form[phx-submit=save_title]", %{title: "After"}) |> render_submit() |
| 134 | 134 | |
| 135 | 135 | {:ok, _hist, html} = live(log_in_user(conn, user), issue_path(repo, issue) <> "/history") |
modified
test/git_gud_web/live/pr_live/title_history_test.exs
+2
−2
@@ -31,7 +31,7 @@ defmodule GitGudWeb.PrLive.TitleHistoryTest do
| 31 | 31 | |
| 32 | 32 | {:ok, lv, _html} = live(log_in_user(conn, user), pr_path(repo, pr)) |
| 33 | 33 | |
| 34 | − _ = lv |> element("button", "Edit") |> render_click() | |
| 34 | + _ = lv |> element("button[phx-click=edit_title]") |> render_click() | |
| 35 | 35 | html = lv |> form("form[phx-submit=save_title]", %{title: "Renamed PR"}) |> render_submit() |
| 36 | 36 | |
| 37 | 37 | assert html =~ "Renamed PR" |
@@ -57,7 +57,7 @@ defmodule GitGudWeb.PrLive.TitleHistoryTest do
| 57 | 57 | pr = open_pr!(repo, user, "PR before") |
| 58 | 58 | |
| 59 | 59 | {:ok, lv, _html} = live(log_in_user(conn, user), pr_path(repo, pr)) |
| 60 | − _ = lv |> element("button", "Edit") |> render_click() | |
| 60 | + _ = lv |> element("button[phx-click=edit_title]") |> render_click() | |
| 61 | 61 | _ = lv |> form("form[phx-submit=save_title]", %{title: "PR after"}) |> render_submit() |
| 62 | 62 | |
| 63 | 63 | {:ok, _hist, html} = live(log_in_user(conn, user), pr_path(repo, pr) <> "/history") |
Parents: d1b9a04