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

Commit

Permalink
Implement Xgit.Core.Tree.from_object/1. (#164)
Browse files Browse the repository at this point in the history
* Implement Xgit.Core.Tree.from_object/1.

* Test-internal function shouldn't be marked public.

* Cover not-a-tree case.

* More test coverage.

* More test coverage.

* More coverage.

* Last bit of coverage?

*  Cover invalid file mode case.
  • Loading branch information
scouten committed Sep 10, 2019
1 parent c800c86 commit 98db06a
Show file tree
Hide file tree
Showing 2 changed files with 242 additions and 0 deletions.
79 changes: 79 additions & 0 deletions lib/xgit/core/tree.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ defmodule Xgit.Core.Tree do
@moduledoc ~S"""
Represents a git `tree` object in memory.
"""
alias Xgit.Core.ContentSource
alias Xgit.Core.FileMode
alias Xgit.Core.FilePath
alias Xgit.Core.Object
alias Xgit.Core.ObjectId

Expand Down Expand Up @@ -117,6 +119,83 @@ defmodule Xgit.Core.Tree do

defp entries_sorted?([_]), do: cover(true)

@typedoc ~S"""
Error response codes returned by `from_object/1`.
"""
@type from_object_reason :: :not_a_tree | :invalid_format | :invalid_tree

@doc ~S"""
Renders a tree structure from an `Xgit.Core.Object`.
## Return Values
`{:ok, tree}` if the object contains a valid `tree` object.
`{:error, :not_a_tree}` if the object contains an object of a different type.
`{:error, :invalid_format}` if the object says that is of type `tree`, but
can not be parsed as such.
`{:error, :invalid_tree}` if the object can be parsed as a tree, but the
entries are not well formed or not properly sorted.
"""
@spec from_object(object :: Object.t()) :: {:ok, tree :: t} | {:error, from_object_reason}
def from_object(object)

def from_object(%Object{type: :tree, content: content} = _object) do
content
|> ContentSource.stream()
|> Enum.to_list()
|> from_object_internal([])
end

def from_object(%Object{} = _object), do: cover({:error, :not_a_tree})

defp from_object_internal(data, entries_acc)

defp from_object_internal([], entries_acc) do
tree = %__MODULE__{entries: Enum.reverse(entries_acc)}

if valid?(tree) do
cover {:ok, tree}
else
cover {:error, :invalid_tree}
end
end

defp from_object_internal(data, entries_acc) do
with {:ok, file_mode, data} <- parse_file_mode(data, 0),
true <- FileMode.valid?(file_mode),
{name, [0 | data]} <- path_and_object_id(data),
:ok <- FilePath.check_path_segment(name),
{raw_object_id, data} <- Enum.split(data, 20),
20 <- Enum.count(raw_object_id),
false <- Enum.all?(raw_object_id, &(&1 == 0)) do
this_entry = %__MODULE__.Entry{
name: name,
mode: file_mode,
object_id: ObjectId.from_binary_iodata(raw_object_id)
}

from_object_internal(data, [this_entry | entries_acc])
else
_ -> cover {:error, :invalid_format}
end
end

defp parse_file_mode([], _mode), do: cover({:error, :invalid_mode})

defp parse_file_mode([?\s | data], mode), do: cover({:ok, mode, data})

defp parse_file_mode([?0 | _data], 0), do: cover({:error, :invalid_mode})

defp parse_file_mode([c | data], mode) when c >= ?0 and c <= ?7,
do: parse_file_mode(data, mode * 8 + (c - ?0))

defp parse_file_mode([_c | _data], _mode), do: cover({:error, :invalid_mode})

defp path_and_object_id(data), do: Enum.split_while(data, &(&1 != 0))

@doc ~S"""
Renders this tree structure into a corresponding `Xgit.Core.Object`.
"""
Expand Down
163 changes: 163 additions & 0 deletions test/xgit/core/tree_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,169 @@ defmodule Xgit.Core.TreeTest do
end
end

describe "from_object/1" do
setup do
Temp.track!()
repo = Temp.mkdir!()

{_output, 0} = System.cmd("git", ["init"], cd: repo)
objects_dir = Path.join([repo, ".git", "objects"])

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

{:ok, repo: repo, objects_dir: objects_dir, xgit: xgit}
end

defp write_git_tree_and_read_xgit_tree_entries(repo, xgit) do
{output, 0} = System.cmd("git", ["write-tree", "--missing-ok"], cd: repo)
tree_id = String.trim(output)

assert {:ok, %Object{} = object} = Repository.get_object(xgit, tree_id)
assert {:ok, %Tree{entries: entries} = _tree} = Tree.from_object(object)

entries
end

test "empty tree", %{repo: repo, xgit: xgit} do
assert write_git_tree_and_read_xgit_tree_entries(repo, xgit) == []
end

test "tree with one entry", %{repo: repo, xgit: xgit} do
{_output, 0} =
System.cmd(
"git",
[
"update-index",
"--add",
"--cacheinfo",
"100644",
"18832d35117ef2f013c4009f5b2128dfaeff354f",
"hello.txt"
],
cd: repo
)

assert write_git_tree_and_read_xgit_tree_entries(repo, xgit) == [
%Entry{
name: 'hello.txt',
object_id: "18832d35117ef2f013c4009f5b2128dfaeff354f",
mode: 0o100644
}
]
end

test "tree with multiple entries", %{repo: repo, xgit: xgit} do
{_output, 0} =
System.cmd(
"git",
[
"update-index",
"--add",
"--cacheinfo",
"100644",
"18832d35117ef2f013c4009f5b2128dfaeff354f",
"hello.txt"
],
cd: repo
)

{_output, 0} =
System.cmd(
"git",
[
"update-index",
"--add",
"--cacheinfo",
"100755",
"d670460b4b4aece5915caf5c68d12f560a9fe3e4",
"test_content.txt"
],
cd: repo
)

assert write_git_tree_and_read_xgit_tree_entries(repo, xgit) == [
%Entry{
name: 'hello.txt',
object_id: "18832d35117ef2f013c4009f5b2128dfaeff354f",
mode: 0o100644
},
%Entry{
name: 'test_content.txt',
object_id: "d670460b4b4aece5915caf5c68d12f560a9fe3e4",
mode: 0o100755
}
]
end

test "object is not a tree" do
object = %Object{
type: :blob,
content: 'test content\n',
size: 13,
id: "d670460b4b4aece5915caf5c68d12f560a9fe3e4"
}

assert {:error, :not_a_tree} = Tree.from_object(object)
end

test "object is an invalid tree (ends after file mode)" do
object = %Object{
type: :tree,
size: 42,
id: "d670460b4b4aece5915caf5c68d12f560a9fe3e4",
content: '100644'
}

assert {:error, :invalid_format} = Tree.from_object(object)
end

test "object is an invalid tree (invalid file mode)" do
object = %Object{
type: :tree,
size: 42,
id: "d670460b4b4aece5915caf5c68d12f560a9fe3e4",
content: '100648 A 12345678901234567890'
}

assert {:error, :invalid_format} = Tree.from_object(object)
end

test "object is an invalid tree (invalid file mode, leading 0)" do
object = %Object{
type: :tree,
size: 42,
id: "d670460b4b4aece5915caf5c68d12f560a9fe3e4",
content: '0100644 A 12345678901234567890'
}

assert {:error, :invalid_format} = Tree.from_object(object)
end

test "object is an invalid tree (not properly sorted)" do
object = %Object{
type: :tree,
size: 42,
id: "d670460b4b4aece5915caf5c68d12f560a9fe3e4",
content:
'100644 B' ++
Enum.map(0..20, fn x -> x end) ++ '100644 A' ++ Enum.map(0..20, fn x -> x end)
}

assert {:error, :invalid_tree} = Tree.from_object(object)
end

test "object is a badly-formatted tree" do
object = %Object{
type: :tree,
size: 42,
id: "d670460b4b4aece5915caf5c68d12f560a9fe3e4",
content: '100644 A' ++ Enum.map(0..20, fn _ -> 0 end)
}

assert {:error, :invalid_format} = Tree.from_object(object)
end
end

describe "to_object/1" do
test "empty tree" do
assert_same_output(
Expand Down

0 comments on commit 98db06a

Please sign in to comment.