-- 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 function repomethod:get_ref(ref) return ll.git2.lookup_sha_from_ref(self.git2.repo, ref) 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.lookup_sha_from_ref(self.git2.repo, sha) if refobj then fullsha = refobj 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 commitish_1 = self:get(sha_1) local commitish_2 = self:get(sha_2) pcall(function() if commitish_1.type == "tag" then commitish_1 = commitish_1.content.object end if commitish_2.type == "tag" then commitish_2 = commitish_2.content.object end end) local oid_base, err = ll.git2.merge_base(self.git2.repo, commitish_1.sha, commitish_2.sha) if not oid_base then if tostring(err) == "ENOTFOUND" then return true end return nil, err end return 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) if toref then -- Set the ref... local ok, err = ll.git2.set_symbolic_ref(self.git2.repo, name, toref) if not ok then return nil, err end end local symref, err = ll.git2.lookup_symbolic_ref(self.git2.repo, name) if not symref then return nil, "No such ref: " .. tostring(toref) .. " (" .. err .. ")" end return true, symref 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 --[[ -- TODO: Add config support to git2.c and convert this 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.open_repo(repopath) if not git2 then return nil, "Unable to find Git repository at " .. path end retrepo.git2 = { repo = git2 } symref = ll.git2.lookup_symbolic_ref(git2, "HEAD") 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 --[[ -- This seems redundant 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 }