Skip to content

Commit

Permalink
Add GitRekt.ReceivePack and GitRekt.UploadPack
Browse files Browse the repository at this point in the history
See issue #1 for more details.
  • Loading branch information
redrabbit committed Dec 15, 2017
1 parent 80a6187 commit 8615286
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 113 deletions.
33 changes: 14 additions & 19 deletions apps/gitgud/lib/gitgud/ssh_server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ defmodule GitGud.SSHServer do
@behaviour :ssh_daemon_channel
@behaviour :ssh_server_key_api

defstruct [:conn, :chan, :user, :repo, :exec]
defstruct [:conn, :chan, :user, :exec]

@type t :: %__MODULE__{
conn: :ssh_connection.ssh_connection_ref,
chan: :ssh_connection.ssh_channel_id,
user: User.t,
repo: Repo.t
exec: Module.t
}

@doc """
Expand Down Expand Up @@ -87,27 +87,25 @@ defmodule GitGud.SSHServer do
end

@impl true
def handle_ssh_msg({:ssh_cm, conn, {:data, chan, _type, "0000"}}, %__MODULE__{conn: conn, chan: chan} = state) do
:ssh_connection.close(conn, chan)
{:ok, state}
end

@impl true
def handle_ssh_msg({:ssh_cm, conn, {:data, chan, _type, data}}, %__MODULE__{conn: conn, chan: chan, repo: repo, exec: exec} = state) do
:ssh_connection.send(conn, chan, execute(exec, repo, data))
:ssh_connection.send_eof(conn, chan)
:ssh_connection.close(conn, chan)
{:ok, state}
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)
if io, do: :ssh_connection.send(conn, chan, io)
if WireProtocol.done?(exec), do: :ssh_connection.close(conn, chan)
{:ok, %{state|exec: exec}}
end

@impl true
def handle_ssh_msg({:ssh_cm, conn, {:exec, chan, _reply, cmd}}, %__MODULE__{conn: conn, chan: chan, user: user} = state) do
[exec|args] = String.split(to_string(cmd))
[repo|_args] = parse_args(args)
[repo|args] = parse_args(args)
if has_permission?(user, repo, exec) do
{:ok, repo} = Git.repository_open(Repo.workdir(repo))
:ssh_connection.send(conn, chan, WireProtocol.reference_discovery(repo, exec))
{:ok, %{state|repo: repo, exec: exec}}
{exec, io} =
repo
|> WireProtocol.service(exec)
|> WireProtocol.flush()
if io, do: :ssh_connection.send(conn, chan, io)
{:ok, %{state|exec: exec}}
else
{:stop, chan, state}
end
Expand Down Expand Up @@ -154,7 +152,4 @@ defmodule GitGud.SSHServer do

defp has_permission?(user, repo, "git-upload-pack"), do: Repo.can_read?(user, repo)
defp has_permission?(user, repo, "git-receive-pack"), do: Repo.can_write?(user, repo)

defp execute("git-upload-pack", repo, data), do: WireProtocol.upload_pack(repo, data)
defp execute("git-receive-pack", repo, data), do: WireProtocol.receive_pack(repo, data)
end
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,14 @@ defmodule GitGud.Web.GitBackendController do

defp compressed?(conn), do: "gzip" in get_req_header(conn, "content-encoding")

defp git_command("git-upload-pack", handle, body), do: WireProtocol.upload_pack(handle, body)
defp git_command("git-receive-pack", handle, body), do: WireProtocol.receive_pack(handle, body)

defp git_info_refs(conn, repo, service) do
if has_permission?(conn, repo, service) do
{:ok, handle} = Git.repository_open(Repo.workdir(repo))
refs = WireProtocol.reference_discovery(handle, service)
refs = [WireProtocol.pkt_line("# service=#{service}"), WireProtocol.pkt_line] ++ refs
refs = WireProtocol.encode(["# service=#{service}", :flush] ++ refs)
conn
|> put_resp_content_type("application/x-#{service}-advertisement")
|> send_resp(:ok, Enum.join(refs))
|> send_resp(:ok, refs)
end
end

Expand All @@ -132,11 +129,18 @@ defmodule GitGud.Web.GitBackendController do
if has_permission?(conn, repo, service) do
with {:ok, body, conn} <- read_body(conn),
{:ok, handle} <- Git.repository_open(Repo.workdir(repo)) do
body = if compressed?(conn), do: :zlib.gunzip(body), else: body
data = if compressed?(conn), do: :zlib.gunzip(body), else: body
conn
|> put_resp_content_type("application/x-#{service}-result")
|> send_resp(:ok, git_command(service,handle, body))
|> send_resp(:ok, git_exec(handle, service, data))
end
end
end

defp git_exec(handle, exec, data) do
handle
|> WireProtocol.service(exec)
|> WireProtocol.next(data)
|> elem(1)
end
end
5 changes: 4 additions & 1 deletion apps/gitrekt/lib/gitrekt/packfile.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ defmodule GitRekt.Packfile do

alias GitRekt.Git

@type obj :: {Git.obj_type, binary}
@type obj_list :: [obj]

@doc """
Returns a *PACK* file for the given `oids` list.
"""
Expand All @@ -21,7 +24,7 @@ defmodule GitRekt.Packfile do
@doc """
Returns a list of ODB objects and their type for the given *PACK* `data`.
"""
@spec parse(binary) :: {[{Git.obj_type, binary}], binary}
@spec parse(binary) :: {obj_list, binary}
def parse("PACK" <> pack), do: parse(pack)
def parse(<<version::32, count::32, data::binary>> = _pack), do: unpack(version, count, data)

Expand Down
127 changes: 41 additions & 86 deletions apps/gitrekt/lib/gitrekt/wire_protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,39 +18,48 @@ defmodule GitRekt.WireProtocol do
|> List.flatten()
|> Enum.map(&format_ref_line/1)
|> List.update_at(0, &(&1 <> "\0" <> server_capabilities(service)))
|> Enum.map(&pkt_line/1)
|> Enum.concat([pkt_line()])
|> Enum.concat([:flush])
end

@doc """
Sends objects packed back to *git-fetch-pack*.
Returns a new service object for the given `repo` and `executable`.
"""
@spec upload_pack(Git.repo, binary) :: binary
def upload_pack(repo, pkt) do
case parse_upload_pkt(pkt) do
{:done, wants, _shallows, _haves, _caps} ->
encode(["NAK", Packfile.create(repo, wants)])
end
@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 """
Receives what is pushed into the repository.
Returns `true` if the service can read more data; elsewhise returns `false`.
"""
@spec receive_pack(Git.repo, binary) :: binary
def receive_pack(repo, pkt) do
{refs, pack, caps} = parse_receive_pkt(pkt)
{:ok, odb} = Git.repository_get_odb(repo)
Enum.each(pack, &apply_pack_obj(odb, &1))
Enum.each(refs, &rename_ref(repo, &1))
if "report-status" in caps,
do: encode(["unpack ok", Enum.into(refs, "", &"ok #{elem(&1, 2)}"), :flush]),
else: []
@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`.
"""
@spec encode(Enumerable.t) :: [binary]
@spec encode(Enumerable.t) :: iolist
def encode(lines) do
Enum.map(lines, &pkt_line/1)
end
Expand All @@ -69,6 +78,7 @@ defmodule GitRekt.WireProtocol do
@spec pkt_line(binary|:flush) :: binary
def pkt_line(data \\ :flush)
def pkt_line(:flush), do: "0000"
def pkt_line(:nak), do: pkt_line("NAK")
def pkt_line(<<"PACK", _rest::binary>> = pack), do: pack
def pkt_line(data) when is_binary(data) do
data
Expand All @@ -88,6 +98,8 @@ defmodule GitRekt.WireProtocol do
defp server_capabilities("git-upload-pack"), do: Enum.join(@upload_caps, " ")
defp server_capabilities("git-receive-pack"), do: Enum.join(@receive_caps, " ")

