Packages

Elixir implementation of the Git object storage, but with the goal to implement the same semantic with other storage and topics

Current section

Files

Jump to
gitex lib gitex.ex
Raw

lib/gitex.ex

defmodule Gitex do
@type tag :: {:tag,String.t}
@type branch :: {:branch,String.t}
@type remote :: {:remote,remote::String.t,:head | branch::String.t}
@type ref :: :head | tag | branch | remote
@moduledoc """
Git API to a repo which is a struct implementing `Gitex.Repo`
"""
@doc "get the decoded GIT object associated with a given hash"
@spec object(nil | Gitex.Repo.hash,Gitex.Repo.t) :: Gitex.Repo.gitobj
def object(nil,_repo), do: nil
def object(hash,repo), do:
Gitex.Repo.decode(repo,hash,Gitex.Repo.get_obj(repo,hash))
@doc "save the GIT object and return the created hash"
@spec save_object(Gitex.Repo.gitobj,Gitex.Repo.t) :: Gitex.Repo.hash
def save_object(elem,repo), do:
Gitex.Repo.put_obj(repo,{typeof(elem),Gitex.Repo.encode(repo,elem)})
@doc "basically `get_hash |> object`"
def get(ref,repo), do: object(get_hash(ref,repo),repo)
def get(ref,repo,path), do: object(get_hash(ref,repo,path),repo)
@doc """
Get the decoded GIT object hash from a fuzzy
reference : can be either a `ref` or a binary which
will be tested for each reference type in this order : branch,tag,remote
"""
@spec get_hash((someref::binary) | Gitex.Repo.commit | Gitex.Repo.tag,Gitex.Repo.t) :: Gitex.Repo.hash
def get_hash(%{hash: hash},_repo), do: hash
def get_hash(ref,repo), do: fuzzy_ref(ref,repo)
@doc "from a reference, use a path to get the wanted object : tree or blob"
@spec get_hash((someref::binary) | Gitex.Repo.commit | Gitex.Repo.tag | Gitex.Repo.tree,Gitex.Repo.t) :: Gitex.Repo.hash
def get_hash(%{tree: tree},repo,path), do: get_hash(tree,object(tree,repo),repo,path)
def get_hash(%{object: ref},repo,path), do: get_hash(object(ref,repo),repo,path)
def get_hash(tree,repo,path) when is_list(tree), do: get_hash(nil,tree,repo,path)
def get_hash(ref,repo,path), do: get_hash(get(ref,repo),repo,path)
def get_hash(hash,tree,repo,path), do:
get_hash_path(hash,tree,repo,path |> String.strip(?/) |> String.split("/"))
@doc """
- from some reference or object, get a root tree to alter
- save the missing or changed trees and blob from the root
- return the new root tree hash
"""
@spec put((someref::binary) | Gitex.Repo.commit | Gitex.Repo.tag | Gitex.Repo.tree,Gitex.Repo.t,path::binary,elem::Gitex.Repo.blob | Gitex.Repo.tree) :: Gitex.Repo.hash
def put(%{tree: tree},repo,path,elem), do: put(object(tree,repo),repo,path,elem)
def put(%{object: ref},repo,path,elem), do: put(object(ref,repo),repo,path,elem)
def put(tree,repo,path,elem) when is_list(tree), do:
({:tree,ref}=put_path(tree,repo,path |> String.strip(?/) |> String.split("/"),elem); ref)
def put(ref,repo,path,elem), do:
put(get(ref,repo),repo,path,elem)
@doc """
save a new commit :
- `tree_hash` is the tree which must be referenced by this commit, use "put" to construct it
- `branches` hashes will be commit parents, and these branches specs will be updated after commit
- committer and author are taken from `Gitex.Repo.user` or `Application.get_env(:gitex,:anonymous_user)` if `nil`
"""
@spec commit(Gitex.Repo.hash,Gitex.Repo.t,[branch::binary]|branch::binary,binary) :: Gitex.Repo.hash
def commit(tree_hash,repo,branches,message) when is_list(branches) do
committer = author = user_now(repo)
parents = Enum.map(branches,&Gitex.Repo.resolve_ref(repo,refpath({:branch,&1}))) |> Enum.filter(&!is_nil(&1))
commit_hash = save_object(%{tree: tree_hash, message: message,parent: parents,committer: committer, author: author},repo)
Enum.each(branches,&Gitex.Repo.set_ref(repo,refpath({:branch,&1}),commit_hash))
commit_hash
end
def commit(tree_hash,repo,branch,message), do: commit(tree_hash,repo,[branch],message)
@doc "create a new soft tag to reference an object"
def tag(hash,repo,tag), do: (Gitex.Repo.set_ref(repo,refpath({:tag,tag}),hash); hash)
@doc "create an annottated tag to reference an object"
def tag(hash,repo,tag,message) do
{type,_} = Gitex.Repo.get_obj(repo,hash)
tag(save_object(%{tag: tag,type: type,object: hash,message: message,tagger: user_now(repo)},repo),repo,tag)
end
@doc "lazily stream parents of a reference commit, sorted by date"
@spec history(someref::binary,Gitex.Repo.t) :: Stream.t
def history(ref,repo) do
Stream.resource(fn-> {object(fuzzy_ref(ref,repo),repo),[]} end,fn
:nomore->{:halt,nil}
{current,others}->
to_compare = Enum.concat(Enum.map(parent_list(current),&object(&1,repo)) ,others)
nexts = to_compare |> Enum.uniq(& &1.hash) |> Enum.sort_by(& &1.committer.utc_time) |> Enum.reverse
case nexts do
[next|others]-> {[current],{next,others}}
[]-> {[current],:nomore}
end
end,fn _->:done end)
end
@doc """
take a commit stream (from `history/2`) and lazily
add an index of the current branch to ease visualization and tree drawing
"""
@spec align_history(Stream.t(Gitex.Repo.commit)) :: Stream.t({integer,Gitex.Repo.commit})
def align_history(history) do
Stream.transform(history,{1,HashDict.new}, fn commit, {nextlevel,levels}=acc->
level = Dict.get(levels,commit.hash,0)
acc = case parent_list(commit) do
[head|tail]->
levels = Dict.update(levels,head,level,&min(&1,level))
Enum.reduce(tail,{nextlevel,levels},fn h,{nextlevel,levels}->
levels[h] && {nextlevel,levels} || {nextlevel+1,Dict.put(levels,h,nextlevel)}
end)
[]->acc
end
{[{level,commit}],acc}
end)
end
defp parent_list(%{parent: parents}) when is_list(parents), do: Enum.reverse(parents)
defp parent_list(%{parent: parent}), do: [parent]
defp parent_list(_), do: []
defp refpath(:head), do: "HEAD"
defp refpath({:branch,ref}), do: "refs/heads/#{ref}"
defp refpath({:tag,ref}), do: "refs/tags/#{ref}"
defp refpath({:remote,remote,:head}), do: "refs/remotes/#{remote}/HEAD"
defp refpath({:remote,remote,ref}), do: "refs/remotes/#{remote}/#{ref}"
defp fuzzy_ref(ref,repo) when is_atom(ref) or is_tuple(ref), do:
Gitex.Repo.resolve_ref(repo,refpath(ref))
defp fuzzy_ref(ref,repo) when is_binary(ref) do
Gitex.Repo.resolve_ref(repo,refpath({:branch,ref}))
|| Gitex.Repo.resolve_ref(repo,refpath({:tag,ref}))
|| case String.split(ref,"/") do
[remote,ref]->Gitex.Repo.resolve_ref(repo,refpath({:remote,remote,ref}))
[remote]->Gitex.Repo.resolve_ref(repo,refpath({:remote,remote,:head}))
end
|| ref
end
defp get_hash_path(hash,_tree,_repo,[]), do: hash
defp get_hash_path(hash,_tree,_repo,[""]), do: hash
defp get_hash_path(nil,_tree,_repo,[]), do: nil
defp get_hash_path(_hash,tree,repo,[name|subpath]) when is_list(tree) do
if (elem=Enum.find(tree,& &1.name == name)), do:
get_hash_path(elem.ref,object(elem.ref,repo),repo,subpath)
end
defp get_hash_path(_,_,_,_), do: nil
defp put_path(_,repo,[],elem), do: {typeof(elem),save_object(elem,repo)}
defp put_path(tree,repo,[name|path],elem) do
childtree = if (e=Enum.find(tree,& &1.name==name and &1.type==:dir)), do: object(e.ref,repo), else: []
{type,ref} = put_path(childtree,repo,path,elem)
{type,mode} = if type==:tree do {:dir,"40000"} else {:file,"00777"} end
{:tree,save_object([%{name: name, mode: mode, type: type, ref: ref}|Enum.reject(tree, & &1.name==name)],repo)}
end
defp user_now(repo) do
now = :erlang.now
base = %{name: "anonymous", email: "anonymous@localhost",
local_time: :calendar.now_to_local_time(now), utc_time: :calendar.now_to_universal_time(now)}
Enum.into(Gitex.Repo.user(repo) || Application.get_env(:gitex,:anonymous_user,[]),base)
end
defp typeof(tree) when is_list(tree), do: :tree
defp typeof(blob) when is_binary(blob), do: :blob
defp typeof(%{tag: _}), do: :tag
defp typeof(%{committer: _}), do: :commit
end