From aa86f7182c6426061d253c568a8ad8cb9c495991 Mon Sep 17 00:00:00 2001 From: Eric Scouten Date: Sun, 25 Aug 2019 08:45:40 -0700 Subject: [PATCH] Implement Xgit.Core.DirCache.remove_entries/2. --- lib/xgit/core/dir_cache.ex | 67 ++++++++++++++++++++ test/xgit/core/dir_cache_test.exs | 102 ++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+) diff --git a/lib/xgit/core/dir_cache.ex b/lib/xgit/core/dir_cache.ex index 0580e03..9c6a029 100644 --- a/lib/xgit/core/dir_cache.ex +++ b/lib/xgit/core/dir_cache.ex @@ -309,4 +309,71 @@ defmodule Xgit.Core.DirCache do :gt -> [new_head | combine_entries(existing_entries, new_tail)] end end + + @typedoc ~S""" + An entry for the `remove` option for `remove_entries/3`. + """ + @type entry_to_remove :: {path :: [byte], stage :: 0..3 | :all} + + @doc ~S""" + Returns a dir cache that has some directory entries removed. + + ## Parameters + + `entries_to_remove` is a list of `{path, stage}` tuples identifying tuples to be removed. + + * `path` should be a byte list for the path. + * `stage` should be 0..3 or `:all`, meaning any entry that matches the path, + regardless of stage, should be removed. + + ## Return Value + + `{:ok, dir_cache}` where `dir_cache` is the original `dir_cache` with any matching + entries removed. + + `{:error, :invalid_dir_cache}` if the original `dir_cache` was invalid. + + `{:error, :invalid_entries}` if one or more of the entries is invalid. + """ + @spec remove_entries(dir_cache :: t, entries_to_remove :: [entry_to_remove]) :: + {:ok, t} | {:error, :invalid_dir_cache | :invalid_entries} + def remove_entries(%__MODULE__{entries: existing_entries} = dir_cache, entries_to_remove) + when is_list(entries_to_remove) do + with {:dir_cache_valid?, true} <- {:dir_cache_valid?, valid?(dir_cache)}, + {:entries_valid?, true} <- + {:entries_valid?, Enum.all?(entries_to_remove, &valid_remove_entry?/1)} do + updated_entries = remove_matching_entries(existing_entries, Enum.sort(entries_to_remove)) + {:ok, %{dir_cache | entry_count: Enum.count(updated_entries), entries: updated_entries}} + else + {:dir_cache_valid?, _} -> {:error, :invalid_dir_cache} + {:entries_valid?, _} -> {:error, :invalid_entries} + end + end + + defp valid_remove_entry?({path, :all}) when is_list(path), do: true + + defp valid_remove_entry?({path, stage}) + when is_list(path) and is_integer(stage) and stage >= 0 and stage <= 3, + do: true + + defp valid_remove_entry?(_), do: false + + defp remove_matching_entries(sorted_existing_entries, sorted_entries_to_remove) + + defp remove_matching_entries([], _sorted_entries_to_remove), do: [] + defp remove_matching_entries(sorted_existing_entries, []), do: sorted_existing_entries + + defp remove_matching_entries([%__MODULE__.Entry{name: path} | existing_tail], [ + {path, :all} | remove_tail + ]), + do: + remove_matching_entries(Enum.drop_while(existing_tail, &(&1.name == path)), remove_tail) + + defp remove_matching_entries([%__MODULE__.Entry{name: path, stage: stage} | existing_tail], [ + {path, stage} | remove_tail + ]), + do: remove_matching_entries(existing_tail, remove_tail) + + defp remove_matching_entries([existing_head | existing_tail], sorted_entries_to_remove), + do: [existing_head | remove_matching_entries(existing_tail, sorted_entries_to_remove)] end diff --git a/test/xgit/core/dir_cache_test.exs b/test/xgit/core/dir_cache_test.exs index ec09a3c..e7dee7b 100644 --- a/test/xgit/core/dir_cache_test.exs +++ b/test/xgit/core/dir_cache_test.exs @@ -204,4 +204,106 @@ defmodule Xgit.Core.DirCacheTest do end end end + + describe "remove_entries/2" do + test "happy path: removing from an empty list" do + assert {:ok, + %DirCache{ + version: 2, + entry_count: 0, + entries: [] + }} = DirCache.remove_entries(DirCache.empty(), [{'hello.txt', 0}]) + end + + test "happy path: removing something that doesn't exist from a non-empty list" do + assert {:ok, @valid} = DirCache.remove_entries(@valid, [{'goodbye.txt', 0}]) + end + + test "happy path: ignores mismatch on stage" do + assert {:ok, @valid} = DirCache.remove_entries(@valid, [{'hello.txt', 1}]) + end + + test "happy path: removes only item in list" do + assert {:ok, + %DirCache{ + version: 2, + entry_count: 0, + entries: [] + }} = DirCache.remove_entries(@valid, [{'hello.txt', 0}]) + end + + test "happy path: removes all matching entries via stage :all" do + assert {:ok, + %DirCache{ + version: 2, + entry_count: 0, + entries: [] + }} = + DirCache.remove_entries( + %DirCache{ + version: 2, + entry_count: 3, + entries: [ + Map.put(@valid_entry, :stage, 0), + Map.put(@valid_entry, :stage, 1), + Map.put(@valid_entry, :stage, 3) + ] + }, + [{'hello.txt', :all}] + ) + end + + test "happy path: removes only matching entries via name" do + assert {:ok, @valid} = + DirCache.remove_entries( + %DirCache{ + version: 2, + entry_count: 2, + entries: [Map.put(@valid_entry, :name, 'abc.txt'), @valid_entry] + }, + [{'abc.txt', 0}] + ) + end + + test "happy path: sorts list of items to remove" do + assert {:ok, @valid} = + DirCache.remove_entries( + %DirCache{ + version: 2, + entry_count: 3, + entries: [ + @valid_entry, + Map.put(@valid_entry, :name, 'other.txt'), + Map.put(@valid_entry, :name, 'xgit.txt') + ] + }, + [{'xgit.txt', 0}, {'other.txt', 0}] + ) + end + + test "{:error, :invalid_dir_cache}" do + assert {:error, :invalid_dir_cache} = + DirCache.remove_entries(Map.put(@valid, :entry_count, 999), [ + {'xgit.txt', 0}, + {'other.txt', 0} + ]) + end + + test "{:error, :invalid_entries}" do + assert {:error, :invalid_entries} = + DirCache.remove_entries(DirCache.empty(), [{'hello.txt', 7}]) + end + + test "FunctionClauseError: not a DirCache" do + assert_raise FunctionClauseError, fn -> + DirCache.remove_entries("trust me, it's a DirCache", [{'hello.txt', 0}]) + end + end + + test "FunctionClauseError: not a list of entries" do + assert_raise FunctionClauseError, fn -> + DirCache.remove_entries(DirCache.empty(), {'hello.txt', 0}) + end + end + end end