defp format_ref_line({oid, refname}), do: "#{Git.oid_fmt(oid)} #{refname}"

defp reference_head(repo) do
case Git.reference_resolve(repo, "HEAD") do
{:ok, _refname, _shorthand, oid} -> {oid, "HEAD"}
Expand Down Expand Up @@ -115,32 +127,18 @@ defmodule GitRekt.WireProtocol do
{:ok, tag_oid} <- Git.object_id(commit) do
[{tag_oid, refname}, {oid, refname <> "^{}"}]
else
{:ok, :commit, _commit} ->
{oid, refname}
{:ok, :commit, _commit} -> {oid, refname}
end
end

defp rename_ref(repo, {_old_oid, new_oid, refname}) do
Git.reference_create(repo, refname, :oid, new_oid, true)
end

defp apply_pack_obj(odb, {:delta_reference, {base_oid, _base_obj_size, _result_obj_size, cmds}}) do
{:ok, obj_type, obj_data} = Git.odb_read(odb, base_oid)
new_data = apply_delta_chain(obj_data, "", cmds)
{:ok, _oid} = apply_pack_obj(odb, {obj_type, new_data})
end

defp apply_pack_obj(odb, {obj_type, obj_data}) do
{:ok, _oid} = Git.odb_write(odb, obj_data, obj_type)
end

