Skip to content

Commit

Permalink
Add GitRekt.WireProtocol.Service
Browse files Browse the repository at this point in the history
See issues #1 and #8 for more details.
  • Loading branch information
redrabbit committed Dec 15, 2017
1 parent 8615286 commit 142d921
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 76 deletions.
10 changes: 5 additions & 5 deletions apps/gitgud/lib/gitgud/ssh_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ defmodule GitGud.SSHServer do
alias GitGud.RepoQuery

alias GitRekt.Git
alias GitRekt.WireProtocol
alias GitRekt.WireProtocol.Service

@behaviour :ssh_daemon_channel
@behaviour :ssh_server_key_api
Expand Down Expand Up @@ -88,9 +88,9 @@ defmodule GitGud.SSHServer do

@impl true
def handle_ssh_msg({:ssh_cm, conn, {:data, chan, _type, data}}, %__MODULE__{conn: conn, chan: chan, exec: exec} = state) do
{exec, io} = WireProtocol.next(exec, data)
{exec, io} = Service.next(exec, data)
if io, do: :ssh_connection.send(conn, chan, io)
if WireProtocol.done?(exec), do: :ssh_connection.close(conn, chan)
if Service.done?(exec), do: :ssh_connection.close(conn, chan)
{:ok, %{state|exec: exec}}
end

Expand All @@ -102,8 +102,8 @@ defmodule GitGud.SSHServer do
{:ok, repo} = Git.repository_open(Repo.workdir(repo))
{exec, io} =
repo
|> WireProtocol.service(exec)
|> WireProtocol.flush()
|> Service.new(exec)
|> Service.flush()
if io, do: :ssh_connection.send(conn, chan, io)
{:ok, %{state|exec: exec}}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ defmodule GitGud.Web.GitBackendController do

alias GitRekt.Git
alias GitRekt.WireProtocol
alias GitRekt.WireProtocol.Service

alias GitGud.User
alias GitGud.Repo
Expand Down Expand Up @@ -139,8 +140,8 @@ defmodule GitGud.Web.GitBackendController do

defp git_exec(handle, exec, data) do
handle
|> WireProtocol.service(exec)
|> WireProtocol.next(data)
|> Service.new(exec)
|> Service.next(data)
|> elem(1)
end
end
49 changes: 2 additions & 47 deletions apps/gitrekt/lib/gitrekt/wire_protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ defmodule GitRekt.WireProtocol do
@receive_caps ~w(report-status delete-refs)

@doc """
Returns a *PKT-LINE* stream describing each ref and it current value.
Returns a stream describing each ref and it current value.
"""
@spec reference_discovery(Git.repo, binary) :: [binary]
@spec reference_discovery(Git.repo, binary) :: iolist
def reference_discovery(repo, service) do
[reference_head(repo), reference_list(repo), reference_tags(repo)]
|> List.flatten()
Expand All @@ -21,41 +21,6 @@ defmodule GitRekt.WireProtocol do
|> Enum.concat([:flush])
end

@doc """
Returns a new service object for the given `repo` and `executable`.
"""
@spec service(Git.repo, binary) :: struct
def service(repo, executable) do
struct(exec_mod(executable), repo: repo)
end

@doc """
Transist the `service` struct to the next state by parsing the given `data`.
"""
@spec next(struct, binary) :: {struct, iolist}
def next(service, data) do
service
|> read_all(Enum.to_list(decode(data)))
|> flush()
end

@doc """
Returns `true` if the service can read more data; elsewhise returns `false`.
"""
@spec done?(struct) :: boolean
def done?(service), do: service.state == :done

@doc """
Flushes the server response for the given `service` struct.
"""
@spec flush(Module.t) :: {Module.t, iolist}
def flush(service) do
case apply(service.__struct__, :run, [service]) do
{handle, []} -> {handle, nil}
{handle, io} -> {handle, encode(io)}
end
end

@doc """
Returns an *PKT-LINE* encoded representation of the given `lines`.
"""
Expand Down Expand Up @@ -131,16 +96,6 @@ defmodule GitRekt.WireProtocol do
end
end

defp exec_mod("git-upload-pack"), do: Module.concat(__MODULE__, UploadPack)
defp exec_mod("git-receive-pack"), do: Module.concat(__MODULE__, ReceivePack)

