├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── lib ├── add.ex ├── cli.ex ├── commit.ex ├── database.ex ├── helpers.ex ├── init.ex ├── types │ ├── author.ex │ ├── blob.ex │ ├── commit.ex │ ├── entry.ex │ ├── index.ex │ ├── layout.ex │ ├── refs.ex │ └── tree.ex └── workspace.ex ├── mix.exs ├── mix.lock └── test ├── egit_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /doc 5 | /.fetch 6 | erl_crash.dump 7 | *.ez 8 | *.beam 9 | /config/*.secret.exs 10 | .elixir_ls/ 11 | /egit 12 | /.idea 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Meraj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elixir_git 2 | An Elixir implementation of Git version control system 3 | 4 | # Installation 5 | * mix deps.get 6 | * mix escript.build 7 | 8 | # Configuration 9 | * Set EGIT_AUTHOR_NAME and EGIT_AUTHOR_EMAIL environment variables in your .bashrc or shell config file 10 | 11 | # Usage 12 | * ./egit init [dir] 13 | * ./egit commit 14 | * ./egit add file(s)|dir(s) -------------------------------------------------------------------------------- /lib/add.ex: -------------------------------------------------------------------------------- 1 | defmodule Egit.Add do 2 | @moduledoc """ 3 | An Elixir implementation of Git version control system 4 | """ 5 | 6 | alias Egit.{Workspace, Database} 7 | alias Egit.Types.{BLOB, Index} 8 | 9 | def add(path) do 10 | indices = 11 | Enum.map(path, fn p -> 12 | Workspace.list_files(p) 13 | |> Enum.reduce( 14 | %Index{}, 15 | fn file_path, index -> 16 | data = Workspace.read_file(file_path) 17 | stat = Workspace.stat_file(file_path) 18 | 19 | blob = BLOB.build(data) 20 | Database.store(blob) 21 | 22 | index 23 | |> Index.add(file_path, blob.oid, stat) 24 | end 25 | ) 26 | end) 27 | 28 | entries = 29 | List.foldl(indices, [], fn x, acc -> acc ++ [x.entries] end) 30 | |> Enum.reduce(fn x, y -> 31 | Map.merge(x, y, fn _k, v1, v2 -> v2 ++ v1 end) 32 | end) 33 | 34 | %Index{entries: entries} 35 | |> Index.write_updates() 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/cli.ex: -------------------------------------------------------------------------------- 1 | defmodule Egit.CLI do 2 | @moduledoc """ 3 | An Elixir implementation of Git version control system 4 | """ 5 | 6 | def main(argv \\ []) do 7 | argv 8 | |> parse_args() 9 | |> process() 10 | end 11 | 12 | defp parse_args(argv) do 13 | argv 14 | |> OptionParser.parse(strict: [init: :string, commit: :string, add: :string]) 15 | |> elem(1) 16 | |> args_to_internal_representation() 17 | end 18 | 19 | defp args_to_internal_representation(["init", dir]) do 20 | {:init, dir} 21 | end 22 | 23 | defp args_to_internal_representation(["init"]) do 24 | {:ok, dir} = File.cwd() 25 | {:init, dir} 26 | end 27 | 28 | defp args_to_internal_representation(["commit"]) do 29 | if File.exists?(".git") do 30 | {:ok, dir} = File.cwd() 31 | {:commit, dir} 32 | else 33 | IO.puts(:stderr, "repo not initialized") 34 | exit(:fatal) 35 | end 36 | end 37 | 38 | defp args_to_internal_representation(["add" | path]) do 39 | if File.exists?(".git") do 40 | {:add, path} 41 | else 42 | IO.puts(:stderr, "fatal: not a git repository") 43 | exit(:fatal) 44 | end 45 | end 46 | 47 | defp args_to_internal_representation(command) do 48 | {:help, command} 49 | end 50 | 51 | defp process({:help, _command}) do 52 | IO.puts(:stderr, """ 53 | usage: egit help 54 | egit init [dir] 55 | egit commit 56 | egit add 57 | """) 58 | end 59 | 60 | defp process({:init, dir}) do 61 | Egit.Init.init(dir) 62 | end 63 | 64 | defp process({:commit, dir}) do 65 | author = System.get_env("EGIT_AUTHOR_NAME") 66 | email = System.get_env("EGIT_AUTHOR_EMAIL") 67 | Egit.Commit.commit(dir, %{name: author, email: email}) 68 | end 69 | 70 | defp process({:add, path}) do 71 | Egit.Add.add(path) 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/commit.ex: -------------------------------------------------------------------------------- 1 | defmodule Egit.Commit do 2 | @moduledoc """ 3 | An Elixir implementation of Git version control system 4 | """ 5 | 6 | alias Egit.{Workspace, Database, Helpers} 7 | alias Egit.Types.{BLOB, Tree, Entry, Author, Commit, Refs, Layout} 8 | 9 | def commit(root_path, config) do 10 | head_path = root_path |> Helpers.head_path() 11 | parent = Refs.read_head(head_path) 12 | 13 | entries = 14 | Enum.map( 15 | Workspace.list_files(), 16 | fn path -> 17 | case File.read(path) do 18 | {:ok, data} -> 19 | blob = build_blob(data) 20 | Database.store(blob) 21 | %Entry{name: path, oid: blob.oid, stat: Workspace.stat_file(path)} 22 | 23 | {:error, _} -> 24 | nil 25 | end 26 | end 27 | ) 28 | |> Enum.reject(&is_nil/1) 29 | 30 | root = 31 | Layout.build(entries) 32 | |> traverse() 33 | 34 | author = %Author{name: config.name, email: config.email, time: DateTime.utc_now()} 35 | commit = build_commit(author, root, parent) 36 | Database.store(commit) 37 | 38 | Refs.update_head(head_path, commit.oid) 39 | 40 | message = 41 | case parent do 42 | nil -> "(root-commit) " 43 | _ -> "" 44 | end 45 | 46 | IO.puts("[#{message}#{commit.oid}] #{commit.message}") 47 | end 48 | 49 | defp traverse(root = %Tree{}) do 50 | new_entries = 51 | Map.new(root.entries, fn {name, entry} -> 52 | new_root = Tree.build_content(traverse(entry)) 53 | Database.store(new_root) 54 | {name, new_root} 55 | end) 56 | 57 | new_root = 58 | Map.replace!(root, :entries, new_entries) 59 | |> Tree.build_content() 60 | 61 | Database.store(new_root) 62 | new_root 63 | end 64 | 65 | defp traverse(root), do: root 66 | 67 | defp build_blob(data) do 68 | object = %BLOB{data: data} 69 | string = BLOB.to_s(object) 70 | content = "#{BLOB.type(object)} #{byte_size(string)}\0#{string}" 71 | object = %{object | oid: String.downcase(:crypto.hash(:sha, content) |> Base.encode16())} 72 | %{object | content: content} 73 | end 74 | 75 | defp build_commit(author, tree, parent) do 76 | message = IO.gets("Enter commit message: \n") 77 | 78 | object = %Commit{ 79 | tree: tree, 80 | message: String.trim(message, "\n"), 81 | author: author 82 | } 83 | 84 | string = Commit.to_s(object, parent) 85 | content = "#{Commit.type(object)} #{byte_size(string)}\0" <> string 86 | object = %{object | oid: String.downcase(:crypto.hash(:sha, content) |> Base.encode16())} 87 | %{object | content: content} 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/database.ex: -------------------------------------------------------------------------------- 1 | defmodule Egit.Database do 2 | @moduledoc """ 3 | An Elixir implementation of Git version control system 4 | """ 5 | 6 | alias Egit.Helpers 7 | 8 | def store(object) do 9 | unless File.exists?(object_path(object.oid)) do 10 | write_object(object_path(object.oid), object.content) 11 | end 12 | end 13 | 14 | defp object_path(oid) do 15 | {:ok, dir} = File.cwd() 16 | db_path = db_path(dir) 17 | Path.join([db_path, String.slice(oid, 0..1), String.slice(oid, 2..-1)]) 18 | end 19 | 20 | defp write_object(object_path, content) do 21 | dir_name = Path.dirname(object_path) 22 | temp_path = Path.join([dir_name, generate_temp_name()]) 23 | 24 | file = 25 | case File.open(temp_path, [:read, :write, :exclusive]) do 26 | {:ok, file} -> 27 | file 28 | 29 | {:error, :enoent} -> 30 | File.mkdir(dir_name) 31 | {:ok, file} = File.open(temp_path, [:read, :write, :exclusive]) 32 | file 33 | 34 | _ -> 35 | :noop 36 | end 37 | 38 | compressed = :zlib.compress(content) 39 | IO.binwrite(file, compressed) 40 | File.close(file) 41 | 42 | File.rename(temp_path, object_path) 43 | end 44 | 45 | defp db_path(root_path) do 46 | root_path 47 | |> Helpers.db_path() 48 | end 49 | 50 | defp generate_temp_name() do 51 | "tmp_obj_#{Helpers.generate_random_string(6)}" 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Egit.Helpers do 2 | @moduledoc """ 3 | An Elixir implementation of Git version control system 4 | """ 5 | 6 | def git_path(root_path) do 7 | root_path 8 | |> Path.join(".git") 9 | end 10 | 11 | def head_path(root_path) do 12 | root_path 13 | |> git_path() 14 | |> Path.join("HEAD") 15 | end 16 | 17 | def db_path(root_path) do 18 | root_path 19 | |> git_path() 20 | |> Path.join("objects") 21 | end 22 | 23 | def index_path(git_path) do 24 | git_path 25 | |> Path.join("index") 26 | end 27 | 28 | def ls_r(path \\ ".") do 29 | cond do 30 | File.regular?(path) -> 31 | [path] 32 | 33 | File.dir?(path) -> 34 | File.ls!(path) 35 | |> Enum.map(&Path.join(path, &1)) 36 | |> Enum.map(&ls_r/1) 37 | |> Enum.concat() 38 | 39 | true -> 40 | [] 41 | end 42 | end 43 | 44 | @bytes Enum.concat([?a..?z, ?A..?Z, ?0..?9]) |> List.to_string() 45 | def generate_random_string(length) do 46 | for _ <- 1..length, into: <<>> do 47 | index = :rand.uniform(byte_size(@bytes)) - 1 48 | <<:binary.at(@bytes, index)>> 49 | end 50 | end 51 | 52 | def datetime_to_seconds(datetime) do 53 | {:ok, native} = NaiveDateTime.from_erl(datetime) 54 | {:ok, utc} = DateTime.from_naive(native, "Etc/UTC") 55 | DateTime.to_unix(utc) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/init.ex: -------------------------------------------------------------------------------- 1 | defmodule Egit.Init do 2 | @moduledoc """ 3 | An Elixir implementation of Git version control system 4 | """ 5 | 6 | alias Egit.Helpers 7 | 8 | def init(root_path) do 9 | root_path 10 | |> Helpers.git_path() 11 | |> make_dirs() 12 | end 13 | 14 | defp make_dirs(git_path) do 15 | case File.exists?(git_path) do 16 | false -> 17 | Enum.each(["objects", "refs"], fn dir -> 18 | try do 19 | File.mkdir_p(Path.join(git_path, dir)) 20 | rescue 21 | e in File.Error -> 22 | IO.puts(:stderr, "fatal: #{e.message}") 23 | exit(:fatal) 24 | end 25 | end) 26 | 27 | IO.puts("Initialized empty egit repository in #{git_path}") 28 | 29 | true -> 30 | IO.puts(:stderr, "#{git_path} already initialized") 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/types/author.ex: -------------------------------------------------------------------------------- 1 | defmodule Egit.Types.Author do 2 | @moduledoc """ 3 | An Elixir implementation of Git version control system 4 | """ 5 | 6 | defstruct name: nil, email: nil, time: nil 7 | 8 | def to_s(author) do 9 | timestamp = "#{DateTime.to_unix(author.time)}" 10 | 11 | "#{author.name} <#{author.email}> " <> timestamp 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/types/blob.ex: -------------------------------------------------------------------------------- 1 | defmodule Egit.Types.BLOB do 2 | @moduledoc """ 3 | An Elixir implementation of Git version control system 4 | """ 5 | 6 | defstruct data: nil, oid: nil, content: nil 7 | 8 | def type(_blob) do 9 | "blob" 10 | end 11 | 12 | def to_s(blob) do 13 | blob.data 14 | end 15 | 16 | def build(data) do 17 | object = %Egit.Types.BLOB{data: data} 18 | string = to_s(object) 19 | content = "#{type(object)} #{byte_size(string)}\0#{string}" 20 | object = %{object | oid: String.downcase(:crypto.hash(:sha, content) |> Base.encode16())} 21 | %{object | content: content} 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/types/commit.ex: -------------------------------------------------------------------------------- 1 | defmodule Egit.Types.Commit do 2 | @moduledoc """ 3 | An Elixir implementation of Git version control system 4 | """ 5 | 6 | alias Egit.Types.Author 7 | 8 | defstruct tree: nil, author: nil, message: nil, oid: nil, content: nil 9 | 10 | def type(_commit) do 11 | "commit" 12 | end 13 | 14 | def to_s(commit, parent \\ nil) do 15 | lines = Enum.concat([], ["tree #{commit.tree.oid}"]) 16 | 17 | lines = 18 | case parent do 19 | nil -> lines 20 | _ -> Enum.concat(lines, ["parent #{parent}"]) 21 | end 22 | 23 | lines = Enum.concat(lines, ["author " <> Author.to_s(commit.author)]) 24 | lines = Enum.concat(lines, ["committer " <> Author.to_s(commit.author)]) 25 | lines = Enum.concat(lines, [""]) 26 | lines = Enum.concat(lines, [commit.message]) 27 | 28 | Enum.join(lines, "\n") 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/types/entry.ex: -------------------------------------------------------------------------------- 1 | defmodule Egit.Types.Entry do 2 | @moduledoc """ 3 | An Elixir implementation of Git version control system 4 | """ 5 | 6 | use Bitwise 7 | 8 | @regular_mode "100644" 9 | @executable_mode "100755" 10 | @directory_mode "40000" 11 | 12 | defstruct name: nil, oid: nil, content: nil, stat: nil 13 | 14 | def parent_dirs(entry) do 15 | descend(entry.name) 16 | |> Enum.drop(-1) 17 | end 18 | 19 | def dir_mode do 20 | @directory_mode 21 | end 22 | 23 | def mode(entry) do 24 | if executable?(entry.stat.mode), do: @executable_mode, else: @regular_mode 25 | end 26 | 27 | defp executable?(mode) do 28 | <<_::1, _::1, o_exec::1, _::1, _::1, g_exec::1, _::1, _::1, a_exec::1>> = <> 29 | 30 | case bor(bor(o_exec, g_exec), a_exec) do 31 | 1 -> true 32 | 0 -> false 33 | end 34 | end 35 | 36 | def basename(name) do 37 | Path.basename(name) 38 | end 39 | 40 | defp descend(path) do 41 | Path.split(path) 42 | |> Enum.reduce([], fn dir, results -> 43 | case List.last(results) do 44 | nil -> 45 | [dir] 46 | 47 | root -> 48 | if root == "/" do 49 | results ++ [root <> dir] 50 | else 51 | results ++ [root <> "/" <> dir] 52 | end 53 | end 54 | end) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/types/index.ex: -------------------------------------------------------------------------------- 1 | defmodule Egit.Types.Index do 2 | @moduledoc """ 3 | An Elixir implementation of Git version control system 4 | """ 5 | 6 | alias Egit.Helpers 7 | 8 | defstruct entries: %{} 9 | 10 | def add(index, pathname, oid, stat) do 11 | entry = Egit.Types.Index.Entry.create(pathname, oid, stat) 12 | %{index | entries: Map.put(index.entries, pathname, entry)} 13 | end 14 | 15 | def write_updates(index) do 16 | {:ok, root_path} = File.cwd() 17 | 18 | index_path = 19 | root_path 20 | |> Helpers.git_path() 21 | |> Helpers.index_path() 22 | 23 | lock_path = index_path <> ".lock" 24 | 25 | case File.open(lock_path, [:write, :exclusive]) do 26 | {:ok, file} -> 27 | sha = begin_write() 28 | 29 | version = <<2::32>> 30 | size = <> 31 | header = "DIRC" <> version <> size 32 | 33 | sha = write(file, header, sha) 34 | 35 | Enum.each(index.entries, fn {_key, entry} -> 36 | sha = write(file, Egit.Types.Index.Entry.to_s(entry), sha) 37 | end) 38 | 39 | finish_write(file, sha) 40 | 41 | File.rename(lock_path, index_path) 42 | 43 | {:error, :eexist} -> 44 | IO.puts(:stderr, "lock file exists") 45 | 46 | _ -> 47 | IO.puts(:stderr, "error creating lock file") 48 | end 49 | end 50 | 51 | defp begin_write() do 52 | :crypto.hash_init(:sha) 53 | end 54 | 55 | defp write(file, data, sha) do 56 | IO.binwrite(file, data) 57 | :crypto.hash_update(sha, data) 58 | end 59 | 60 | defp finish_write(file, sha) do 61 | sha = :crypto.hash_final(sha) 62 | IO.binwrite(file, sha) 63 | end 64 | 65 | defmodule Entry do 66 | use Bitwise 67 | 68 | @max_path_size 0xFFF 69 | @entry_block 8 70 | 71 | defstruct [ 72 | :ctime, 73 | :ctime_nsec, 74 | :mtime, 75 | :mtime_nsec, 76 | :dev, 77 | :ino, 78 | :mode, 79 | :uid, 80 | :gid, 81 | :size, 82 | :oid, 83 | :flags, 84 | :path 85 | ] 86 | 87 | def create(path, oid, stat) do 88 | flags = min(byte_size(path), @max_path_size) 89 | 90 | %Entry{ 91 | ctime: Helpers.datetime_to_seconds(stat.ctime), 92 | ctime_nsec: 0, 93 | mtime: Helpers.datetime_to_seconds(stat.ctime), 94 | mtime_nsec: 0, 95 | dev: stat.major_device, 96 | ino: stat.inode, 97 | mode: stat.mode, 98 | uid: stat.uid, 99 | gid: stat.gid, 100 | size: stat.size, 101 | oid: oid, 102 | flags: flags, 103 | path: path 104 | } 105 | end 106 | 107 | def to_s(entry) do 108 | {:ok, oid} = Base.decode16(entry.oid, case: :lower) 109 | 110 | string = 111 | <> <> 112 | <> <> 113 | <> <> 114 | <> <> 115 | <> <> 116 | <> <> 117 | <> <> 118 | <> <> 119 | <> <> 120 | <> <> oid <> <> <> entry.path <> <<0>> 121 | 122 | pad_nulls(string) 123 | end 124 | 125 | defp pad_nulls(binary) do 126 | rem = Integer.mod(byte_size(binary), @entry_block) 127 | padding = if rem == 0, do: 0, else: @entry_block - rem 128 | bits = padding * 8 129 | binary <> <<0::size(bits)>> 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/types/layout.ex: -------------------------------------------------------------------------------- 1 | defmodule Egit.Types.Layout do 2 | @moduledoc """ 3 | An Elixir implementation of Git version control system 4 | """ 5 | 6 | alias Egit.Types.{Entry, Tree} 7 | 8 | def add_entry([], entry, root) do 9 | %{root | entries: Map.put(root.entries, Entry.basename(entry.name), entry)} 10 | end 11 | 12 | def add_entry([parent | rest], entry, root) do 13 | tree = 14 | case Map.get(root.entries, Entry.basename(parent)) do 15 | nil -> %Tree{} 16 | tree -> tree 17 | end 18 | 19 | tree = add_entry(rest, entry, tree) 20 | %{root | entries: Map.put(root.entries, Entry.basename(parent), tree)} 21 | end 22 | 23 | def build(entries) do 24 | entries = Enum.sort_by(entries, & &1.name) 25 | 26 | Enum.reduce(entries, %Tree{}, fn entry, root -> 27 | add_entry(Entry.parent_dirs(entry), entry, root) 28 | end) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/types/refs.ex: -------------------------------------------------------------------------------- 1 | defmodule Egit.Types.Refs do 2 | @moduledoc """ 3 | An Elixir implementation of Git version control system 4 | """ 5 | 6 | def update_head(head_path, oid) do 7 | lock_path = head_path <> ".lock" 8 | 9 | case File.open(lock_path, [:write, :exclusive]) do 10 | {:ok, file} -> 11 | IO.binwrite(file, oid) 12 | File.rename(lock_path, head_path) 13 | 14 | {:error, :eexist} -> 15 | IO.puts(:stderr, "lock file exists") 16 | 17 | _ -> 18 | IO.puts(:stderr, "error creating lock file") 19 | end 20 | end 21 | 22 | def read_head(head_path) do 23 | case File.exists?(head_path) do 24 | true -> 25 | {:ok, id} = File.read(head_path) 26 | id 27 | 28 | false -> 29 | nil 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/types/tree.ex: -------------------------------------------------------------------------------- 1 | defmodule Egit.Types.Tree do 2 | @moduledoc """ 3 | An Elixir implementation of Git version control system 4 | """ 5 | 6 | alias Egit.Types.Entry 7 | 8 | defstruct entries: %{}, oid: nil, content: nil 9 | 10 | def type(_tree) do 11 | "tree" 12 | end 13 | 14 | def to_s(tree) do 15 | entries = 16 | tree.entries 17 | |> Enum.map(fn {name, entry} -> 18 | {:ok, oid} = Base.decode16(entry.oid, case: :lower) 19 | 20 | case entry do 21 | %Egit.Types.Tree{} = _ -> 22 | "#{Entry.dir_mode()} #{name}\0" <> oid 23 | 24 | %Entry{} = entry -> 25 | "#{Entry.mode(entry)} #{name}\0" <> oid 26 | end 27 | end) 28 | 29 | Enum.join(entries, "") 30 | end 31 | 32 | def build_content(object = %Egit.Types.Tree{}) do 33 | string = to_s(object) 34 | content = "#{type(object)} #{byte_size(string)}\0#{string}" 35 | oid = String.downcase(:crypto.hash(:sha, content) |> Base.encode16()) 36 | %Egit.Types.Tree{entries: object.entries, content: content, oid: oid} 37 | end 38 | 39 | def build_content(entry = %Entry{}) do 40 | entry 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/workspace.ex: -------------------------------------------------------------------------------- 1 | defmodule Egit.Workspace do 2 | @moduledoc """ 3 | An Elixir implementation of Git version control system 4 | """ 5 | 6 | alias Egit.Helpers 7 | 8 | @ignore_path [".git"] 9 | 10 | def list_files(path \\ ".") do 11 | cond do 12 | File.regular?(path) -> 13 | [path] 14 | 15 | true -> 16 | list = Path.wildcard(Path.join(path, "/*"), match_dot: true) -- @ignore_path 17 | 18 | Enum.map(list, fn path -> Helpers.ls_r(path) end) 19 | |> List.flatten() 20 | end 21 | end 22 | 23 | def stat_file(path) do 24 | case File.stat(path) do 25 | {:ok, stat} -> 26 | stat 27 | 28 | {:error, reason} -> 29 | IO.puts(:stderr, "stat failed - #{reason}") 30 | end 31 | end 32 | 33 | def read_file(path) do 34 | case File.exists?(path) do 35 | true -> 36 | {:ok, data} = File.read(path) 37 | data 38 | 39 | false -> 40 | nil 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Egit.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :egit, 7 | version: "0.1.0", 8 | elixir: "~> 1.11", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | escript: escript() 12 | ] 13 | end 14 | 15 | # Run "mix help compile.app" to learn about applications. 16 | def application do 17 | [ 18 | extra_applications: [:logger, :crypto] 19 | ] 20 | end 21 | 22 | # Run "mix help deps" to learn about dependencies. 23 | defp deps do 24 | [ 25 | {:stream_hash, "~> 0.3.1"} 26 | ] 27 | end 28 | 29 | defp escript do 30 | [main_module: Egit.CLI] 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"}, 3 | "certifi": {:hex, :certifi, "2.5.3", "70bdd7e7188c804f3a30ee0e7c99655bc35d8ac41c23e12325f36ab449b70651", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "ed516acb3929b101208a9d700062d520f3953da3b6b918d866106ffa980e1c10"}, 4 | "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, 5 | "gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"}, 6 | "hackney": {:hex, :hackney, "1.17.0", "717ea195fd2f898d9fe9f1ce0afcc2621a41ecfe137fae57e7fe6e9484b9aa99", [:rebar3], [{:certifi, "~>2.5", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "64c22225f1ea8855f584720c0e5b3cd14095703af1c9fbc845ba042811dc671c"}, 7 | "hexate": {:hex, :hexate, "0.6.1", "1cea42e462c1daa32223127d4752e71016c3d933d492b9bb7fa4709a4a0fd50d", [:mix], [], "hexpm", "667c429c0970e3097107c9fafcc645636302888388845d78a3947a739fd946b7"}, 8 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 9 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 10 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 11 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 12 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 13 | "stream_hash": {:hex, :stream_hash, "0.3.1", "eab3ecf00361f93e4b57d43629a1b6436cea05e20c95da8546113fd936fb83dc", [:mix], [], "hexpm", "3941e9d304d94bc947b6d33fb1c85c615ac2ae44e0a13cf8c77553d7810dacca"}, 14 | "timex": {:hex, :timex, "3.6.4", "137a49450b8d1f80efff82de4b78ab9ad2e367f06346825704310599733f338b", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f6fce3f07ab67f525043af5b1f68ed5fa12a41b9dab95a9a98bb6acfb30ecadc"}, 15 | "tzdata": {:hex, :tzdata, "0.1.8", "965c504ce0b65a0fa5ad11dddfb45ad995d7abccb1fdf23247c0b2bcbbe98add", [:mix], [], "hexpm", "68af93cc1e0e0e6be76a583faaf15d57972a1afbf1e39f9d70524da87dfae825"}, 16 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 17 | } 18 | -------------------------------------------------------------------------------- /test/egit_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EgitTest do 2 | use ExUnit.Case 3 | doctest Egit 4 | end 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------