@@ -1,0 +1,209 @@
defmodule ExGitObjectstore.Integration.GitRepoTest do
@moduledoc """
Integration tests that create real git repos with the git CLI,
then verify our library can read the objects correctly.
"""
use ExUnit.Case, async: true
alias ExGitObjectstore.{Object, Repo}
alias ExGitObjectstore.Object.{Blob, Tree, Commit}
alias ExGitObjectstore.Storage.Filesystem
@moduletag :integration
setup do
tmp_dir = System.tmp_dir!()
base = Path.join(tmp_dir, "git_integration_#{:erlang.unique_integer([:positive])}")
File.mkdir_p!(base)
# Create a working git repo
work_dir = Path.join(base, "work")
File.mkdir_p!(work_dir)
git!(work_dir, ["init"])
git!(work_dir, ["config", "user.email", "test@example.com"])
git!(work_dir, ["config", "user.name", "Test User"])
# Create a bare clone for our library to read from
bare_dir = Path.join(base, "bare.git")
on_exit(fn -> File.rm_rf!(base) end)
%{base: base, work_dir: work_dir, bare_dir: bare_dir}
end
describe "reading loose objects from a git repo" do
test "read blob created by git", %{work_dir: work_dir, base: base} do
# Create a file and commit it
File.write!(Path.join(work_dir, "hello.txt"), "Hello, World!\n")
git!(work_dir, ["add", "hello.txt"])
git!(work_dir, ["commit", "-m", "Add hello.txt"])
# Get the blob SHA from git
blob_sha = git!(work_dir, ["hash-object", "hello.txt"]) |> String.trim()
# Create a bare clone
bare_dir = Path.join(base, "bare.git")
git!(base, ["clone", "--bare", work_dir, bare_dir])
# Now read through our library using filesystem backend
# The bare repo's object store is at bare_dir/objects/
# We need to map our storage to point at the bare repo
# Our layout is: <root>/<prefix>/objects/... so we set root and prefix accordingly
repo = make_repo_from_bare(bare_dir)
{:ok, decoded} = Object.read(repo, blob_sha)
assert %Blob{content: "Hello, World!\n"} = decoded
end
test "read tree created by git", %{work_dir: work_dir, base: base} do
File.write!(Path.join(work_dir, "a.txt"), "aaa\n")
File.write!(Path.join(work_dir, "b.txt"), "bbb\n")
git!(work_dir, ["add", "."])
git!(work_dir, ["commit", "-m", "Add files"])
# Get tree SHA
tree_sha = git!(work_dir, ["rev-parse", "HEAD^{tree}"]) |> String.trim()
bare_dir = Path.join(base, "bare.git")
git!(base, ["clone", "--bare", work_dir, bare_dir])
repo = make_repo_from_bare(bare_dir)
{:ok, decoded} = Object.read(repo, tree_sha)
assert %Tree{entries: entries} = decoded
names = Enum.map(entries, & &1.name)
assert "a.txt" in names
assert "b.txt" in names
end
test "read commit created by git", %{work_dir: work_dir, base: base} do
File.write!(Path.join(work_dir, "file.txt"), "content\n")
git!(work_dir, ["add", "."])
git!(work_dir, ["commit", "-m", "Initial commit"])
commit_sha = git!(work_dir, ["rev-parse", "HEAD"]) |> String.trim()
bare_dir = Path.join(base, "bare.git")
git!(base, ["clone", "--bare", work_dir, bare_dir])
repo = make_repo_from_bare(bare_dir)
{:ok, decoded} = Object.read(repo, commit_sha)
assert %Commit{} = decoded
assert decoded.message == "Initial commit\n"
assert decoded.author =~ "Test User"
assert decoded.parents == []
end
test "read commit with parent", %{work_dir: work_dir, base: base} do
File.write!(Path.join(work_dir, "file.txt"), "v1\n")
git!(work_dir, ["add", "."])
git!(work_dir, ["commit", "-m", "First"])
parent_sha = git!(work_dir, ["rev-parse", "HEAD"]) |> String.trim()
File.write!(Path.join(work_dir, "file.txt"), "v2\n")
git!(work_dir, ["add", "."])
git!(work_dir, ["commit", "-m", "Second"])
commit_sha = git!(work_dir, ["rev-parse", "HEAD"]) |> String.trim()
bare_dir = Path.join(base, "bare.git")
git!(base, ["clone", "--bare", work_dir, bare_dir])
repo = make_repo_from_bare(bare_dir)
{:ok, decoded} = Object.read(repo, commit_sha)
assert decoded.parents == [parent_sha]
end
test "read refs from bare repo", %{work_dir: work_dir, base: base} do
File.write!(Path.join(work_dir, "file.txt"), "content\n")
git!(work_dir, ["add", "."])
git!(work_dir, ["commit", "-m", "Init"])
# Create a branch
git!(work_dir, ["branch", "feature"])
bare_dir = Path.join(base, "bare.git")
git!(base, ["clone", "--bare", work_dir, bare_dir])
repo = make_repo_from_bare(bare_dir)
# Check HEAD
assert {:ok, head} = ExGitObjectstore.Ref.get_head(repo)
assert head =~ "ref: refs/heads/"
# Check branches
{:ok, branches} = ExGitObjectstore.branches(repo)
branch_names = Enum.map(branches, &elem(&1, 0))
assert "refs/heads/main" in branch_names or "refs/heads/master" in branch_names
assert "refs/heads/feature" in branch_names
end
test "our SHA matches git's SHA for objects we create", %{base: _base} do
# Verify that blobs we create in-memory have the same SHA as git would produce
blob = Blob.from_content("verification test\n")
our_sha = Object.hash(blob)
# Write to a temp file and hash with git
tmp = Path.join(System.tmp_dir!(), "verify_#{:erlang.unique_integer([:positive])}")
File.write!(tmp, "verification test\n")
git_sha = System.cmd("git", ["hash-object", tmp]) |> elem(0) |> String.trim()
File.rm!(tmp)
assert our_sha == git_sha
end
end
describe "reading nested trees" do
test "tree with subdirectory", %{work_dir: work_dir, base: base} do
File.mkdir_p!(Path.join(work_dir, "src"))
File.write!(Path.join(work_dir, "src/main.ex"), "defmodule Main do\nend\n")
File.write!(Path.join(work_dir, "README.md"), "# Project\n")
git!(work_dir, ["add", "."])
git!(work_dir, ["commit", "-m", "Add src and readme"])
tree_sha = git!(work_dir, ["rev-parse", "HEAD^{tree}"]) |> String.trim()
bare_dir = Path.join(base, "bare.git")
git!(base, ["clone", "--bare", work_dir, bare_dir])
repo = make_repo_from_bare(bare_dir)
{:ok, decoded} = Object.read(repo, tree_sha)
assert %Tree{entries: entries} = decoded
names = Enum.map(entries, & &1.name)
assert "README.md" in names
assert "src" in names
# Find the src directory entry and read its tree
src_entry = Enum.find(entries, &(&1.name == "src"))
assert src_entry.mode == "40000"
{:ok, src_tree} = Object.read(repo, src_entry.sha)
assert %Tree{entries: [src_file]} = src_tree
assert src_file.name == "main.ex"
end
end
# -- Helpers --
defp git!(dir, args) do
{output, 0} = System.cmd("git", args, cd: dir, stderr_to_stdout: true)
output
end
# Map a bare git repo to our storage layout.
# Bare repo has objects at <bare_dir>/objects/<sha[0:2]>/<sha[2:]>
# Our layout is: <root>/<prefix>/objects/<sha[0:2]>/<sha[2:]>
# So root=<parent_of_bare_dir>, prefix=<bare_dir_name minus .git>
# Actually easier: root=<bare_dir>, prefix="" but our Repo prefixes with "repos/<id>"
# Simplest: make root point so that root/repos/<id> = bare_dir
defp make_repo_from_bare(bare_dir) do
repo_id = "test"
# We need root/repos/test = bare_dir, so root = bare_dir/../ and symlink
parent = Path.dirname(bare_dir)
link_base = Path.join([parent, "repos", repo_id])
File.mkdir_p!(Path.dirname(link_base))
File.ln_s!(bare_dir, link_base)
Repo.new(repo_id, storage: {Filesystem, %{root: parent}})
end
end