4.7 KiB · text 5af55d9
defmodule GitGud.BranchProtectionsTest do
use GitGud.DataCase, async: false
alias GitGud.BranchProtections
alias GitGud.Git
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()
{:ok, repo: repo}
end
end
test "with no rule, any ref update is allowed", %{repo: repo} do
{:ok, _} = BranchProtections.create_rule(repo, %{"pattern" => "release/*"})
fake_old = <<1::size(160)>>
fake_new = <<2::size(160)>>
assert :ok =
BranchProtections.check_ref_update(repo, "main", fake_old, fake_new)
end
test "blocks deletion of a protected branch", %{repo: repo} do
{:ok, _} = BranchProtections.create_rule(repo, %{"pattern" => "main"})
assert {:error, :deletion_blocked} =
BranchProtections.check_ref_update(repo, "main", <<1::size(160)>>, <<0::size(160)>>)
end
test "allows deletion when allow_deletions is true", %{repo: repo} do
{:ok, _} =
BranchProtections.create_rule(repo, %{
"pattern" => "main",
"allow_deletions" => true
})
assert :ok =
BranchProtections.check_ref_update(repo, "main", <<1::size(160)>>, <<0::size(160)>>)
end
test "blocks direct push when approvals required without a PR context", %{repo: repo} do
{:ok, _} =
BranchProtections.create_rule(repo, %{
"pattern" => "main",
"required_approvals" => 1
})
# Need real commits for the force-push check to make sense; build two
# commits where new is a descendant of old.
{old_sha, new_sha} = seed_linear_history(repo)
assert {:error, :approvals_required} =
BranchProtections.check_ref_update(repo, "main", old_sha, new_sha)
end
test "force-push: blocked by default, allowed when configured", %{repo: repo} do
{:ok, rule} = BranchProtections.create_rule(repo, %{"pattern" => "main"})
# `new_sha` is `old → new`; `divergent_sha` is `old → divergent`.
# A force push from new → divergent means `new` is NOT an ancestor of
# `divergent`, so it's a force push.
{_old, new_sha, divergent_sha} = seed_force_push_history(repo)
assert {:error, :force_push_blocked} =
BranchProtections.check_ref_update(repo, "main", new_sha, divergent_sha)
{:ok, _} = BranchProtections.update_rule(rule, %{"allow_force_push" => true})
assert :ok =
BranchProtections.check_ref_update(repo, "main", new_sha, divergent_sha)
end
defp seed_linear_history(repo) do
path = repo.disk_path
blob = write_blob(path, "a\n")
tree = make_tree(path, nil, "file.txt", blob)
{:ok, old} =
Git.commit_tree(path, tree, [], "old", author: {"t", "t@t", DateTime.utc_now(:second)})
blob2 = write_blob(path, "b\n")
tree2 = make_tree(path, old, "file.txt", blob2)
{:ok, new} =
Git.commit_tree(path, tree2, [old], "new", author: {"t", "t@t", DateTime.utc_now(:second)})
{old, new}
end
defp seed_force_push_history(repo) do
{old, new} = seed_linear_history(repo)
# Create a sibling that is not a descendant of `new`.
path = repo.disk_path
blob3 = write_blob(path, "c\n")
tree3 = make_tree(path, old, "file.txt", blob3)
{:ok, divergent} =
Git.commit_tree(path, tree3, [old], "divergent",
author: {"t", "t@t", DateTime.utc_now(:second)}
)
{old, new, divergent}
end
defp write_blob(path, contents) do
tmp = Path.join(System.tmp_dir!(), "bp-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!(), "bp-index-#{System.unique_integer([:positive])}")
work = Path.join(System.tmp_dir!(), "bp-wt-#{System.unique_integer([:positive])}")
File.mkdir_p!(work)
env = [{"GIT_INDEX_FILE", index}, {"GIT_WORK_TREE", work}, {"GIT_DIR", path}]
try do
if parent_sha do
{:ok, parent} = Git.commit(path, parent_sha)
{_, 0} = System.cmd("git", ["read-tree", Git.to_hex(parent.tree_sha)], env: env)
end
{_, 0} =
System.cmd(
"git",
[
"update-index",
"--add",
"--cacheinfo",
"100644," <> Git.to_hex(blob_sha) <> "," <> filename
],
env: env
)
{hex, 0} = System.cmd("git", ["write-tree"], env: env)
{:ok, sha} = Git.from_hex(String.trim(hex))
sha
after
_ = File.rm(index)
_ = File.rm_rf(work)
end
end
end