From b54fe3d51bbee1225e5e0511e3f3aa92aba1d7b8 Mon Sep 17 00:00:00 2001 From: Ronan Harmegnies Date: Wed, 11 Nov 2015 23:21:32 +0100 Subject: [PATCH] Add GenServer Rpc implementation of Gitex.Repo --- README.md | 22 ++++-- lib/server_git.ex | 44 +++++++++++ test/server_git_test.exs | 158 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 lib/server_git.ex create mode 100644 test/server_git_test.exs diff --git a/README.md b/README.md index d084b8e..c38bd88 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ See API documentation at [http://hexdocs.pm/gitex](http://hexdocs.pm/gitex). TODO: -- test it (only for regression, currently it works on many open source git repo, so it can be considered as tested) -- add a `Gitex.merge` helper to help you construct a commit tree from multiple trees -- add impl `Gitex.Repo` for Pid as a GenServer RPC -- implementation example of previous GenServer maintaining ETS LRU cache of standard git fs objects and deltas -- add some useful alternative implementations, currently only standard object encoding and storage +- [x] test it (only for regression, currently it works on many open source git repo, so it can be considered as tested) +- [ ] add a `Gitex.merge` helper to help you construct a commit tree from multiple trees +- [x] add impl `Gitex.Repo` for Pid as a GenServer RPC +- [ ] implementation example of previous GenServer maintaining ETS LRU cache of standard git fs objects and deltas +- [ ] add some useful alternative implementations, currently only standard object encoding and storage ## Usage example @@ -56,6 +56,18 @@ history stream in order to construct a pretty visualizer very easily (d3.js for Gitex.history(:head,repo) |> Gitex.align_history ``` +`Gitex.Server` provides a GenServer implementation (implementing `Gitex.Repo`for PIDs). +This implementation relies on an underlying `Gitex.Repo` that is provided at initialization: + +```elixir +r = Gitex.Git.open # Create a standard (reference impl) Gitex.Repo +{:ok, repo_pid} = Gitex.Server.start_link(r) # Create a GenServer Gitex.Repo +Gitex.history("master", repo_pid) # print history stream +|> Stream.each(&IO.puts "* #{String.slice(&1.hash, 0 ,7)} #{&1.message}") +|> Stream.run +``` + + ## The Gitex.Repo protocol Any repo implementing the `Gitex.Repo` protocol : (basically object codec, ref diff --git a/lib/server_git.ex b/lib/server_git.ex new file mode 100644 index 0000000..3a3099d --- /dev/null +++ b/lib/server_git.ex @@ -0,0 +1,44 @@ +defmodule Gitex.Server do + use GenServer + + @moduledoc """ + Implementation of `Gitex.Repo` protocol for PID (GenServer) + + The server will proxy Gitex.Repo calls to a Gitex.Repo implementation + given at initialization. + """ + + def start_link(repo, opts \\ []) do + GenServer.start_link(__MODULE__, repo, opts) + end + + def init(repo), do: {:ok, repo} + + # Server impl + def handle_call({:decode, hash, bintype}, _from, repo), do: {:reply, Gitex.Repo.decode(repo, hash, bintype), repo} + def handle_call({:encode, obj}, _from, repo), do: {:reply, Gitex.Repo.encode(repo, obj), repo} + def handle_call({:resolve_ref, ref}, _from, repo), do: {:reply, Gitex.Repo.resolve_ref(repo, ref), repo} + def handle_call({:set_ref, ref, hash}, _from, repo), do: {:reply, Gitex.Repo.set_ref(repo, ref, hash), repo} + def handle_call({:get_obj, hash}, _from, repo), do: {:reply, Gitex.Repo.get_obj(repo, hash), repo} + def handle_call({:put_obj, bintype}, _from, repo), do: {:reply, Gitex.Repo.put_obj(repo, bintype), repo} + def handle_call({:user}, _from, repo), do: {:reply, Gitex.Repo.user(repo), repo} + + def handle_call(request, from, state) do + # Call the default implementation from GenServer + super(request, from, state) + end + + # Implementation of Gitex.Repo for PID: simply send the request to the server + defimpl Gitex.Repo, for: PID do + # Client API + def decode(repo_pid, hash, bintype), do: GenServer.call(repo_pid, {:decode, hash, bintype}) + def encode(repo_pid, obj), do: GenServer.call(repo_pid, {:encode, obj}) + def resolve_ref(repo_pid, ref), do: GenServer.call(repo_pid, {:resolve_ref, ref}) + def set_ref(repo_pid, ref, hash), do: GenServer.call(repo_pid, {:set_ref, ref, hash}) + def get_obj(repo_pid, hash), do: GenServer.call(repo_pid, {:get_obj, hash}) + def put_obj(repo_pid, bintype), do: GenServer.call(repo_pid, {:put_obj, bintype}) + def user(repo_pid), do: GenServer.call(repo_pid, {:user}) + end +end + + diff --git a/test/server_git_test.exs b/test/server_git_test.exs new file mode 100644 index 0000000..fcbb6f9 --- /dev/null +++ b/test/server_git_test.exs @@ -0,0 +1,158 @@ +defmodule ServerGitTest do + use ExUnit.Case + import ExUnit.CaptureIO + + setup do + repo = create_standard_git_repo + on_exit(fn->clean_standard_git_repo(repo) end) + {:ok, repo_pid} = Gitex.Server.start_link(repo) + {:ok, repo: repo_pid} + end + + test "create a branch", %{repo: repo} do + hash = Gitex.commit([], repo, "branch1", "first commit") + assert %{hash: ^hash} = Gitex.get("branch1", repo) + end + + test "create a tag", %{repo: repo} do + hash = Gitex.commit([], repo,"master", "first commit") |> Gitex.tag(repo, "tag1") + assert %{hash: ^hash} = Gitex.get("tag1", repo) + end + + test "branch history", %{repo: repo} do + tree1 = Gitex.put([], repo, "/path1/file1", "file1 content") + |> Gitex.put(repo, "/path1/subpath1/file2", "file2 content") + hash1 = Gitex.commit(tree1, repo, "master", "first commit") + + _some_other_hash = Gitex.put(hash1, repo, "/path1/file1", "file1 content2") + |> Gitex.put(repo, "/path2/subpath1/file2", "file3 content") + |> Gitex.commit(repo, "branch1", "other commit") + + tree2 = Gitex.put(hash1, repo, "/path1/file1", "file1 content3") + |> Gitex.put(repo, "/path2/subpath1/file2", "file3 content") + hash2 = Gitex.commit(tree2, repo, "master", "second commit") + history = Gitex.history("master", repo) + + assert [ + %{hash: ^hash2, tree: ^tree2, parent: ^hash1}, + %{hash: ^hash1, tree: ^tree1} + ] = Enum.to_list(history) + end + + test "stream of version history for a given file", %{repo: repo} do + tree1 = Gitex.put([], repo, "/path1/file1", "file1 content") + |> Gitex.put(repo, "/path1/subpath1/file2", "file2 content") + hash1 = Gitex.commit(tree1, repo, "master", "first commit") + + _some_other_hash = Gitex.put(hash1, repo, "/path1/file1", "file1 content2") + |> Gitex.put(repo, "/path2/subpath1/file2", "file3 content") + |> Gitex.commit(repo, "branch1", "other commit") + + tree2 = Gitex.put(hash1, repo, "/path1/file1", "file1 content3") + |> Gitex.put(repo, "/path2/subpath1/file2", "file3 content") + Gitex.commit(tree2, repo,"master", "second commit") + + file1_version_history = Gitex.history("master", repo) + |> Stream.map(&Gitex.get_hash(&1, repo, "/path1/file1")) + |> Stream.dedup + |> Stream.map(&Gitex.object(&1, repo)) + + assert ["file1 content3", "file1 content"] = Enum.to_list(file1_version_history) + end + + test "simple get tree object", %{repo: repo} do + Gitex.put([], repo,"/path1/file1","file1 content") + |> Gitex.put(repo,"/path1/subpath1/file2","file2 content") + |> Gitex.commit(repo,"master","first commit") + + assert [ + %{name: "file1", type: :file, mode: "00777"}, + %{name: "subpath1", type: :dir, mode: "40000"} + ] = Gitex.get("master", repo, "/path1") + end + + test "get tree with several commits", %{repo: repo} do + tree_hash1 = Gitex.put([], repo, "/file1", "file1 content") + |> Gitex.put(repo, "/file2", "file2 content") + |> Gitex.put(repo, "/path1/file3", "file3 content") + |> Gitex.put(repo, "/path2/file4", "file4 content") + |> Gitex.commit(repo, "master", "first commit") + + Gitex.put(tree_hash1, repo,"/file1","file1 content2") + |> Gitex.put(repo,"/path1/file5","file5 content") + |> Gitex.put(repo,"/file6","file6 content") + |> Gitex.put(repo,"/path3/file7","file5 content") + |> Gitex.commit(repo,"master","second commit") + + assert [ + %{name: "file1", type: :file, mode: "00777"}, + %{name: "file2", type: :file, mode: "00777"}, + %{name: "file6", type: :file, mode: "00777"}, + %{name: "path1", type: :dir, mode: "40000"}, + %{name: "path2", type: :dir, mode: "40000"}, + %{name: "path3", type: :dir, mode: "40000"} + ] = Gitex.get("master", repo, "/") + end + + test "remove a file from tree", %{repo: repo} do + tree_hash1 = Gitex.put([], repo, "/file1", "file1 content") + |> Gitex.put(repo, "/path1/file2", "file2 content") + |> Gitex.put(repo, "/path1/file3", "file2 content") + |> Gitex.commit(repo, "master", "first commit") + + # prepare a tree for path1 containing a single file + subtree_hash2 = Gitex.put([], repo, "/file2", "file2 content2") + + # commit modified subtree + Gitex.put(tree_hash1, repo,"path1", Gitex.object(subtree_hash2, repo)) + |> Gitex.commit(repo, "master", "second commit") + + # check the root tree + assert [ + %{name: "file1", type: :file, mode: "00777"}, + %{name: "path1", type: :dir, mode: "40000"}, + ] = Gitex.get("master", repo, "/") + + # path1 should contain a single file + assert [ + %{name: "file2", type: :file, mode: "00777"}, + ] = Gitex.get("master", repo, "/path1") + end + + test "print history", %{repo: repo} do + tree_hash1 = Gitex.put([], repo, "/file1", "file1 content") + |> Gitex.put(repo, "/path1/file2", "file2 content") + |> Gitex.put(repo, "/path1/file3", "file2 content") + |> Gitex.commit(repo, "master", "first commit") + + # prepare a tree for path1 containing a single file + subtree_hash2 = Gitex.put([], repo, "/file2", "file2 content2") + + # commit modified subtree + tree_hash2 = Gitex.put(tree_hash1, repo,"path1", Gitex.object(subtree_hash2, repo)) + |> Gitex.commit(repo, "master", "second commit") + + fun = fn -> + # print history of the master branch + Gitex.history("master", repo) + |> Stream.each(&IO.puts "* #{String.slice(&1.hash, 0, 7)}: #{&1.message}") + |> Stream.run + end + + assert capture_io(fun) == """ + * #{String.slice(tree_hash2, 0, 7)}: second commit + * #{String.slice(tree_hash1, 0, 7)}: first commit + """ + end + + defp gen_tmp_repo_name, do: "repo_#{:os.system_time(:nano_seconds)}" + + defp create_standard_git_repo do + repo_path = Path.join(System.tmp_dir!, gen_tmp_repo_name) + :ok = File.mkdir(repo_path) + Gitex.Git.init(repo_path) + end + + defp clean_standard_git_repo(%{home_dir: repo_dir}), do: File.rm_rf!(Path.dirname(repo_dir)) + +end