defmodule GitGudWeb.RepoLive.MergeTest do
@moduledoc """
The in-browser merge editor. Its two callers — a conflicting pull
request and a branch that diverged from upstream — arrive at the same
route with the same two commits, so the tests drive it the same way.
"""
use GitGudWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import GitGud.ForgeFixtures
import GitGud.AccountsFixtures
alias GitGud.Git
alias GitGud.Repositories.Storage
@moduletag :git_required
setup :register_and_log_in_user
setup %{user: user} do
if System.find_executable("git") == nil do
{:skip, "git not on PATH"}
else
{:ok, repo} =
GitGud.Repositories.create_repository(user, %{"name" => "conflicted#{uniq()}"})
repo = GitGud.Repo.preload(repo, [:owner, :organization])
base = commit(repo, %{"f.txt" => "line1\nline2\nline3\n", "keep.txt" => "keep\n"}, [])
ours = commit(repo, %{"f.txt" => "line1\nOURS\nline3\n", "keep.txt" => "keep\n"}, [base])
theirs =
commit(repo, %{"f.txt" => "line1\nTHEIRS\nline3\n", "keep.txt" => "keep\n"}, [base])
:ok = Git.update_ref(repo.disk_path, "refs/heads/main", ours, nil)
:ok = Git.update_ref(repo.disk_path, "refs/heads/feature", theirs, nil)
path = "/r/#{Storage.repo_handle(repo)}/#{repo.name}/merge?into=main&from=feature"
%{repo: repo, base: base, ours: ours, theirs: theirs, path: path}
end
end
test "lists the conflicts and opens git's marked-up merge", ctx do
{:ok, lv, _html} = live(ctx.conn, ctx.path)
html = render_async(lv)
assert html =~ "f.txt"
# keep.txt merged cleanly, so it isn't the operator's problem.
refute html =~ "keep.txt"
assert html =~ "<<<<<<<"
assert html =~ "OURS"
assert html =~ "THEIRS"
# Nothing is resolved yet, so merging is not offered.
assert html =~ "1 still to resolve"
end
test "taking one side merges it and moves the branch", ctx do
{:ok, lv, _html} = live(ctx.conn, ctx.path)
render_async(lv)
lv
|> element("button[phx-click=take][phx-value-side=theirs]")
|> render_click()
assert render(lv) =~ "(theirs)"
lv |> element("button[phx-click=merge]") |> render_click()
assert_redirect(lv)
assert {:ok, head} = Git.resolve(ctx.repo.disk_path, "refs/heads/main")
assert {:ok, commit} = Git.commit(ctx.repo.disk_path, head)
assert commit.parent_shas == [ctx.ours, ctx.theirs]
{:ok, entry} = Git.tree_entry_at(ctx.repo.disk_path, commit.tree_sha, "f.txt")
assert {:ok, "line1\nTHEIRS\nline3\n"} = Git.blob_bytes(ctx.repo.disk_path, entry.sha)
end
test "an edited resolution is what gets committed", ctx do
{:ok, lv, _html} = live(ctx.conn, ctx.path)
render_async(lv)
resolved = "line1\nboth of them\nline3\n"
lv
|> form("form[phx-change=edit]", %{"path" => "f.txt", "text" => resolved})
|> render_change()
lv
|> form("form[phx-change=set_message]", %{"message" => "Settle f.txt"})
|> render_change()
lv |> element("button[phx-click=merge]") |> render_click()
assert_redirect(lv)
{:ok, head} = Git.resolve(ctx.repo.disk_path, "refs/heads/main")
{:ok, commit} = Git.commit(ctx.repo.disk_path, head)
assert commit.message =~ "Settle f.txt"
{:ok, entry} = Git.tree_entry_at(ctx.repo.disk_path, commit.tree_sha, "f.txt")
assert {:ok, ^resolved} = Git.blob_bytes(ctx.repo.disk_path, entry.sha)
end
test "leaving the markers in is refused rather than committed", ctx do
{:ok, lv, _html} = live(ctx.conn, ctx.path)
html = render_async(lv)
# Pretend the operator edited nothing and hit merge: the seeded text
# still carries markers, so it has to be typed in explicitly.
assert html =~ "<<<<<<<"
marked = "<<<<<<< ours\nOURS\n=======\nTHEIRS\n>>>>>>> theirs\n"
lv
|> form("form[phx-change=edit]", %{"path" => "f.txt", "text" => marked})
|> render_change()
lv |> element("button[phx-click=merge]") |> render_click()
# The merge runs off the LiveView process, so the refusal arrives
# after the click returns.
html = render_async(lv)
assert html =~ "Conflict markers left in f.txt"
# And the branch is where it was.
assert {:ok, sha} = Git.resolve(ctx.repo.disk_path, "refs/heads/main")
assert sha == ctx.ours
end
test "a branch that moved underneath is reported, not overwritten", ctx do
{:ok, lv, _html} = live(ctx.conn, ctx.path)
render_async(lv)
lv |> element("button[phx-click=take][phx-value-side=ours]") |> render_click()
# Someone pushes to main while the page is open.
moved = commit(ctx.repo, %{"f.txt" => "line1\nSOMEONE ELSE\nline3\n"}, [ctx.ours])
:ok = Git.update_ref(ctx.repo.disk_path, "refs/heads/main", moved, ctx.ours)
lv |> element("button[phx-click=merge]") |> render_click()
html = render_async(lv)
assert html =~ "moved while you were resolving"
assert {:ok, ^moved} = Git.resolve(ctx.repo.disk_path, "refs/heads/main")
end
test "a clean merge says so instead of showing an editor", ctx do
clean =
commit(ctx.repo, %{"f.txt" => "line1\nOURS\nline3\n", "new.txt" => "new\n"}, [ctx.ours])
:ok = Git.update_ref(ctx.repo.disk_path, "refs/heads/clean", clean, nil)
{:ok, lv, _html} =
live(
ctx.conn,
"/r/#{Storage.repo_handle(ctx.repo)}/#{ctx.repo.name}/merge?into=main&from=clean"
)
html = render_async(lv)
assert html =~ "merge cleanly"
refute html =~ "still to resolve"
end
test "a stranger can't open the merge page of a private repository", ctx do
other = user_fixture()
conn = log_in_user(build_conn(), other)
# Repositories default to private, so this one is. It used to
# redirect with "you don't have permission to merge", which is a
# sentence that confirms the repository exists — `RepoAuth` now
# refuses the mount first, identically to a repository that isn't
# there.
assert_raise Ecto.NoResultsError, fn -> live(conn, ctx.path) end
end
test "a stranger who can read but not write is told why", %{repo: repo, path: path} do
{:ok, _} = GitGud.Repositories.update_repository(repo, %{"visibility" => "public"})
conn = log_in_user(build_conn(), user_fixture())
# Readable, so the read gate lets them through and the merge page's
# own write check is what answers — which is the behaviour the
# previous version of this test was actually about, on a repository
# where saying so leaks nothing.
assert {:error, {:redirect, %{to: to, flash: flash}}} = live(conn, path)
assert to == "/r/#{Storage.repo_handle(repo)}/#{repo.name}"
assert flash["error"] =~ "permission"
end
# ── helpers ──────────────────────────────────────────────────────────
defp uniq, do: System.unique_integer([:positive])
defp commit(repo, files, parents) do
path = repo.disk_path
entries =
Enum.map_join(files, "", fn {name, contents} ->
"100644 blob #{Git.to_hex(blob(path, contents))}\t#{name}\n"
end)
tmp = Path.join(System.tmp_dir!(), "tree-#{uniq()}")
File.write!(tmp, entries)
{out, 0} = System.shell("git -C #{path} mktree < #{tmp}", stderr_to_stdout: true)
File.rm(tmp)
{:ok, tree} = Git.from_hex(String.trim(out))
{:ok, sha} =
Git.commit_tree(path, tree, parents, "c#{uniq()}",
author: {"t", "t@t", DateTime.utc_now(:second)}
)
sha
end
defp blob(path, contents) do
tmp = Path.join(System.tmp_dir!(), "blob-#{uniq()}")
File.write!(tmp, contents)
{hex, 0} = System.cmd("git", ["-C", path, "hash-object", "-w", tmp])
File.rm(tmp)
{:ok, sha} = Git.from_hex(String.trim(hex))
sha
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
7.7 KiB · text
History
b868907