Skip to content
This repository has been archived by the owner on Oct 8, 2020. It is now read-only.

Commit

Permalink
Merge 7d0a487 into 6da8d94
Browse files Browse the repository at this point in the history
  • Loading branch information
scouten committed Jul 13, 2019
2 parents 6da8d94 + 7d0a487 commit 81ae9c4
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 6 deletions.
98 changes: 98 additions & 0 deletions lib/xgit/repository.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
defmodule Xgit.Repository do
@moduledoc ~S"""
Represents an abstract git repository.
## Design Goals
Xgit intends to allow repositories to be stored in multiple different mechanisms.
While it includes built-in support for local on-disk repositories
(see `Xgit.Repository.OnDisk`), you could envision repositories stored entirely
in memory, or on a remote file system or database.
In Elixir/OTP terms, a repository is both a `GenServer` and a behaviour module.
In git terms, a repository (and thus any module that implements the behaviours
defined here) is primary a **plumbing** module.
The git **porcelain** implementations are largely implemented in a way that is
intended to be agnostic with regard to storage. They are largely implemented
in separate "command" modules. (See `Xgit.Command.*`, though no such module
exists as of yet.)
## Implementing a Storage Architecture
To define a new mechanism for storing a git repo, start by creating a new module
that `use`s this module and implements the required callbacks. Consider the
information stored in a typical `.git` directory in a local repository. You will
be building an alternative to that storage mechanism.
"""

use GenServer

require Logger

@typedoc ~S"""
The process ID for a `Repository` process.
"""
@type t :: pid

@doc """
Starts a `Repository` process linked to the current process.
_IMPORTANT:_ You should not invoke this function directly unless you are
implementing a new storage implementation module that implements this behaviour.
## Parameters
`module` is the name of a module that implements the callbacks defined in this module.
`init_arg` is passed to the `init/1` function of `module`.
`options` are passed to `GenServer.start_link/3`.
## Return Value
See `GenServer.start_link/3`.
"""
@spec start_link(module :: module, init_arg :: term, GenServer.options()) ::
GenServer.on_start()
def start_link(module, init_arg, options) when is_atom(module) and is_list(options),
do: GenServer.start_link(__MODULE__, {module, init_arg}, options)

@impl true
def init({mod, mod_init_arg}) do
case mod.init(mod_init_arg) do
{:ok, state} -> {:ok, {mod, state}}
{:stop, reason} -> {:stop, reason}
end
end

@doc ~S"""
Returns `true` if the argument is a PID representing a valid `Repository` process.
"""
@spec valid?(repository :: term) :: boolean
def valid?(repository) when is_pid(repository),
do:
Process.alive?(repository) &&
GenServer.call(repository, :valid_repository?) == :valid_repository

def valid?(_), do: false

@impl true
def handle_call(:valid_repository?, _from, state), do: {:reply, :valid_repository, state}

def handle_call(message, _from, state) do
Logger.warn("Repository received unrecognized call #{inspect(message)}")
{:reply, {:error, :unknown_message}, state}
end

defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
use GenServer, opts

alias Xgit.Repository

# @behaviour Repository (not yet, but it will be)
end
end
end
41 changes: 36 additions & 5 deletions lib/xgit/repository/on_disk.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,52 @@ defmodule Xgit.Repository.OnDisk do
@moduledoc ~S"""
Implementation of `Xgit.Repository` that stores content on the local file system.
**IMPORTANT NOTE:** This is intended as a reference implementation largely
_IMPORTANT NOTE:_ This is intended as a reference implementation largely
for testing purposes and may not necessarily handle all of the edge cases that
the traditional `git` command-line interface will handle.
That said, it does intentionally use the same `.git` folder format as command-line
git so that results may be compared for similar operations.
"""

use Xgit.Repository

@doc ~S"""
Start an on-disk git repository.
## Options
* `:work_dir` (required): Top-level working directory. A `.git` directory should
exist at this path. Use `create/1` to create an empty on-disk repository if
necessary.
Any other options are passed through to `GenServer.start_link/3`.
## Return Value
See `GenServer.start_link/3`.
Use the functions in `Xgit.Repository` to interact with this repository process.
"""
@spec start_link(opts :: Keyword.t()) :: GenServer.on_start()
def start_link(opts \\ []), do: Repository.start_link(__MODULE__, opts, opts)

@impl GenServer
def init(opts) when is_list(opts) do
with work_dir when is_binary(work_dir) <- Keyword.get(opts, :work_dir) do
{:ok, %{work_dir: work_dir}}
else
_ -> {:stop, :missing_arguments}
end
end

@doc ~S"""
Creates a new, empty git repository on the local file system.
Analogous to [`git init`](https://git-scm.com/docs/git-init).
**NOTE:** We use the name `create` here so as to avoid a naming conflict with
the `GenServer` callback named `init/1`.
_NOTE:_ We use the name `create` here so as to avoid a naming conflict with
`c:GenServer.init/1`.
## Options
Expand All @@ -33,6 +64,6 @@ defmodule Xgit.Repository.OnDisk do
Will raise `File.Error` or similar if unable to create the directory.
"""
@spec create(work_dir: String.t()) :: :ok
defdelegate create(opts), to: Xgit.Repository.OnDisk.Create
@spec create!(work_dir: String.t()) :: :ok
defdelegate create!(opts), to: Xgit.Repository.OnDisk.Create
end
2 changes: 1 addition & 1 deletion lib/xgit/repository/on_disk/create.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Xgit.Repository.OnDisk.Create do
@moduledoc false
# Implements Xgit.Repository.OnDisk.create/1.

def create(opts) when is_list(opts) do
def create!(opts) when is_list(opts) do
work_dir = Keyword.get(opts, :work_dir)

unless is_binary(work_dir) do
Expand Down
33 changes: 33 additions & 0 deletions test/xgit/repository/on_disk_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule Xgit.Repository.OnDiskTest do
use Xgit.GitInitTestCase, async: true

alias Xgit.Repository
alias Xgit.Repository.OnDisk

import ExUnit.CaptureLog

describe "start_link/1" do
test "happy path: starts and is valid", %{xgit: xgit} do
assert :ok = OnDisk.create(work_dir: xgit)

assert {:ok, repo} = OnDisk.start_link(work_dir: xgit)
assert is_pid(repo)

assert Repository.valid?(repo)
end

test "handles unknown message", %{xgit: xgit} do
assert :ok = OnDisk.create(work_dir: xgit)
assert {:ok, repo} = OnDisk.start_link(work_dir: xgit)

assert capture_log(fn ->
assert {:error, :unknown_message} = GenServer.call(repo, :random_unknown_message)
end) =~ "Repository received unrecognized call :random_unknown_message"
end

test "error: missing work_dir" do
Process.flag(:trap_exit, true)
assert {:error, :missing_arguments} = OnDisk.start_link([])
end
end
end

0 comments on commit 81ae9c4

Please sign in to comment.