Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions lib/server_git.ex
Original file line number Diff line number Diff line change
@@ -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


158 changes: 158 additions & 0 deletions test/server_git_test.exs
Original file line number Diff line number Diff line change
@@ -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