diff --git a/lib/xgit/repository.ex b/lib/xgit/repository.ex index 3cb8a2d..4a78f1d 100644 --- a/lib/xgit/repository.ex +++ b/lib/xgit/repository.ex @@ -111,6 +111,29 @@ defmodule Xgit.Repository do when is_pid(repository) and is_pid(working_tree), do: GenServer.call(repository, {:set_default_working_tree, working_tree}) + @doc ~S""" + Returns `true` if all objects in the list are present in the object dictionary. + + This limit is not enforced, but it's recommended to query for no more than ~100 object + IDs at a time. + """ + @spec has_all_object_ids?(repository :: t, object_ids :: [ObjectId.t()]) :: boolean + def has_all_object_ids?(repository, object_ids) when is_pid(repository) and is_list(object_ids), + do: GenServer.call(repository, {:has_all_object_ids?, object_ids}) + + @doc ~S""" + Checks for presence of multiple object Ids. + + Called when `has_all_object_ids?/2` is called. + + ## Return Value + + Should return `{:ok, has_all_object_ids?, state}` where `has_all_object_ids?` is `true` + if all object IDs can be found in the object dictionary; `false` otherwise. + """ + @callback handle_has_all_object_ids?(state :: any, object_ids :: [ObjectId.t()]) :: + {:ok, has_all_object_ids? :: boolean, state :: any} + @typedoc ~S""" Error codes that can be returned by `get_object/2`. """ @@ -205,6 +228,9 @@ defmodule Xgit.Repository do def handle_call({:set_default_working_tree, _working_tree}, _from, state), do: {:reply, :error, state} + def handle_call({:has_all_object_ids?, object_ids}, _from, state), + do: delegate_boolean_call_to(state, :handle_has_all_object_ids?, [object_ids]) + def handle_call({:get_object, object_id}, _from, state), do: delegate_call_to(state, :handle_get_object, [object_id]) @@ -224,6 +250,11 @@ defmodule Xgit.Repository do end end + defp delegate_boolean_call_to(state, function, args) do + {:reply, {:ok, response}, state} = delegate_call_to(state, function, args) + cover {:reply, response, state} + end + defmacro __using__(opts) do quote location: :keep, bind_quoted: [opts: opts] do use GenServer, opts diff --git a/lib/xgit/repository/in_memory.ex b/lib/xgit/repository/in_memory.ex index c6a2f6b..1dd34c6 100644 --- a/lib/xgit/repository/in_memory.ex +++ b/lib/xgit/repository/in_memory.ex @@ -30,6 +30,12 @@ defmodule Xgit.Repository.InMemory do @impl true def init(opts) when is_list(opts), do: cover({:ok, %{loose_objects: %{}}}) + @impl true + def handle_has_all_object_ids?(%{loose_objects: objects} = state, object_ids) do + has_all_objects? = Enum.all?(object_ids, fn object_id -> Map.has_key?(objects, object_id) end) + cover {:ok, has_all_objects?, state} + end + @impl true def handle_get_object(%{loose_objects: objects} = state, object_id) do # Currently only checks for loose objects. diff --git a/lib/xgit/repository/on_disk.ex b/lib/xgit/repository/on_disk.ex index e1b019e..b240ef9 100644 --- a/lib/xgit/repository/on_disk.ex +++ b/lib/xgit/repository/on_disk.ex @@ -86,6 +86,10 @@ defmodule Xgit.Repository.OnDisk do @spec create(work_dir :: Path.t()) :: :ok | {:error, :work_dir_must_not_exist} defdelegate create(work_dir), to: Xgit.Repository.OnDisk.Create + @impl true + defdelegate handle_has_all_object_ids?(state, object_ids), + to: Xgit.Repository.OnDisk.HasAllObjectIds + @impl true defdelegate handle_get_object(state, object_id), to: Xgit.Repository.OnDisk.GetObject diff --git a/lib/xgit/repository/on_disk/has_all_object_ids.ex b/lib/xgit/repository/on_disk/has_all_object_ids.ex new file mode 100644 index 0000000..4328de9 --- /dev/null +++ b/lib/xgit/repository/on_disk/has_all_object_ids.ex @@ -0,0 +1,30 @@ +defmodule Xgit.Repository.OnDisk.HasAllObjectIds do + @moduledoc false + # Implements Xgit.Repository.OnDisk.handle_has_all_objects?/2. + + import Xgit.Util.ForceCoverage + + alias Xgit.Core.ObjectId + + @spec handle_has_all_object_ids?(state :: any, object_ids :: [ObjectId.t()]) :: + {:ok, has_all_object_ids? :: boolean, state :: any} + | {:error, reason :: any, state :: any} + def handle_has_all_object_ids?(%{git_dir: git_dir} = state, object_ids) do + has_all_object_ids? = + Enum.all?(object_ids, fn object_id -> has_object_id?(git_dir, object_id) end) + + cover {:ok, has_all_object_ids?, state} + end + + defp has_object_id?(git_dir, object_id) do + loose_object_path = + Path.join([ + git_dir, + "objects", + String.slice(object_id, 0, 2), + String.slice(object_id, 2, 38) + ]) + + File.regular?(loose_object_path) + end +end diff --git a/test/xgit/repository/in_memory/has_all_object_ids_test.exs b/test/xgit/repository/in_memory/has_all_object_ids_test.exs new file mode 100644 index 0000000..bc88816 --- /dev/null +++ b/test/xgit/repository/in_memory/has_all_object_ids_test.exs @@ -0,0 +1,60 @@ +defmodule Xgit.Repository.InMemory.HasAllObjectIdsTest do + use ExUnit.Case, async: true + + alias Xgit.Core.Object + alias Xgit.Repository + alias Xgit.Repository.InMemory + + describe "has_all_object_ids?/2" do + @test_content 'test content\n' + @test_content_id "d670460b4b4aece5915caf5c68d12f560a9fe3e4" + + setup do + assert {:ok, repo} = InMemory.start_link() + + object = %Object{type: :blob, content: @test_content, size: 13, id: @test_content_id} + assert :ok = Repository.put_loose_object(repo, object) + + # Yes, the hash is wrong, but we'll ignore that for now. + object = %Object{ + type: :blob, + content: @test_content, + size: 15, + id: "c1e116090ad56f172370351ab3f773eb0f1fe89e" + } + + assert :ok = Repository.put_loose_object(repo, object) + + {:ok, repo: repo} + end + + test "happy path: zero object IDs", %{repo: repo} do + assert Repository.has_all_object_ids?(repo, []) + end + + test "happy path: one object ID", %{repo: repo} do + assert Repository.has_all_object_ids?(repo, [@test_content_id]) + end + + test "happy path: two object IDs", %{repo: repo} do + assert Repository.has_all_object_ids?(repo, [ + @test_content_id, + "c1e116090ad56f172370351ab3f773eb0f1fe89e" + ]) + end + + test "happy path: partial match", %{repo: repo} do + refute Repository.has_all_object_ids?(repo, [ + @test_content_id, + "b9e3a9e3ea7dde01d652f899a783b75a1518564c" + ]) + end + + test "happy path: no match", %{repo: repo} do + refute Repository.has_all_object_ids?(repo, [ + @test_content_id, + "6ee878a55ed36e2cda2c68452d2336ce3bd692d1" + ]) + end + end +end diff --git a/test/xgit/repository/on_disk/has_all_object_ids_test.exs b/test/xgit/repository/on_disk/has_all_object_ids_test.exs new file mode 100644 index 0000000..def5359 --- /dev/null +++ b/test/xgit/repository/on_disk/has_all_object_ids_test.exs @@ -0,0 +1,64 @@ +defmodule Xgit.Repository.OnDisk.HasAllObjectIdsTest do + use Xgit.GitInitTestCase, async: true + + alias Xgit.Core.Object + alias Xgit.Repository + alias Xgit.Repository.OnDisk + + describe "has_all_object_ids?/2" do + @test_content 'test content\n' + @test_content_id "d670460b4b4aece5915caf5c68d12f560a9fe3e4" + + setup %{xgit: xgit} do + assert :ok = OnDisk.create(xgit) + assert {:ok, repo} = OnDisk.start_link(work_dir: xgit) + + object = %Object{type: :blob, content: @test_content, size: 13, id: @test_content_id} + assert :ok = Repository.put_loose_object(repo, object) + + # Yes, the hash is wrong, but we'll ignore that for now. + object = %Object{ + type: :blob, + content: @test_content, + size: 15, + id: "c1e116090ad56f172370351ab3f773eb0f1fe89e" + } + + assert :ok = Repository.put_loose_object(repo, object) + + {:ok, repo: repo} + end + + test "happy path: zero object IDs", %{repo: repo} do + assert true == Repository.has_all_object_ids?(repo, []) + end + + test "happy path: one object ID", %{repo: repo} do + assert true == Repository.has_all_object_ids?(repo, [@test_content_id]) + end + + test "happy path: two object IDs", %{repo: repo} do + assert true == + Repository.has_all_object_ids?(repo, [ + @test_content_id, + "c1e116090ad56f172370351ab3f773eb0f1fe89e" + ]) + end + + test "happy path: partial match", %{repo: repo} do + assert false == + Repository.has_all_object_ids?(repo, [ + @test_content_id, + "b9e3a9e3ea7dde01d652f899a783b75a1518564c" + ]) + end + + test "happy path: no match", %{repo: repo} do + assert false == + Repository.has_all_object_ids?(repo, [ + @test_content_id, + "6ee878a55ed36e2cda2c68452d2336ce3bd692d1" + ]) + end + end +end