defmodule TarakanWeb.RepositoryCommitsLiveTest do use TarakanWeb.ConnCase, async: false import Phoenix.LiveViewTest alias Tarakan.Accounts alias Tarakan.HostedRepositories alias Tarakan.HostedRepositories.Storage @moduletag :tmp_dir setup %{tmp_dir: tmp_dir} do on_exit(fn -> File.rm_rf(Application.fetch_env!(:tarakan, Tarakan.HostedRepositories)[:root]) end) Tarakan.RepositoryCode.Cache.clear() account = account_fixture() scope = Accounts.scope_for_account(account) {:ok, repository} = HostedRepositories.create(scope, %{"name" => "history"}) %{repository: repository, tmp_dir: tmp_dir} end defp push_commit(repository, tmp_dir, dir_name, message) do work = Path.join(tmp_dir, dir_name) {_output, 0} = System.cmd("git", ["clone", "--quiet", Storage.dir(repository), work], stderr_to_stdout: true ) File.write!(Path.join(work, "README.md"), "# #{message}\n") env = [ {"GIT_AUTHOR_NAME", "t"}, {"GIT_AUTHOR_EMAIL", "t@example.com"}, {"GIT_COMMITTER_NAME", "t"}, {"GIT_COMMITTER_EMAIL", "t@example.com"} ] {_output, 0} = System.cmd("git", ["-C", work, "add", "."], env: env) {_output, 0} = System.cmd("git", ["-C", work, "commit", "--quiet", "-m", message], env: env) {_output, 0} = System.cmd("git", ["-C", work, "push", "--quiet", "origin", "HEAD"], env: env, stderr_to_stdout: true ) {sha, 0} = System.cmd("git", ["-C", work, "rev-parse", "HEAD"]) String.trim(sha) end defp push_merge_commit(repository, tmp_dir, dir_name, branch, message) do work = Path.join(tmp_dir, dir_name) {_output, 0} = System.cmd("git", ["clone", "--quiet", Storage.dir(repository), work], stderr_to_stdout: true ) env = [ {"GIT_AUTHOR_NAME", "t"}, {"GIT_AUTHOR_EMAIL", "t@example.com"}, {"GIT_COMMITTER_NAME", "t"}, {"GIT_COMMITTER_EMAIL", "t@example.com"} ] {main, 0} = System.cmd("git", ["-C", work, "rev-parse", "--abbrev-ref", "HEAD"]) main = String.trim(main) {_output, 0} = System.cmd("git", ["-C", work, "checkout", "--quiet", "-b", branch], env: env) File.write!(Path.join(work, "side.txt"), "side\n") {_output, 0} = System.cmd("git", ["-C", work, "add", "."], env: env) {_output, 0} = System.cmd("git", ["-C", work, "commit", "--quiet", "-m", "side change"], env: env) {_output, 0} = System.cmd("git", ["-C", work, "checkout", "--quiet", main], env: env) File.write!(Path.join(work, "main.txt"), "main\n") {_output, 0} = System.cmd("git", ["-C", work, "add", "."], env: env) {_output, 0} = System.cmd("git", ["-C", work, "commit", "--quiet", "-m", "main change"], env: env) {_output, 0} = System.cmd("git", ["-C", work, "merge", "--quiet", "--no-ff", "-m", message, branch], env: env, stderr_to_stdout: true ) {_output, 0} = System.cmd("git", ["-C", work, "push", "--quiet", "origin", "HEAD"], env: env, stderr_to_stdout: true ) {sha, 0} = System.cmd("git", ["-C", work, "rev-parse", "HEAD"]) String.trim(sha) end defp push_branch_commit(repository, tmp_dir, dir_name, branch, message) do work = Path.join(tmp_dir, dir_name) {_output, 0} = System.cmd("git", ["clone", "--quiet", Storage.dir(repository), work], stderr_to_stdout: true ) File.write!(Path.join(work, "FEATURE.md"), "# #{message}\n") env = [ {"GIT_AUTHOR_NAME", "t"}, {"GIT_AUTHOR_EMAIL", "t@example.com"}, {"GIT_COMMITTER_NAME", "t"}, {"GIT_COMMITTER_EMAIL", "t@example.com"} ] {_output, 0} = System.cmd("git", ["-C", work, "checkout", "--quiet", "-b", branch], env: env) {_output, 0} = System.cmd("git", ["-C", work, "add", "."], env: env) {_output, 0} = System.cmd("git", ["-C", work, "commit", "--quiet", "-m", message], env: env) {_output, 0} = System.cmd("git", ["-C", work, "push", "--quiet", "origin", branch], env: env, stderr_to_stdout: true ) {sha, 0} = System.cmd("git", ["-C", work, "rev-parse", "HEAD"]) String.trim(sha) end test "lists the default branch history and links each commit to its detail page", %{ conn: conn, repository: repository, tmp_dir: tmp_dir } do first_sha = push_commit(repository, tmp_dir, "work-first", "initial") tip = push_commit(repository, tmp_dir, "work-second", "second") {:ok, view, _html} = live(conn, "/#{repository.owner}/#{repository.name}/commits") render_async(view, 1_000) assert has_element?(view, "#repository-commits-tab[aria-current='page']") # The active tab is still a link: from a commit detail page it points at the # commit list, which is the obvious way back and used to be unclickable. assert has_element?(view, "a#repository-commits-tab[aria-current='page'][href]") assert has_element?(view, "#commits-list a", "second") assert has_element?(view, "#commits-list a", "initial") assert has_element?( view, "#commits-list a[href='/#{repository.owner}/#{repository.name}/commits/#{tip}']" ) assert has_element?( view, "#commits-list a[href='/#{repository.owner}/#{repository.name}/commits/#{first_sha}']" ) refute has_element?(view, "#commits-load-more") end test "the commit detail page shows the full message, patch, and code link", %{ conn: conn, repository: repository, tmp_dir: tmp_dir } do _first_sha = push_commit(repository, tmp_dir, "work-first", "initial") tip = push_commit(repository, tmp_dir, "work-second", "second") {:ok, view, _html} = live(conn, "/#{repository.owner}/#{repository.name}/commits/#{tip}") render_async(view, 1_000) assert has_element?(view, "#commit-subject", "second") assert has_element?(view, "#commit-detail-sha", String.slice(tip, 0, 7)) assert has_element?(view, "#commit-patch", "+# second") assert has_element?( view, "#commit-browse-code[href='/#{repository.owner}/#{repository.name}/code/#{tip}']" ) # The code link opens the browser pinned at this exact commit. {:ok, code_view, _html} = live(conn, "/#{repository.owner}/#{repository.name}/code/#{tip}") render_async(code_view, 1_000) assert has_element?(code_view, "#code-tree") end test "merge commits render with an empty patch state", %{ conn: conn, repository: repository, tmp_dir: tmp_dir } do _main_sha = push_commit(repository, tmp_dir, "work-first", "initial") merge_sha = push_merge_commit(repository, tmp_dir, "work-merge", "side", "merge side") {:ok, view, _html} = live(conn, "/#{repository.owner}/#{repository.name}/commits/#{merge_sha}") render_async(view, 1_000) assert has_element?(view, "#commit-subject", "merge side") assert has_element?(view, "#commit-patch-empty", "No textual changes") refute has_element?(view, "#commits-error") end test "the commit detail page rejects unreachable commits", %{ conn: conn, repository: repository, tmp_dir: tmp_dir } do _sha = push_commit(repository, tmp_dir, "work-first", "initial") unknown = String.duplicate("d", 40) {:ok, view, _html} = live(conn, "/#{repository.owner}/#{repository.name}/commits/#{unknown}") render_async(view, 1_000) assert has_element?(view, "#commits-error", "Commit not found") refute has_element?(view, "#commit-detail") end test "the branch picker switches the listed history", %{ conn: conn, repository: repository, tmp_dir: tmp_dir } do _main_sha = push_commit(repository, tmp_dir, "work-first", "initial") feature_sha = push_branch_commit(repository, tmp_dir, "work-feature", "feature", "feature work") {:ok, view, _html} = live(conn, "/#{repository.owner}/#{repository.name}/commits") render_async(view, 1_000) assert has_element?(view, "#commits-branch-select") refute has_element?(view, "#commits-list a", "feature work") view |> form("#commits-branch-form", %{branch: "feature"}) |> render_change() render_async(view, 1_000) assert has_element?(view, "#commits-list a", "feature work") assert has_element?( view, "#commits-list a[href='/#{repository.owner}/#{repository.name}/commits/#{feature_sha}']" ) end test "an empty repository shows an empty state", %{conn: conn, repository: repository} do {:ok, view, _html} = live(conn, "/#{repository.owner}/#{repository.name}/commits") render_async(view, 1_000) assert has_element?(view, "#commits-error", "No commits yet") refute has_element?(view, "#commits-list") end test "unknown repositories 404", %{conn: conn} do assert_raise Ecto.NoResultsError, fn -> live(conn, "/nobody/here/commits") end end end