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

Implement Xgit.Repository.put_loose_object/2. #47

Merged
merged 2 commits into from
Jul 24, 2019
Merged
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
43 changes: 41 additions & 2 deletions lib/xgit/repository.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ defmodule Xgit.Repository do
information stored in a typical `.git` directory in a local repository. You will
be building an alternative to that storage mechanism.
"""

use GenServer

alias Xgit.Core.Object
alias Xgit.Util.GenServerUtils

require Logger

@typedoc ~S"""
Expand Down Expand Up @@ -78,9 +80,46 @@ defmodule Xgit.Repository do

def valid?(_), do: false

@doc ~S"""
Writes a loose object to the repository.

## Return Value

`:ok` if written successfully.

`{:error, "reason"}` if unable to write the object.
"""
@spec put_loose_object(repository :: t, object :: Object.t()) ::
:ok | {:error, reason :: String.t()}
def put_loose_object(repository, %Object{} = object) when is_pid(repository),
do: GenServer.call(repository, {:put_loose_object, object})

@doc ~S"""
Writes a loose object to the repository.

Called when `put_loose_object/2` is called.

## Return Value

Should return `:ok` if written successfully.

Should return `{:error, "reason"}` if unable to write the object.
"""
@callback handle_put_loose_object(state :: any, object :: Object.t()) ::
{:ok, state :: any} | {:error, reason :: String.t(), state :: any}

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

def handle_call({:put_loose_object, %Object{} = object}, _from, {mod, mod_state}) do
GenServerUtils.delegate_call_to(
mod,
:handle_put_loose_object,
[mod_state, object],
mod_state
)
end

def handle_call(message, _from, state) do
Logger.warn("Repository received unrecognized call #{inspect(message)}")
{:reply, {:error, :unknown_message}, state}
Expand All @@ -92,7 +131,7 @@ defmodule Xgit.Repository do

alias Xgit.Repository

# @behaviour Repository (not yet, but it will be)
@behaviour Repository
end
end
end
4 changes: 4 additions & 0 deletions lib/xgit/repository/on_disk.ex
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,8 @@ defmodule Xgit.Repository.OnDisk do
"""
@spec create(work_dir :: String.t()) :: :ok | {:error, reason :: String.t()}
defdelegate create(work_dir), to: Xgit.Repository.OnDisk.Create

@impl true
defdelegate handle_put_loose_object(state, object),
to: Xgit.Repository.OnDisk.PutLooseObject
end
66 changes: 66 additions & 0 deletions lib/xgit/repository/on_disk/put_loose_object.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
defmodule Xgit.Repository.OnDisk.PutLooseObject do
@moduledoc false
# Implements Xgit.Repository.OnDisk.handle_put_loose_object/2.

alias Xgit.Core.ContentSource
alias Xgit.Core.Object

@spec handle_put_loose_object(state :: any, object :: Object.t()) ::
{:ok, state :: any} | {:error, reason :: String.t(), state :: any}
def handle_put_loose_object(%{git_dir: git_dir} = state, %Object{id: id} = object) do
object_dir = Path.join([git_dir, "objects", String.slice(id, 0, 2)])
path = Path.join(object_dir, String.slice(id, 2, 38))

with {:mkdir, :ok} <-
{:mkdir, File.mkdir_p(object_dir)},
{:file, {:ok, :ok}} <-
{:file,
File.open(path, [:write, :binary, :exclusive], fn file_pid ->
deflate_and_write(file_pid, object)
end)} do
{:ok, state}
else
{:mkdir, _} ->
{:error, :cant_create_dir, state}

{:file, {:error, :eexist}} ->
{:error, :object_exists, state}
end
end

defp deflate_and_write(file, %Object{type: type, size: size, content: content}) do
z = :zlib.open()
:ok = :zlib.deflateInit(z, 1)

deflate_and_write_bytes(file, z, '#{type} #{size}')
deflate_and_write_bytes(file, z, [0])

if is_list(content) do
deflate_and_write_bytes(file, z, content, :finish)
else
deflate_content(file, z, content)
deflate_and_write_bytes(file, z, [], :finish)
end

:zlib.deflateEnd(z)
end

defp deflate_content(file, z, content) do
content
|> ContentSource.stream()
|> Stream.each(fn chunk ->
deflate_and_write_bytes(file, z, [chunk])
end)
|> Stream.run()
end

defp deflate_and_write_bytes(file, z, bytes, flush \\ :none) do
compressed =
z
|> :zlib.deflate(bytes, flush)
|> Enum.join()
|> :binary.bin_to_list()

IO.write(file, compressed)
end
end
85 changes: 85 additions & 0 deletions test/xgit/repository/on_disk/put_loose_object_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
defmodule Xgit.Repository.OnDisk.PutLooseObjectTest do
use Xgit.GitInitTestCase, async: true

alias Xgit.Core.ContentSource
alias Xgit.Core.FileContentSource
alias Xgit.Core.Object
alias Xgit.Repository
alias Xgit.Repository.OnDisk

import FolderDiff

describe "put_loose_object/2" do
@test_content 'test content\n'
@test_content_id "d670460b4b4aece5915caf5c68d12f560a9fe3e4"

test "happy path matches command-line git (small file)", %{ref: ref, xgit: xgit} do
Temp.track!()
path = Temp.path!()
File.write!(path, "test content\n")

{output, 0} = System.cmd("git", ["hash-object", "-w", path], cd: ref)
assert String.trim(output) == @test_content_id

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

object = %Object{type: :blob, content: @test_content, size: 13, id: @test_content_id}
assert :ok = Repository.put_loose_object(repo, object)

assert_folders_are_equal(ref, xgit)
end

test "happy path matches command-line git (large file)", %{ref: ref, xgit: xgit} do
Temp.track!()
path = Temp.path!()

content =
1..1000
|> Enum.map(fn _ -> "foobar" end)
|> Enum.join()

File.write!(path, content)

{output, 0} = System.cmd("git", ["hash-object", "-w", path], cd: ref)
content_id = String.trim(output)

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

fcs = FileContentSource.new(path)
object = %Object{type: :blob, content: fcs, size: ContentSource.length(fcs), id: content_id}
assert :ok = Repository.put_loose_object(repo, object)

assert_folders_are_equal(ref, xgit)
end

test "error: can't create objects dir", %{xgit: xgit} do
assert :ok = OnDisk.create(xgit)
assert {:ok, repo} = OnDisk.start_link(work_dir: xgit)

objects_dir = Path.join([xgit, ".git", "objects", String.slice(@test_content_id, 0, 2)])
File.mkdir_p!(Path.join([xgit, ".git", "objects"]))
File.write!(objects_dir, "sand in the gears")

object = %Object{type: :blob, content: @test_content, size: 13, id: @test_content_id}
assert {:error, :cant_create_dir} = Repository.put_loose_object(repo, object)
end

test "error: object exists already", %{xgit: xgit} do
assert :ok = OnDisk.create(xgit)
assert {:ok, repo} = OnDisk.start_link(work_dir: xgit)

objects_dir = Path.join([xgit, ".git", "objects", String.slice(@test_content_id, 0, 2)])
File.mkdir_p!(objects_dir)

File.write!(
Path.join(objects_dir, String.slice(@test_content_id, 2, 38)),
"sand in the gears"
)

object = %Object{type: :blob, content: @test_content, size: 13, id: @test_content_id}
assert {:error, :object_exists} = Repository.put_loose_object(repo, object)
end
end
end