├── .gitignore ├── LICENSE ├── README.md ├── Rakefile ├── TODO.txt ├── bin └── egit ├── contrib └── benchmarks.es ├── etest ├── git_io_test.erl ├── git_test.erl └── run.es ├── src ├── git.erl ├── git.hrl ├── git_io.erl ├── git_object.erl ├── hex.erl ├── packfile.erl └── packindex.erl └── test_git ├── COMMIT_EDITMSG ├── HEAD ├── config ├── description ├── hooks ├── applypatch-msg.sample ├── commit-msg.sample ├── post-commit.sample ├── post-receive.sample ├── post-update.sample ├── pre-applypatch.sample ├── pre-commit.sample ├── pre-rebase.sample ├── prepare-commit-msg.sample └── update.sample ├── index ├── info ├── exclude └── refs ├── logs ├── HEAD └── refs │ └── heads │ └── master ├── objects ├── 19 │ └── 306733ab5f0c49d46fb43778a527dfddc323de ├── 25 │ └── f9782940ad877fbc6900a678cdc5d032df4d53 ├── 47 │ └── e3fc6cde2a1552c28ab97361ff54bbae3c6f60 ├── 8b │ └── bad744fe9597cc130e8ef7cfd201f36d9cba76 ├── 8d │ └── 47f3435ce5dfd0b2ab5758590c2db21b5294b4 ├── d8 │ └── de215b38971591797f31f916b457d2ca2538cf ├── info │ └── packs └── pack │ ├── pack-d2e5da2ce623cd3a19db4c788fb27f3373f97dcf.idx │ ├── pack-d2e5da2ce623cd3a19db4c788fb27f3373f97dcf.pack │ ├── test.idx │ └── test.pack ├── packed-refs └── refs ├── heads └── master └── tags └── v1.2 /.gitignore: -------------------------------------------------------------------------------- 1 | ebin 2 | *.dump 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Scott Chacon 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 | ErlanGit 2 | ==================== 3 | 4 | This is a pure-Erlang implementation of a bunch of Git functionality. 5 | 6 | Currently it is capable of reading Git objects out of loose or packfile 7 | formats and parsing tree and commit objects into more meaningful data 8 | structures. 9 | 10 | It is currently slower than a slow-motion replay of a cold-weather molasses 11 | drip-race judged by sloths who just took Nyquil. Slowly. 12 | 13 | Installation 14 | ==================== 15 | 16 | To compile ErlanGit, run the following from within the project directory: 17 | 18 | $ rake build 19 | 20 | To test that everything is working properly, run the tests: 21 | 22 | $ rake 23 | 24 | When you run the test, the project is built with tests enabled. Before 25 | deploying to a production system make sure you do a normal build. 26 | 27 | Basic API 28 | ==================== 29 | 30 | First you initialize a Git object with the path to a Git repository: 31 | 32 | Git = git:open("test_git") 33 | 34 | You can get the type, size and data for any object in Git with the 35 | git:object_data call: 36 | 37 | {Type, Size, Data} = git:object_data(Git, "8d47f3435ce5dfd0b2ab5758590c2db21b5294b4") 38 | 39 | Type is a term (one of commit, tree, blob, tag), Size is an int of the size of 40 | the object and Data is a binary of the object data. 41 | 42 | You can use object_exists to get a true/false back as to if a given object 43 | exists in the database. 44 | 45 | Result = git:object_exists(Git, "8d47f3435ce5dfd0b2ab5758590c2db21b5294b4") 46 | 47 | You can get a topographically ordered listing of the commit SHAs reachable 48 | froma given array of commit SHAs with the git:rev_list command 49 | 50 | RevList = git:rev_list(Git, ["25daa907ccb6feb267bfec70a130d5fe13e48a79"]) 51 | 52 | That command will return an array of commit SHA strings: 53 | 54 | RevList: ["25daa907ccb6feb267bfec70a130d5fe13e48a79", 55 | "dd991a4966e8807d448305e67a9b3727efc6060c", 56 | "be62addb149d286893e2ec254e0dc783a871e8af", 57 | "208fc4a6a08fb1e5136cf9943d79ad81097a0f36", 58 | "ef8285c69ceac3f2ff2fee154d3d6ad6a71fb826", 59 | "61fa970afbf82f79611220203213cd9562a809e3", 60 | "a361a3163a1b521f277e3877232d94b787750c36"] 61 | 62 | You can get formatted data back for tree and commit objects (and blobs, but it 63 | just returns a string of the blob) with the git:object/2 call 64 | 65 | {ok, Commit} = git:object(Git, "25daa907ccb6feb267bfec70a130d5fe13e48a79") 66 | 67 | That will return a Commit object who's record looks like this: 68 | 69 | -record(commit, {sha, parents, tree, 70 | author, committer, encoding, message}). 71 | 72 | So you can then get the array of parent SHAs with Commit#commit.parents, or 73 | the commit message with Commit#commit.message. 74 | 75 | If you run git:object/2 with a tree SHA, it will give you back an array of 76 | tree entries, each of which is a tree record: 77 | 78 | -record(tree, {mode, name, sha}). 79 | 80 | IO Git API 81 | =================== 82 | 83 | There are also a few output functions to help look at the data returned. You 84 | can get the equivalent of the 'git log' output with the git_io:print_log command. 85 | 86 | git_io:print_log(Git, ["25daa907ccb6feb267bfec70a130d5fe13e48a79"]) 87 | 88 | That will print to stdout something like this: 89 | 90 | commit 61fa970afbf82f79611220203213cd9562a809e3 91 | Author: Scott Chacon 1258278496 +0100 92 | 93 | added test one 94 | 95 | commit a361a3163a1b521f277e3877232d94b787750c36 96 | Author: Scott Chacon 1258278448 +0100 97 | 98 | first commit 99 | 100 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | ERLC_FLAGS = "+debug_info -W2 -o ebin" 5 | 6 | task :default => :test 7 | 8 | task :chdir do 9 | Dir.chdir(File.join(File.dirname(__FILE__), *%w[.])) 10 | end 11 | 12 | task :build => :chdir do 13 | sh "mkdir -p ebin" 14 | sh "erlc #{ERLC_FLAGS} src/*.erl" 15 | end 16 | 17 | task :test => :chdir do 18 | sh "erlc #{ERLC_FLAGS} -DTEST -I etest src/*.erl" 19 | sh "etest/run.es" 20 | end 21 | 22 | task :console => :chdir do 23 | sh "erl +Bc +K true -smp enable -pa ebin -sname local_console_#{$$} -kernel start_boot_server true" 24 | end 25 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | TODO v1 2 | ============ 3 | 4 | * get references 5 | * rev-parse 6 | 7 | * parse tag objects 8 | * parse author data 9 | 10 | * write commit, tree, tag, blob objects 11 | * write/update references 12 | 13 | * performance - 14 | spin up an OTP server so we don't have to reparse the index 15 | files each time? 16 | 17 | TODO v2 18 | ============ 19 | 20 | * diff 21 | * merge-base calculation 22 | * simple merges 23 | 24 | * read index file (ls-files) 25 | * write index entries (read-tree) 26 | 27 | * revlist caching 28 | * incremental log 29 | 30 | * git/http fetch 31 | * git/http/ssh push 32 | 33 | * git/http serve 34 | 35 | -------------------------------------------------------------------------------- /bin/egit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %% -*- erlang -*- 3 | %%! -pa ./ebin -sasl -boot start_sasl -noshell 4 | 5 | main([Command|_Args]) -> 6 | try 7 | Git = git:open("test_git"), %% TODO: look for real git repo from cwd 8 | case Command of 9 | "log" -> 10 | RevList = git:rev_list(Git, ["25daa907ccb6feb267bfec70a130d5fe13e48a79"]), %% blob 11 | io:fwrite("RevList: ~p~n", [RevList]); 12 | _Else -> 13 | usage() 14 | end 15 | catch 16 | _:_ -> 17 | usage() 18 | end. 19 | 20 | usage() -> 21 | io:format("usage: egit command\n"), 22 | halt(1). 23 | 24 | -------------------------------------------------------------------------------- /contrib/benchmarks.es: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %% -*- erlang -*- 3 | %%! -pa ./ebin -sasl -boot start_sasl -noshell 4 | 5 | main(_) -> 6 | Git = git:open("test_git"), 7 | {Time, Value} = timer:tc(git, object_data, [Git, "be62addb149d286893e2ec254e0dc783a871e8af"]), 8 | io:fwrite("Data: ~p:~p~n", [Time, Value]). 9 | 10 | -------------------------------------------------------------------------------- /etest/git_io_test.erl: -------------------------------------------------------------------------------- 1 | -include_lib("eunit/include/eunit.hrl"). 2 | 3 | print_log_test() -> 4 | Git = git:open("test_git"), 5 | git_io:print_log(Git, ["25daa907ccb6feb267bfec70a130d5fe13e48a79"]). 6 | 7 | ls_tree_test() -> 8 | Git = git:open("test_git"), 9 | {ok, Tree} = git:object(Git, "c67da89afe12df86e7b8324f1ac5fa470de2bb48"), 10 | git_io:print_tree(Tree). 11 | -------------------------------------------------------------------------------- /etest/git_test.erl: -------------------------------------------------------------------------------- 1 | -include_lib("eunit/include/eunit.hrl"). 2 | 3 | object_data_blob_test() -> 4 | Git = git:open("test_git"), 5 | {blob, 13, <<"test\nanother\n">>} = git:object_data(Git, "8d47f3435ce5dfd0b2ab5758590c2db21b5294b4"). 6 | 7 | object_data_tree_test() -> 8 | Git = git:open("test_git"), 9 | Data = <<49,48,48,54,52,52,32,82,69,65,68,77,69,0,157,174,175,185,134,76,244,48, 10 | 85,174,147,190,176,175,214,199,209,68,191,164,52,48,48,48,48,32,100, 11 | 105,114,111,110,101,0,128,43,239,80,141,163,41,195,101,202,199,105,170, 12 | 6,7,56,74,5,166,173,52,48,48,48,48,32,100,105,114,116,119,111,0,1,50, 13 | 186,7,196,230,60,172,232,178,133,56,3,228,235,228,20,151,111,253>>, 14 | {tree, 100, Data} = git:object_data(Git, "aa7dfe7c2a634cb9e7a9d5838eb58fe150ebd7fb"). 15 | 16 | object_exists_test() -> 17 | Git = git:open("test_git"), 18 | true = git:object_exists(Git, "8d47f3435ce5dfd0b2ab5758590c2db21b5294b4"), %% LooseT 19 | false = git:object_exists(Git, "ad47f3435ce5afd0b2ab5758590c2db21b5294b4"), %% LooseF 20 | true = git:object_exists(Git, "be62addb149d286893e2ec254e0dc783a871e8af"), %% PackT 21 | false = git:object_exists(Git, "ae62addb149d286893e2ec254e0dc783a871e8af"). %% PackF 22 | 23 | cat_file_packed_delta_test() -> 24 | Git = git:open("test_git"), 25 | Data = <<49,48,48,54,52,52,32,82,69,65,68,77,69,0,157,174,175,185,134,76,244,48, 26 | 85,174,147,190,176,175,214,199,209,68,191,164,52,48,48,48,48,32,100, 27 | 105,114,111,110,101,0,128,43,239,80,141,163,41,195,101,202,199,105,170, 28 | 6,7,56,74,5,166,173>>, 29 | {tree, 67, Data} = git:object_data(Git, "9cad8d7e8ee5b3b6fcb401ac9dcc557dd808762d"). 30 | 31 | cat_file_packed_ref_delta_test() -> 32 | Git = git:open("test_git"), 33 | Data = <<"# -*- ruby -*-\n\nrequire 'rubygems'\nrequire 'hoe'\nrequire './lib/grit.rb'\n\nHoe.new('grit', Grit::VERSION) do |p|\n p.rubyforge_name = 'grit'\n # p.author = 'FIX'\n # p.email = 'FIX'\n # p.summary = 'FIX'\n # p.description = p.paragraphs_of('README.txt', 2..5).join(\"\\n\\n\")\n # p.url = p.paragraphs_of('README.txt', 0).first.split(/\\n/)[1..-1]\n p.changes = p.paragraphs_of('History.txt', 0..1).join(\"\\n\\n\")\nend\n\n# vim: syntax=Ruby\n">>, 34 | {blob, 430, Data} = git:object_data(Git, "ff69c3684a18592c741332b290492aa39d980e02"). 35 | 36 | rev_list_test() -> 37 | Git = git:open("test_git"), 38 | RevList = ["25daa907ccb6feb267bfec70a130d5fe13e48a79", 39 | "dd991a4966e8807d448305e67a9b3727efc6060c", 40 | "be62addb149d286893e2ec254e0dc783a871e8af", 41 | "208fc4a6a08fb1e5136cf9943d79ad81097a0f36", 42 | "ef8285c69ceac3f2ff2fee154d3d6ad6a71fb826", 43 | "61fa970afbf82f79611220203213cd9562a809e3", 44 | "a361a3163a1b521f277e3877232d94b787750c36"], 45 | RevList = git:rev_list(Git, ["25daa907ccb6feb267bfec70a130d5fe13e48a79"]). 46 | -------------------------------------------------------------------------------- /etest/run.es: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env escript 2 | %% -*- erlang -*- 3 | %%! -pa ./ebin -sasl -boot start_sasl -noshell 4 | 5 | main(_) -> 6 | git:test(), 7 | git_io:test(). 8 | -------------------------------------------------------------------------------- /src/git.erl: -------------------------------------------------------------------------------- 1 | %% -*- erlang-indent-level: 2 -*- 2 | %% 3 | %% Partial Git Implementation 4 | %% 5 | 6 | -module(git). 7 | -export([open/1, object_data/2, object/2, object_exists/2, rev_list/2]). 8 | 9 | -ifdef(TEST). 10 | -include("etest/git_test.erl"). 11 | -endif. 12 | 13 | -include("git.hrl"). 14 | 15 | %%-define(cassandra_ZERO, 0). 16 | 17 | open(Path) -> 18 | % normalize the path (look for .git, etc) 19 | #git{path = Path}. 20 | 21 | %references(Git) -> 22 | % read all the refs from disk/packed-refs and return an array 23 | %{Git}. 24 | 25 | rev_list(Git, Shas) -> 26 | Graph = digraph:new(), 27 | rev_list(Git, Graph, Shas), 28 | digraph_utils:topsort(Graph). 29 | 30 | rev_list(Git, Graph, [Sha|Shas]) -> 31 | {ok, Commit} = object(Git, Sha), 32 | digraph:add_vertex(Graph, Sha), 33 | Parents = Commit#commit.parents, 34 | AddParents = rev_list_add_edges(Graph, Sha, Parents), 35 | rev_list(Git, Graph, AddParents ++ Shas); 36 | rev_list(_Git, _Graph, []) -> 37 | ok. 38 | 39 | rev_list_add_edges(Graph, Sha, [Parent|Rest]) -> 40 | Vertex = case digraph:vertex(Graph, Parent) of 41 | false -> 42 | digraph:add_vertex(Graph, Parent); 43 | _Else -> 44 | [] 45 | end, 46 | digraph:add_edge(Graph, Sha, Parent), 47 | [Vertex|rev_list_add_edges(Graph, Sha, Rest)]; 48 | rev_list_add_edges(_Graph, _Commit, []) -> 49 | []. 50 | 51 | object(Git, Sha) -> 52 | {Type, _Size, Data} = object_data(Git, Sha), 53 | git_object:parse_object(Sha, Data, Type). 54 | 55 | git_dir(Git) -> 56 | Git#git.path. 57 | 58 | object_exists(Git, ObjectSha) -> 59 | LoosePath = get_loose_object_path(Git, ObjectSha), 60 | case filelib:is_file(LoosePath) of 61 | true -> 62 | true; 63 | false -> 64 | case find_packfile_with_object(Git, ObjectSha) of 65 | {ok, _PackFilePath, _Offset} -> 66 | true; 67 | _Else -> 68 | false 69 | end 70 | end. 71 | 72 | % get the raw object data out of loose or packed formats 73 | % see if the object is loose, read the data 74 | % else check the packfile indexes and get the object out of a packfile 75 | % TODO: cache calls to this (at least for commit/tree objects) 76 | object_data(Git, ObjectSha) -> 77 | LoosePath = get_loose_object_path(Git, ObjectSha), 78 | case file:read_file(LoosePath) of 79 | {ok, Data} -> 80 | extract_loose_object_data(Data); 81 | _Else -> 82 | get_packfile_object_data(Git, ObjectSha) 83 | end. 84 | 85 | get_loose_object_path(Git, ObjectSha) -> 86 | First = string:substr(ObjectSha, 1, 2), 87 | Second = string:substr(ObjectSha, 3, 38), 88 | git_dir(Git) ++ "/objects/" ++ First ++ "/" ++ Second. 89 | 90 | extract_loose_object_data(CompData) -> 91 | extract_loose_object_1(zlib:uncompress(CompData), []). 92 | 93 | extract_loose_object_1(<<$\s,T/binary>>, Type0) -> 94 | Type = list_to_atom(lists:reverse(Type0)), 95 | extract_loose_object_2(T, Type, 0); 96 | extract_loose_object_1(<>, Type) -> 97 | extract_loose_object_1(T, [C|Type]). 98 | 99 | extract_loose_object_2(<<0,Data/binary>>, Type, Sz) -> 100 | {Type, Sz, Data}; 101 | extract_loose_object_2(<>, Type, Sz) -> 102 | extract_loose_object_2(T, Type, Sz*10+Digit-$0). 103 | 104 | get_packfile_object_data(Git, ObjectSha) -> 105 | case find_packfile_with_object(Git, ObjectSha) of 106 | {ok, PackFilePath, Offset} -> 107 | packfile:get_packfile_data(Git, PackFilePath, Offset); 108 | _Else -> 109 | invalid 110 | end. 111 | 112 | find_packfile_with_object(Git, ObjectSha) -> 113 | %io:fwrite("SHA:~p~n", [ObjectSha]), 114 | PackIndex = git_dir(Git) ++ "/objects/pack", 115 | case file:list_dir(PackIndex) of 116 | {ok, Filenames} -> 117 | Indexes = lists:filter(fun(X) -> string_ends_with(X, ".idx") end, Filenames), 118 | case get_packfile_with_object(Git, Indexes, ObjectSha) of 119 | {ok, Packfile, Offset} -> 120 | PackFilePath = git_dir(Git) ++ "/objects/pack/" ++ Packfile, 121 | {ok, PackFilePath, Offset}; 122 | _Else -> 123 | invalid 124 | end; 125 | _Else -> 126 | invalid 127 | end. 128 | 129 | get_packfile_with_object(Git, [Index|Rest], ObjectSha) -> 130 | PackIndex = git_dir(Git) ++ "/objects/pack/" ++ Index, 131 | %io:fwrite("Looking for ~p in ~p~n", [ObjectSha, PackIndex]), 132 | case file:read_file(PackIndex) of 133 | {ok, Data} -> 134 | case packindex:extract_packfile_index(Data) of 135 | {ok, IndexData} -> 136 | %io:fwrite("PackIndex Size:~p~n", [IndexData#index.size]), 137 | %io:fwrite("IndexData:~p~n", [IndexData]), 138 | case packindex:object_offset(IndexData, ObjectSha) of 139 | {ok, Offset} -> 140 | Packfile = replace_string_ending(Index, ".idx", ".pack"), 141 | {ok, Packfile, Offset}; 142 | not_found -> 143 | get_packfile_with_object(Git, Rest, ObjectSha) 144 | end; 145 | Else -> 146 | io:fwrite("Invalid, Biatch~p~n", [Else]), 147 | invalid 148 | end; 149 | _Else -> 150 | invalid 151 | end; 152 | get_packfile_with_object(_Git, [], _ObjectSha) -> 153 | not_found. 154 | 155 | replace_string_ending(String, Ending, NewEnding) -> 156 | Base = string:substr(String, 1, length(String) - length(Ending)), 157 | Base ++ NewEnding. 158 | 159 | string_ends_with(File, Ending) -> 160 | FileEnding = string:substr(File, length(File) - length(Ending) + 1, length(Ending)), 161 | FileEnding =:= Ending. 162 | 163 | -------------------------------------------------------------------------------- /src/git.hrl: -------------------------------------------------------------------------------- 1 | -record(git, {path}). 2 | 3 | -record(index, {header, 4 | version, 5 | size, 6 | fanout, 7 | shalist, 8 | crclist, 9 | offsets, 10 | packcs}). 11 | 12 | -record(commit, {sha, 13 | parents, 14 | tree, 15 | author, 16 | committer, 17 | encoding, 18 | message}). 19 | 20 | -record(tree, {mode, name, sha}). 21 | 22 | -------------------------------------------------------------------------------- /src/git_io.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Git Output Printing Functions 3 | %% 4 | 5 | -module(git_io). 6 | -export([print_log/2, print_tree/1]). 7 | 8 | -ifdef(TEST). 9 | -include("etest/git_io_test.erl"). 10 | -endif. 11 | 12 | -include("git.hrl"). 13 | 14 | %print_branches(Git) -> 15 | % print branches out to stdout 16 | %io:fwrite("Branches:~n"). 17 | 18 | print_tree([Entry|Rest]) -> 19 | io:fwrite("~-7.6s", [Entry#tree.mode]), 20 | io:fwrite("~s ", [Entry#tree.sha]), 21 | io:fwrite("~s~n", [Entry#tree.name]), 22 | print_tree(Rest); 23 | print_tree([]) -> 24 | ok. 25 | 26 | print_log(Git, Refs) -> 27 | % traverse the reference, printing out all the log information to stdout 28 | Shas = Refs, % TODO - revparse these 29 | RevList = git:rev_list(Git, Shas), 30 | print_log_entries(Git, RevList). 31 | 32 | print_log_entries(Git, [Sha|Rest]) -> 33 | {ok, Commit} = git:object(Git, Sha), 34 | io:fwrite("commit ~s~n", [Commit#commit.sha]), 35 | io:fwrite("Author: ~s~n", [Commit#commit.author]), 36 | io:fwrite("~n"), 37 | io:fwrite("~s~n", [Commit#commit.message]), 38 | io:fwrite("~n"), 39 | print_log_entries(Git, Rest); 40 | print_log_entries(_Git, []) -> 41 | ok. 42 | 43 | -------------------------------------------------------------------------------- /src/git_object.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Git Object Parsers 3 | %% 4 | 5 | %% TODO: replace regexp:first_match with re 6 | 7 | -module(git_object). 8 | -export([parse_object/3]). 9 | 10 | -include("git.hrl"). 11 | 12 | parse_object(_Sha, Data, blob) -> 13 | binary_to_list(Data); 14 | 15 | parse_object(_Sha, Data, tree) -> 16 | % mode(6) SP Filename \0 SHA(20) 17 | TreeString = binary_to_list(Data), 18 | Tree = parse_tree_string(TreeString), 19 | {ok, Tree}; 20 | 21 | parse_object(Sha, Data, commit) -> 22 | CommitString = binary_to_list(Data), 23 | {match, [{Offset, Len}]} = re:run(CommitString, "\n\n"), 24 | {Meta, Message} = lists:split(Offset + Len - 1, CommitString), 25 | Parents = parse_commit_parents(Meta), 26 | Tree = extract_one(Meta, "tree (.*)"), 27 | Author = extract_one(Meta, "author (.*)"), 28 | Committer = extract_one(Meta, "committer (.*)"), 29 | Encoding = extract_one(Meta, "encoding (.*)"), 30 | %io:format("Parents:~p~nTree:~p~nAuthor:~p~nMessage:~p~n~n", [Parents, Tree, Author, Message]), 31 | Commit = #commit{sha=Sha, tree=Tree, parents=Parents, 32 | author=Author, committer=Committer, 33 | encoding=Encoding, message=Message}, 34 | {ok, Commit}. 35 | 36 | parse_commit_parents(Data) -> 37 | Parents = extract_multi(Data, "parent (.*)"), 38 | extract_matches(Parents). 39 | 40 | parse_tree_string([]) -> 41 | []; 42 | parse_tree_string(Tree) -> 43 | {Mode, Rest} = read_until(Tree, 32), 44 | {FileName, Rest2} = read_until(Rest, 0), 45 | {Sha, Rest3} = lists:split(20, Rest2), 46 | ShaHex = hex:list_to_hexstr(Sha), 47 | TreeObj = #tree{sha=ShaHex, mode=Mode, name=FileName}, 48 | [TreeObj | parse_tree_string(Rest3)]. 49 | 50 | read_until(String, Find) -> 51 | {Front, Back} = lists:splitwith(fun(A) -> A /= Find end, String), 52 | {_Found, Rest} = lists:split(1, Back), 53 | {Front, Rest}. 54 | 55 | extract_matches([Match|Rest]) -> 56 | [_Full, Data] = Match, 57 | [Data|extract_matches(Rest)]; 58 | extract_matches([]) -> 59 | []. 60 | 61 | extract_multi(Data, Regex) -> 62 | case re:run(Data, Regex, [global, {capture, all, list}]) of 63 | {match, Captured} -> 64 | Captured; 65 | _Else -> 66 | "" 67 | end. 68 | 69 | extract_one(Data, Regex) -> 70 | case re:run(Data, Regex, [{capture, all, list}]) of 71 | {match, Captured} -> 72 | [_Full, Value] = Captured, 73 | Value; 74 | _Else -> 75 | "" 76 | end. 77 | 78 | -------------------------------------------------------------------------------- /src/hex.erl: -------------------------------------------------------------------------------- 1 | % 2 | % via Steve Vinoski 3 | % http://necrobious.blogspot.com/2008/03/binary-to-hex-string-back-to-binary-in.html 4 | % 5 | -module(hex). 6 | -export([bin_to_hexstr/1, list_to_hexstr/1, hexstr_to_bin/1]). 7 | 8 | bin_to_hexstr(Bin) -> 9 | lists:flatten([io_lib:format("~2.16.0b", [X]) || 10 | X <- binary_to_list(Bin)]). 11 | 12 | list_to_hexstr(Bin) -> 13 | lists:flatten([io_lib:format("~2.16.0b", [X]) || 14 | X <- Bin]). 15 | 16 | hexstr_to_bin(S) -> 17 | hexstr_to_bin(S, []). 18 | hexstr_to_bin([], Acc) -> 19 | list_to_binary(lists:reverse(Acc)); 20 | hexstr_to_bin([X,Y|T], Acc) -> 21 | {ok, [V], []} = io_lib:fread("~16u", [X,Y]), 22 | hexstr_to_bin(T, [V | Acc]). 23 | 24 | -------------------------------------------------------------------------------- /src/packfile.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Partial Git Implementation 3 | %% 4 | 5 | -module(packfile). 6 | -export([get_packfile_data/3]). 7 | 8 | get_packfile_data(Git, PackFilePath, Offset) -> 9 | case file:open(PackFilePath, [read, binary]) of 10 | {ok, IoDevice} -> 11 | read_packfile_object_offset(Git, IoDevice, Offset); 12 | {error, _Reason} -> 13 | error 14 | end. 15 | 16 | read_packfile_object_offset(Git, IoDevice, Offset) -> 17 | file:position(IoDevice, Offset), 18 | {ok, Byte} = file:read(IoDevice, 1), 19 | <> = Byte, 20 | TypeTerm = type_int_to_term(Type), 21 | %io:fwrite("Object Data: ~p ~p ~p~n", [ContinueBit, Type, InitSize]), 22 | {FinalType, Size, Data} = read_object_data(Git, IoDevice, ContinueBit, InitSize, 4, Offset, Offset + 1, TypeTerm), 23 | %io:fwrite("Final Object Data: ~p ~p ~p~n", [Size, FinalType, Data]), 24 | {FinalType, Size, Data}. 25 | 26 | read_object_data(Git, IoDevice, 1, Size, Shift, OrigOffset, FileOffset, TypeTerm) -> 27 | {ok, Byte} = file:read(IoDevice, 1), 28 | <> = Byte, 29 | % bit shift the size 30 | SizeOr = NextSize bsl Shift, 31 | NewSize = Size bor SizeOr, 32 | NextShift = Shift + 7, 33 | %io:fwrite("Object Data: [~p] ~p ~p (~p)~n", [FileOffset, ContinueBit, NextSize, NewSize]), 34 | read_object_data(Git, IoDevice, ContinueBit, NewSize, NextShift, OrigOffset, FileOffset + 1, TypeTerm); 35 | read_object_data(Git, IoDevice, 0, Size, Shift, OrigOffset, FileOffset, ofs_delta) -> 36 | read_ofs_deltified_object_data(Git, IoDevice, Size, Shift, FileOffset, OrigOffset); 37 | read_object_data(Git, IoDevice, 0, Size, Shift, OrigOffset, FileOffset, ref_delta) -> 38 | read_ref_deltified_object_data(Git, IoDevice, Size, Shift, FileOffset, OrigOffset); 39 | read_object_data(_Git, IoDevice, 0, Size, _Shift, _OrigOffset, FileOffset, NormalType) -> 40 | %io:fwrite("File Offset:~p~n", [FileOffset]), 41 | file:position(IoDevice, FileOffset), 42 | Z = zlib:open(), 43 | ok = zlib:inflateInit(Z), 44 | Data = inflate_object_data(Z, IoDevice, []), 45 | %io:fwrite("Bits:~p~n", [Data]), 46 | ok = zlib:inflateEnd(Z), 47 | zlib:close(Z), 48 | {NormalType, Size, Data}. 49 | 50 | % git rev-list --objects --all | git pack-objects --window=250 --depth=250 --stdout > #{packfile_pack} 51 | % git index-pack -o #{packfile_idx} #{packfile_pack} 52 | read_ref_deltified_object_data(Git, IoDevice, Size, Offset, FileOffset, OrigOffset) -> 53 | {ok, Sha} = file:read(IoDevice, 20), 54 | HexSha = hex:bin_to_hexstr(Sha), 55 | {TypeTerm, _BaseSize, BaseData} = git:object_data(Git, HexSha), 56 | %io:fwrite("Base:~p~n", [BaseSize]), 57 | {TypeTerm, _DeltaSize, DeltaData} = read_object_data(Git, IoDevice, 0, Size, Offset, OrigOffset, FileOffset + 20, TypeTerm), 58 | %io:fwrite("DeltaData: ~p:~p:~p~n", [TypeTerm, DeltaSize, DeltaData]), 59 | PatchedData = patch_delta(DeltaData, BaseData), 60 | {TypeTerm, length(PatchedData), list_to_binary(PatchedData)}. 61 | 62 | read_ofs_deltified_object_data(Git, IoDevice, Size, Offset, FileOffset, OrigOffset) -> 63 | {ok, BaseOffset, BytesRead} = get_base_offset(IoDevice), 64 | NewOffset = OrigOffset - BaseOffset, 65 | %io:fwrite("BaseOffset: ~p:~p:~p~n", [NewOffset, OrigOffset, BaseOffset]), 66 | {TypeTerm, _BaseSize, BaseData} = read_packfile_object_offset(Git, IoDevice, NewOffset), 67 | file:position(IoDevice, FileOffset + BytesRead), 68 | {TypeTerm, _DeltaSize, DeltaData} = read_object_data(Git, IoDevice, 0, Size, Offset, OrigOffset, FileOffset + BytesRead, TypeTerm), 69 | %io:fwrite("DeltaData: ~p:~p:~p~n", [TypeTerm, DeltaSize, DeltaData]), 70 | PatchedData = patch_delta(DeltaData, BaseData), 71 | {TypeTerm, length(PatchedData), list_to_binary(PatchedData)}. 72 | 73 | patch_delta(DeltaData, BaseData) -> 74 | DeltaList = binary_to_list(DeltaData), 75 | BaseList = binary_to_list(BaseData), 76 | {ok, _SrcSize, PosA} = patch_delta_header_size(DeltaList, 1), 77 | % SrcSize should == BaseData.size 78 | %io:fwrite("Patch:SrcSize : ~p:~p~n", [SrcSize, PosA]), 79 | {ok, _DestSize, PosB} = patch_delta_header_size(DeltaList, PosA), 80 | %io:fwrite("Patch:DestSize: ~p:~p~n", [DestSize, PosB]), 81 | PatchedData = patch_delta([], BaseList, DeltaList, PosB, length(DeltaList) + 1), 82 | %io:fwrite("PatchData: ~p~n", [length(PatchedData)]), 83 | PatchedData. 84 | 85 | patch_delta(PatchedList, _BaseList, _DeltaList, _DeltaPos, _DeltaPos) -> 86 | PatchedList; 87 | patch_delta(PatchedList, BaseList, DeltaList, DeltaPos, Size) -> 88 | Byte = lists:nth(DeltaPos, DeltaList), 89 | StartPos = DeltaPos + 1, 90 | <> = <>, 91 | %io:fwrite("Patch: ~p:~p:~p~n", [PatchBit, DeltaPos, Size]), 92 | case PatchBit of 93 | 1 -> 94 | % append data from base list 95 | <<_Pb:1, S16:1, S8:1, S0:1, Of24:1, Of16:1, Of8:1, Of0:1>> = <>, 96 | {CpOffA, DeltaOffA} = calc_cp(DeltaList, StartPos, Of0, 0, 0), 97 | {CpOffB, DeltaOffB} = calc_cp(DeltaList, DeltaOffA, Of8, 8, CpOffA), 98 | {CpOffC, DeltaOffC} = calc_cp(DeltaList, DeltaOffB, Of16, 16, CpOffB), 99 | {CpOff, DeltaOffD} = calc_cp(DeltaList, DeltaOffC, Of24, 24, CpOffC), 100 | {CpSizeA, DeltaOffE} = calc_cp(DeltaList, DeltaOffD, S0, 0, 0), 101 | {CpSizeB, DeltaOffF} = calc_cp(DeltaList, DeltaOffE, S8, 8, CpSizeA), 102 | {CpSize, DeltaOff} = calc_cp(DeltaList, DeltaOffF, S16, 16, CpSizeB), 103 | %io:fwrite("Off/Size: ~p:~p:~p~n", [CpOff, CpSize, DeltaOff]), 104 | PatchDataList = lists:sublist(BaseList, CpOff + 1, CpSize), 105 | patch_delta(PatchedList ++ PatchDataList, BaseList, DeltaList, DeltaOff, Size); 106 | 0 -> 107 | % append data from delta list 108 | %io:fwrite("Patch Delta Data (~p:~p)~n", [StartPos, PatchData]), 109 | PatchDataList = lists:sublist(DeltaList, StartPos, PatchData), 110 | patch_delta(PatchedList ++ PatchDataList, BaseList, DeltaList, StartPos + PatchData, Size) 111 | end. 112 | 113 | calc_cp(DList, DPos, 1, Shift, Cp) -> 114 | Data = lists:nth(DPos, DList), 115 | ShiftCp = Data bsl Shift, 116 | NewCp = Cp bor ShiftCp, 117 | {NewCp, DPos + 1}; 118 | calc_cp(_DList, DPos, 0, _Shift, Cp) -> 119 | {Cp, DPos}. 120 | 121 | patch_delta_header_size(DeltaData, Pos) -> 122 | patch_delta_header_size(DeltaData, Pos, 0, 1, 0). 123 | 124 | patch_delta_header_size(DeltaData, Pos, Shift, 1, Size) -> 125 | Byte = lists:nth(Pos, DeltaData), 126 | <> = <>, 127 | %io:fwrite("Byte: ~p:~p:~p:~p:~p:~p~n", [Byte, Pos, Shift, Size, ContinueBit, NextSize]), 128 | SizeOr = NextSize bsl Shift, 129 | NewSize = Size bor SizeOr, 130 | patch_delta_header_size(DeltaData, Pos + 1, Shift + 7, ContinueBit, NewSize); 131 | patch_delta_header_size(_DeltaData, Pos, _Shift, 0, Size) -> 132 | {ok, Size, Pos}. 133 | 134 | get_base_offset(IoDevice) -> 135 | {ok, Byte} = file:read(IoDevice, 1), 136 | <> = Byte, 137 | get_base_offset(IoDevice, ContinueBit, NextData, 1). 138 | 139 | get_base_offset(IoDevice, 1, Offset, BytesRead) -> 140 | {ok, Byte} = file:read(IoDevice, 1), 141 | <> = Byte, 142 | %io:fwrite("Data:~p:~p~n", [ContinueBit, NextData]), 143 | Offset2 = Offset + 1, 144 | Offset3 = Offset2 bsl 7, 145 | NewOffset = Offset3 bor NextData, 146 | get_base_offset(IoDevice, ContinueBit, NewOffset, BytesRead + 1); 147 | get_base_offset(_IoDevice, 0, Offset, BytesRead) -> 148 | {ok, Offset, BytesRead}. 149 | 150 | inflate_object_data(Z, IoDevice, SoFar) -> 151 | case file:read(IoDevice, 4096) of 152 | {ok, Bytes} -> 153 | _Inflated = case catch zlib:inflate(Z, Bytes) of 154 | {'EXIT', {'data_error', _Backtrace} } -> 155 | %io:format("zlib:inflate data_error,~n"), 156 | SoFar; 157 | {'EXIT', _Reason} -> 158 | %io:format("zlib:inflate error -> [~p]~n", [Reason]), 159 | SoFar; 160 | [] -> 161 | SoFar; 162 | Iolist -> 163 | [Data] = Iolist, 164 | ListData = list_to_binary([binary_to_list(Data)|SoFar]), 165 | %io:fwrite("Size: ~p~n", [size(ListData)]), 166 | inflate_object_data(Z, IoDevice, ListData) 167 | end; 168 | _Else -> 169 | SoFar 170 | end. 171 | 172 | type_int_to_term(ObjInt) -> 173 | case ObjInt of 174 | 1 -> commit; 175 | 2 -> tree; 176 | 3 -> blob; 177 | 4 -> tag; 178 | 6 -> ofs_delta; 179 | 7 -> ref_delta 180 | end. 181 | 182 | -------------------------------------------------------------------------------- /src/packindex.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Partial Git Implementation 3 | %% 4 | 5 | -module(packindex). 6 | -export([extract_packfile_index/1, object_offset/2]). 7 | 8 | -include("git.hrl"). 9 | 10 | %%% 11 | % get an object offset from an index record 12 | %%% 13 | object_offset(Index, ObjectSha) -> 14 | ShaOffsets = lists:zip(Index#index.shalist, Index#index.offsets), 15 | case lists:keyfind(ObjectSha, 1, ShaOffsets) of 16 | {ObjectSha, Offset} -> 17 | {ok, Offset}; 18 | _Else -> 19 | not_found 20 | end. 21 | 22 | %%% 23 | % extract a sha offset from packfile index data 24 | % TODO: check to see if this is version 1 or version 2 (currently only v2) 25 | % TODO: parse large offset section if it exists (curr. assumes it does not) 26 | %%% 27 | extract_packfile_index(Data) -> 28 | {Header, Data2} = split_binary(Data, 4), % \377tOc 29 | {Version, Data3} = split_binary(Data2, 4), % 0002 30 | {FanoutTable, Size, Data4} = extract_fanout(Data3), 31 | {ShaList, Data5} = extract_sha_list(Data4, Size), 32 | {CrcList, Data6} = extract_crc(Data5, Size), 33 | {OffsetList, Data7} = extract_offsets(Data6, Size), 34 | {PackCs, Data8} = split_binary(Data7, 20), 35 | {_IdxCs, _Empty} = split_binary(Data8, 20), 36 | Index = #index{header=Header, version=Version, size=Size, fanout=FanoutTable, 37 | shalist=ShaList, crclist=CrcList, offsets=OffsetList, packcs=hex:bin_to_hexstr(PackCs)}, 38 | {ok, Index}. 39 | 40 | %%% 41 | % extract offset list from packfile index 42 | %%% 43 | extract_offsets(IndexData, Size) -> 44 | extract_offsets(IndexData, Size, 0, []). 45 | extract_offsets(IndexData, Size, Size, Listing) -> 46 | {lists:reverse(Listing), IndexData}; 47 | extract_offsets(IndexData, Size, Total, Listing) -> 48 | {Offset, IndexDataRem} = split_binary(IndexData, 4), % Offset 49 | <> = Offset, 50 | extract_offsets(IndexDataRem, Size, Total + 1, [OffsetInt|Listing]). 51 | 52 | %%% 53 | % extract crc checksums from packfile index 54 | %%% 55 | extract_crc(IndexData, Size) -> 56 | extract_crc(IndexData, Size, 0, []). 57 | extract_crc(IndexData, Size, Size, Listing) -> 58 | {lists:reverse(Listing), IndexData}; 59 | extract_crc(IndexData, Size, Total, Listing) -> 60 | {Crc, IndexDataRem} = split_binary(IndexData, 4), % CRC 61 | extract_crc(IndexDataRem, Size, Total + 1, [Crc|Listing]). 62 | 63 | %%% 64 | % extract sha listing from packfile index 65 | %%% 66 | extract_sha_list(IndexData, Size) -> 67 | extract_sha_list(IndexData, Size, 0, []). 68 | extract_sha_list(IndexData, Size, Size, Listing) -> 69 | {lists:reverse(Listing), IndexData}; 70 | extract_sha_list(IndexData, Size, Total, Listing) -> 71 | {Sha, IndexDataRem} = split_binary(IndexData, 20), % SHA 72 | HexSha = hex:bin_to_hexstr(Sha), 73 | extract_sha_list(IndexDataRem, Size, Total + 1, [HexSha|Listing]). 74 | 75 | %%% 76 | % extract fanout table from packfile index 77 | %%% 78 | extract_fanout(IndexData) -> 79 | extract_fanout(IndexData, 0, []). 80 | extract_fanout(IndexData, 255, Fanout) -> 81 | {Size, IndexDataRem} = split_binary(IndexData, 4), % size 82 | <> = Size, 83 | {lists:reverse(Fanout), SizeInt, IndexDataRem}; 84 | extract_fanout(IndexData, Count, Fanout) -> 85 | {Fan, IndexDataRem} = split_binary(IndexData, 4), % fanout entry 86 | <> = Fan, 87 | extract_fanout(IndexDataRem, Count + 1, [FanInt|Fanout]). 88 | 89 | -------------------------------------------------------------------------------- /test_git/COMMIT_EDITMSG: -------------------------------------------------------------------------------- 1 | test content 2 | -------------------------------------------------------------------------------- /test_git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /test_git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | ignorecase = true 7 | -------------------------------------------------------------------------------- /test_git/description: -------------------------------------------------------------------------------- 1 | Unnamed repository; edit this file 'description' to name the repository. 2 | -------------------------------------------------------------------------------- /test_git/hooks/applypatch-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message taken by 4 | # applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. The hook is 8 | # allowed to edit the commit message file. 9 | # 10 | # To enable this hook, rename this file to "applypatch-msg". 11 | 12 | . git-sh-setup 13 | test -x "$GIT_DIR/hooks/commit-msg" && 14 | exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"} 15 | : 16 | -------------------------------------------------------------------------------- /test_git/hooks/commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message. 4 | # Called by git-commit with one argument, the name of the file 5 | # that has the commit message. The hook should exit with non-zero 6 | # status after issuing an appropriate message if it wants to stop the 7 | # commit. The hook is allowed to edit the commit message file. 8 | # 9 | # To enable this hook, rename this file to "commit-msg". 10 | 11 | # Uncomment the below to add a Signed-off-by line to the message. 12 | # Doing this in a hook is a bad idea in general, but the prepare-commit-msg 13 | # hook is more suited to it. 14 | # 15 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 16 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 17 | 18 | # This example catches duplicate Signed-off-by lines. 19 | 20 | test "" = "$(grep '^Signed-off-by: ' "$1" | 21 | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { 22 | echo >&2 Duplicate Signed-off-by lines. 23 | exit 1 24 | } 25 | -------------------------------------------------------------------------------- /test_git/hooks/post-commit.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script that is called after a successful 4 | # commit is made. 5 | # 6 | # To enable this hook, rename this file to "post-commit". 7 | 8 | : Nothing 9 | -------------------------------------------------------------------------------- /test_git/hooks/post-receive.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script for the "post-receive" event. 4 | # 5 | # The "post-receive" script is run after receive-pack has accepted a pack 6 | # and the repository has been updated. It is passed arguments in through 7 | # stdin in the form 8 | # 9 | # For example: 10 | # aa453216d1b3e49e7f6f98441fa56946ddcd6a20 68f7abf4e6f922807889f52bc043ecd31b79f814 refs/heads/master 11 | # 12 | # see contrib/hooks/ for a sample, or uncomment the next line and 13 | # rename the file to "post-receive". 14 | 15 | #. /usr/share/doc/git-core/contrib/hooks/post-receive-email 16 | -------------------------------------------------------------------------------- /test_git/hooks/post-update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare a packed repository for use over 4 | # dumb transports. 5 | # 6 | # To enable this hook, rename this file to "post-update". 7 | 8 | exec git-update-server-info 9 | -------------------------------------------------------------------------------- /test_git/hooks/pre-applypatch.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed 4 | # by applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. 8 | # 9 | # To enable this hook, rename this file to "pre-applypatch". 10 | 11 | . git-sh-setup 12 | test -x "$GIT_DIR/hooks/pre-commit" && 13 | exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"} 14 | : 15 | -------------------------------------------------------------------------------- /test_git/hooks/pre-commit.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by git-commit with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message if 6 | # it wants to stop the commit. 7 | # 8 | # To enable this hook, rename this file to "pre-commit". 9 | 10 | # If you want to allow non-ascii filenames set this variable to true. 11 | allownonascii=$(git config hooks.allownonascii) 12 | 13 | # Cross platform projects tend to avoid non-ascii filenames; prevent 14 | # them from being added to the repository. We exploit the fact that the 15 | # printable range starts at the space character and ends with tilde. 16 | if [ "$allownonascii" != "true" ] && 17 | # Note that the use of brackets around a tr range is ok here, (it's 18 | # even required, for portability to Solaris 10's /usr/bin/tr), since 19 | # the square bracket bytes happen to fall in the designated range. 20 | test "$(git diff --cached --name-only --diff-filter=A -z | 21 | LC_ALL=C tr -d '[ -~]\0')" 22 | then 23 | echo "Error: Attempt to add a non-ascii file name." 24 | echo 25 | echo "This can cause problems if you want to work" 26 | echo "with people on other platforms." 27 | echo 28 | echo "To be portable it is advisable to rename the file ..." 29 | echo 30 | echo "If you know what you are doing you can disable this" 31 | echo "check using:" 32 | echo 33 | echo " git config hooks.allownonascii true" 34 | echo 35 | exit 1 36 | fi 37 | 38 | if git-rev-parse --verify HEAD >/dev/null 2>&1 39 | then 40 | against=HEAD 41 | else 42 | # Initial commit: diff against an empty tree object 43 | against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 44 | fi 45 | 46 | exec git diff-index --check --cached $against -- 47 | -------------------------------------------------------------------------------- /test_git/hooks/pre-rebase.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (c) 2006, 2008 Junio C Hamano 4 | # 5 | # The "pre-rebase" hook is run just before "git-rebase" starts doing 6 | # its job, and can prevent the command from running by exiting with 7 | # non-zero status. 8 | # 9 | # The hook is called with the following parameters: 10 | # 11 | # $1 -- the upstream the series was forked from. 12 | # $2 -- the branch being rebased (or empty when rebasing the current branch). 13 | # 14 | # This sample shows how to prevent topic branches that are already 15 | # merged to 'next' branch from getting rebased, because allowing it 16 | # would result in rebasing already published history. 17 | 18 | publish=next 19 | basebranch="$1" 20 | if test "$#" = 2 21 | then 22 | topic="refs/heads/$2" 23 | else 24 | topic=`git symbolic-ref HEAD` || 25 | exit 0 ;# we do not interrupt rebasing detached HEAD 26 | fi 27 | 28 | case "$topic" in 29 | refs/heads/??/*) 30 | ;; 31 | *) 32 | exit 0 ;# we do not interrupt others. 33 | ;; 34 | esac 35 | 36 | # Now we are dealing with a topic branch being rebased 37 | # on top of master. Is it OK to rebase it? 38 | 39 | # Does the topic really exist? 40 | git show-ref -q "$topic" || { 41 | echo >&2 "No such branch $topic" 42 | exit 1 43 | } 44 | 45 | # Is topic fully merged to master? 46 | not_in_master=`git-rev-list --pretty=oneline ^master "$topic"` 47 | if test -z "$not_in_master" 48 | then 49 | echo >&2 "$topic is fully merged to master; better remove it." 50 | exit 1 ;# we could allow it, but there is no point. 51 | fi 52 | 53 | # Is topic ever merged to next? If so you should not be rebasing it. 54 | only_next_1=`git-rev-list ^master "^$topic" ${publish} | sort` 55 | only_next_2=`git-rev-list ^master ${publish} | sort` 56 | if test "$only_next_1" = "$only_next_2" 57 | then 58 | not_in_topic=`git-rev-list "^$topic" master` 59 | if test -z "$not_in_topic" 60 | then 61 | echo >&2 "$topic is already up-to-date with master" 62 | exit 1 ;# we could allow it, but there is no point. 63 | else 64 | exit 0 65 | fi 66 | else 67 | not_in_next=`git-rev-list --pretty=oneline ^${publish} "$topic"` 68 | perl -e ' 69 | my $topic = $ARGV[0]; 70 | my $msg = "* $topic has commits already merged to public branch:\n"; 71 | my (%not_in_next) = map { 72 | /^([0-9a-f]+) /; 73 | ($1 => 1); 74 | } split(/\n/, $ARGV[1]); 75 | for my $elem (map { 76 | /^([0-9a-f]+) (.*)$/; 77 | [$1 => $2]; 78 | } split(/\n/, $ARGV[2])) { 79 | if (!exists $not_in_next{$elem->[0]}) { 80 | if ($msg) { 81 | print STDERR $msg; 82 | undef $msg; 83 | } 84 | print STDERR " $elem->[1]\n"; 85 | } 86 | } 87 | ' "$topic" "$not_in_next" "$not_in_master" 88 | exit 1 89 | fi 90 | 91 | exit 0 92 | 93 | ################################################################ 94 | 95 | This sample hook safeguards topic branches that have been 96 | published from being rewound. 97 | 98 | The workflow assumed here is: 99 | 100 | * Once a topic branch forks from "master", "master" is never 101 | merged into it again (either directly or indirectly). 102 | 103 | * Once a topic branch is fully cooked and merged into "master", 104 | it is deleted. If you need to build on top of it to correct 105 | earlier mistakes, a new topic branch is created by forking at 106 | the tip of the "master". This is not strictly necessary, but 107 | it makes it easier to keep your history simple. 108 | 109 | * Whenever you need to test or publish your changes to topic 110 | branches, merge them into "next" branch. 111 | 112 | The script, being an example, hardcodes the publish branch name 113 | to be "next", but it is trivial to make it configurable via 114 | $GIT_DIR/config mechanism. 115 | 116 | With this workflow, you would want to know: 117 | 118 | (1) ... if a topic branch has ever been merged to "next". Young 119 | topic branches can have stupid mistakes you would rather 120 | clean up before publishing, and things that have not been 121 | merged into other branches can be easily rebased without 122 | affecting other people. But once it is published, you would 123 | not want to rewind it. 124 | 125 | (2) ... if a topic branch has been fully merged to "master". 126 | Then you can delete it. More importantly, you should not 127 | build on top of it -- other people may already want to 128 | change things related to the topic as patches against your 129 | "master", so if you need further changes, it is better to 130 | fork the topic (perhaps with the same name) afresh from the 131 | tip of "master". 132 | 133 | Let's look at this example: 134 | 135 | o---o---o---o---o---o---o---o---o---o "next" 136 | / / / / 137 | / a---a---b A / / 138 | / / / / 139 | / / c---c---c---c B / 140 | / / / \ / 141 | / / / b---b C \ / 142 | / / / / \ / 143 | ---o---o---o---o---o---o---o---o---o---o---o "master" 144 | 145 | 146 | A, B and C are topic branches. 147 | 148 | * A has one fix since it was merged up to "next". 149 | 150 | * B has finished. It has been fully merged up to "master" and "next", 151 | and is ready to be deleted. 152 | 153 | * C has not merged to "next" at all. 154 | 155 | We would want to allow C to be rebased, refuse A, and encourage 156 | B to be deleted. 157 | 158 | To compute (1): 159 | 160 | git-rev-list ^master ^topic next 161 | git-rev-list ^master next 162 | 163 | if these match, topic has not merged in next at all. 164 | 165 | To compute (2): 166 | 167 | git-rev-list master..topic 168 | 169 | if this is empty, it is fully merged to "master". 170 | -------------------------------------------------------------------------------- /test_git/hooks/prepare-commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare the commit log message. 4 | # Called by git-commit with the name of the file that has the 5 | # commit message, followed by the description of the commit 6 | # message's source. The hook's purpose is to edit the commit 7 | # message file. If the hook fails with a non-zero status, 8 | # the commit is aborted. 9 | # 10 | # To enable this hook, rename this file to "prepare-commit-msg". 11 | 12 | # This hook includes three examples. The first comments out the 13 | # "Conflicts:" part of a merge commit. 14 | # 15 | # The second includes the output of "git diff --name-status -r" 16 | # into the message, just before the "git status" output. It is 17 | # commented because it doesn't cope with --amend or with squashed 18 | # commits. 19 | # 20 | # The third example adds a Signed-off-by line to the message, that can 21 | # still be edited. This is rarely a good idea. 22 | 23 | case "$2,$3" in 24 | merge,) 25 | perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; 26 | 27 | # ,|template,) 28 | # perl -i.bak -pe ' 29 | # print "\n" . `git diff --cached --name-status -r` 30 | # if /^#/ && $first++ == 0' "$1" ;; 31 | 32 | *) ;; 33 | esac 34 | 35 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 36 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 37 | -------------------------------------------------------------------------------- /test_git/hooks/update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to blocks unannotated tags from entering. 4 | # Called by git-receive-pack with arguments: refname sha1-old sha1-new 5 | # 6 | # To enable this hook, rename this file to "update". 7 | # 8 | # Config 9 | # ------ 10 | # hooks.allowunannotated 11 | # This boolean sets whether unannotated tags will be allowed into the 12 | # repository. By default they won't be. 13 | # hooks.allowdeletetag 14 | # This boolean sets whether deleting tags will be allowed in the 15 | # repository. By default they won't be. 16 | # hooks.allowmodifytag 17 | # This boolean sets whether a tag may be modified after creation. By default 18 | # it won't be. 19 | # hooks.allowdeletebranch 20 | # This boolean sets whether deleting branches will be allowed in the 21 | # repository. By default they won't be. 22 | # hooks.denycreatebranch 23 | # This boolean sets whether remotely creating branches will be denied 24 | # in the repository. By default this is allowed. 25 | # 26 | 27 | # --- Command line 28 | refname="$1" 29 | oldrev="$2" 30 | newrev="$3" 31 | 32 | # --- Safety check 33 | if [ -z "$GIT_DIR" ]; then 34 | echo "Don't run this script from the command line." >&2 35 | echo " (if you want, you could supply GIT_DIR then run" >&2 36 | echo " $0 )" >&2 37 | exit 1 38 | fi 39 | 40 | if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then 41 | echo "Usage: $0 " >&2 42 | exit 1 43 | fi 44 | 45 | # --- Config 46 | allowunannotated=$(git config --bool hooks.allowunannotated) 47 | allowdeletebranch=$(git config --bool hooks.allowdeletebranch) 48 | denycreatebranch=$(git config --bool hooks.denycreatebranch) 49 | allowdeletetag=$(git config --bool hooks.allowdeletetag) 50 | allowmodifytag=$(git config --bool hooks.allowmodifytag) 51 | 52 | # check for no description 53 | projectdesc=$(sed -e '1q' "$GIT_DIR/description") 54 | case "$projectdesc" in 55 | "Unnamed repository"* | "") 56 | echo "*** Project description file hasn't been set" >&2 57 | exit 1 58 | ;; 59 | esac 60 | 61 | # --- Check types 62 | # if $newrev is 0000...0000, it's a commit to delete a ref. 63 | zero="0000000000000000000000000000000000000000" 64 | if [ "$newrev" = "$zero" ]; then 65 | newrev_type=delete 66 | else 67 | newrev_type=$(git-cat-file -t $newrev) 68 | fi 69 | 70 | case "$refname","$newrev_type" in 71 | refs/tags/*,commit) 72 | # un-annotated tag 73 | short_refname=${refname##refs/tags/} 74 | if [ "$allowunannotated" != "true" ]; then 75 | echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 76 | echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 77 | exit 1 78 | fi 79 | ;; 80 | refs/tags/*,delete) 81 | # delete tag 82 | if [ "$allowdeletetag" != "true" ]; then 83 | echo "*** Deleting a tag is not allowed in this repository" >&2 84 | exit 1 85 | fi 86 | ;; 87 | refs/tags/*,tag) 88 | # annotated tag 89 | if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 90 | then 91 | echo "*** Tag '$refname' already exists." >&2 92 | echo "*** Modifying a tag is not allowed in this repository." >&2 93 | exit 1 94 | fi 95 | ;; 96 | refs/heads/*,commit) 97 | # branch 98 | if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then 99 | echo "*** Creating a branch is not allowed in this repository" >&2 100 | exit 1 101 | fi 102 | ;; 103 | refs/heads/*,delete) 104 | # delete branch 105 | if [ "$allowdeletebranch" != "true" ]; then 106 | echo "*** Deleting a branch is not allowed in this repository" >&2 107 | exit 1 108 | fi 109 | ;; 110 | refs/remotes/*,commit) 111 | # tracking branch 112 | ;; 113 | refs/remotes/*,delete) 114 | # delete tracking branch 115 | if [ "$allowdeletebranch" != "true" ]; then 116 | echo "*** Deleting a tracking branch is not allowed in this repository" >&2 117 | exit 1 118 | fi 119 | ;; 120 | *) 121 | # Anything else (is there anything else?) 122 | echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 123 | exit 1 124 | ;; 125 | esac 126 | 127 | # --- Finished 128 | exit 0 129 | -------------------------------------------------------------------------------- /test_git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schacon/erlangit/814a058ce2abcf8e46027d79d65754611f96b97c/test_git/index -------------------------------------------------------------------------------- /test_git/info/exclude: -------------------------------------------------------------------------------- 1 | # git-ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | -------------------------------------------------------------------------------- /test_git/info/refs: -------------------------------------------------------------------------------- 1 | 25daa907ccb6feb267bfec70a130d5fe13e48a79 refs/heads/master 2 | 208fc4a6a08fb1e5136cf9943d79ad81097a0f36 refs/tags/v1.0 3 | 062f5d0268c0a956aff49d96062a62b00fe4eacc refs/tags/v1.1 4 | 208fc4a6a08fb1e5136cf9943d79ad81097a0f36 refs/tags/v1.1^{} 5 | -------------------------------------------------------------------------------- /test_git/logs/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 a361a3163a1b521f277e3877232d94b787750c36 Scott Chacon 1258278448 +0100 commit (initial): first commit 2 | a361a3163a1b521f277e3877232d94b787750c36 61fa970afbf82f79611220203213cd9562a809e3 Scott Chacon 1258278496 +0100 commit: added test one 3 | 61fa970afbf82f79611220203213cd9562a809e3 ef8285c69ceac3f2ff2fee154d3d6ad6a71fb826 Scott Chacon 1258278527 +0100 commit: added test three 4 | ef8285c69ceac3f2ff2fee154d3d6ad6a71fb826 208fc4a6a08fb1e5136cf9943d79ad81097a0f36 Scott Chacon 1258278581 +0100 commit: last commit outside 5 | 208fc4a6a08fb1e5136cf9943d79ad81097a0f36 be62addb149d286893e2ec254e0dc783a871e8af Scott Chacon 1258278712 +0100 commit: new rakefile 6 | be62addb149d286893e2ec254e0dc783a871e8af dd991a4966e8807d448305e67a9b3727efc6060c Scott Chacon 1258278762 +0100 commit: rakefile three 7 | dd991a4966e8807d448305e67a9b3727efc6060c 25daa907ccb6feb267bfec70a130d5fe13e48a79 Scott Chacon 1258278772 +0100 commit: rakefile four 8 | 25daa907ccb6feb267bfec70a130d5fe13e48a79 8bbad744fe9597cc130e8ef7cfd201f36d9cba76 Scott Chacon 1258278898 +0100 commit: test content 9 | -------------------------------------------------------------------------------- /test_git/logs/refs/heads/master: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 a361a3163a1b521f277e3877232d94b787750c36 Scott Chacon 1258278448 +0100 commit (initial): first commit 2 | a361a3163a1b521f277e3877232d94b787750c36 61fa970afbf82f79611220203213cd9562a809e3 Scott Chacon 1258278496 +0100 commit: added test one 3 | 61fa970afbf82f79611220203213cd9562a809e3 ef8285c69ceac3f2ff2fee154d3d6ad6a71fb826 Scott Chacon 1258278527 +0100 commit: added test three 4 | ef8285c69ceac3f2ff2fee154d3d6ad6a71fb826 208fc4a6a08fb1e5136cf9943d79ad81097a0f36 Scott Chacon 1258278581 +0100 commit: last commit outside 5 | 208fc4a6a08fb1e5136cf9943d79ad81097a0f36 be62addb149d286893e2ec254e0dc783a871e8af Scott Chacon 1258278712 +0100 commit: new rakefile 6 | be62addb149d286893e2ec254e0dc783a871e8af dd991a4966e8807d448305e67a9b3727efc6060c Scott Chacon 1258278762 +0100 commit: rakefile three 7 | dd991a4966e8807d448305e67a9b3727efc6060c 25daa907ccb6feb267bfec70a130d5fe13e48a79 Scott Chacon 1258278772 +0100 commit: rakefile four 8 | 25daa907ccb6feb267bfec70a130d5fe13e48a79 8bbad744fe9597cc130e8ef7cfd201f36d9cba76 Scott Chacon 1258278898 +0100 commit: test content 9 | -------------------------------------------------------------------------------- /test_git/objects/19/306733ab5f0c49d46fb43778a527dfddc323de: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schacon/erlangit/814a058ce2abcf8e46027d79d65754611f96b97c/test_git/objects/19/306733ab5f0c49d46fb43778a527dfddc323de -------------------------------------------------------------------------------- /test_git/objects/25/f9782940ad877fbc6900a678cdc5d032df4d53: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schacon/erlangit/814a058ce2abcf8e46027d79d65754611f96b97c/test_git/objects/25/f9782940ad877fbc6900a678cdc5d032df4d53 -------------------------------------------------------------------------------- /test_git/objects/47/e3fc6cde2a1552c28ab97361ff54bbae3c6f60: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schacon/erlangit/814a058ce2abcf8e46027d79d65754611f96b97c/test_git/objects/47/e3fc6cde2a1552c28ab97361ff54bbae3c6f60 -------------------------------------------------------------------------------- /test_git/objects/8b/bad744fe9597cc130e8ef7cfd201f36d9cba76: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schacon/erlangit/814a058ce2abcf8e46027d79d65754611f96b97c/test_git/objects/8b/bad744fe9597cc130e8ef7cfd201f36d9cba76 -------------------------------------------------------------------------------- /test_git/objects/8d/47f3435ce5dfd0b2ab5758590c2db21b5294b4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schacon/erlangit/814a058ce2abcf8e46027d79d65754611f96b97c/test_git/objects/8d/47f3435ce5dfd0b2ab5758590c2db21b5294b4 -------------------------------------------------------------------------------- /test_git/objects/d8/de215b38971591797f31f916b457d2ca2538cf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schacon/erlangit/814a058ce2abcf8e46027d79d65754611f96b97c/test_git/objects/d8/de215b38971591797f31f916b457d2ca2538cf -------------------------------------------------------------------------------- /test_git/objects/info/packs: -------------------------------------------------------------------------------- 1 | P pack-d2e5da2ce623cd3a19db4c788fb27f3373f97dcf.pack 2 | 3 | -------------------------------------------------------------------------------- /test_git/objects/pack/pack-d2e5da2ce623cd3a19db4c788fb27f3373f97dcf.idx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schacon/erlangit/814a058ce2abcf8e46027d79d65754611f96b97c/test_git/objects/pack/pack-d2e5da2ce623cd3a19db4c788fb27f3373f97dcf.idx -------------------------------------------------------------------------------- /test_git/objects/pack/pack-d2e5da2ce623cd3a19db4c788fb27f3373f97dcf.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schacon/erlangit/814a058ce2abcf8e46027d79d65754611f96b97c/test_git/objects/pack/pack-d2e5da2ce623cd3a19db4c788fb27f3373f97dcf.pack -------------------------------------------------------------------------------- /test_git/objects/pack/test.idx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schacon/erlangit/814a058ce2abcf8e46027d79d65754611f96b97c/test_git/objects/pack/test.idx -------------------------------------------------------------------------------- /test_git/objects/pack/test.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schacon/erlangit/814a058ce2abcf8e46027d79d65754611f96b97c/test_git/objects/pack/test.pack -------------------------------------------------------------------------------- /test_git/packed-refs: -------------------------------------------------------------------------------- 1 | # pack-refs with: peeled 2 | 25daa907ccb6feb267bfec70a130d5fe13e48a79 refs/heads/master 3 | 208fc4a6a08fb1e5136cf9943d79ad81097a0f36 refs/tags/v1.0 4 | 062f5d0268c0a956aff49d96062a62b00fe4eacc refs/tags/v1.1 5 | ^208fc4a6a08fb1e5136cf9943d79ad81097a0f36 6 | -------------------------------------------------------------------------------- /test_git/refs/heads/master: -------------------------------------------------------------------------------- 1 | 8bbad744fe9597cc130e8ef7cfd201f36d9cba76 2 | -------------------------------------------------------------------------------- /test_git/refs/tags/v1.2: -------------------------------------------------------------------------------- 1 | 25f9782940ad877fbc6900a678cdc5d032df4d53 2 | --------------------------------------------------------------------------------