4.8 KiB · text 5af55d9
defmodule GitGud.PullRequestsTest do
use GitGud.DataCase, async: false
alias GitGud.Git
alias GitGud.PullRequests
alias GitGud.Repositories
import GitGud.ForgeFixtures
@moduletag :git_required
setup do
if System.find_executable("git") == nil do
{:skip, "git not on PATH"}
else
{user, repo} = repository_fixture()
seed_two_branches(repo, user)
{:ok, user: user, repo: repo}
end
end
test "create + mergeable + merge", %{repo: repo, user: user} do
assert {:ok, pr} =
PullRequests.create_pull_request(repo, user, %{
"title" => "merge me",
"source_ref" => "feature",
"target_ref" => "main"
})
assert pr.state == "open"
assert {:ok, true} = PullRequests.check_mergeability(pr)
assert {:ok, merged} = PullRequests.merge!(pr, user)
assert merged.state == "merged"
assert is_binary(merged.merge_commit_sha)
# Target ref now points at the new merge commit.
refreshed = Repositories.get_repository!(repo.id)
{:ok, head_sha} = Repositories.resolve(refreshed, "main")
assert head_sha == merged.merge_commit_sha
end
test "conflict surfaces as mergeable=false", %{repo: repo, user: user} do
create_conflict(repo)
{:ok, pr} =
PullRequests.create_pull_request(repo, user, %{
"title" => "conflict",
"source_ref" => "conflict",
"target_ref" => "main"
})
assert {:ok, false} = PullRequests.check_mergeability(pr)
assert {:error, :conflict} = PullRequests.merge!(pr, user)
end
# ── helpers ──────────────────────────────────────────────────────────
defp seed_two_branches(repo, user) do
path = repo.disk_path
# main: README.md "main\n"
main_sha = commit_file(path, user, nil, "README.md", "main\n", "initial")
:ok = Git.update_ref(path, "refs/heads/main", main_sha, nil)
# feature: README.md "main\n" + a separate file
feat_sha =
commit_file(path, user, main_sha, "feature.txt", "added on feature\n", "feature commit")
:ok = Git.update_ref(path, "refs/heads/feature", feat_sha, nil)
end
defp create_conflict(repo) do
# Diverge main from feature by changing the same file feature also
# changes. Add commit on main that touches feature.txt; then add a
# commit on a new "conflict" branch from feature also touching it.
path = repo.disk_path
{:ok, main_sha} = Git.resolve(path, "main")
{:ok, feat_sha} = Git.resolve(path, "feature")
new_main =
commit_file(
path,
%{email: "main@local"},
main_sha,
"feature.txt",
"main version\n",
"main diverge"
)
:ok = Git.update_ref(path, "refs/heads/main", new_main, main_sha)
conflict_sha =
commit_file(
path,
%{email: "c@local"},
feat_sha,
"feature.txt",
"conflict version\n",
"conflict commit"
)
:ok = Git.update_ref(path, "refs/heads/conflict", conflict_sha, nil)
end
defp commit_file(path, user_or_attrs, parent_sha, filename, contents, message) do
blob_sha = write_blob(path, contents)
tree_sha = make_tree(path, parent_sha, filename, blob_sha)
parents = if parent_sha, do: [parent_sha], else: []
email = Map.get(user_or_attrs, :email) || "test@example.com"
name = email |> String.split("@") |> hd()
{:ok, commit_sha} =
Git.commit_tree(path, tree_sha, parents, message,
author: {name, email, DateTime.utc_now(:second)}
)
commit_sha
end
defp write_blob(path, contents) do
tmp = Path.join(System.tmp_dir!(), "pr-test-blob-#{System.unique_integer([:positive])}")
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
defp make_tree(path, parent_sha, filename, blob_sha) do
index = Path.join(System.tmp_dir!(), "pr-test-index-#{System.unique_integer([:positive])}")
env = [{"GIT_INDEX_FILE", index}]
try do
if parent_sha do
{:ok, parent_commit} = Git.commit(path, parent_sha)
{_, 0} =
System.cmd("git", ["-C", path, "read-tree", Git.to_hex(parent_commit.tree_sha)],
env: env
)
end
{_, 0} =
System.cmd(
"git",
[
"-C",
path,
"update-index",
"--add",
"--cacheinfo",
"100644," <> Git.to_hex(blob_sha) <> "," <> filename
],
env: env
)
{hex, 0} = System.cmd("git", ["-C", path, "write-tree"], env: env)
{:ok, sha} = Git.from_hex(String.trim(hex))
sha
after
_ = File.rm(index)
end
end
end