-- gall.repository -- -- Git Abstraction Layer for Lua -- Repository interface -- -- Copyright 2012 Daniel Silverstone -- -- local ll = require "gall.ll" local object = require "gall.object" local tree = require "gall.tree" 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 "" 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") return self:get(tree.empty_sha) 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 if ll.git2 then local orig_get_ref = repomethod.get_ref function repomethod:get_ref(ref) local rref, msg = ll.git2.Reference.lookup(self.git2.repo, ref) if not rref then -- Our libgit2 code sometimes cannot cope with packed-refs -- which contain peeled refs not in refs/tags/ so we try -- again with the underlying git install if we get this -- message if msg == "The packed references file is corrupted" then return orig_ret_ref(self, ref) end return nil end if rref:type() ~= ll.git2.REF_OID then return nil end return tostring(rref:oid()) end 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 fullsha if ll.git2 then local refobj = ll.git2.Reference.lookup(self.git2.repo, sha) if refobj then if refobj:type() == ll.git2.REF_SYMBOLIC then refobj = ll.git2.Reference.lookup(self.git2.repo, refobj:target()) end if refobj:type() == ll.git2.REF_OID then fullsha = tostring(refobj:oid()) end end end if not fullsha then local ok, out, err = self:_run_with_input(sha, chomp, "cat-file", "--batch-check") if ok ~= 0 then error((out or "") .. "\n" .. (err or "")) end fullsha = out:match("^("..string.rep("[0-9a-f]", 40)..")") end if fullsha then return fullsha end 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 args.stderr = true local ok, out, err = ll.merge_base(args) if ok ~= 0 and ok ~= 1 then return nil, (out or "") .. "\n" .. (err or "") 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 if ll.git2 then local old_merge_base = repomethod.merge_base function repomethod:merge_base(sha_1, sha_2, get_all) if get_all then return old_merge_base(self, sha_1, sha_2, get_all) end local oid_1 = ll.git2.OID.hex(sha_1) local oid_2 = ll.git2.OID.hex(sha_2) local oid_base, err = ll.git2.merge.base(self.git2.repo, oid_1, oid_2) if not oid_base then if tostring(err) == "ENOTFOUND" then return true end return nil, err end return tostring(oid_base), err end 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 args.stderr = true local ok, out = ll.rev_list(args) if ok ~= 0 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 toref then local ok, ref, err = ll.symbolic_ref { "-q", name, toref, stderr=true, repo=self.path } if ok ~= 0 or err:find("Unable to create") then return nil, "Could not set " .. tostring(name) .. " to " .. tostring(toref) end end local ok, ref, err = ll.symbolic_ref { "-q", name, stderr=true, repo=self.path } if ok == 0 then return true, ref end if ok == 1 then return false end return nil, (ref or "") .. "\n" .. (err or "") end if ll.git2 then function repomethod:symbolic_ref(name, toref) local symref = ll.git2.Reference.lookup(self.git2.repo, name) if not symref then return nil, "No such ref: " .. tostring(toref) end if symref:type() ~= ll.git2.REF_SYMBOLIC then return false end if toref then symref:set_target(toref) end return true, symref:target() end 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 if ll.git2 then local old_config = repomethod.config function repomethod:config(confname, value) local conf = ll.git2.Config.open(self.path .. "/config" ) if not conf then return old_config(self, confname, value) end if not value then local v = conf:get_string(confname) return (v and true or nil), (v and v or "Unknown config: " .. confname) else if type(value) == "number" then conf:set_int64(value) else conf:set_string(confname, tostring(value)) end return true end 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 = luxio.stat(repopath .. "/.git") if ok == 0 then repopath = repopath .. "/.git" workpath = path end local symref if ll.git2 then local git2, msg = ll.git2.Repository(repopath) if not git2 then return nil, "Unable to find Git repository at " .. path end local odb = git2:odb() retrepo.git2 = { repo = git2, odb = odb } symref = ll.git2.Reference.lookup(git2, "HEAD") symref = symref:target() else ok, symref = ll.symbolic_ref { "-q", "HEAD", stderr=true, repo=repopath } if ok ~= 0 then return nil, "Unable to find Git repository at " .. path end end retrepo.path = repopath retrepo.work = workpath retrepo.HEAD = symref if ll.git2 then local git2, msg = ll.git2.Repository(retrepo.path) if not git2 then return nil, msg end local odb = git2:odb() retrepo.git2 = { repo = git2, odb = odb } end 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 }