summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Silverstone <dsilvers@digital-scurf.org>2012-09-08 14:06:28 +0100
committerDaniel Silverstone <dsilvers@digital-scurf.org>2012-09-08 14:06:28 +0100
commitd1c919e95956c4c883aa052dbdbacbe1b6129387 (patch)
tree86c6cb34c8baa91ac20f0dd4ee8e257c96b6923e
parent2678f0d052da02483e828da9db43f9a2d6734de6 (diff)
downloadgall-d1c919e95956c4c883aa052dbdbacbe1b6129387.tar.gz
GALL: Initial import of Gitano library gitano.git as gall
-rw-r--r--lib/gall.lua18
-rw-r--r--lib/gall/commit.lua165
-rw-r--r--lib/gall/ll.lua98
-rw-r--r--lib/gall/object.lua78
-rw-r--r--lib/gall/repository.lua279
-rw-r--r--lib/gall/tag.lua97
-rw-r--r--lib/gall/tree.lua209
-rw-r--r--lib/gall/util.lua29
-rw-r--r--test/test-gall.lua17
9 files changed, 990 insertions, 0 deletions
diff --git a/lib/gall.lua b/lib/gall.lua
index 4a2dbb8..a2f613a 100644
--- a/lib/gall.lua
+++ b/lib/gall.lua
@@ -10,8 +10,26 @@ local ABI_VERSION = 0
local VERSION = "Gall v1." .. tostring(API_VERSION)
+local util = require "gall.util"
+local ll = require "gall.ll"
+
+local repository = require "gall.repository"
+local tag = require "gall.tag"
+local commit = require "gall.commit"
+local tree = require "gall.tree"
+local object = require "gall.object"
+
return {
_API_VERSION = API_VERSION,
_ABI_VERSION = ABI_VERSION,
VERSION = VERSION,
+
+ repository = repository,
+ tag = tag,
+ commit = commit,
+ tree = tree,
+ object = object,
+
+ ll = ll,
+ util = util,
}
diff --git a/lib/gall/commit.lua b/lib/gall/commit.lua
new file mode 100644
index 0000000..92afff8
--- /dev/null
+++ b/lib/gall/commit.lua
@@ -0,0 +1,165 @@
+-- gall.commit
+--
+-- Git Abstraction Layer for Lua -- Commit object interface
+--
+-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
+--
+--
+
+local ll = require "gall.ll"
+
+local objs = setmetatable({}, {__mode="k"})
+local repos = setmetatable({}, {__mode="k"})
+local parsed = setmetatable({}, {__mode="k"})
+
+local _new
+
+local function parse_person(pers)
+ local real, email, when, tz = pers:match("^(.-) <([^>]+)> ([0-9]+) ([+-][0-9]+)$")
+ return {
+ realname = real,
+ email = email,
+ unixtime = when,
+ timezone = tz
+ }
+end
+
+local function parse_parents(repo, ps)
+ local ret = {}
+ for _, psha in ipairs(ps) do
+ ret[_] = repo:get(psha)
+ end
+ return ret
+end
+
+local function commitindex(commit, field)
+ if not parsed[commit] then
+ local raw = objs[commit].raw
+ local headers, body, signature = {}, ""
+ local state = "headers"
+
+ for l in raw:gmatch("([^\n]*)\n") do
+ if state == "headers" then
+ local first, second = l:match("^([^ ]+) (.+)$")
+ if first then
+ if headers[first] then
+ headers[first][#headers[first]+1] = second
+ else
+ headers[first] = { second }
+ end
+ else
+ state = "message"
+ end
+ elseif state == "message" then
+ if l == PGP_SIG_START then
+ signature = l .. "\n"
+ state = "signature"
+ else
+ body = body .. l .. "\n"
+ end
+ else
+ signature = signature .. l .. "\n"
+ end
+ end
+
+ -- there's always one tree
+ rawset(commit, "tree", repos[commit]:get(headers.tree[1]))
+ -- Always one author
+ rawset(commit, "author", parse_person(headers.author[1]))
+ -- Always one committer
+ rawset(commit, "committer", parse_person(headers.committer[1]))
+ -- Zero or more parents
+ headers.parent = headers.parent or {}
+ rawset(commit, "parents", parse_parents(repos[commit], headers.parent))
+ -- A message
+ rawset(commit, "message", body)
+ -- And an optional signature
+ rawset(commit, "signature", signature)
+ -- Promote the SHA
+ rawset(commit, "sha", objs[commit].sha)
+
+ -- Signal we are parsed
+ parsed[commit] = true
+ end
+
+ return rawget(commit, field)
+end
+
+local function committostring(commit)
+ return "<GitCommit(" .. tostring(objs[commit].sha) .. ") in " .. tostring(repos[commit]) .. ">"
+end
+
+local commitmeta = {
+ __index = commitindex,
+ __tostring = committostring
+}
+
+function _new(repo, obj)
+ local ret = setmetatable({}, commitmeta)
+ objs[ret] = obj
+ repos[ret] = repo
+ return ret
+end
+
+function _create(repo, data)
+ if not data.tree then
+ return nil, "No tree?"
+ end
+ if not data.author then
+ return nil, "No author?"
+ end
+ if not data.committer then
+ return nil, "No committer?"
+ end
+ if not data.author.realname then
+ return nil, "No author name?"
+ end
+ if not data.author.email then
+ return nil, "No author email?"
+ end
+ if not data.committer.realname then
+ return nil, "No committer name?"
+ end
+ if not data.committer.email then
+ return nil, "No committer email?"
+ end
+ if not data.message then
+ return nil, "No message?"
+ end
+ if not data.parents then
+ data.parents = {}
+ end
+
+ -- Construct the commandline and environment
+ local env = {
+ GIT_AUTHOR_NAME = data.author.realname,
+ GIT_AUTHOR_EMAIL = data.author.email,
+ GIT_COMMITTER_NAME = data.committer.realname,
+ GIT_COMMITTER_EMAIL = data.committer.email,
+ }
+ local cmd = {
+ "commit-tree", data.tree.sha
+ }
+ for i, v in ipairs(data.parents) do
+ cmd[#cmd+1] = "-p"
+ cmd[#cmd+1] = v.sha
+ end
+
+ if not data.message:match("\n$") then
+ data.message = data.message .. "\n"
+ end
+
+ local why, sha = repo:_run_with_input_and_env(env, data.message,
+ ll.chomp, unpack(cmd))
+
+ if why ~= 0 then
+ return nil, "commit-tree returned " .. tostring(why)
+ end
+
+ return repo:get(sha)
+end
+
+return {
+ create = _create,
+ new = _new
+}
diff --git a/lib/gall/ll.lua b/lib/gall/ll.lua
new file mode 100644
index 0000000..7f20683
--- /dev/null
+++ b/lib/gall/ll.lua
@@ -0,0 +1,98 @@
+-- gall.ll
+--
+-- Git Abstraction Layer for Lua -- Low level interface
+--
+-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
+--
+--
+
+local sp = require "luxio.subprocess"
+local util = require "gall.util"
+
+local assert = assert
+
+local git_exe = "git"
+
+local function _rungit(t)
+ assert(t.repo, "No repository?")
+
+ local proc_args = {
+ env = {},
+ git_exe, unpack(t)
+ }
+
+ for k, v in pairs(t.env or {}) do
+ proc_args.env[k] = v
+ end
+
+ proc_args.env.GIT_DIR = t.repo
+
+ if t.stdin then
+ proc_args.stdin = t.stdin
+ end
+
+ if t.stdout then
+ proc_args.stdout = sp.PIPE
+ end
+
+ if t.stderr then
+ proc_args.stderr = sp.PIPE
+ end
+
+ local proc = sp.spawn_simple(proc_args)
+ local stdout, stderr
+
+ if t.stdout then
+ stdout = proc.stdout:read("*a")
+ proc.stdout:close()
+ if type(t.stdout) == "function" then
+ stdout = t.stdout(stdout)
+ end
+ end
+
+ if t.stderr then
+ stderr = proc.stderr:read("*a")
+ proc.stderr:close()
+ if type(t.stderr) == "function" then
+ stderr = t.stderr(stderr)
+ end
+ end
+
+ local how, why = proc:wait()
+
+ assert(how == "exit", "Not cleanly exited")
+
+ return why, stdout, stderr
+end
+
+local function _setgit(e)
+ git_exe = e
+end
+
+local function _chomp(s)
+ local rest = s:match("^(.*)\n$")
+ return rest or s
+end
+
+local mod_ret = {
+ rungit = _rungit,
+ setgit = _setgit,
+ chomp = _chomp
+}
+
+local simple_cmds = {
+ "cat-file", "symbolic-ref", "show-ref",
+ "hash-object", "ls-tree", "init", "merge-base", "rev-list", "config"
+}
+
+for _, s in pairs(simple_cmds) do
+ local ss = s:gsub("%-", "_")
+ mod_ret[ss] = function(_t)
+ local t = util.deep_copy(_t)
+ table.insert(t, 1, s)
+ t.stdout = _chomp
+ return _rungit(t)
+ end
+end
+
+return mod_ret
diff --git a/lib/gall/object.lua b/lib/gall/object.lua
new file mode 100644
index 0000000..3b904c5
--- /dev/null
+++ b/lib/gall/object.lua
@@ -0,0 +1,78 @@
+-- gall.object
+--
+-- Git Abstraction Layer for Lua -- Generic object interface
+--
+-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
+--
+--
+
+local ll = require "gall.ll"
+local commit = require "gall.commit"
+local tree = require "gall.tree"
+local tag = require "gall.tag"
+
+local repos = setmetatable({}, {__mode="k"})
+
+local function _objectindex(obj, field)
+ local ok, ret
+ if field == "type" then
+ ok, ret = repos[obj]:gather("cat-file", "-t", obj.sha)
+ elseif field == "size" then
+ ok, ret = repos[obj]:gather("cat-file", "-s", obj.sha)
+ elseif field == "raw" then
+ ok, ret = repos[obj]:rawgather("cat-file", (obj.type == "tag" and "tag" or "-p"), obj.sha)
+ elseif field == "content" then
+ if obj.type == "blob" then
+ ok, ret = 0, obj.raw
+ elseif obj.type == "commit" then
+ ok, ret = 0, commit.new(repos[obj], obj)
+ elseif obj.type == "tree" then
+ ok, ret = 0, tree.new(repos[obj], obj)
+ elseif obj.type == "tag" then
+ ok, ret = 0, tag.new(repos[obj], obj)
+ else
+ error("Unknown type <" .. obj.type .. "> for content parse")
+ end
+ else
+ error("Unknown field <" .. tostring(field) .. ">")
+ end
+
+ assert(ok == 0, "Unable to retrieve " .. field)
+
+ if field == "size" then ret = tonumber(ret) end
+
+ rawset(obj, field, ret)
+
+ return ret
+end
+
+function _objecttostring(obj)
+ return "<GitObject(" .. tostring(obj.sha)..") in " .. tostring(repos[obj]) .. ">"
+end
+
+local objectmeta = {
+ __index = _objectindex,
+ __tostring = _objecttostring
+}
+
+local function _new(repo, sha)
+ local ret = setmetatable({sha=sha}, objectmeta)
+ repos[ret] = repo
+ return ret
+end
+
+local function _create(repo, type, content)
+ local why, sha =
+ repo:_run_with_input(content, ll.chomp,
+ "hash-object", "-t", type, "-w", "--stdin")
+ if why ~= 0 then
+ return nil, "hash-object returned " .. tostring(why)
+ end
+
+ return _new(repo, sha)
+end
+
+return {
+ new = _new,
+ create = _create,
+}
diff --git a/lib/gall/repository.lua b/lib/gall/repository.lua
new file mode 100644
index 0000000..af1ded4
--- /dev/null
+++ b/lib/gall/repository.lua
@@ -0,0 +1,279 @@
+-- gall.repository
+--
+-- Git Abstraction Layer for Lua -- Repository interface
+--
+-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
+--
+--
+
+local ll = require "gall.ll"
+local object = require "gall.object"
+
+local chomp = ll.chomp
+
+local repomethod = {}
+
+local pattern = {
+ fullsha = string.rep("[0-9a-f]", 40),
+ shortsha = string.rep("[0-9a-f]", 7),
+ ref = "refs/.+"
+}
+
+for k, v in pairs(pattern) do
+ pattern[k] = ("^%s$"):format(v)
+end
+
+local function _repotostring(repo)
+ return "<GitRepository(" .. repo.path .. ")>"
+end
+
+function repomethod:_run_with_input_and_env(env, input, want_output, ...)
+ local t = {...}
+ t.repo = self.path
+ if want_output then
+ t.stdout = want_output
+ end
+ if input then
+ t.stdin = input
+ end
+ -- We never want to see the stderr dumped to the client, so we eat it.
+ t.stderr = true
+ if env then
+ t.env = env
+ end
+ return ll.rungit(t)
+end
+
+function repomethod:_run_with_input(input, want_output, ...)
+ return self:_run_with_input_and_env(nil, input, want_output, ...)
+end
+
+function repomethod:_run(want_output, ...)
+ return self:_run_with_input(nil, want_output, ...)
+end
+
+function repomethod:gather(...)
+ return self:_run(chomp, ...)
+end
+
+function repomethod:rawgather(...)
+ return self:_run(true, ...)
+end
+
+function repomethod:force_empty_tree()
+ self:_run(true, "hash-object", "-t", "tree", "-w", "/dev/null")
+end
+
+function repomethod:hash_object(type, content, inject)
+ local args = {
+ "hash-object", "-t", type, "--stdin",
+ }
+ if inject then
+ args[#args+1] = "-w"
+ end
+ local ok, sha = self:_run_with_input(content, chomp, unpack(args))
+ return (ok == 0) and sha or nil
+end
+
+function repomethod:get_ref(ref)
+ local ok, sha = self:_run(chomp, "show-ref", "--verify", "-s", ref)
+ return (ok == 0) and sha or nil
+end
+
+function repomethod:update_ref(ref, new_ref, reason, old_ref)
+ if new_ref and not old_ref then
+ old_ref = string.rep("0", 40)
+ end
+ if not reason then
+ reason = "Gall internal operations"
+ end
+
+ local cmd = { "update-ref", "-m", reason }
+
+ if not new_ref then
+ cmd[#cmd+1] = "-d"
+ end
+
+ cmd[#cmd+1] = ref
+
+ if new_ref then
+ cmd[#cmd+1] = new_ref
+ end
+ if old_ref then
+ cmd[#cmd+1] = old_ref
+ end
+
+ local why = self:_run(false, unpack(cmd))
+ if why ~= 0 then
+ return nil, "update-ref returned " .. tostring(why)
+ end
+ return true
+end
+
+function repomethod:update_server_info()
+ local why = self:_run(false, "update-server-info")
+ if why ~= 0 then
+ return nil, "update-server-info returned " .. tostring(why)
+ end
+ return true
+end
+
+function repomethod:all_refs()
+ local ok, refs = self:_run(chomp, "show-ref")
+ if ok ~= 0 then return nil, refs end
+ local reft = {}
+ for sha, ref in refs:gmatch("([0-9a-f]+) (refs/[^\n]+)") do
+ reft[ref] = sha
+ end
+ return reft
+end
+
+function repomethod:normalise(sha)
+ -- Purpose is to take a 'shaish' object and normalise it
+ if sha:match(pattern.fullsha) then
+ return sha
+ elseif sha:match(pattern.ref) then
+ local ref, err = self:get_ref(sha)
+ return ref, err
+ else
+ local ok, out = self:_run_with_input(sha, chomp, "cat-file", "--batch-check")
+ if not ok then
+ error(out)
+ end
+ sha = out:match("^("..string.rep("[0-9a-f]", 40)..")")
+ return sha
+ end
+ return nil, "Unable to normalise " .. tostring(sha)
+end
+
+function repomethod:get(_sha)
+ local sha, err = self:normalise(_sha)
+ if not sha then
+ return nil, err
+ end
+ local ret = self.objmemo[sha]
+ if not ret then
+ ret = object.new(self, sha)
+ self.objmemo[sha] = ret
+ end
+ return ret
+end
+
+function repomethod:merge_base(sha_1, sha_2, get_all)
+ local args = { sha_1, sha_2 }
+ if get_all then
+ args = { "-a", sha_1, sha_2 }
+ end
+ args.repo = self.path
+ local ok, out = ll.merge_base(args)
+ if not ok then
+ return nil, out
+ end
+ local ret = {}
+ for sha in out:gmatch("([a-f0-9]+)") do
+ ret[#ret+1] = sha
+ end
+ if #ret == 0 then
+ return true
+ end
+ return unpack(ret)
+end
+
+function repomethod:rev_list(oldhead, newhead, firstonly)
+ local args = { newhead, "^" .. oldhead }
+ if firstonly then
+ table.insert(args, 1, "--first-parent")
+ end
+ args.repo = self.path
+ local ok, out = ll.rev_list(args)
+ if not ok then
+ return nil, out
+ end
+ local ret = {}
+ for sha in out:gmatch("([a-f0-9]+)") do
+ ret[#ret+1] = sha
+ end
+ if #ret == 0 then
+ return true
+ end
+ return ret
+end
+
+function repomethod:symbolic_ref(name, toref)
+ if not toref then
+ return ll.symbolic_ref { "-q", name,
+ stderr=true, repo=self.path }
+ else
+ return ll.symbolic_ref { "-q", name, toref,
+ stderr=true, repo=self.path }
+ end
+ return false, "argh"
+end
+
+function repomethod:config(confname, value)
+ -- Trivial interface to key/value in config
+ if not value then
+ return self:gather("config", confname)
+ else
+ self:gather("config", "--replace-all", confname, value)
+ end
+end
+
+local repomt = {
+ __index = repomethod,
+ __tostring = _repotostring
+}
+
+local function _new(path)
+ -- return a new git repository object
+ -- with the git_dir set for the provided path
+ -- and, if we had to add /.git then the GIT_WORK_DIR set
+ -- appropriately too
+
+ local retrepo = {objmemo=setmetatable({}, {__mode="v"})}
+
+ local repopath = path
+ local workpath = nil
+
+ local ok, symref = ll.symbolic_ref { "-q", "HEAD", stderr=true, repo=repopath }
+ if ok ~= 0 then
+ repopath = path .. "/.git"
+ workpath = path
+ ok, symref = ll.symbolic_ref { "-q", "HEAD", stderr=true, repo=repopath }
+ end
+
+ if ok ~= 0 then
+ return nil, "Unable to find Git repository at " .. path
+ end
+
+ retrepo.path = repopath
+ retrepo.work = workpath
+ retrepo.HEAD = symref
+
+ return setmetatable(retrepo, repomt)
+end
+
+local function _create(path, full)
+ -- Cause a bare repository to be created (or a non-bare if full is true)
+ local args = {
+ stderr = true,
+ repo = path,
+ "-q"
+ }
+ if not full then
+ args[#args+1] = "--bare"
+ end
+ ok, msg = ll.init(args)
+
+ if ok ~= 0 then
+ return nil, "Unable to create Git repository at " .. path
+ end
+
+ -- Otherwise, return the shiny new repo
+ return _new(path)
+end
+
+return {
+ create = _create,
+ new = _new
+}
diff --git a/lib/gall/tag.lua b/lib/gall/tag.lua
new file mode 100644
index 0000000..707af31
--- /dev/null
+++ b/lib/gall/tag.lua
@@ -0,0 +1,97 @@
+-- gall.tag
+--
+-- Git Abstraction Layer for Lua -- Tag object interface
+--
+-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
+--
+--
+
+local ll = require "gall.ll"
+
+local objs = setmetatable({}, {__mode="k"})
+local repos = setmetatable({}, {__mode="k"})
+local parsed = setmetatable({}, {__mode="k"})
+
+local _new
+
+local function parse_person(pers)
+ local real, email, when, tz = pers:match("^(.-) <([^>]+)> ([0-9]+) ([+-][0-9]+)$")
+ return {
+ realname = real,
+ email = email,
+ unixtime = when,
+ timezone = tz
+ }
+end
+
+local function tagindex(tag, field)
+ if not parsed[tag] then
+ local raw = objs[tag].raw
+ local headers, body, signature = {}, ""
+ local state = "headers"
+
+ for l in raw:gmatch("([^\n]*)\n") do
+ if state == "headers" then
+ local first, second = l:match("^([^ ]+) (.+)$")
+ if first then
+ if headers[first] then
+ headers[first][#headers[first]+1] = second
+ else
+ headers[first] = { second }
+ end
+ else
+ state = "message"
+ end
+ elseif state == "message" then
+ if l == PGP_SIG_START then
+ signature = l .. "\n"
+ state = "signature"
+ else
+ body = body .. l .. "\n"
+ end
+ else
+ signature = signature .. l .. "\n"
+ end
+ end
+
+ -- there's always one object
+ rawset(tag, "object", repos[tag]:get(headers.object[1]))
+ -- Always one type
+ rawset(tag, "type", headers.type[1])
+ -- Always one tag name
+ rawset(tag, "tag", headers.tag[1])
+ -- Always one tagger
+ rawset(tag, "tagger", parse_person(headers.tag[1]))
+ -- A message
+ rawset(tag, "message", body)
+ -- And an optional signature
+ rawset(tag, "signature", signature)
+ -- Promote the SHA
+ rawset(tag, "sha", objs[tag].sha)
+
+ -- Signal we are parsed
+ parsed[tag] = true
+ end
+
+ return rawget(tag, field)
+end
+
+local function tagtostring(tag)
+ return "<GitTag(" .. tostring(objs[tag].sha) .. ") in " .. tostring(repos[tag]) .. ">"
+end
+
+local tagmeta = {
+ __index = tagindex,
+ __tostring = tagtostring
+}
+
+function _new(repo, obj)
+ local ret = setmetatable({}, tagmeta)
+ objs[ret] = obj
+ repos[ret] = repo
+ return ret
+end
+
+return {
+ new = _new
+}
diff --git a/lib/gall/tree.lua b/lib/gall/tree.lua
new file mode 100644
index 0000000..ed7ba4b
--- /dev/null
+++ b/lib/gall/tree.lua
@@ -0,0 +1,209 @@
+-- gall.tree
+--
+-- Git Abstraction Layer for Lua -- Tree object interface
+--
+-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
+--
+--
+
+local ll = require "gall.ll"
+
+local PARSED = {}
+
+local objs = setmetatable({}, {__mode="k"})
+local repos = setmetatable({}, {__mode="k"})
+local parsed = setmetatable({}, {__mode="k"})
+
+local tree_method = {}
+
+local shapattern = ("[a-f0-9]"):rep(40)
+local shacapture = "(" .. shapattern .. ")"
+local modepattern = ("[0-7]"):rep(6)
+local modecapture = "(" .. modepattern .. ")"
+-- startmode, endmode, startsha, endsha, action, optional score, filename(s)
+local treediffline = table.concat {
+ ":", modecapture, " ", modecapture, " ", shacapture, " ", shacapture, " ",
+ "(.)", "([0-9]*)", "\t", "([^\n]+)", "\n"
+}
+
+local mode_to_kind_map = {
+ ["000000"] = "missing",
+ ["160000"] = "submodule",
+ ["100644"] = "blob",
+ ["100755"] = "xblob",
+ ["120000"] = "symlink",
+}
+
+local function unescape_filename(fn)
+ local zfn = fn:gsub("\\\\", "\0")
+ zfn = zfn:gsub("\\n", "\n")
+ zfn = zfn:gsub("\\t", "\t")
+ return (zfn:gsub("%z", "\\"))
+end
+
+function tree_method:diff_to(other)
+ -- Generate a diff from self to other
+ local repo = repos[self]
+ local ok, streediff = repo:rawgather("diff-tree", "-r", "-M", "-C",
+ objs[self].sha, objs[other].sha)
+ if ok ~= 0 then
+ return nil, streediff
+ end
+ local treediff = {}
+ for startmode, endmode, startsha, endsha, action, score, filenames in
+ streediff:gmatch(treediffline) do
+ local diffentry = {
+ startmode = startmode,
+ endmode = endmode,
+ startkind = mode_to_kind_map[startmode] or "UNKNOWN",
+ endkind = mode_to_kind_map[endmode] or "UNKNOWN",
+ startsha = startsha,
+ endsha = endsha,
+ action = action,
+ score = (score ~= "") and score or nil,
+ }
+ if action == "C" or action == "R" then
+ local src, dst = filenames:match("([^\t]+)\t(.+)")
+ diffentry.src_name = unescape_filename(src)
+ diffentry.filename = unescape_filename(dst)
+ diffentry.dst_name = diffentry.filename
+ else
+ diffentry.filename = unescape_filename(filenames)
+ end
+ treediff[#treediff+1] = diffentry
+ treediff[diffentry.filename] = diffentry
+ end
+ return treediff
+end
+
+local function treeindex(tree, field)
+ if tree_method[field] then
+ return tree_method[field]
+ end
+
+ if not parsed[tree] then
+ local raw = objs[tree].raw
+ for l in raw:gmatch("([^\n]+)\n") do
+ local perm, type, sha, name = l:match("^([0-9]+) ([^ ]+) ([0-9a-f]+)\t(.+)$")
+ local t = {
+ permissions = perm,
+ name = name,
+ type = type,
+ obj = repos[tree]:get(sha)
+ }
+ rawset(tree, name, t)
+ end
+ parsed[tree] = true
+ end
+
+ return rawget(tree, field)
+end
+
+local function treetostring(tree)
+ return "<GitTree(" .. tostring(objs[tree].sha) .. ") in " .. tostring(repos[tree]) .. ">"
+end
+
+local treemeta = {
+ __index = treeindex,
+ __tostring = treetostring,
+}
+
+local function _new(repo, obj)
+ local ret = setmetatable({}, treemeta)
+ objs[ret] = obj
+ repos[ret] = repo
+ return ret
+end
+
+local function _realise(t)
+ if not parsed[t] then
+ local ignored = t[parsed]
+ end
+ return t
+end
+
+local function _flatten(t)
+ local ret = {}
+ local function _inner_flatten(pfx, tt)
+ _realise(tt)
+ for k, v in pairs(tt) do
+ local leaf = pfx .. k
+ if v.type == "tree" then
+ _inner_flatten(leaf .. "/", v.obj.content)
+ else
+ ret[leaf] = v
+ end
+ end
+ end
+ _inner_flatten("", t)
+ return ret
+end
+
+local function _create(repo, flat_tree)
+ local function __store(tree, element, content)
+ local prefix, suffix = element:match("^([^/]+)/(.+)$")
+ if not prefix then
+ tree[element] = content
+ else
+ tree[prefix] = tree[prefix] or { [""] = true }
+ __store(tree[prefix], suffix, content)
+ end
+ end
+ local t = {[""] = true}
+ for k, v in pairs(flat_tree) do
+ __store(t, k, v)
+ end
+
+ -- t is now a 'tree' so we need to turn any tables into trees recursively
+
+ local function __treeify(t)
+ -- Step one, ensure any trees inside t are treeified
+ for k, v in pairs(t) do
+ if k ~= "" and rawget(v, "") then
+ local _v, reason = __treeify(v)
+ if not _v then
+ return nil, reason
+ end
+ t[k] = _v
+ end
+ end
+ -- Next, construct a mktree input
+ local tree_ent = ""
+ local mode = {
+ tree = "040000",
+ blob = "100644",
+ }
+ for k, v in pairs(t) do
+ if k ~= "" then
+ local ok, obj = pcall(function() return v.obj end)
+ if ok then
+ v = obj
+ end
+ if not mode[v.type] then
+ return nil, "Unknown type " .. v.type
+ end
+ tree_ent = tree_ent ..
+ ("%s %s %s\t%s\0"):format(mode[v.type], v.type, v.sha, k)
+ end
+ end
+
+ local why, sha = repo:_run_with_input(tree_ent, ll.chomp,
+ "mktree", "-z")
+ if why ~= 0 then
+ return nil, "mktree returned " .. tostring(why)
+ end
+ return repo:get(sha)
+ end
+
+ return __treeify(t)
+end
+
+return {
+ realise = _realise,
+ flatten = _flatten,
+ new = _new,
+ create = _create,
+
+ -- Magic SHA1 for empty tree
+ empty_sha = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
+}
diff --git a/lib/gall/util.lua b/lib/gall/util.lua
new file mode 100644
index 0000000..d6c5297
--- /dev/null
+++ b/lib/gall/util.lua
@@ -0,0 +1,29 @@
+-- gall.ll
+--
+-- Git Abstraction Layer for Lua -- Utility functions
+--
+-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
+--
+--
+
+local function deep_copy(t, memo)
+ if not memo then memo = {} end
+ if memo[t] then return memo[t] end
+ local ret = {}
+ local kk, vv
+ for k, v in pairs(t) do
+ kk, vv = k, v
+ if type(k) == "table" then
+ kk = _deep_copy(k)
+ end
+ if type(v) == "table" then
+ vv = _deep_copy(v)
+ end
+ ret[kk] = vv
+ end
+ return ret
+end
+
+return {
+ deep_copy = deep_copy,
+}
diff --git a/test/test-gall.lua b/test/test-gall.lua
index 2cd691a..3f0558d 100644
--- a/test/test-gall.lua
+++ b/test/test-gall.lua
@@ -11,8 +11,18 @@
local luacov = require 'luacov'
+local ents = {}
+ents.util = require 'gall.util'
+ents.ll = require 'gall.ll'
+ents.object = require 'gall.object'
+ents.tree = require 'gall.tree'
+ents.commit = require 'gall.commit'
+ents.tag = require 'gall.tag'
+ents.repository = require 'gall.repository'
+
local gall = require 'gall'
+
local testnames = {}
local real_assert = assert
@@ -34,6 +44,13 @@ function suite.test_gall_not_empty()
assert(next(gall), "Gall module is empty")
end
+for _, ent in ipairs {
+ "repository", "util", "ll", "tag", "commit", "tree", "object" } do
+ suite["test_gall_"..ent.."_is_"..ent] = function()
+ assert(gall[ent] == ents[ent], "gall."..ent.." module is not gall."..ent.." entry")
+ end
+end
+
local count_ok = 0
for _, testname in ipairs(testnames) do
-- print("Run: " .. testname)