diff options
Diffstat (limited to 'macros/luatex/latex/gitinfo-lua/scripts')
-rw-r--r-- | macros/luatex/latex/gitinfo-lua/scripts/gitinfo-lua-cmd.lua | 186 | ||||
-rw-r--r-- | macros/luatex/latex/gitinfo-lua/scripts/gitinfo-lua.lua | 345 |
2 files changed, 531 insertions, 0 deletions
diff --git a/macros/luatex/latex/gitinfo-lua/scripts/gitinfo-lua-cmd.lua b/macros/luatex/latex/gitinfo-lua/scripts/gitinfo-lua-cmd.lua new file mode 100644 index 0000000000..7e27bc05ba --- /dev/null +++ b/macros/luatex/latex/gitinfo-lua/scripts/gitinfo-lua-cmd.lua @@ -0,0 +1,186 @@ +-- gitinfo-lua-cmd.lua +-- Copyright 2023 E. Nijenhuis +-- +-- This work may be distributed and/or modified under the +-- conditions of the LaTeX Project Public License, either version 1.3c +-- of this license or (at your option) any later version. +-- The latest version of this license is in +-- http://www.latex-project.org/lppl.txt +-- and version 1.3c or later is part of all distributions of LaTeX +-- version 2005/12/01 or later. +-- +-- This work has the LPPL maintenance status ‘maintained’. +-- +-- The Current Maintainer of this work is E. Nijenhuis. +-- +-- This work consists of the files gitinfo-lua.sty gitinfo-lua.pdf +-- gitinfo-cmd.lua and gitinfo-lua.lua + +local api = { + cwd = nil, + executable = 'git', + default_sort = '', + attribute_separator = '\\pop', + record_separator = '\\end' +} +local cache = {} +function cache:seek(_key) + for key, value in pairs(self) do + if key == _key then + return true, value + end + end + return false, nil +end + +function api.trim(s) + return (s:gsub("^%s*(.-)%s*$", "%1")) +end + +function api:exec(command, do_caching, target_dir) + local cmd = self.executable .. ' ' .. command + local cwd = target_dir or self.cwd + if cwd then + cmd = 'cd ' .. cwd .. ';' .. cmd + end + if do_caching then + local found, result = cache:seek(cmd) + if found then + return result + end + end + local f = io.popen(cmd) + if f == nil then + return nil, "Couldn't execute git command.\n\tIs option '-shell-escape' turned on?" + end + local s = f:read('*a') + if f:close() then + if do_caching then + cache[cmd] = s + end + return s + else + return nil, 'Error executing git command' + end +end + +function api:shortlog(sort_by_contrib, include_email, target_dir) + local cmd = 'shortlog -s' + if sort_by_contrib then + cmd = cmd .. 'n' + end + if include_email then + cmd = cmd .. 'e' + end + cmd = cmd .. ' HEAD' + return self:exec(cmd, true, target_dir) +end + +function api:parse_opts(options) + if options then + for idx, opt in ipairs(options) do + options[idx] = '--' .. opt + end + return table.concat(options, ' ') + end +end + +function api:format_attribute(attribute, no_separator, with_parenthesis) + if with_parenthesis then + attribute = '%(' .. attribute .. ')' + else + attribute = '%' .. attribute + end + if not no_separator then + attribute = attribute .. self.attribute_separator + end + return attribute +end + +function api:_parse_format_spec(spec, idx, with_parenthesis) + local format = '' + local above_limit = #spec + 1 + while idx and idx <= #spec do + local attr_idx, attr_size, attr = string.find(spec, '([a-z:]+)', idx) + local if_idx, if_size, if_block, if_then, if_else = string.find(spec, '%((.-)%)%((.-)%)%((.-)%)', idx) + if if_idx or attr_idx then + if (if_idx or above_limit) > (attr_idx or above_limit) then + format = format .. self:format_attribute(attr, false, with_parenthesis) + idx = attr_size and (attr_size + 1) + else + local if_token = self:format_attribute(if_block, true, with_parenthesis) + local then_result = self:_parse_format_spec(if_then, idx, with_parenthesis) + local else_result = self:_parse_format_spec(if_else, idx, with_parenthesis) + format = format .. '%(if)' .. if_token .. '%(then)' .. then_result + format = format .. '%(else)' .. else_result .. '%(end)' + idx = if_size and (if_size + 1) + end + end + end + return format +end + +function api:parse_format_spec(spec, with_parenthesis) + if type(spec) ~= 'string' then + return nil, 'Pass the attribute format spec separated by "," in a string en enclosed in three parentheses for if statements' + end + local format = self:_parse_format_spec(spec, 1, with_parenthesis) + return format .. self.record_separator +end + +function api:parse_response(buffer) + local results = {} + for record_buffer in string.gmatch(buffer, '(.-)' .. self.record_separator) do + local record = {} + for attr in string.gmatch(record_buffer, '(.-)' .. self.attribute_separator) do + table.insert(record, self.trim(attr)) + end + table.insert(results, record) + end + return results +end + +function api:log(format_spec, revision, options, target_dir) + local format, err = self:parse_format_spec(format_spec) + if err then + return nil, err + end + local cmd = 'log --pretty=format:"' .. format .. '"' + local opts = self:parse_opts(options) + if opts then + cmd = cmd .. ' ' .. opts + end + if revision and revision ~= '' then + cmd = cmd .. ' ' .. revision + end + local response, err = self:exec(cmd, true, target_dir) + if not response then + return nil, err + end + return self:parse_response(response) +end + +function api:for_each_ref(format_spec, revision_type, options, target_dir) + local err, format, response + format, err = self:parse_format_spec(format_spec, true) + if err then return nil, err end + local cmd = 'for-each-ref --format="' .. format .. '"' + local opts = self:parse_opts(options) + if opts then + cmd = cmd .. ' ' .. opts + end + cmd = cmd .. ' ' .. revision_type + response, err = self:exec(cmd, true, target_dir) + if err then return nil, err end + return self:parse_response(response) +end + +local gitinfo_cmd = {} +local gitinfo_cmd_mt = { + __index = api, + __newindex = nil +} + +setmetatable(gitinfo_cmd, gitinfo_cmd_mt) + +return gitinfo_cmd diff --git a/macros/luatex/latex/gitinfo-lua/scripts/gitinfo-lua.lua b/macros/luatex/latex/gitinfo-lua/scripts/gitinfo-lua.lua new file mode 100644 index 0000000000..4cb0f0f467 --- /dev/null +++ b/macros/luatex/latex/gitinfo-lua/scripts/gitinfo-lua.lua @@ -0,0 +1,345 @@ +-- gitinfo-lua.lua +-- Copyright 2023 E. Nijenhuis +-- +-- This work may be distributed and/or modified under the +-- conditions of the LaTeX Project Public License, either version 1.3c +-- of this license or (at your option) any later version. +-- The latest version of this license is in +-- http://www.latex-project.org/lppl.txt +-- and version 1.3c or later is part of all distributions of LaTeX +-- version 2005/12/01 or later. +-- +-- This work has the LPPL maintenance status ‘maintained’. +-- +-- The Current Maintainer of this work is E. Nijenhuis. +-- +-- This work consists of the files gitinfo-lua.sty gitinfo-lua.pdf +-- gitinfo-cmd.lua and gitinfo-lua.lua + +if not modules then + modules = {} +end + +local module = { + name = 'gitinfo-lua', + info = { + version = 1.0, + comment = "Git info Lua — Git integration with LaTeX", + author = "Erik Nijenhuis", + license = "free" + } +} + +modules[module.name] = module.info + +local api = { + cur_tok = nil, + cmd = require('gitinfo-lua-cmd'), + escape_chars = { + ['&'] = '\\&', + ['%%'] = '\\%%', + ['%$'] = '\\$', + ['#'] = '\\#', + ['_'] = '\\_', + ['{'] = '\\{', + ['}'] = '\\}', + ['~'] = '\\textasciitilde', + ['%^'] = '\\textasciicircum' + } +} +local mt = { + __index = api, + __newindex = nil +} +local gitinfo = {} +setmetatable(gitinfo, mt) + +function api.trim(s) + return (s:gsub("^%s*(.-)%s*$", "%1")) +end + +function api:set_date() + local date, err = self.cmd:log('cs', '-1', {'max-count=1'}) + if date and #date == 1 then + local _, _, year, month, day = date[1][1]:find('(%d+)[-/](%d+)[-/](%d+)') + tex.year = tonumber(year) + tex.month = tonumber(month) + tex.day = tonumber(day) + else + return nil, (err or 'Length of output doesn\'t match one (attempt to set git date)') + end +end + +function api:escape_str(value) + local buf = string.gsub(value, '\\', '\\textbackslash') + for search, replace in pairs(self.escape_chars) do + buf = string.gsub(buf, search, replace) + end + return buf +end + +-- experimental +function api:get_tok() + if self.cur_tok == nil then + self.cur_tok = token.get_next() + end + return self.cur_tok +end + +-- experimental +function api:parse_opts() + local tok = self:get_tok() + if tok.cmdname == 'other_char' then + --token.put_next(tok) + local opts = token.scan_word() + self.cur_tok = nil + -- todo: parse [] + return opts + end +end + +-- experimental +function api:parse_arguments(argc) + local result_list = {} + for _ = 1, argc do + local tok = self:get_tok() + if tok.cmdname == 'left_brace' then + token.put_next(tok) + table.insert(result_list, token.scan_argument()) + self.cur_tok = nil + else + tex.error("Expected left brace") + return + end + end + return table.unpack(result_list) +end + +-- experimental +function api:parse_macro() + --tex.print('\\noexpand') + local tok = self:get_tok() + if (tok.cmdname == 'call') or tok.cmdname == 'long_call' then + self.cur_tok = nil + return tok + else + tex.error("Expected Macro") + for i = 1, 5 do + local _tok = token.get_next() + end + end +end + +function api:dir(path) + self.cmd.cwd = path +end + +function api:version() + return self.trim(self.cmd:exec('describe --tags --always', true)) +end + +function api:write_version() + local version, err = self:version() + if version then + tex.write(version) + else + tex.error(err) + end +end + +-- todo: prevent output to stderr +-- todo: add write variant +-- experimental +function api:is_dirty() + local ok, _ = self.cmd:exec('describe --tags --exact-match') + return ok == nil +end + +function api:local_author() + return self.trim(self.cmd:exec('config user.name', true)) +end + +function api:write_local_author() + local name, err = self:local_author() + if name then + tex.write(name) + else + tex.error(err) + end +end + +function api:local_email() + return self.trim(self.cmd:exec('config user.email', true)) +end + +function api:write_local_email() + local name, err = self:local_email() + if name then + tex.write(name) + else + tex.error(err) + end +end + +function api:authors(sort_by_contrib) + local authors, err = self.cmd:shortlog(sort_by_contrib, true) + if authors then + local author_list = {} + for line in authors:gmatch('(.-)\n') do + local contributions, name, email = line:match("^%s-(%d+)%s-(.-)%s-<(.-)>%s-$") + table.insert(author_list, { + contributions = contributions, + name = name, + email = email + }) + end + return author_list + else + return nil, err + end +end + +function api:cs_for_authors(csname, conjunction, sort_by_contrib) + if token.is_defined(csname) then + local tok = token.create(csname) + local authors, err = self:authors(sort_by_contrib) + if authors then + local next_conj + for _, author in ipairs(authors) do + if next_conj then + tex.print(next_conj) + end + next_conj = conjunction + tex.print(tok, '{' .. self:escape_str(self.trim(author.name)) .. '}', '{' .. self:escape_str(self.trim(author.email)) .. '}') + end + else + tex.error(err) + end + else + tex.error('ERROR: \\' .. csname .. ' not defined') + end +end + +function api:cs_commit(csname, rev, format) + if token.is_defined(csname) then + local tok = token.create(csname) + local log, err = self.cmd:log(format, rev, {'max-count=1'}) + if log then + if #log == 1 then + tex.print(tok) + for _, value in ipairs(log[1]) do + tex.print('{' .. self:escape_str(value) .. '}') + end + else + texio.write_nl('Warning: commit returned none') + end + else + tex.error('ERROR: ' .. (err or 'nil')) + end + else + tex.error('ERROR: \\' .. csname .. ' not defined') + end +end + +function api:cs_last_commit(csname, format) + return self:cs_commit(csname, '-1', format) +end + +function api:cs_for_commit(csname, rev_spec, format) + if token.is_defined(csname) then + local tok = token.create(csname) + local log, err = self.cmd:log(format, rev_spec) + if log then + for _, commit in ipairs(log) do + tex.print(tok) + for _, value in ipairs(commit) do + tex.print('{' .. self:escape_str(value) .. '}') + end + end + else + tex.error('ERROR:\\' .. err) + end + else + tex.error('ERROR: \\' .. csname .. ' not defined') + end +end + +function api:tag_info(format_spec, tag, target_dir) + local err, info + info, err = self.cmd:for_each_ref(format_spec, 'refs/tags', {'count=1', 'contains=' .. tag}, target_dir) + if info and #info == 1 then + return info[1] + else + tex.error(err or 'Result count didn\'t match. (in tag_info)') + end +end + +function api:tags(target_dir) + local tag_list = {} + local tags, err = self.cmd:exec('tag -l --sort=-v:refname', true, target_dir) + if tags then + for tag in tags:gmatch('(.-)\n') do + table.insert(tag_list, self.trim(tag)) + end + else + return nil, err + end + return tag_list +end + +function api:cs_tag(csname, format_spec, tag, target_dir) + if token.is_defined(csname) then + local tok = token.create(csname) + local info = self:tag_info(format_spec, tag, target_dir) + if info then + tex.print(tok) + for _, value in ipairs(info) do + tex.print('{' .. self:escape_str(value) .. '}') + end + end + else + tex.error('ERROR:\\' .. csname .. ' not defined') + end +end + +function api:cs_for_tag(csname, format_spec, target_dir) + if token.is_defined(csname) then + local tok = token.create(csname) + local tags, err = self.cmd:for_each_ref(format_spec, 'refs/tags', {'sort=-authordate'}, target_dir) + if tags then + for _, info in ipairs(tags) do + tex.print(tok) + for _, value in ipairs(info) do + tex.print('{' .. self:escape_str(value) .. '}') + end + end + else + tex.error('ERROR:\\' .. err) + end + else + tex.error('ERROR:\\' .. csname .. ' not defined') + end +end + +function api:cs_for_tag_sequence(csname, target_dir) + if token.is_defined(csname) then + local tok = token.create(csname) + local seq, err = self:tags(target_dir) + if seq then + for idx, tag in ipairs(seq) do + if idx < #seq then + local next = seq[idx + 1] + tex.print(tok, '{' .. tag .. '}{' .. next .. '}{' .. tag .. '...' .. next .. '}') + else + tex.print(tok, '{' .. tag .. '}{}{' .. tag .. '}') + end + end + else + tex.error('ERROR:\\' .. (err or 'Unknown error')) + end + else + tex.error('ERROR:\\' .. csname .. ' not defined') + end +end + +return gitinfo |