# HG changeset patch # User bsw # Date 1448908838 -3600 # Node ID 30523f31b186248fe7eb8e6efd612ebdf19d818b # Parent bb10f001443ec3c8e5b76f926287b7926b683600 Added lf4rcs module and example configuration diff -r bb10f001443e -r 30523f31b186 config/example.lua --- a/config/example.lua Mon Jul 27 23:33:26 2015 +0200 +++ b/config/example.lua Mon Nov 30 19:40:38 2015 +0100 @@ -255,6 +255,118 @@ } +-- Configuration of lf4rcs +-- ------------------------------------------------------------------------ +config.lf4rc = {} + +-- Example configuration for controlling a Git repository +config.lf4rcs.git = { + + render_draft_reference = function(url, draft) + if not draft.external_reference then return end + ui.tag{ content = _"Changeset:" } + slot.put(" ") + ui.link{ + text = draft.external_reference, + external = url .. ";a=commit;h=" .. draft.external_reference + } + end, + + get_remote_user = function() + return os.getenv("REMOTE_USER") + end, + + get_branches = function(path, exec) + local branches = {} + for line in io.lines() do + local oldrev, newrev, branch = string.match(line, "([^ ]+) ([^ ]+) refs/heads/(.+)") + if not branch then + return nil, "unexpected format from git hook environment" + end + branches[branch] = { newrev } + end + return branches + end, + + commit = function(path, exec, branch, target_node_id, close_message, merge_message) + if merge_message then + -- TODO reset on error + exec("git", "-C", path, "checkout", "master") + exec("git", "-C", path, "merge", target_node_id, "-m", merge_message) + exec("git", "-C", path, "push", "origin", "master") + end + end + +} + +-- Example configuration for controlling a Mercurial repository +config.lf4rcs.hg = { + + working_branch_name = "work", + + render_draft_reference = function(url, draft) + if not draft.external_reference then return end + ui.tag{ content = _"Changeset graph:" } + slot.put(" ") + ui.link{ + text = draft.external_reference, + external = url .. "/graph/" .. draft.external_reference + } + end, + + get_remote_user = function() + return os.getenv("REMOTE_USER") + end, + + get_branches = function(path, exec) + local first_node_id = os.getenv("HG_NODE") + if not first_node_id then + return nil, "internal error, no first node ID available" + end + local hg_log = exec( + "hg", "log", "-R", path, "-r", first_node_id .. ":", "--template", "{branches}\n" + ) + local branches = {} + for branch in hg_log:gmatch("(.-)\n") do + if branch == "" then branch = "default" end + if not branches[branch] then + branches[branch] = {} + local head_lines = exec( + "hg", "heads", "-R", path, "--template", "{node}\n", branch + ) + for node_id in string.gmatch(head_lines, "[^\n]+") do + table.insert(branches[branch], node_id) + end + end + end + return branches + end, + + extra_checks = function(path, exec) + local result = exec("hg", "heads", "-t", "-c") + for branch in string.gmatch(result, "[^\n]+") do + if branch == lf4rcs.config.hg.working_branch_name then + return nil, "open head found for branch " .. lf4rcs.config.hg.working_branch_name + end + end + return true + end, + + commit = function(path, exec, branch, target_node_id, close_message, merge_message) + exec("hg", "up", "-R", path, "-C", "-r", target_node_id) + exec("hg", "commit", "-R", path, "--close-branch", "-m", close_message) + if merge_message then + exec("hg", "up", "-R", path, "-C", "-r", "default") + exec("hg", "merge", "-R", path, "-r", "tip") + exec("hg", "commit", "-R", path, "-m", merge_message) + end + end + +} + +lf4rcs.init() + + -- External references -- ------------------------------------------------------------------------ -- Rendering of external references diff -r bb10f001443e -r 30523f31b186 env/lf4rcs/_init.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/lf4rcs/_init.lua Mon Nov 30 19:40:38 2015 +0100 @@ -0,0 +1,1 @@ +lf4rcs.log_prefix = "[lf4rcs] " diff -r bb10f001443e -r 30523f31b186 env/lf4rcs/commit.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/lf4rcs/commit.lua Mon Nov 30 19:40:38 2015 +0100 @@ -0,0 +1,40 @@ +function lf4rcs.commit(issue) + local repository, path, url = lf4rcs.get_config(issue.area.unit) + if not (config.lf4rcs[repository] and config.lf4rcs[repository].commit) then + error("Unsupported repository type") + end + local initiatives = Initiative:new_selector() + :add_where{ "issue_id = ?", issue.id } + :exec() + for i, initiative in ipairs(initiatives) do + local function exec(...) + local command, output, err_message, exit_code = lf4rcs.exec(...) + local log = "Executed: " .. command .. "\n" + if output then + log = log .. output .. "\n" + end + if err_message and #err_message > 0 then + log = log .. "ERROR: " .. err_message .. "\n" + end + if exit_code and exit_code ~= 0 then + log = log .. "Exit code: " .. tostring(exit_code) .. "\n" + end + issue.admin_notice = (issue.admin_notice or "") .. log + issue:save() + end + local close_message, merge_message + if initiative.winner then + close_message = "Initiative i" .. initiative.id + .. " accepted as winner. Closing branch." + merge_message = "Initiative i" .. initiative.id + .. " accepted as winner. Applying branch changesets to upstream." + else + close_message = "Initiative i" .. initiative.id .. " rejected. Closing branch." + end + local target_node_id = initiative.current_draft.external_reference + if target_node_id then + local branch = "i" .. initiative.id + lf4rcs[repository].commit(path, exec, branch, target_node_id, close_message, merge_message) + end + end +end diff -r bb10f001443e -r 30523f31b186 env/lf4rcs/exec.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/lf4rcs/exec.lua Mon Nov 30 19:40:38 2015 +0100 @@ -0,0 +1,12 @@ +function lf4rcs.exec(...) + local output, err_message, exit_code = extos.pfilter(nil, ...) + local command_parts = {...} + for i, part in ipairs(command_parts) do + if string.match(part, " ") then + command_parts[i] = '"' .. part .. '"' + end + end + local command = table.concat(command_parts, " ") + return command, output, err_message, exit_code +end + diff -r bb10f001443e -r 30523f31b186 env/lf4rcs/get_config.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/lf4rcs/get_config.lua Mon Nov 30 19:40:38 2015 +0100 @@ -0,0 +1,8 @@ +function lf4rcs.get_config(unit) + if not unit.external_reference then + error("Unit is not configured for lf4rcs") + end + local repository, path, url = string.match(unit.external_reference, "([^ ]+) ([^ ]+) (.*)") + return repository, path, url +end + diff -r bb10f001443e -r 30523f31b186 env/lf4rcs/init.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/lf4rcs/init.lua Mon Nov 30 19:40:38 2015 +0100 @@ -0,0 +1,12 @@ +function lf4rcs.init() + local super_handler = config.notification_handler_func + config.notification_handler_func = function(event) + if super_handler then super_handler(event) end + lf4rcs.notification_handler(event) + end + config.render_external_reference = { + draft = lf4rcs.render_draft_reference, + initiative = lf4rcs.render_initiative_reference + } +end + diff -r bb10f001443e -r 30523f31b186 env/lf4rcs/notification_handler.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/lf4rcs/notification_handler.lua Mon Nov 30 19:40:38 2015 +0100 @@ -0,0 +1,11 @@ +function lf4rcs.notification_handler(event) + if event.event == "issue_state_changed" and ( + event.state ~= "admission" and + event.state ~= "discussion" and + event.state ~= "verification" and + event.state ~= "voting" + ) then + lf4rcs.commit(event.issue) + end +end + diff -r bb10f001443e -r 30523f31b186 env/lf4rcs/render_draft_reference.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/lf4rcs/render_draft_reference.lua Mon Nov 30 19:40:38 2015 +0100 @@ -0,0 +1,12 @@ +function lf4rcs.render_draft_reference(draft, wrapper) + local repository, path, url = lf4rcs.get_config(draft.initiative.issue.area.unit) + if not (config.lf4rcs[repository] and config.lf4rcs[repository].render_draft_reference) then + error("Unsupported repository type") + end + if draft.external_reference then + wrapper(function() + config.lf4rcs[repository].render_draft_reference(url, draft) + end) + end +end + diff -r bb10f001443e -r 30523f31b186 env/lf4rcs/render_initiative_reference.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/lf4rcs/render_initiative_reference.lua Mon Nov 30 19:40:38 2015 +0100 @@ -0,0 +1,6 @@ +function lf4rcs.render_initiative_reference(initiative, wrapper) + if initiative.current_draft.external_reference then + config.render_external_reference.draft(initiative.current_draft, wrapper) + end +end + diff -r bb10f001443e -r 30523f31b186 env/lf4rcs/update_references.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/env/lf4rcs/update_references.lua Mon Nov 30 19:40:38 2015 +0100 @@ -0,0 +1,100 @@ +function lf4rcs.update_references(repository, path, unit_id) + local function log(message) + print(lf4rcs.log_prefix .. message) + end + if not config.lf4rcs[repository] + or not config.lf4rcs[repository].get_remote_user + or not config.lf4rcs[repository].get_branches + then + log("Unsupported repository type") + os.exit(1) + end + log("inspecting changesets") + local remote_user = config.lf4rcs[repository].get_remote_user() + local function abort(message) + log("TEST FAILED: " .. message) + log("ABORTING and ROLLBACK due to failed test.") + db:query("ROLLBACK") + os.exit(1) + end + db:query("BEGIN") + local member = Member:new_selector() + :add_where{ "login = ?", remote_user } + :optional_object_mode() + :exec() + if not member then + abort( + "internal error, member '" + .. remote_user .. "' not found in database" + ) + end + local function exec(...) + local command, output, err_message, exit_code = lf4rcs.exec(...) + if not output then + log("Could not execute: " .. command) + abort(err_message) + end + if exit_code ~= 0 then + log("Could not execute: " .. command) + abort("Exit code: " .. tostring(exit_code)) + end + return output + end + if config.lf4rcs[repository].extra_checks then + local success, err_message = config.lf4rcs[repository].extra_checks(path, exec) + if not success then + abort(err_message) + end + end + local branches, err = config.lf4rcs[repository].get_branches(path, exec) + if not branches then abort(err) end + for branch, head_node_ids in pairs(branches) do + log('checking branch ' .. branch) + if branch ~= config.lf4rcs[repository].working_branch_name then + local initiative_id = string.match(branch, "^i([0-9]+)$") + if not initiative_id + or initiative_id ~= tostring(tonumber(initiative_id)) + then + abort("this branch name is not allowed") + end + initiative_id = tonumber(initiative_id) + if #head_node_ids > 1 then + abort("number of heads found for branch is greater than 1: " .. #head_node_ids) + end + local initiative = Initiative:by_id(initiative_id) + if not initiative then + abort("initiative i" .. initiative_id .. " not found" ) + end + if initiative.issue.area.unit_id ~= tonumber(unit_id) then + abort("initiative belongs to another unit (unit ID " .. initiative.issue.area.unit_id .. ")") + end + if initiative.issue.state ~= "admission" and initiative.issue.state ~= "discussion" then + abort("issue is already frozen or closed (" .. initiative.issue.state .. ")") + end + if initiative.revoked then + abort("initiative has been revoked") + end + local initiator = Initiator:by_pk(initiative.id, member.id) + if not initiator then + abort("member is not initiator of initiative i" .. initiative_id) + end + if not initiator.accepted then + abort( + "member has not accepted invitation to become initiator of initiative i" + .. initiative_id + ) + end + local node_id = head_node_ids[1] or false + if node_id then + log("adding node " .. node_id .. " to initiative i" .. initiative_id) + else + log("removing node reference from initiative i" .. initiative_id) + end + Draft:update_content(member.id, initiative_id, nil, nil, node_id) + end + end + log("changes cleared. continue committing.") + db:query("COMMIT") + os.exit(0) +end +