defp read_all(service, lines) do
case apply(service.__struct__, :next, [service, lines]) do
{handle, []} -> handle
{handle, lines} -> read_all(handle, lines)
end
end

defp pkt_stream(data) do
Stream.resource(fn -> data end, &pkt_next/1, fn _ -> :ok end)
end
Expand Down
39 changes: 27 additions & 12 deletions apps/gitrekt/lib/gitrekt/wire_protocol/receive_pack.ex
Original file line number Diff line number Diff line change
@@ -1,33 +1,40 @@
defmodule GitRekt.WireProtocol.ReceivePack do
@moduledoc """
Module implementing the `git-receive-pack` command.
"""

import GitRekt.WireProtocol, only: [reference_discovery: 2]
@behaviour GitRekt.WireProtocol.Service

alias GitRekt.Git
alias GitRekt.Packfile

defstruct [:repo, state: :disco, caps: [], cmds: [], pack: []]
import GitRekt.WireProtocol, only: [reference_discovery: 2]

@type state :: :disco | :update_req | :pack | :done
@type command :: :create | :update | :delete
defstruct [:repo, state: :disco, caps: [], cmds: [], pack: []]

@type update_command :: {command, Git.oid, Git.oid, binary}
@type update_list :: [update_command]
@type cmd :: {
:create | :update | :delete,
Git.oid,
Git.oid,
}

@type t :: %__MODULE__{
state: state,
repo: Git.repo,
state: :disco | :upload_wants | :upload_haves | :done,
caps: [binary],
cmds: update_list,
pack: Packfile.obj_list
cmds: [],
pack: Packfile.obj_list,
}

@spec next(t, [term]) :: {t, [term]}
#
# Callbacks
#

@impl true
def next(%__MODULE__{state: :disco} = handle, [:flush] = _lines) do
{struct(handle, state: :done), []}
end

@impl true
def next(%__MODULE__{state: :disco} = handle, lines) do
{_shallows, lines} = Enum.split_while(lines, &obj_match?(&1, :shallow))
{cmds, lines} = Enum.split_while(lines, &is_binary/1)
Expand All @@ -36,26 +43,33 @@ defmodule GitRekt.WireProtocol.ReceivePack do
{struct(handle, state: :update_req, caps: caps, cmds: parse_cmds(cmds)), lines}
end

@impl true
def next(%__MODULE__{state: :update_req} = handle, lines) do
{struct(handle, state: :pack, pack: lines), []}
end

@impl true
def next(%__MODULE__{state: :pack} = handle, []) do
{handle, []}
end

@impl true
def next(%__MODULE__{state: :pack}, _lines), do: raise "Nothing should be run after :pack"

@impl true
def next(%__MODULE__{state: :done}, _lines), do: raise "Cannot call next/2 when state == :done"

@spec run(t) :: {t, [binary]}
@impl true
def run(%__MODULE__{state: :disco} = handle) do
{handle, reference_discovery(handle.repo, "git-receive-pack")}
end

@impl true
def run(%__MODULE__{state: :update_req} = handle) do
{handle, []}
end

@impl true
def run(%__MODULE__{state: :pack} = handle) do
:ok = apply_pack(handle.repo, handle.pack)
:ok = apply_cmds(handle.repo, handle.cmds)
Expand All @@ -65,6 +79,7 @@ defmodule GitRekt.WireProtocol.ReceivePack do
else: {handle, []}
end

@impl true
def run(%__MODULE__{state: :done} = handle) do
{handle, []}
end
Expand Down
60 changes: 60 additions & 0 deletions apps/gitrekt/lib/gitrekt/wire_protocol/service.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
defmodule GitRekt.WireProtocol.Service do
@moduledoc """
Behaviour for implementing Git server-side commands.
See `GitRekt.WireProtocol.UploadPack` and `GitRekt.WireProtocol.ReceivePack` for more details.
"""

import GitRekt.WireProtocol, only: [encode: 1, decode: 1]

@callback run(struct) :: {struct, [term]}
@callback next(struct, [term]) :: {struct, [term]}

@doc """
Returns a new service object for the given `repo` and `executable`.
"""
@spec new(Git.repo, binary) :: struct
def new(repo, executable) do
struct(exec_mod(executable), repo: repo)
end

