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

Commit

Permalink
Implement Xgit.Repository.put_loose_object/2. (#47)
Browse files Browse the repository at this point in the history
* Define new function in Xgit.Repository: put_loose_object/2.

Implement for Xgit.Repository.OnDisk.

Basic case now matches command-line git behaviour; needs more edge-case and error-case testing.

* Finish up edge-case handling.
  • Loading branch information
scouten committed Jul 24, 2019
1 parent f156c82 commit 687827b
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 2 deletions.
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

0 comments on commit 687827b

Please sign in to comment.