local ffi = require "ffi"
ffi.cdef[[
typedef int64_t git_off_t;
typedef int64_t git_time_t;
typedef uint64_t git_object_size_t;
typedef struct git_repository git_repository;
typedef struct git_object git_object;
typedef struct git_commit git_commit;
typedef struct git_tree git_tree;
typedef struct git_tree_entry git_tree_entry;
typedef struct git_diff git_diff;
typedef struct git_blob git_blob;
typedef struct git_tag git_tag;
typedef struct git_revwalk git_revwalk;
typedef enum {
GIT_OBJECT_ANY = -2, /**< Object can be any of the following */
GIT_OBJECT_INVALID = -1, /**< Object is invalid. */
GIT_OBJECT_COMMIT = 1, /**< A commit object. */
GIT_OBJECT_TREE = 2, /**< A tree (directory listing) object. */
GIT_OBJECT_BLOB = 3, /**< A file revision object. */
GIT_OBJECT_TAG = 4, /**< An annotated tag object. */
GIT_OBJECT_OFS_DELTA = 6, /**< A delta, base is given by an offset. */
GIT_OBJECT_REF_DELTA = 7 /**< A delta, base is given by object id. */
} git_object_t;
typedef struct git_oid {
/** raw binary formatted id */
unsigned char id[20];
} git_oid;
typedef struct git_strarray {
char **strings;
size_t count;
} git_strarray;
typedef struct git_time {
git_time_t time; /**< time in seconds from epoch */
int offset; /**< timezone offset, in minutes */
char sign; /**< indicator for questionable '-0000' offsets in signature */
} git_time;
typedef struct git_signature {
char *name; /**< full name of the author */
char *email; /**< email of the author */
git_time when; /**< time when the action happened */
} git_signature;
typedef struct git_diff_options git_diff_options;
typedef enum {
GIT_DIFF_FORMAT_PATCH = 1u, /**< full git diff */
GIT_DIFF_FORMAT_PATCH_HEADER = 2u, /**< just the file headers of patch */
GIT_DIFF_FORMAT_RAW = 3u, /**< like git diff --raw */
GIT_DIFF_FORMAT_NAME_ONLY = 4u, /**< like git diff --name-only */
GIT_DIFF_FORMAT_NAME_STATUS = 5u, /**< like git diff --name-status */
GIT_DIFF_FORMAT_PATCH_ID = 6u /**< git diff as used by git patch-id */
} git_diff_format_t;
typedef enum {
GIT_DELTA_UNMODIFIED = 0, /**< no changes */
GIT_DELTA_ADDED = 1, /**< entry does not exist in old version */
GIT_DELTA_DELETED = 2, /**< entry does not exist in new version */
GIT_DELTA_MODIFIED = 3, /**< entry content changed between old and new */
GIT_DELTA_RENAMED = 4, /**< entry was renamed between old and new */
GIT_DELTA_COPIED = 5, /**< entry was copied from another old entry */
GIT_DELTA_IGNORED = 6, /**< entry is ignored item in workdir */
GIT_DELTA_UNTRACKED = 7, /**< entry is untracked item in workdir */
GIT_DELTA_TYPECHANGE = 8, /**< type of entry changed between old and new */
GIT_DELTA_UNREADABLE = 9, /**< entry is unreadable */
GIT_DELTA_CONFLICTED = 10 /**< entry in the index is conflicted */
} git_delta_t;
typedef struct {
git_oid id;
const char *path;
git_object_size_t size;
uint32_t flags;
uint16_t mode;
uint16_t id_abbrev;
} git_diff_file;
typedef struct {
git_delta_t status;
uint32_t flags; /**< git_diff_flag_t values */
uint16_t similarity; /**< for RENAMED and COPIED, value 0-100 */
uint16_t nfiles; /**< number of files in this delta */
git_diff_file old_file;
git_diff_file new_file;
} git_diff_delta;
typedef struct {
int old_start; /**< Starting line number in old_file */
int old_lines; /**< Number of lines in old_file */
int new_start; /**< Starting line number in new_file */
int new_lines; /**< Number of lines in new_file */
size_t header_len; /**< Number of bytes in header text */
char header[128]; /**< Header text, NUL-byte terminated */
} git_diff_hunk;
typedef struct {
char origin; /**< A git_diff_line_t value */
int old_lineno; /**< Line number in old file or -1 for added line */
int new_lineno; /**< Line number in new file or -1 for deleted line */
int num_lines; /**< Number of newline characters in content */
size_t content_len; /**< Number of bytes of data */
git_off_t content_offset; /**< Offset in the original file to the content */
const char *content; /**< Pointer to diff text, not NUL-byte terminated */
} git_diff_line;
typedef int (git_diff_line_cb)(const git_diff_delta *delta, const git_diff_hunk *hunk, const git_diff_line *line, void *payload);
int git_libgit2_init();
int git_libgit2_shutdown();
int git_libgit2_version(int *major, int *minor, int *rev);
const git_oid * git_object_id(const git_object *obj);
void git_object_free(git_object *object);
int git_repository_open_ext(git_repository **out, const char *path, unsigned int flags, const char *ceiling_dirs);
int git_repository_open_bare(git_repository **out, const char *bare_path);
int git_oid_fromstr(git_oid *out, const char *str);
char * git_oid_tostr(char *out, size_t n, const git_oid *id);
int git_reference_list(git_strarray *array, git_repository *repo);
void git_strarray_free(git_strarray *array);
int git_tag_list(git_strarray *tag_names, git_repository *repo);
int git_revparse_single(git_object **out, git_repository *repo, const char *spec);
int git_commit_lookup(git_commit **commit, git_repository *repo, const git_oid *id);
int git_commit_parent(git_commit **out, const git_commit *commit, unsigned int n);
const git_oid * git_commit_id(const git_commit *commit);
const char * git_commit_message(const git_commit *commit);
const char * git_commit_summary(git_commit *commit);
const git_signature * git_commit_author(const git_commit *commit);
int git_revwalk_new(git_revwalk **out, git_repository *repo);
int git_revwalk_push_range(git_revwalk *walk, const char *range);
int git_revwalk_next(git_oid *out, git_revwalk *walk);
int git_commit_tree(git_tree **tree_out, const git_commit *commit);
size_t git_tree_entrycount(const git_tree *tree);
const git_tree_entry * git_tree_entry_byindex(const git_tree *tree, size_t idx);
git_object_t git_tree_entry_type(const git_tree_entry *entry);
const char * git_tree_entry_name(const git_tree_entry *entry);
int git_tree_entry_bypath(git_tree_entry **out, const git_tree *root, const char *path);
int git_tree_entry_to_object(git_object **object_out, git_repository *repo, const git_tree_entry *entry);
void git_tree_entry_free(git_tree_entry *entry);
int git_blob_is_binary(const git_blob *blob);
git_object_size_t git_blob_rawsize(const git_blob *blob);
const void * git_blob_rawcontent(const git_blob *blob);
int git_diff_tree_to_tree(git_diff **diff, git_repository *repo, git_tree *old_tree, git_tree *new_tree, const git_diff_options *opts);
int git_diff_print(git_diff *diff, git_diff_format_t format, git_diff_line_cb print_cb, void *payload);
]]
local C = ffi.load("git2")
local function strarr_to_table(strarr)
local tab = {}
for i = 1, tonumber(strarr[0].count) do
local str = ffi.string(strarr[0].strings[i-1])
table.insert(tab, str)
end
return tab
end
local Commit = {}
Commit.__index = Commit
local function get_commit(commit, repo)
return setmetatable({commit=commit, repo=repo}, Commit)
end
function Commit:id(len)
len = len or 16
local oid = ffi.new("char ["..(len+1).."]")
C.git_oid_tostr(oid, len, C.git_commit_id(self.commit))
return ffi.string(oid)
end
function Commit:signature()
local sign = C.git_commit_author(self.commit)
return {
time_=tonumber(sign.when.time),
offset=sign.when.offset,
name=ffi.string(sign.name),
email=ffi.string(sign.email),
}
end
function Commit:message()
return ffi.string(C.git_commit_message(self.commit))
end
function Commit:summary()
return ffi.string(C.git_commit_summary(self.commit))
end
function Commit:parent(n)
n = n or 1
local pcommit = ffi.new("git_commit *[1]")
local ret = C.git_commit_parent(pcommit, self.commit, n-1)
if ret == 0 then
return get_commit(pcommit[0], self.repo)
else
return nil
end
end
function Commit:tree()
local ptree = ffi.new("git_tree *[1]")
local ret = C.git_commit_tree(ptree, self.commit)
if ret == 0 then
return ptree[0]
else
return nil
end
end
function Commit:children(tree)
local n = tonumber(C.git_tree_entrycount(tree))
local children = {}
for i = 1, n do
local entry = C.git_tree_entry_byindex(tree, i-1)
local entry_type = C.git_tree_entry_type(entry)
local entry_name = ffi.string(C.git_tree_entry_name(entry))
local child
if entry_type == 2 then
child = entry_name.."/"
elseif entry_type == 3 then
child = entry_name
end
table.insert(children, child)
end
return children
end
function Commit:node(entry)
local entry_type = C.git_tree_entry_type(entry)
local entry_name = C.git_tree_entry_name(entry)
local pobj = ffi.new("git_object *[1]")
C.git_tree_entry_to_object(pobj, self.repo, entry)
if entry_type == 2 then
local tree = ffi.cast("git_tree *", pobj[0])
local children = self:children(tree)
return {type_="dir", name=name, children=children}
elseif entry_type == 3 then
local blob = ffi.cast("git_blob *", pobj[0])
local data = ffi.string(C.git_blob_rawcontent(blob), C.git_blob_rawsize(blob))
local bin = C.git_blob_is_binary(blob) ~= 0
return {type_="file", name=name, data=data, bin=bin}
else
return nil
end
end
function Commit:tree_entry(path)
local tree = self:tree()
if tree == nil then
return nil
end
if path == "" then
local children = self:children(tree)
return {type_="dir", name="", children=children}
end
local pentry = ffi.new("git_tree_entry *[1]")
local ret = C.git_tree_entry_bypath(pentry, tree, path)
if ret == 0 then
return self:node(pentry[0])
else
return nil
end
end
local Repo = {}
Repo.__index = Repo
function Repo:refs()
local strarr = ffi.new("git_strarray[1]")
C.git_reference_list(strarr, self.repo)
local refs = strarr_to_table(strarr)
C.git_strarray_free(strarr);
return refs
end
function Repo:branches()
local branches = {}
for _, ref in ipairs(self:refs()) do
local branch_name = ref:match("refs/heads/(.*)")
if branch_name ~= nil then
table.insert(branches, branch_name)
end
end
return branches
end
function Repo:tags()
local strarr = ffi.new("git_strarray[1]")
C.git_tag_list(strarr, self.repo)
local tags = strarr_to_table(strarr)
C.git_strarray_free(strarr);
return tags
end
function Repo:commit(rev)
local pobj = ffi.new("git_object *[1]")
C.git_revparse_single(pobj, self.repo, rev)
local obj = pobj[0]
local oid = C.git_object_id(obj)
local pcommit = ffi.new("git_commit *[1]")
C.git_commit_lookup(pcommit, self.repo, oid)
local commit = pcommit[0]
C.git_object_free(obj)
return get_commit(commit, self.repo)
end
function Repo:find_prev(cid, dist)
for i, bname in ipairs(self:branches()) do
local cids = {}
local commit = self:commit(bname)
while commit do
local cur_cid = commit:id()
table.insert(cids, cur_cid)
if cur_cid == cid then
break
end
commit = commit:parent()
end
if cids[#cids] == cid then
if #cids > 1 then
local index = math.max(1, #cids - dist)
return self:commit(cids[index])
else
return nil
end
end
end
return nil
end
function Repo:diff(commit, custom_cb)
local new_tree = commit:tree()
local parent = commit:parent()
local old_tree = parent and parent:tree()
local pdiff = ffi.new("git_diff *[1]")
C.git_diff_tree_to_tree(pdiff, self.repo, old_tree, new_tree, nil)
local diff = pdiff[0]
local s = ""
local function cb(delta, hunk, line, payload)
local lua_line = ffi.string(line.content, line.content_len)
local line_type = string.char(line.origin)
local diff_line
if custom_cb ~= nil then
diff_line = custom_cb(line_type, lua_line)
else
diff_line = line_type .. lua_line
end
s = s .. diff_line
return 0
end
C.git_diff_print(diff, C.GIT_DIFF_FORMAT_PATCH, cb, nil)
return s
end
local function open(path)
local self = setmetatable({}, Repo)
local prepo = ffi.new("git_repository *[1]")
assert(C.git_repository_open_bare(prepo, path) == 0)
self.repo = prepo[0]
return self
end
return {init=C.git_libgit2_init, shutdown=C.git_libgit2_shutdown, open=open}