Current section

Files

Jump to
xgit lib xgit core commit.ex
Raw

lib/xgit/core/commit.ex

defmodule Xgit.Core.Commit do
@moduledoc ~S"""
Represents a git `commit` object in memory.
"""
alias Xgit.Core.ContentSource
alias Xgit.Core.Object
alias Xgit.Core.ObjectId
alias Xgit.Core.PersonIdent
import Xgit.Util.ForceCoverage
import Xgit.Util.ParseHeader, only: [next_header: 1]
@typedoc ~S"""
This struct describes a single `commit` object so it can be manipulated in memory.
## Struct Members
* `:tree`: (`Xgit.Core.ObjectId`) tree referenced by this commit
* `:parents`: (list of `Xgit.Core.ObjectId`) parent(s) of this commit
* `:author`: (`Xgit.Core.PersonIdent`) author of this commit
* `:committer`: (`Xgit.Core.PersonIdent`) committer for this commit
* `:message`: (bytelist) user-entered commit message (encoding unspecified)
**TO DO:** Support signatures and other extensions.
https://github.com/elixir-git/xgit/issues/202
"""
@type t :: %__MODULE__{
tree: ObjectId.t(),
parents: [ObjectId.t()],
author: PersonIdent.t(),
committer: PersonIdent.t(),
message: [byte]
}
@enforce_keys [:tree, :author, :committer, :message]
defstruct [:tree, :author, :committer, :message, parents: []]
@doc ~S"""
Return `true` if the value is a commit struct that is valid.
"""
@spec valid?(commit :: any) :: boolean
def valid?(commit)
def valid?(%__MODULE__{
tree: tree,
parents: parents,
author: %PersonIdent{} = author,
committer: %PersonIdent{} = committer,
message: message
})
when is_binary(tree) and is_list(parents) and is_list(message) do
ObjectId.valid?(tree) &&
Enum.all?(parents, &ObjectId.valid?(&1)) &&
PersonIdent.valid?(author) &&
PersonIdent.valid?(committer) &&
not Enum.empty?(message)
end
def valid?(_), do: cover(false)
@typedoc ~S"""
Error response codes returned by `from_object/1`.
"""
@type from_object_reason :: :not_a_commit | :invalid_commit
@doc ~S"""
Renders a commit structure from an `Xgit.Core.Object`.
## Return Values
`{:ok, commit}` if the object contains a valid `commit` object.
`{:error, :not_a_commit}` if the object contains an object of a different type.
`{:error, :invalid_commit}` if the object says that is of type `commit`, but
can not be parsed as such.
"""
@spec from_object(object :: Object.t()) :: {:ok, commit :: t} | {:error, from_object_reason}
def from_object(object)
def from_object(%Object{type: :commit, content: content} = _object) do
content
|> ContentSource.stream()
|> Enum.to_list()
|> from_object_internal()
end
def from_object(%Object{} = _object), do: cover({:error, :not_a_commit})
defp from_object_internal(data) do
with {:tree, {'tree', tree_id_str, data}} <- {:tree, next_header(data)},
{:tree_id, {tree_id, []}} <- {:tree_id, ObjectId.from_hex_charlist(tree_id_str)},
{:parents, {parents, data}} when is_list(data) <-
{:parents, read_parents(data, [])},
{:author, {'author', author_str, data}} <- {:author, next_header(data)},
{:author_id, %PersonIdent{} = author} <-
{:author_id, PersonIdent.from_byte_list(author_str)},
{:committer, {'committer', committer_str, data}} <-
{:committer, next_header(data)},
{:committer_id, %PersonIdent{} = committer} <-
{:committer_id, PersonIdent.from_byte_list(committer_str)},
message when is_list(message) <- drop_if_lf(data) do
# TO DO: Support signatures and other extensions.
# https://github.com/elixir-git/xgit/issues/202
cover {:ok,
%__MODULE__{
tree: tree_id,
parents: parents,
author: author,
committer: committer,
message: message
}}
else
_ -> cover {:error, :invalid_commit}
end
end
defp read_parents(data, parents_acc) do
with {'parent', parent_id, next_data} <- next_header(data),
{:parent_id, {parent_id, []}} <- {:parent_id, ObjectId.from_hex_charlist(parent_id)} do
read_parents(next_data, [parent_id | parents_acc])
else
{:parent_id, _} -> cover :error
_ -> cover {Enum.reverse(parents_acc), data}
end
end
defp drop_if_lf([10 | data]), do: cover(data)
defp drop_if_lf([]), do: cover([])
defp drop_if_lf(_), do: cover(:error)
@doc ~S"""
Renders this commit structure into a corresponding `Xgit.Core.Object`.
If duplicate parents are detected, they will be silently de-duplicated.
If the commit structure is not valid, will raise `ArgumentError`.
"""
@spec to_object(commit :: t) :: Object.t()
def to_object(commit)
def to_object(
%__MODULE__{
tree: tree,
parents: parents,
author: %PersonIdent{} = author,
committer: %PersonIdent{} = committer,
message: message
} = commit
) do
unless valid?(commit) do
raise ArgumentError, "Xgit.Core.Commit.to_object/1: commit is not valid"
end
rendered_parents =
parents
|> Enum.uniq()
|> Enum.flat_map(&'parent #{&1}\n')
rendered_commit =
'tree #{tree}\n' ++
rendered_parents ++
'author #{PersonIdent.to_external_string(author)}\n' ++
'committer #{PersonIdent.to_external_string(committer)}\n' ++
'\n' ++
message
# TO DO: Support signatures and other extensions.
# https://github.com/elixir-git/xgit/issues/202
%Object{
type: :commit,
content: rendered_commit,
size: Enum.count(rendered_commit),
id: ObjectId.calculate_id(rendered_commit, :commit)
}
end
end