1 | -- resty-gitweb@git/git_commands.lua |
2 | -- git commands and parser functions |
3 |
|
4 | -- Copyright (c) 2020 Joshua 'joshuas3' Stockin |
5 | -- <https://git.joshstock.in/resty-gitweb> |
6 | -- This software is licensed under the MIT License. |
7 |
|
8 | local ffi = require("ffi") |
9 | local utils = require("utils/utils") |
10 |
|
11 | local _M = {} |
12 |
|
13 | local git = function(repo_dir, command) |
14 | local formatted_command = string.format( |
15 | "git --git-dir=%s %s", |
16 | repo_dir, command |
17 | ) |
18 | return utils.process(formatted_command) |
19 | end |
20 |
|
21 | local function git2_error(err, message) |
22 | if err < 0 then |
23 | local git2_error_message = ffi.string(git2.git_error_last().message) |
24 | error(string.format([[libgit2 Error - %s ("%s")]], message, git2_error_message)) |
25 | end |
26 | end |
27 |
|
28 | _M.show_file = function(repo_dir, ref_name, file_path) |
29 | local err = 0 |
30 |
|
31 | -- Open git repo |
32 | local repo_obj = ffi.new("git_repository*[1]") |
33 | err = git2.git_repository_open(ffi.cast("git_repository**", repo_obj), repo_dir) |
34 | git2_error(err, "Failed opening git repository") |
35 | repo_obj = repo_obj[0] |
36 |
|
37 | -- Get tree root |
38 | local tree = ffi.new("git_object*[1]") |
39 | err = git2.git_revparse_single(ffi.cast("git_object**", tree), repo_obj, ref_name) |
40 | git2_error(err, "Failed to look up tree from rev name") |
41 | tree = tree[0] |
42 |
|
43 | -- Get tree entry object (blob) |
44 | local blob = ffi.new("git_object*[1]") |
45 | err = git2.git_object_lookup_bypath(ffi.cast("git_object**", blob), tree, file_path, git2.GIT_OBJECT_BLOB) |
46 | git2_error(err, "Failed to look up blob") |
47 | blob = ffi.cast("git_blob*", blob[0]) |
48 |
|
49 | -- Get blob content |
50 | local buf = ffi.new("git_buf") |
51 | err = git2.git_blob_filter(buf, blob, file_path, nil) |
52 | git2_error(err, "Failed to filter blob") |
53 | local raw = ffi.string(buf.ptr) |
54 |
|
55 | -- Free everything |
56 | git2.git_buf_free(buf) |
57 | git2.git_blob_free(blob) |
58 | git2.git_object_free(tree) |
59 | git2.git_repository_free(repo_obj) |
60 |
|
61 | return raw |
62 | end |
63 |
|
64 | _M.get_head = function(repo_dir, ref_name) |
65 | local err = 0 |
66 | ref_name = ref_name or "HEAD" |
67 |
|
68 | -- Open git repo |
69 | local repo_obj = ffi.new("git_repository*[1]") |
70 | err = git2.git_repository_open(ffi.cast("git_repository**", repo_obj), repo_dir) |
71 | git2_error(err, "Failed opening git repository") |
72 | repo_obj = repo_obj[0] |
73 |
|
74 | -- Get object/reference |
75 | local object = ffi.new("git_object*[1]") |
76 | local reference = ffi.new("git_reference*[1]") |
77 | err = git2.git_revparse_ext(ffi.cast("git_object**", object), ffi.cast("git_reference**", reference), repo_obj, ref_name) |
78 | git2_error(err, "Failed to find reference") |
79 | object = object[0] |
80 | reference = reference[0] |
81 |
|
82 | -- Get full name if intermediate reference exists |
83 | local name = ref_name |
84 | if reference ~= nil then |
85 | local ref_type = git2.git_reference_type(reference) |
86 | if ref_type == git2.GIT_REFERENCE_SYMBOLIC then |
87 | name = ffi.string(git2.git_reference_symbolic_target(reference)) |
88 | else |
89 | name = ffi.string(git2.git_reference_name(reference)) |
90 | end |
91 | end |
92 |
|
93 | -- Get OID |
94 | local oid = git2.git_object_id(object) |
95 |
|
96 | -- Format oid as SHA1 hash |
97 | local hash = ffi.new("char[41]") -- SHA1 length (40 chars) + \0 |
98 | err = git2.git_oid_fmt(hash, oid) |
99 | git2_error(err, "Failed formatting OID") |
100 | hash = ffi.string(hash) |
101 |
|
102 | -- Free all |
103 | git2.git_object_free(object) |
104 | if reference ~= nil then |
105 | git2.git_reference_free(reference) |
106 | end |
107 | git2.git_repository_free(repo_obj) |
108 |
|
109 | -- Format |
110 | local ref = {} |
111 | ref.full = name |
112 | if name:match("^refs/heads/") then |
113 | ref.name = string.sub(name, 12, string.len(name)) |
114 | elseif name:match("^refs/tags/") then |
115 | ref.name = string.sub(name, 11, string.len(name)) |
116 | else |
117 | ref.name = ref_name -- just pass input as default output |
118 | end |
119 | ref.hash = hash |
120 | ref.shorthash = string.sub(hash, 1, 7) |
121 |
|
122 | return ref |
123 | end |
124 |
|
125 | _M.count = function(repo_dir, hash) |
126 | hash = hash or "@" |
127 | local output = git(repo_dir, "rev-list --count "..hash.." --") |
128 | return tonumber(string.trim(output)) |
129 | end |
130 |
|
131 | _M.log = function(repo_dir, hash, file, number, skip, gpg) |
132 | hash = hash or "@" |
133 | file = file or "" |
134 | number = tostring(number or 25) |
135 | skip = tostring(skip or 0) |
136 | gpg = gpg or false |
137 | local output |
138 | if gpg then |
139 | output = git(repo_dir, "log --pretty=tformat:'%x00%x01%H%x00%cI%x00%cn%x00%ce%x00%s%x00%b%x00%G?%x00%GK%x00%GG%x00' --numstat -n "..number.." --skip "..skip.." "..hash.." -- "..file) |
140 | else |
141 | output = git(repo_dir, "log --pretty=tformat:'%x00%x01%H%x00%cI%x00%cn%x00%ce%x00%s%x00%b%x00' --numstat -n "..number.." --skip "..skip.." "..hash.." -- "..file) |
142 | end |
143 | local commits = {} |
144 | local a = string.split(output,"\0\1") |
145 | local f = false |
146 | for i,v in pairs(a) do |
147 | if f == true then |
148 | local commit = {} |
149 | local c = string.split(v, "\0") |
150 | commit.hash = c[1] |
151 | commit.shorthash = string.sub(c[1], 1,7) |
152 | commit.timestamp = c[2] |
153 | commit.author = c[3] |
154 | commit.email = c[4] |
155 | commit.subject = c[5] |
156 | commit.body = string.trim(c[6]) |
157 | local diffs |
158 | if gpg then |
159 | commit.gpggood = c[7] |
160 | commit.gpgkey = c[8] |
161 | commit.gpgfull = string.trim(c[9]) |
162 | diffs = string.trim(c[10]) |
163 | else |
164 | diffs = string.trim(c[7]) |
165 | end |
166 | commit.diff = {} |
167 | local b = string.split(diffs, "\n") |
168 | commit.diff.plus = 0 |
169 | commit.diff.minus = 0 |
170 | commit.diff.num = 0 |
171 | commit.diff.files = {} |
172 | for i,v in pairs(b) do |
173 | local d = string.split(v,"\t") |
174 | local x = {} |
175 | x.plus = tonumber(d[1]) or 0 |
176 | commit.diff.plus = commit.diff.plus + x.plus |
177 | x.minus = tonumber(d[2]) or 0 |
178 | commit.diff.minus = commit.diff.minus + x.minus |
179 | commit.diff.files[d[3]] = x |
180 | commit.diff.num = commit.diff.num + 1 |
181 | end |
182 | table.insert(commits, commit) |
183 | else |
184 | f = true |
185 | end |
186 | end |
187 | return commits |
188 | end |
189 |
|
190 | _M.commit = function(repo_dir, hash) |
191 | local commit = _M.log(repo_dir, hash, "", 1, 0, true)[1] |
192 | commit.count = _M.count(repo_dir, hash) |
193 | return commit |
194 | end |
195 |
|
196 | _M.list_refs = function(repo_dir) |
197 | -- Open git repo |
198 | local repo_obj = ffi.new("git_repository*[1]") |
199 | local err = git2.git_repository_open(ffi.cast("git_repository**", repo_obj), repo_dir) |
200 | repo_obj = repo_obj[0] |
201 |
|
202 | -- List refs |
203 | local refs = ffi.new("git_strarray") |
204 | local err = git2.git_reference_list(ffi.cast("git_strarray*", refs), repo_obj) |
205 |
|
206 | local ret = {} |
207 | ret.heads = {} |
208 | ret.tags = {} |
209 |
|
210 | for i = 0, tonumber(refs.count)-1 do |
211 |
|
212 | local name = ffi.string(refs.strings[i]) |
213 |
|
214 | local dest |
215 | local prefix_len |
216 | if name:match("^refs/heads/") then |
217 | dest = ret.heads |
218 | prefix_len = 12 |
219 | elseif name:match("^refs/tags/") then |
220 | dest = ret.tags |
221 | prefix_len = 11 |
222 | end |
223 |
|
224 | if dest then |
225 | local oid = ffi.new("git_oid") |
226 | local err = git2.git_reference_name_to_id(ffi.cast("git_oid*", oid), repo_obj, refs.strings[i]) |
227 |
|
228 | -- Format oid as SHA1 hash |
229 | local hash = ffi.new("char[41]") -- SHA1 length (40 chars) + \0 |
230 | local err = git2.git_oid_fmt(hash, ffi.cast("git_oid*", oid)) |
231 | hash = ffi.string(hash) |
232 |
|
233 | local ref = {} |
234 | ref.name = string.sub(name, prefix_len, string.len(name)) |
235 | ref.full = name |
236 | ref.hash = hash |
237 | ref.shorthash = string.sub(hash, 1, 7) |
238 | table.insert(dest, ref) |
239 | end |
240 |
|
241 | end |
242 |
|
243 | if refs then |
244 | git2.git_strarray_free(ffi.cast("git_strarray*", refs)) |
245 | end |
246 | if repo_obj then |
247 | git2.git_repository_free(ffi.cast("git_repository*", repo_obj)) |
248 | end |
249 |
|
250 | return ret |
251 | end |
252 |
|
253 | local list_dirs = function(repo_dir, hash, path) |
254 | hash = hash or "@" |
255 | path = path or "" |
256 | local output = git(repo_dir, "ls-tree -d --name-only "..hash.." -- "..path) |
257 | local dirs = string.split(output, "\n") |
258 | table.remove(dirs, #dirs) -- remove trailing \n |
259 | return dirs |
260 | end |
261 |
|
262 | local list_all = function(repo_dir, hash, path) |
263 | hash = hash or "@" |
264 | path = path or "" |
265 | local output = git(repo_dir, "ls-tree --name-only "..hash.." -- "..path) |
266 | local all = string.split(output, "\n") |
267 | table.remove(all, #all) -- remove trailing \n |
268 | return all |
269 | end |
270 |
|
271 | _M.list_tree = function(repo_dir, hash, path) |
272 | hash = hash or "@" |
273 | path = path or "" |
274 | local files = list_all(repo_dir, hash, path) |
275 | local dirs = list_dirs(repo_dir, hash, path) |
276 | local ret = {} |
277 | ret.dirs = {} |
278 | ret.files = {} |
279 | for i,v in pairs(files) do -- iterate over all objects, separate directories from files |
280 | local not_dir = true |
281 | for _,d in pairs(dirs) do -- check if object is directory |
282 | if v == d then |
283 | not_dir = false |
284 | break |
285 | end |
286 | end |
287 | if not_dir then |
288 | table.insert(ret.files, v) |
289 | else |
290 | table.insert(ret.dirs, v) |
291 | end |
292 | end |
293 | return ret |
294 | end |
295 |
|
296 | return _M |
297 |
|