defp apply_delta_chain(_source, target, []), do: target
defp apply_delta_chain(source, target, [{:insert, chunk}|cmds]) do
apply_delta_chain(source, target <> chunk, cmds)
end
defp exec_mod("git-upload-pack"), do: Module.concat(__MODULE__, UploadPack)
defp exec_mod("git-receive-pack"), do: Module.concat(__MODULE__, ReceivePack)

defp apply_delta_chain(source, target, [{:copy, {offset, size}}|cmds]) do
apply_delta_chain(source, target <> binary_part(source, offset, size), cmds)
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
Expand All @@ -167,49 +165,6 @@ defmodule GitRekt.WireProtocol do
defp pkt_decode("done"), do: :done
defp pkt_decode("want " <> hash), do: {:want, hash}
defp pkt_decode("have " <> hash), do: {:have, hash}
defp pkt_decode("shallow " <> hash), do: {:shallow, hash}
defp pkt_decode(pkt_line), do: pkt_line

defp parse_upload_pkt(pkt) do
lines = decode(pkt)
{wants, lines} = Enum.split_while(lines, &upload_line_type?(&1, :want))
{wants, capabilities} = parse_upload_caps(wants)
{shallows, lines} = Enum.split_while(lines, &upload_line_type?(&1, :shallow))
[:flush|lines] = lines
{haves, lines} = Enum.split_while(lines, &upload_line_type?(&1, :have))
[last_line] = lines
{last_line, format_cmd_lines(wants), format_cmd_lines(shallows), format_cmd_lines(haves), capabilities}
end

defp parse_upload_caps([{obj_type, first_ref}|wants]) do
case String.split(first_ref, "\0", parts: 2) do
[first_ref] -> {[{obj_type, first_ref}|wants], []}
[first_ref, caps] -> {[{obj_type, first_ref}|wants], String.split(caps, " ", trim: true)}
end
end

defp parse_receive_pkt(pkt) do
lines = decode(pkt)
{refs, lines} = Enum.split_while(lines, &is_binary/1)
{refs, capabilities} = parse_receive_caps(refs)
[:flush|pack] = lines
{Enum.map(refs, &parse_receive_ref/1), pack, capabilities}
end

defp parse_receive_caps([first_ref|refs]) do
case String.split(first_ref, "\0", parts: 2) do
[first_ref] -> {[first_ref|refs], []}
[first_ref, caps] -> {[first_ref|refs], String.split(caps, " ", trim: true)}
end
end

defp parse_receive_ref(ref) do
[old, new, name] = String.split(ref)
{Git.oid_parse(old), Git.oid_parse(new), name}
end

defp upload_line_type?({type, _oid}, type), do: true
defp upload_line_type?(_line, _type), do: false

defp format_ref_line({oid, refname}), do: "#{Git.oid_fmt(oid)} #{refname}"
defp format_cmd_lines(lines), do: Enum.uniq(Enum.map(lines, &Git.oid_parse(elem(&1, 1))))
end
Loading

0 comments on commit 8615286

Please sign in to comment.