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