From 142d921755580edcccf99bc2594e80534c452918 Mon Sep 17 00:00:00 2001 From: Mario Flach Date: Fri, 15 Dec 2017 01:28:30 +0100 Subject: [PATCH] Add GitRekt.WireProtocol.Service See issues #1 and #8 for more details. --- apps/gitgud/lib/gitgud/ssh_server.ex | 10 ++-- .../controllers/git_backend_controller.ex | 5 +- apps/gitrekt/lib/gitrekt/wire_protocol.ex | 49 +-------------- .../lib/gitrekt/wire_protocol/receive_pack.ex | 39 ++++++++---- .../lib/gitrekt/wire_protocol/service.ex | 60 +++++++++++++++++++ .../lib/gitrekt/wire_protocol/upload_pack.ex | 34 +++++++---- 6 files changed, 121 insertions(+), 76 deletions(-) create mode 100644 apps/gitrekt/lib/gitrekt/wire_protocol/service.ex diff --git a/apps/gitgud/lib/gitgud/ssh_server.ex b/apps/gitgud/lib/gitgud/ssh_server.ex index fa72a711..568c3a5b 100644 --- a/apps/gitgud/lib/gitgud/ssh_server.ex +++ b/apps/gitgud/lib/gitgud/ssh_server.ex @@ -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 @@ -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 @@ -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 diff --git a/apps/gitgud_web/lib/gitgud_web/controllers/git_backend_controller.ex b/apps/gitgud_web/lib/gitgud_web/controllers/git_backend_controller.ex index fb16fc1d..27763919 100644 --- a/apps/gitgud_web/lib/gitgud_web/controllers/git_backend_controller.ex +++ b/apps/gitgud_web/lib/gitgud_web/controllers/git_backend_controller.ex @@ -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 @@ -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 diff --git a/apps/gitrekt/lib/gitrekt/wire_protocol.ex b/apps/gitrekt/lib/gitrekt/wire_protocol.ex index 407263a1..32d0d8fe 100644 --- a/apps/gitrekt/lib/gitrekt/wire_protocol.ex +++ b/apps/gitrekt/lib/gitrekt/wire_protocol.ex @@ -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() @@ -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`. """ @@ -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 diff --git a/apps/gitrekt/lib/gitrekt/wire_protocol/receive_pack.ex b/apps/gitrekt/lib/gitrekt/wire_protocol/receive_pack.ex index f7ba41ff..2c07822f 100644 --- a/apps/gitrekt/lib/gitrekt/wire_protocol/receive_pack.ex +++ b/apps/gitrekt/lib/gitrekt/wire_protocol/receive_pack.ex @@ -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) @@ -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) @@ -65,6 +79,7 @@ defmodule GitRekt.WireProtocol.ReceivePack do else: {handle, []} end + @impl true def run(%__MODULE__{state: :done} = handle) do {handle, []} end diff --git a/apps/gitrekt/lib/gitrekt/wire_protocol/service.ex b/apps/gitrekt/lib/gitrekt/wire_protocol/service.ex new file mode 100644 index 00000000..9ddc70bd --- /dev/null +++ b/apps/gitrekt/lib/gitrekt/wire_protocol/service.ex @@ -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 diff --git a/apps/gitrekt/lib/gitrekt/wire_protocol/upload_pack.ex b/apps/gitrekt/lib/gitrekt/wire_protocol/upload_pack.ex index fb52d62b..2aa80301 100644 --- a/apps/gitrekt/lib/gitrekt/wire_protocol/upload_pack.ex +++ b/apps/gitrekt/lib/gitrekt/wire_protocol/upload_pack.ex @@ -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) @@ -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