@doc """
Transist the `service` struct to the next state by parsing the given `data`.
"""
@spec next(struct, binary) :: {struct, iolist}
def next(service, data) do
lines = Enum.to_list(decode(data))
flush(read_all(service, lines))
end

@doc """
Returns `true` if the service can read more data; elsewhise returns `false`.
"""
@spec done?(struct) :: boolean
def done?(service), do: service.state == :done

@doc """
Flushes the server response for the given `service` struct.
"""
@spec flush(Module.t) :: {Module.t, iolist}
def flush(service) do
case apply(service.__struct__, :run, [service]) do
{handle, []} -> {handle, nil}
{handle, io} -> {handle, encode(io)}
end
end

#
# Helpers
#

defp exec_mod("git-upload-pack"), do: GitRekt.WireProtocol.UploadPack
defp exec_mod("git-receive-pack"), do: GitRekt.WireProtocol.ReceivePack

defp read_all(service, lines) do
case apply(service.__struct__, :next, [service, lines]) do
{handle, []} -> handle
{handle, lines} -> read_all(handle, lines)
end
end
end
34 changes: 24 additions & 10 deletions apps/gitrekt/lib/gitrekt/wire_protocol/upload_pack.ex
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
defmodule GitRekt.WireProtocol.UploadPack do
@moduledoc """
Module implementing the `git-upload-pack` command.
"""

import GitRekt.WireProtocol, only: [reference_discovery: 2]
@behaviour GitRekt.WireProtocol.Service

alias GitRekt.Git
alias GitRekt.Packfile

defstruct [:repo, state: :disco, caps: [], wants: [], haves: []]
import GitRekt.Packfile, only: [create: 2]
import GitRekt.WireProtocol, only: [reference_discovery: 2]

@type state :: :disco | :upload_wants | :upload_haves | :done
defstruct [:repo, state: :disco, caps: [], wants: [], haves: []]

@type t :: %__MODULE__{
state: state,
repo: Git.repo,
state: :disco | :upload_wants | :upload_haves | :done,
caps: [binary],
wants: [Git.oid],
haves: [Git.oid]
haves: [Git.oid],
}

@spec next(t, [term]) :: {t, [term]}
def next(%__MODULE__{state: :disco} = handle, [:flush] = _lines) do
#
# Callbacks
#

@impl true
def next(%__MODULE__{state: :disco} = handle, [:flush]) do
{struct(handle, state: :done), []}
end

@impl true
def next(%__MODULE__{state: :disco} = handle, lines) do
{wants, lines} = Enum.split_while(lines, &obj_match?(&1, :want))
{caps, wants} = parse_caps(wants)
Expand All @@ -32,31 +38,39 @@ defmodule GitRekt.WireProtocol.UploadPack do
{struct(handle, state: :upload_wants, caps: caps, wants: parse_cmds(wants)), lines}
end

@impl true
def next(%__MODULE__{state: :upload_wants} = handle, lines) do
{haves, lines} = Enum.split_while(lines, &obj_match?(&1, :have))
{struct(handle, state: :upload_haves, haves: parse_cmds(haves)), lines}
end

@impl true
def next(%__MODULE__{state: :upload_haves} = handle, [:done]) do
{handle, []}
end

@impl true
def next(%__MODULE__{state: :upload_haves}, _lines), do: raise "Nothing should be run after :upload_haves"

@impl true
def next(%__MODULE__{state: :done}, _lines), do: raise "Cannot call next/2 when state == :done"

@spec run(t) :: {t, [binary]}
@impl true
def run(%__MODULE__{state: :disco} = handle) do
{handle, reference_discovery(handle.repo, "git-upload-pack")}
end

@impl true
def run(%__MODULE__{state: :upload_wants} = handle) do
{handle, []}
end

@impl true
def run(%__MODULE__{state: :upload_haves} = handle) do
{struct(handle, state: :done), [:nak, Packfile.create(handle.repo, handle.wants)]}
{struct(handle, state: :done), [:nak, create(handle.repo, handle.wants)]}
end

@impl true
def run(%__MODULE__{state: :done} = handle) do
{handle, []}
end
Expand Down

0 comments on commit 142d921

Please sign in to comment.