diff -Nru gitano-1.0/bin/gitano-post-receive-hook.in gitano-1.1/bin/gitano-post-receive-hook.in --- gitano-1.0/bin/gitano-post-receive-hook.in 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/bin/gitano-post-receive-hook.in 2017-08-03 15:11:42.000000000 +0000 @@ -36,6 +36,7 @@ -- @@GITANO_LUA_PATH local gitano = require "gitano" +local pat = gitano.patterns local gall = require "gall" local luxio = require "luxio" local sio = require "luxio.simple" @@ -113,7 +114,7 @@ -- emails, ensuring that new rules are applied, etc) local updates = {} -for oldsha, newsha, refname in (sio.stdin:read("*a")):gmatch("([^ ]+) ([^ ]+) ([^\n]+)\n?") do +for oldsha, newsha, refname in (sio.stdin:read("*a")):gmatch(pat.GITHOOK_PARSE_CHANGESET) do gitano.log.ddebug("post-receive:", oldsha, newsha, refname) updates[refname] = {oldsha, newsha, oldsha=oldsha, newsha=newsha} end @@ -140,71 +141,94 @@ end end -if repo.name == "gitano-admin" and updates[admin_repo.HEAD] then - -- Updating the 'master' of gitano-admin, let's iterate all the repositories +function post_receive_core_handler(repo, updates) + if repo.name == "gitano-admin" and updates[admin_repo.HEAD] then + -- Updating the 'master' of gitano-admin, let's iterate all the repositories + + gitano.log.syslog.info("Updating gitano-admin") + + local msg = gitano.i18n.expand("SCANNING_FOR_UPDATES") + gitano.log.chat(msg) + gitano.log.syslog.info(msg) + + local ok, msg = gitano.repository.foreach(config, report_repo) + if not ok then + gitano.log.crit(msg) + end + + msg = gitano.i18n.expand("ALL_UPDATES_DONE") + gitano.log.chat(msg) + gitano.log.syslog.info(msg) + + local proc = sp.spawn({ + gitano.config.lib_bin_path() .. "/gitano-update-ssh", + gitano.config.repo_path() + }) + local how, why = proc:wait() + if how ~= "exit" or why ~= 0 then + gitano.log.crit(gitano.i18n.expand("ERROR_UPDATE_SSH_NOT_WORK")) + end + elseif repo.name ~= "gitano-admin" then + -- Not gitano-admin at all, so run the update-server-info stuff + gitano.log.info(gitano.i18n.expand("UPDATE_HTTP_INFO")) + local ok, err = repo.git:update_server_info() + if not ok then + gitano.log.warn(err) + end + gitano.log.info(gitano.i18n.expand("UPDATE_LASTMOD_DATE")) + local shas = {} + for _, t in pairs(updates) do + shas[#shas+1] = t.newsha + end + local ok, err = repo:update_modified_date(shas) + if not ok then + gitano.log.warn(err) + end + end + return "continue" +end + +function post_receive_run_supple(repo, updates) + if repo:uses_hook("post-receive") then + gitano.log.debug("Configuring for post-receive hook") + gitano.actions.set_supple_globals("post-receive") + + local msg = gitano.i18n.expand("RUNNING_POST_RECEIVE_HOOK") + gitano.log.info(msg) + gitano.log.syslog.info(msg) + + local info = { + username = username, + keytag = keytag, + source = source, + realname = (config.users[username] or {}).real_name or "", + email = (config.users[username] or {}).email_address or "", + } + local ok, msg = gitano.supple.run_hook("post-receive", repo, info, updates) + if not ok then + gitano.log.crit(msg or gitano.i18n.expand("ERROR_NO_ERROR_FOUND")) + end + gitano.log.info(gitano.i18n.expand("FINISHED")) + end + return "continue" +end + +function post_receive_check_head(repo, updates) + -- Check that HEAD is now resolvable in the repo + if not repo.git:get("HEAD") then + gitano.log.warn("") + gitano.log.warn(gitano.i18n.expand("WARN_HEAD_DANGLING")) + gitano.log.warn("") + end + return "continue" +end + +gitano.hooks.add(gitano.hooks.names.POST_RECEIVE, -1000, + post_receive_core_handler) +gitano.hooks.add(gitano.hooks.names.POST_RECEIVE, 0, post_receive_run_supple) +gitano.hooks.add(gitano.hooks.names.POST_RECEIVE, 1000, post_receive_check_head) - gitano.log.syslog.info("Updating gitano-admin") - - local msg = gitano.i18n.expand("SCANNING_FOR_UPDATES") - gitano.log.chat(msg) - gitano.log.syslog.info(msg) - - local ok, msg = gitano.repository.foreach(config, report_repo) - if not ok then - gitano.log.crit(msg) - end - - msg = gitano.i18n.expand("ALL_UPDATES_DONE") - gitano.log.chat(msg) - gitano.log.syslog.info(msg) - - local proc = sp.spawn({ - gitano.config.lib_bin_path() .. "/gitano-update-ssh", - gitano.config.repo_path() - }) - local how, why = proc:wait() - if how ~= "exit" or why ~= 0 then - gitano.log.crit(gitano.i18n.expand("ERROR_UPDATE_SSH_NOT_WORK")) - end -elseif repo.name ~= "gitano-admin" then - -- Not gitano-admin at all, so run the update-server-info stuff - gitano.log.info(gitano.i18n.expand("UPDATE_HTTP_INFO")) - local ok, err = repo.git:update_server_info() - if not ok then - gitano.log.warn(err) - end - gitano.log.info(gitano.i18n.expand("UPDATE_LASTMOD_DATE")) - local shas = {} - for _, t in pairs(updates) do - shas[#shas+1] = t.newsha - end - local ok, err = repo:update_modified_date(shas) - if not ok then - gitano.log.warn(err) - end -end - -if repo:uses_hook("post-receive") then - gitano.log.debug("Configuring for post-receive hook") - gitano.actions.set_supple_globals("post-receive") - - local msg = gitano.i18n.expand("RUNNING_POST_RECEIVE_HOOK") - gitano.log.info(msg) - gitano.log.syslog.info(msg) - - local info = { - username = username, - keytag = keytag, - source = source, - realname = (config.users[username] or {}).real_name or "", - email = (config.users[username] or {}).email_address or "", - } - local ok, msg = gitano.supple.run_hook("post-receive", repo, info, updates) - if not ok then - gitano.log.crit(msg or gitano.i18n.expand("ERROR_NO_ERROR_FOUND")) - end - gitano.log.info(gitano.i18n.expand("FINISHED")) -end +gitano.hooks.run(gitano.hooks.names.POST_RECEIVE, repo, updates) gitano.log.syslog.close() diff -Nru gitano-1.0/bin/gitano-pre-receive-hook.in gitano-1.1/bin/gitano-pre-receive-hook.in --- gitano-1.0/bin/gitano-pre-receive-hook.in 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/bin/gitano-pre-receive-hook.in 2017-08-03 15:11:42.000000000 +0000 @@ -36,6 +36,7 @@ -- @@GITANO_LUA_PATH local gitano = require "gitano" +local pat = gitano.patterns local gall = require "gall" local luxio = require "luxio" local sio = require "luxio.simple" @@ -116,7 +117,7 @@ -- you. local updates = {} -for oldsha, newsha, refname in (sio.stdin:read("*a")):gmatch("([^ ]+) ([^ ]+) ([^\n]+)") do +for oldsha, newsha, refname in (sio.stdin:read("*a")):gmatch(pat.GITHOOK_PARSE_CHANGESET) do gitano.log.ddebug("pre-receive:", oldsha, newsha, refname) updates[refname] = {oldsha, newsha, oldsha=oldsha, newsha=newsha} end diff -Nru gitano-1.0/bin/gitano-setup.in gitano-1.1/bin/gitano-setup.in --- gitano-1.0/bin/gitano-setup.in 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/bin/gitano-setup.in 2017-08-03 15:11:42.000000000 +0000 @@ -36,6 +36,7 @@ -- @@GITANO_LUA_PATH local gitano = require "gitano" +local pat = gitano.patterns local gall = require "gall" local luxio = require "luxio" local sio = require "luxio.simple" @@ -60,10 +61,20 @@ gitano.log.set_prefix("gitano-setup") gitano.log.bump_level(gitano.log.level.CHAT) +local force_batch = false for i = #possible_answers, 1, -1 do - gitano.log.debug(gitano.i18n.expand("SETUP_DEBUG_PARSING_ANSWERS", { file=possible_answers[1] })) - local one_conf = assert(clod.parse(assert(io.open(possible_answers[1], "r")):read "*a", - "@" .. possible_answers[1])) + local answer_file = possible_answers[i] + gitano.log.debug(gitano.i18n.expand("SETUP_DEBUG_PARSING_ANSWERS", { file=answer_file })) + local file_content, file_name + if answer_file == "-" then + file_content = io.stdin:read "*a" + file_name = "@stdin" + force_batch = true + else + file_content = assert(io.open(answer_file, "r")):read "*a" + file_name = "@" .. answer_file + end + local one_conf = assert(clod.parse(file_content, file_name)) gitano.log.debug(gitano.i18n.expand("SETUP_DEBUG_COMBINE_ANSWERS")) for k,v in one_conf:each() do gitano.log.ddebug(tostring(k) .. " = " .. tostring(v)) @@ -71,6 +82,10 @@ end end +if force_batch then + conf.settings["setup.batch"] = true +end + gitano.log.chat(gitano.i18n.expand("SETUP_WELCOME")) gitano.log.chat(gitano.i18n.expand("SETUP_DO_CHECKS")) @@ -156,8 +171,8 @@ return true end -function validate_name(n) - if not n:match("^[a-z_][a-z0-9_%-]*$") then +function validate_name(n, pattern) + if not n:match(pattern) then error(gitano.i18n.expand("SETUP_ERROR_INVALID_NAME", { name=n }), 2) end end @@ -200,7 +215,7 @@ get("paths.home") .. "/repos") validate_name(ask_for("admin.username", gitano.i18n.expand("SETUP_ADMIN_USERNAME_INFO"), - "admin")) + "admin"), pat.VALID_USERNAME) ask_for("admin.realname", gitano.i18n.expand("SETUP_ADMIN_REALNAME_INFO"), "Administrator") @@ -208,7 +223,7 @@ "admin@administrator.local") validate_name(ask_for("admin.keyname", gitano.i18n.expand("SETUP_ADMIN_KEYNAME_INFO"), - "default")) + "default"), pat.VALID_SSHKEYNAME) ask_for("site.name", gitano.i18n.expand("SETUP_SITE_NAME_INFO"), "a random Gitano instance") ask_for("log.prefix", gitano.i18n.expand("SETUP_LOG_PREFIX_INFO"), "gitano") diff -Nru gitano-1.0/debian/changelog gitano-1.1/debian/changelog --- gitano-1.0/debian/changelog 2017-01-18 23:21:46.000000000 +0000 +++ gitano-1.1/debian/changelog 2017-08-03 15:12:31.000000000 +0000 @@ -1,8 +1,9 @@ -gitano (1.0-2) unstable; urgency=medium +gitano (1.1-1) unstable; urgency=medium - * Fix missing copyright field + * New upstream release + * Also copy the multimail plugin and its README to usr/share/doc - -- Daniel Silverstone Wed, 18 Jan 2017 23:21:46 +0000 + -- Daniel Silverstone Thu, 03 Aug 2017 11:12:31 -0400 gitano (1.0-1) unstable; urgency=medium diff -Nru gitano-1.0/debian/control gitano-1.1/debian/control --- gitano-1.0/debian/control 2017-01-15 16:21:01.000000000 +0000 +++ gitano-1.1/debian/control 2017-08-03 15:12:31.000000000 +0000 @@ -6,7 +6,7 @@ lua-lace, lua-supple (>=1.0.7), lua-clod, lua-gall, lua-scrypt, git, lua5.1, lua-rex-pcre, lua-tongue, rsync, gnupg, apache2-utils, lighttpd, procps, pandoc, texlive-latex-recommended, texlive-xetex, texlive-luatex, lmodern, - texlive-fonts-recommended + texlive-fonts-recommended, wget Standards-Version: 3.9.8 Homepage: https://www.gitano.org.uk/ diff -Nru gitano-1.0/debian/copyright gitano-1.1/debian/copyright --- gitano-1.0/debian/copyright 2017-01-18 23:21:16.000000000 +0000 +++ gitano-1.1/debian/copyright 2017-08-03 15:12:31.000000000 +0000 @@ -11,7 +11,6 @@ License: BSD-3-clause Files: doc/admin/* -Copyright: Copyright 2017 Daniel Silverstone, Richard Maw, Lars Wirzenius License: CC-BY-SA Files: doc/admin/*.jpg diff -Nru gitano-1.0/debian/gitano.install gitano-1.1/debian/gitano.install --- gitano-1.0/debian/gitano.install 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/debian/gitano.install 2017-08-03 15:12:31.000000000 +0000 @@ -0,0 +1,2 @@ +plugins/git-multimail.lua usr/share/doc/gitano/ +plugins/README.multimail usr/share/doc/gitano/ diff -Nru gitano-1.0/doc/admin/000.mdwn gitano-1.1/doc/admin/000.mdwn --- gitano-1.0/doc/admin/000.mdwn 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/doc/admin/000.mdwn 2017-08-03 15:11:42.000000000 +0000 @@ -419,7 +419,7 @@ * Otherwise, without the global prefix, it is included from the per-project rules - (`refs/gitano-admin` branch of the repository being operated on). + (`refs/gitano/admin` branch of the repository being operated on). Include statements can be made conditional by adding a predicate after the path, so you can split up your rules: @@ -433,6 +433,143 @@ # Branches must not be tags deny "Branches may only be commits" [ref prefix refs/heads/] [newtype exact commit] +# Git Hook scripts + +Gitano administrators may create, or delegate creation of, +[Lua][lua] scripts to take action or provide further authentication +when content is pushed to a repository. + +These [Lua][lua] scripts are sandboxed using [Supple][supple], +to make it safer to run user-defined code. + +## How to add a hook + +Hooks are stored in the `gitano-admin.git` repository +and the `refs/gitano/admin` branch of each other repository, +so that there can be traceability of who is responsible for each hook. + +Global hooks in `gitano-admin.git` are loaded +from the `global-hooks` subdirectory. +Per-repository hooks in the `refs/gitano/admin` branch +are loaded from the `hooks` subdirectory. + +In both cases hooks are loaded from a file named after the hook +as defined by [githooks(5)][] suffixed with `.lua`. + +Currently the only hooks that are supported are: + +1. pre-receive +2. update +3. post-receive + +So a global pre-receive hook would be in the file `global-hooks/pre-receive.lua` +and a per-repository post-receive hook would be in `hooks/post-receive.lua`. + +## APIs available to hooks + +1. `log.$level(...)` to emit text at that log level. + $level can be one of: + + 1. `state` + 2. `crit` or `critical` + 3. `err` or `error` + 4. `warn` or `warning` + 5. `chat` + 6. `info` + 7. `debug` + 8. `ddebug` or `deepdebug` + +2. `fetch(url, headers, body, content_type)` during post-receive hooks. + +3. A `repo` object as the first parameter to per-repository hooks, + with the following methods: + + ------------------------------------ ---------------------------------------------------------------------------- + `:get(sha1ish)` Get the contents of a git object from anything matching [gitrevisions(7)][]. + `:get_config(confname)` Get the value confname from the repo config if it is a single value + `:get_config_list(confname)` Get the value confname from the repo config if it is a list value + `:check_signature(obj, keyringname)` Check whether `obj` is an object signed by a key in keyring `keyringname`. + ------------------------------------ ---------------------------------------------------------------------------- + + Objects returned from `get` are [gall][] objects. + Refer to [gall's API documentation][gall-api] for details. + +4. An `actor` table in the global environment, + containing the following indices: + + -------- -------------------------------------------------------------- + username Gitano username of user doing the push. + keytag Name of the ssh key the user is pushing with. + source "ssh", "http" or "git" depending on the protocol used to push. + realname Real name of user from user configuration. + email E-Mail of user from user configuration. + -------- -------------------------------------------------------------- + +5. The `refname`, `oldsha` and `newsha` as the second to fourth parameters + during per-repository update hooks, as described in [githooks(5)][]. + + Checking whether the master ref is deleted would be accomplished by: + + local _, refname, oldsha, newsha = ... + if refname == "refs/heads/master" and newsha == ("0"):rep(40) then + log.state("Master deleted") + end + +6. For pre-receive or post-receive per-repository hooks as the second parameter, + a table indexed by the `refnames` being modified + containing tables of the `oldsha` and `newsha` indexed + either at positional indices `1` and `2`, + or by name `oldsha` and `newsha`. + Checking whether the master ref is deleted would be accomplished by: + + local _, updates = ... + if (updates["refs/heads/master"] or {}).newsha == ("0"):rep(40) then + log.state("Master deleted") + end + +7. Global hooks work the same as per-repository hooks, + except they are passed a callable function first, followed by the + positional parameters as described above. The callable will run the + per-repository hook when called, + so global hooks may either replace or augment per-repository hooks. + + To unconditionally run some global hook code and then call a local hook + your global hook would be structured something like: + + -- Example global-hooks/update.lua + local hookf, repo, refname, oldsha, newsha = ... + -- Do stuff with repo/refname/oldsha/newsha here + -- + -- And finally (optionally) tail-chain through to the per-repo hook + return hookf(repo, refname, oldsha, newsha) + + To unconditionally ignore update hooks and log why: + + -- Example global-hooks/update.lua + local hookf, repo, refname, oldsha, newsha = ... + local treeish = repo:get("refs/gitano/admin").sha + if repo:get(treeish..":hooks/update.lua") then + log.state("Update hooks are disabled") + end + + To provide a default if a hook is not defined: + + -- Example global-hooks/update.lua + local hookf, repo, refname, oldsha, newsha = ... + local treeish = repo:get("refs/gitano/admin").sha + if repo:get(treeish..":hooks/update.lua") then + return hookf(repo, refname, oldsha, newsha) + end + -- Do stuff with repo/refname/oldsha/newsha here + + +[lua]: https://www.lua.org/ +[supple]: https://www.gitano.org.uk/supple/ +[githooks(5)]: https://git-scm.com/docs/githooks +[gitrevisions(7)]: https://git-scm.com/docs/gitrevisions +[gall]: https://www.gitano.org.uk/gall/ +[gall-api]: file:///usr/share/doc/lua-gall-doc/html/index.html + # Backup and restore of a Gitano instance When gitano-setup is run, the admin needs to specify a Unix user in diff -Nru gitano-1.0/.editorconfig gitano-1.1/.editorconfig --- gitano-1.0/.editorconfig 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/.editorconfig 2017-08-03 15:11:42.000000000 +0000 @@ -11,7 +11,7 @@ trim_trailing_whitespace = true # Lua code should be space indented, to 3 spaces -[{*.lua,bin/*.in,testing/gitano-test-tool.in,utils/install-lua-bin}] +[{*.lua,bin/*.in,testing/gitano-test-tool.in,utils/install-lua-bin,utils/merge-luacov-stats,.luacov}] indent_style = space indent_size = 3 @@ -24,3 +24,5 @@ [Makefile] indent_style = tab +[*.patch] +trim_trailing_whitespace = false diff -Nru gitano-1.0/.gitmodules gitano-1.1/.gitmodules --- gitano-1.0/.gitmodules 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/.gitmodules 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,3 @@ +[submodule "extras/luacov"] + path = extras/luacov + url = git://git.gitano.org.uk/luacov.git diff -Nru gitano-1.0/lang/en.lua gitano-1.1/lang/en.lua --- gitano-1.0/lang/en.lua 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/lang/en.lua 2017-08-03 15:11:42.000000000 +0000 @@ -130,6 +130,7 @@ UPDATE_HTTP_INFO = "Updating server info for dumb HTTP transport", UPDATE_LASTMOD_DATE = "Updating last-modified date", RUNNING_POST_RECEIVE_HOOK = "Running repository post-receive hook", + WARN_HEAD_DANGLING = "HEAD remains dangling", -- Messages from pre-receive RUNNING_PRE_RECEIVE_HOOK = "Running repository pre-receive hook", @@ -162,7 +163,8 @@ BYPASS_USER_BANNER_HEADER = "**** ALERT **** ALERT **** PAY CAREFUL ATTENTION **** ALERT **** ALERT ****", BYPASS_USER_ALERT_MESSAGE = "**** You are acting as the bypass user. Rules and hooks WILL NOT APPLY ****", BYPASS_USER_BANNER_FOOTER = "**** ALERT **** ALERT **** DO NOT DO THIS NORMALLY **** ALERT **** ALERT ****", - + PREAUTH_CMDLINE_HOOK_DECLINED = "Pre-authorization command line hook declined to permit action: ${reason}", + PREAUTH_CMDLINE_HOOK_ABORTED = "Pre-authorization command line hook aborted: ${reason}", -- Messages from the config module NO_SITE_CONF = "No site.conf", NO_CORE_RULES = "No core rules file", diff -Nru gitano-1.0/lib/gitano/actions.lua gitano-1.1/lib/gitano/actions.lua --- gitano-1.0/lib/gitano/actions.lua 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/lib/gitano/actions.lua 2017-08-03 15:11:42.000000000 +0000 @@ -34,6 +34,7 @@ local log = require "gitano.log" local gall = require "gall" local config = require "gitano.config" +local pat = require "gitano.patterns" local i18n = require 'gitano.i18n' local sio = require 'luxio.simple' local supple = require 'gitano.supple' @@ -84,7 +85,7 @@ return "500", "headers must be a table if provided", {} , "" end if method == "POST" then - headers["Content-Type"] = content_type or application/octet-stream + headers["Content-Type"] = content_type or "application/octet-stream" headers["Content-Length"] = tostring(#body) args[#args+1] = "--data-binary" args[#args+1] = "@-" @@ -106,9 +107,9 @@ if (how ~= "exit" or why ~= 0) then return "500", err, {}, "" end - local code, msg, _headers, content = response:match("^HTTP/1.[01] (...) ?([^\r\n]+)\r?\n(.-)\r?\n\r?\n(.*)$") + local code, msg, _headers, content = response:match(pat.HTTP_RESPONSE) local headers = {} - for k, v in _headers:gmatch("([^:\r\n]+): *([^\r\n]+)") do + for k, v in _headers:gmatch(pat.HTTP_HEADER) do local r = headers[k] or {} r[#r+1] = v headers[k] = r diff -Nru gitano-1.0/lib/gitano/admincommand.lua gitano-1.1/lib/gitano/admincommand.lua --- gitano-1.0/lib/gitano/admincommand.lua 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/lib/gitano/admincommand.lua 2017-08-03 15:11:42.000000000 +0000 @@ -34,6 +34,7 @@ local util = require 'gitano.util' local repository = require 'gitano.repository' local config = require 'gitano.config' +local pat = require 'gitano.patterns' local clod = require 'clod' local luxio = require 'luxio' local sio = require 'luxio.simple' @@ -123,6 +124,37 @@ return cmdline.cmd.run(conf, cmdline.repo, cmdline.copy, env) end +local function update_user_in_htpasswd(conf, userfrom, userto) + if conf.clod.settings["use_htpasswd"] ~= "yes" then + return + end + local htpasswd_path = os.getenv("HOME") .. "/htpasswd" + local lock = util.lockfile(htpasswd_path .. ".lock") + local fh = io.open(htpasswd_path, "r") + if not fh then return end + local to_write = {} + for l in fh:lines() do + if l:sub(1, #userfrom + 1) == userfrom .. ":" then + if userto then + to_write[#to_write + 1] = userto .. ":" .. l:sub(#userfrom + 2, -1) + end + else + to_write[#to_write+1] = l + end + end + fh:close() + fh = assert(io.open(htpasswd_path .. ".new", "w")) + fh:write(table.concat(to_write, "\n")) + fh:write("\n") + fh:close() + local ok, errno = luxio.rename(htpasswd_path .. ".new", htpasswd_path) + if ok ~= 0 then + log.warn(i18n.expand("ERROR_UNABLE_TO_RENAME_INTO_PLACE", + {what="htpasswd", reason=luxio.strerror(errno)})) + end + util.unlockfile(lock) +end + local builtin_user_short = "Manage users in Gitano" local builtin_user_helptext = [[ usage: user [list] @@ -176,7 +208,7 @@ log.error("user add takes a username, email address and real name") return false end - if cmdline[2] == "add" and not cmdline[3]:match("^[a-z][a-z0-9_.-]+$") then + if cmdline[2] == "add" and not cmdline[3]:match(pat.VALID_USERNAME) then log.error("user name '" .. cmdline[3] .. "' not valid.") return false end @@ -343,6 +375,23 @@ log.fatal(commit) end log.state("Committed: " .. reason) + if cmdline[2] == "rename" then + update_user_in_htpasswd(conf, cmdline[3], cmdline[4]) + local function reown_repo(_, repo) + if repo:conf_get("project.owner") == cmdline[3] then + local ok, msg = repo:conf_set_and_save( + "project.owner", cmdline[4], + env.GITANO_USER, env.GITANO_ORIG_USER) + if not ok then + log.error(msg) + return "exit", 1 + end + end + end + repository.foreach(conf, reown_repo) + elseif cmdline[2] == "del" then + update_user_in_htpasswd(conf, cmdline[3], nil) + end end return "exit", 0 end @@ -406,7 +455,7 @@ log.error("Add takes a group name and a description") return false end - if cmdline[2] == "add" and not cmdline[3]:match("^[a-z][a-z0-9_.-]+$") then + if cmdline[2] == "add" and not cmdline[3]:match(pat.VALID_GROUPNAME) then log.error("group name '" .. cmdline[3] .. "' not valid.") return false end @@ -739,7 +788,7 @@ if #cmdline > 3 then local ok = true for i = 4, #cmdline do - if not cmdline[i]:match("^" .. string.rep("[0-9A-Fa-f]", 40) .. "$") then + if not cmdline[i]:match(pat.VALID_KEY_FINGERPINRT) then log.error("error: '" .. cmdline[i] .. "' is not a valid fingerprint") ok = false end @@ -750,7 +799,7 @@ end if cmdline[2] == "delkey" then if #cmdline == 4 or #cmdline == 5 then - if not cmdline[4]:match("^" .. string.rep("[0-9A-Fa-f]", 40) .. "$") then + if not cmdline[4]:match(pat.VALID_KEY_FINGERPRINT) then log.error("error: '" .. cmdline[i] .. "' is not a valid fingerprint") return false end @@ -795,7 +844,7 @@ log.error("Keyring " .. keyringname .. " already exists") return "exit", 1 end - if not keyringname:match("^[a-z][a-z0-9_.-]+$") then + if not keyringname:match(pat.VALID_KEYRING_NAME) then log.error("Keyring " .. keyringname .. " is not lower-alphanumeric") return "exit", 1 end @@ -867,7 +916,7 @@ if code ~= 0 then log.fatal("Unable to list keyring: GPG returned " .. tostring(code)) end - for fingerprint in alloutput:gmatch("fpr:::::::::([0-9A-F]+):") do + for fingerprint in alloutput:gmatch(pat.GPG_OUTPUT_FINGERPRINT_MATCH) do log.stdout(fingerprint) end end @@ -1149,7 +1198,7 @@ repeat e, i = luxio.readdir(dirp) if e == 0 then - if not i.d_name:find("^%.") then + if not i.d_name:find(pat.DOTFILE) then log.stdout(i.d_name) end end @@ -1200,7 +1249,7 @@ repeat e, i = luxio.readdir(dirp) if e == 0 then - if not i.d_name:find("^%.") then + if not i.d_name:find(pat.DOTFILE) then if not match or (match == i.d_name) then to_remove[#to_remove+1] = i.d_name end diff -Nru gitano-1.0/lib/gitano/auth.lua gitano-1.1/lib/gitano/auth.lua --- gitano-1.0/lib/gitano/auth.lua 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/lib/gitano/auth.lua 2017-08-03 15:11:42.000000000 +0000 @@ -37,6 +37,7 @@ local repository = require 'gitano.repository' local util = require 'gitano.util' local i18n = require 'gitano.i18n' +local hooks = require 'gitano.hooks' local gall = require 'gall' local luxio = require 'luxio' @@ -121,6 +122,23 @@ i18n.expand("CLIENT_CONNECTED", { ip=ip, user=user, key=keytag, cmdline=cmdline})) + local cancel + cancel, ip, user, keytag, parsed_cmdline = + (function(c,i,u,k,...) + return c, i, u, k, {...} + end)(hooks.run(hooks.names.PREAUTH_CMDLINE, false, + ip, user, keytag, unpack(parsed_cmdline))) + + if cancel == nil then + log.syslog.err(i18n.expand("PREAUTH_CMDLINE_HOOK_ABORTED", {reason=ip})) + log.critical(i18n.expand("PREAUTH_CMDLINE_HOOK_DECLINED", {reason=ip})) + return nil + end + if cancel then + log.critical(i18n.expand("PREAUTH_CMDLINE_HOOK_DECLINED", {reason=ip})) + return nil + end + local cmd = command.get(parsed_cmdline[1]) if not cmd then diff -Nru gitano-1.0/lib/gitano/command.lua gitano-1.1/lib/gitano/command.lua --- gitano-1.0/lib/gitano/command.lua 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/lib/gitano/command.lua 2017-08-03 15:11:42.000000000 +0000 @@ -33,6 +33,7 @@ local log = require 'gitano.log' local util = require 'gitano.util' local repository = require 'gitano.repository' +local pattern = require 'gitano.patterns' local sio = require "luxio.simple" @@ -241,7 +242,7 @@ log.state(cmd.name, do_sep(cmd), desc) if cmd.helptext then log.state("") - for line in (cmd.helptext):gmatch("([^\n]*)\n") do + for line in (cmd.helptext):gmatch(pattern.TEXT_LINE) do log.state("=>", line) end end @@ -304,14 +305,7 @@ local function builtin_receive_pack_run(config, repo, cmdline, env) local cmdcopy = {"receive-pack", env=env} for i = 2, #cmdline do cmdcopy[i] = cmdline[i] end - local how, why = repo:git_command(cmdcopy) - -- Check that HEAD is now resolvable in the repo - if how == "exit" and why == 0 and not repo.git:get("HEAD") then - log.warn("") - log.warn("HEAD remains dangling") - log.warn("") - end - return how, why + return repo:git_command(cmdcopy) end assert(register_cmd("git-receive-pack", nil, nil, @@ -453,9 +447,9 @@ return false end cmdline.orig_key = cmdline[4] - if cmdline[4]:match("%.%*$") then + if cmdline[4]:match(pattern.CONF_ENDS_WILDCARD) then -- Doing a wild removal, expand it now - local prefix = cmdline[4]:match("^(.+)%.%*$") + local prefix = cmdline[4]:match(pattern.CONF_WILDCARD) cmdline[4] = nil for k in repo.project_config:each(prefix) do cmdline[#cmdline+1] = k @@ -525,7 +519,7 @@ for i = 1, #slist do local key = slist[i] local value = repo.project_config.settings[key] - local prefix = key:match("^(.+)%.i_[0-9]+$") + local prefix = key:match(pattern.CONF_ARRAY_INDEX) if prefix then local neatkey = prefix .. ".*" for i = 4, #cmdline do @@ -539,7 +533,7 @@ end elseif cmdline[3] == "set" then local key, value = cmdline[4], cmdline[5] - local vtype, rest = value:match("^([sbi]):(.*)$") + local vtype, rest = value:match(pattern.CONF_SET_TYPE_PREFIX) if vtype then if vtype == "s" then value = rest @@ -839,9 +833,9 @@ pat = pat .. ".*" end if used_evil then - pat = "^/" .. pat .. "%.git$" + pat = "^/" .. pat .. pattern.GIT_REPO_SUFFIX else - pat = "/" .. pat .. "%.git$" + pat = "/" .. pat .. pattern.GIT_REPO_SUFFIX end log.debug("PAT:", pat) pats[#pats+1] = pat diff -Nru gitano-1.0/lib/gitano/config.lua gitano-1.1/lib/gitano/config.lua --- gitano-1.0/lib/gitano/config.lua 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/lib/gitano/config.lua 2017-08-03 15:11:42.000000000 +0000 @@ -38,6 +38,7 @@ local log = require 'gitano.log' local lace = require 'gitano.lace' local i18n = require 'gitano.i18n' +local pat = require 'gitano.patterns' local luxio = require 'luxio' local sio = require 'luxio.simple' local clod = require 'clod' @@ -109,7 +110,7 @@ -- Gather the users local users = {} for filename, obj in pairs(flat_tree) do - local prefix, username = filename:match("^(users/.-)([a-z][a-z0-9_.-]+)/user%.conf$") + local prefix, username = filename:match(pat.USER_CONF_MATCH) if prefix and username then if not is_blob(obj) then return nil, prefix .. username .. "/user.conf is not a blob?" @@ -144,7 +145,7 @@ -- Now gather the users' keys local all_keys = {} for filename, obj in pairs(flat_tree) do - local prefix, username, keyname = filename:match("^(users/.-)([a-z][a-z0-9_.-]+)/([a-z][a-z0-9_.-]+)%.key$") + local prefix, username, keyname = filename:match(pat.USER_KEY_MATCH) if prefix and username and keyname then if not users[username] then return nil, i18n.expand("ERROR_ORPHAN_KEY", @@ -158,7 +159,7 @@ return nil, i18n.expand("ERROR_BAD_KEY_NEWLINES", {filename=filename}) end - local keytype, keydata, keytag = this_key:match("^([^ ]+) ([^ ]+) ([^ ].*)$") + local keytype, keydata, keytag = this_key:match(pat.SSH_KEY_CONTENTS) if not (keytype and keydata and keytag) then return nil, i18n.expand("ERROR_BAD_KEY_SMELL", {filename=filename}) end @@ -190,7 +191,7 @@ -- Now gather the groups local groups = {} for filename, obj in pairs(flat_tree) do - local prefix, groupname = filename:match("^(groups/.-)([a-z][a-z0-9_.-]+)%.conf$") + local prefix, groupname = filename:match(pat.GROUP_CONF_MATCH) if prefix and groupname then if groups[groupname] then return nil, i18n.expand("ERROR_DUPLICATE_GROUP", {name=groupname}) @@ -269,7 +270,7 @@ -- Now gather the keyrings local keyrings = {} for filename, obj in pairs(flat_tree) do - local prefix, keyringname = filename:match("^(keyrings/.-)([a-z][a-z0-9_.-]+)%.gpg$") + local prefix, keyringname = filename:match(pat.KEYRING_MATCH) if prefix and keyringname then if keyrings[keyringname] then return nil, i18n.expand("ERROR_DUPLICATE_KEYRING", {name=keyringname}) @@ -440,9 +441,9 @@ -- Shallow copy the tree ready for mods, skipping keyrings, users and groups for k,v in pairs(conf.content) do - if not (k:match("^users/") or - k:match("^groups/") or - k:match("^keyrings/")) then + if not (k:match(pat.USER_INFO_PREFIX) or + k:match(pat.GROUP_INFO_PREFIX) or + k:match(pat.KEYRING_INFO_PREFIX)) then newtree[k] = v end end diff -Nru gitano-1.0/lib/gitano/coverage.lua.in gitano-1.1/lib/gitano/coverage.lua.in --- gitano-1.0/lib/gitano/coverage.lua.in 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/lib/gitano/coverage.lua.in 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,52 @@ +-- gitano.coverage +-- +-- Coverage generation for when running Gitano under test. +-- NOTE: Not to be installed as part of Gitano, not to be loaded from the +-- top level gitano module +-- +-- Copyright 2017 Daniel Silverstone +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions +-- are met: +-- 1. Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- 2. Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- 3. Neither the name of the author nor the names of their contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +-- ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +-- SUCH DAMAGE. +-- + +local ok, runner = pcall(require, "luacov.runner") + +local luxio = require("luxio") + +local conf = assert(loadfile("@@.luacov@@"))() + +local function begin(basepath, part) + if not ok then return end + basepath = basepath or "./" + local pidstr = (".%d"):format(luxio.getpid()) + part = (part or "general") .. pidstr + conf.statsfile = basepath .. "luacov.stats-" .. part .. ".out" + runner.init(conf) +end + +return { + begin = begin, +} diff -Nru gitano-1.0/lib/gitano/hooks.lua gitano-1.1/lib/gitano/hooks.lua --- gitano-1.0/lib/gitano/hooks.lua 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/lib/gitano/hooks.lua 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,137 @@ +-- gitano.hooks +-- +-- Hook management routines for Gitano +-- +-- Copyright 2017 Daniel Silverstone +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions +-- are met: +-- 1. Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- 2. Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- 3. Neither the name of the author nor the names of their contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +-- ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +-- SUCH DAMAGE. +-- +-- +-- + +local hooks = {} + +-- In order to centralise and ensure hook names are good, these +-- are the names of the hooks Gitano uses internally. +-- Plugins are at liberty to add their own hooks if they want, but +-- Gitano is unlikely to call them itself. + +local hook_names = { + -- Called by gitano.auth.is_authorized to allow manipulation of the + -- command line if wanted. Hook functions should take the form: + -- function preauth_cmdline_hook(cancel, config, ip, user, keytag, ...) + -- -- Decide what to do, if we're rejecting the command then set cancel + -- -- to something truthy such as: + -- return "stop", true, "Some reason" + -- -- otherwise try + -- return "continue" + -- -- or + -- return "update", cancel, config, ip, user, keytag, ... + -- end + PREAUTH_CMDLINE = "PREAUTH_CMDLINE", + -- Called by gitano-post-receive to allow processing to occur on the git + -- post-receive event if needed. The hook carries all the usual functions + -- as well as any registered by plugins. + -- Core admin stuff (running gitano-admin updates, update-server-info, etc) + -- runs at -1000. The supple hooks run at 0. Hook functions take the form: + -- function post_receive_hook(repo, updates) + -- -- Decide what to do. If we want to stop the hooks, return "stop" + -- -- but only do that if we MUST, since it will alter expected behaviour. + -- return "stop" + -- -- Otherwise, normally we'd just continue + -- return "continue" + -- -- Finally we can update if we want to alter the updates table + -- return "update", repo, different_updates + -- end + POST_RECEIVE = "POST_RECEIVE", +} + +local function _get_hook(hookname) + local ret = hooks[hookname] or {} + hooks[hookname] = ret + return ret +end + +local function _sort(hooktab) + table.sort(hooktab, function(a,b) return a[1] < b[1] end) +end + +local function add_to_hook(hookname, priority, func) + assert(hookname, "Cannot add to a nil hook") + assert(type(priority) == "number", "Cannot use a non-numerical priority") + assert(type(func) == "function", "Cannot use a non-function hook func") + local h = _get_hook(hookname) + h[#h+1] = {priority, func} +end + +local function _allbutone(_, ...) + return ... +end + +local function run_hook(hookname, ...) + assert(hookname, "Cannot run a nil hook") + local h = _get_hook(hookname) + _sort(h) + local args = {...} + for _, entry in ipairs(h) do + local result = { entry[2](unpack(args)) } + if result[1] == nil then + return unpack(result) + elseif type(result[1]) ~= "string" then + return nil, "Bad results", unpack(result) + elseif result[1] == "stop" then + return _allbutone(unpack(result)) + elseif result[1] == "update" then + args = {_allbutone(unpack(result))} + elseif result[1] == "continue" then + -- Nothing to do + else + return nil, "Bad results", unpack(result) + end + end + return unpack(args) +end + +-- Hook functions take the form: +-- action, ... = hookfunc(...) +-- where the ... is chained through, and returned verbatim at +-- the end. +-- action can be nil (on error) or else one of: +-- continue --> call the next hook function if there is one (not chaining ...) +-- update --> as for 'continue' but chaining the ... +-- stop --> stop now and return the rest of the results. + +-- Wherever Gitano registers a hook for something, the hook priority +-- of zero will be Gitano's action. So if you want to alter what is +-- passed to Gitano's default behaviour, register with a negative value +-- and if you want to just do more afterwards, register with a positive +-- value + +return { + add = add_to_hook, + run = run_hook, + names = hook_names, +} diff -Nru gitano-1.0/lib/gitano/lace.lua gitano-1.1/lib/gitano/lace.lua --- gitano-1.0/lib/gitano/lace.lua 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/lib/gitano/lace.lua 2017-08-03 15:11:42.000000000 +0000 @@ -35,6 +35,7 @@ local gall = require 'gall' local log = require 'gitano.log' local i18n = require 'gitano.i18n' +local pat = require 'gitano.patterns' local pcre = require "rex_pcre" @@ -45,7 +46,7 @@ } local function _loader(ctx, _name) - local global_name = _name:match("^global:(.+)$") + local global_name = _name:match(pat.LACE_GLOBAL_DEFINITION) local name, tree, sha = global_name or _name if not global_name then -- Project load diff -Nru gitano-1.0/lib/gitano/patterns.lua gitano-1.1/lib/gitano/patterns.lua --- gitano-1.0/lib/gitano/patterns.lua 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/lib/gitano/patterns.lua 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,74 @@ +-- gitano.patterns +-- +-- Centralised pattern definitions. Not stable ABI. +-- +-- Copyright 2017 Richard Maw +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions +-- are met: +-- 1. Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- 2. Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- 3. Neither the name of the author nor the names of their contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +-- ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +-- SUCH DAMAGE. +-- + +local _NICE_NAME = "[a-z][a-z0-9_.-]+" + +local USER_INFO_PREFIX = "^(users/.-)(".. _NICE_NAME .. ")/" +local GROUP_INFO_PREFIX = "^(groups/.-)(".. _NICE_NAME .. ")" +local KEYRING_INFO_PREFIX = "^(keyrings/.-)(".. _NICE_NAME .. ")" + +local CONF_ENDS_WILDCARD = "%.%*$" + +local GIT_REPO_SUFFIX = "%.git$" + +return { + TEXT_LINE = "([^\n]*)\n", + DOTFILE = "^%.", + VALID_USERNAME = "^" .. _NICE_NAME .. "$", + VALID_SSHKEYNAME = "^" .. _NICE_NAME .. "$", + USER_INFO_PREFIX = USER_INFO_PREFIX, + USER_CONF_MATCH = USER_INFO_PREFIX .. "user%.conf$", + USER_KEY_MATCH = USER_INFO_PREFIX .. "(" .. _NICE_NAME .. ")%.key$", + SSH_KEY_CONTENTS = "^([^ ]+) ([^ ]+) ([^ ].*)$", + VALID_GROUPNAME = "^" .. _NICE_NAME .. "$", + GROUP_INFO_PREFIX = GROUP_INFO_PREFIX, + GROUP_CONF_MATCH = GROUP_INFO_PREFIX .. "%.conf$", + VALID_KEY_FINGERPRINT = "^" .. string.rep("[0-9A-Fa-f]", 40) .. "$", + VALID_KEYRING_NAME = "^" .. _NICE_NAME .. "$", + KEYRING_INFO_PREFIX = KEYRING_INFO_PREFIX, + KEYRING_MATCH = KEYRING_INFO_PREFIX .. "%.gpg$", + GPG_OUTPUT_FINGERPRINT_MATCH = "fpr:::::::::([0-9A-F]+):", + HTTP_RESPONSE = "^HTTP/1.[01] (...) ?([^\r\n]+)\r?\n(.-)\r?\n\r?\n(.*)$", + HTTP_HEADER = "([^:\r\n]+): *([^\r\n]+)", + CONF_ENDS_WILDCARD = CONF_ENDS_WILDCARD, + CONF_WILDCARD = "^(.+)" .. CONF_ENDS_WILDCARD, + CONF_ARRAY_INDEX = "^(.+)%.i_[0-9]+$", + CONF_SET_TYPE_PREFIX = "^([sbi]):(.*)$", + GIT_REPO_SUFFIX = GIT_REPO_SUFFIX, + GIT_REPO_NAME_MATCH = "^(.+)" .. GIT_REPO_SUFFIX, + LACE_GLOBAL_DEFINITION = "^global:(.+)$", + PLUGIN_NAME = "^([^_]+)%.lua$", + REF_IS_NORMALISED = "^refs/", + PARSE_TIME_AND_TZOFFSET = "^([0-9]+) ([+-][0-9]+)$", + SUPPLE_MODULE_LOAD_MATCH = "^([^%.]+)%.(.+)$", + GITHOOK_PARSE_CHANGESET = "([^ ]+) ([^ ]+) ([^\n]+)\n?", +} diff -Nru gitano-1.0/lib/gitano/plugins.lua gitano-1.1/lib/gitano/plugins.lua --- gitano-1.0/lib/gitano/plugins.lua 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/lib/gitano/plugins.lua 2017-08-03 15:11:42.000000000 +0000 @@ -33,14 +33,13 @@ local util = require "gitano.util" local log = require "gitano.log" local i18n = require "gitano.i18n" +local pat = require "gitano.patterns" local luxio = require "luxio" local sio = require "luxio.simple" local gfind = string.gfind -local plugin_name_pattern = "^([^_]+)%.lua$" - local function find_plugins(path) local ret = {} for _, entry in ipairs(path) do @@ -50,7 +49,7 @@ {dir=entry, reason=err})) else for filename, fileinfo in dirp:iterate() do - local plugin_name = filename:match(plugin_name_pattern) + local plugin_name = filename:match(pat.PLUGIN_NAME) if plugin_name then if not ret[plugin_name] then ret[plugin_name] = entry diff -Nru gitano-1.0/lib/gitano/repository.lua gitano-1.1/lib/gitano/repository.lua --- gitano-1.0/lib/gitano/repository.lua 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/lib/gitano/repository.lua 2017-08-03 15:11:42.000000000 +0000 @@ -40,6 +40,7 @@ local util = require 'gitano.util' local lace = require 'gitano.lace' local i18n = require 'gitano.i18n' +local pat = require 'gitano.patterns' local clod = require 'clod' local base_rules = [[ @@ -455,8 +456,8 @@ if not self.is_nascent then local lists_to_add = {} for k, v in self.project_config:each() do - if k:match("%.i_[0-9]+$") then - lists_to_add[k:gsub("%.i_[0-9]+$", "")] = true + if k:match(pat.CONF_ARRAY_INDEX) then + lists_to_add[k:match(pat.CONF_ARRAY_INDEX)] = true else local confkey = "config/" .. k:gsub("%.", "/") context[confkey] = v @@ -489,6 +490,11 @@ self.git = r self.is_nascent = nil + r, msg = self.git:update_server_info() + if not r then + return false, msg + end + -- Finally, we're not nascent, validate the repo return self:run_checks() end @@ -507,7 +513,7 @@ end function repo_method:set_head(newhead, author, committer) - if not newhead:match("^refs/") then + if not newhead:match(pat.REF_IS_NORMALISED) then newhead = "refs/heads/" .. newhead end local oldhead = self:conf_get "project.head" @@ -563,15 +569,27 @@ return true end +local function normalise_repo_path(reponame) + -- Inject a leading '/' + reponame = "/" .. reponame + -- Remove any spaces, tabs, newlines or nulls + reponame = reponame:gsub("[%s%z]+", "") + -- Remove any '.' which follows a '/' + reponame = reponame:gsub("/%.+", "/") + -- simplify any sequence of '/' to a single '/' + reponame = reponame:gsub("/+", "/") + -- Remove any leading or trailing / + reponame = reponame:match("^/*(.-)/*$") + -- Remove trailing .git if present. + if reponame:match("."..pat.GIT_REPO_SUFFIX) then + reponame = reponame:match(pat.GIT_REPO_NAME_MATCH) + end + return reponame +end + function repo_method:rename_to(somename) -- Same cleanup as in find... - if somename:match(".%.git$") then - somename = somename:match("^(.+)%.git$") - end - -- Remove any '.' - somename = somename:gsub("%.", "") - -- Remove any leading or trailing / - somename = somename:match("^/*(.-)/*$") + somename = normalise_repo_path(somename) local newpath = self.fs_path({name=somename,config=self.config}) @@ -674,7 +692,7 @@ if f then local s = f:read("*l") if s then - local cur_mod_time, cur_mod_offset = s:find("^([0-9]+) ([+-][0-9]+)$") + local cur_mod_time, cur_mod_offset = s:find(pat.PARSE_TIME_AND_TZOFFSET) if cur_mod_time then update_based_on(cur_mod_time, cur_mod_offset) end @@ -825,20 +843,7 @@ -- -- If the repository exists, then it is examined and brought up-to-date -- with any global config changes before being returned. - -- Inject a leading '/' - reponame = "/" .. reponame - -- Remove any spaces, tabs, newlines or nulls - reponame = reponame:gsub("[%s%z]+", "") - -- Remove any '.' which follows a '/' - reponame = reponame:gsub("/%.+", "/") - -- simplify any sequence of '/' to a single '/' - reponame = reponame:gsub("/+", "/") - -- Remove any leading or trailing / - reponame = reponame:match("^/*(.-)/*$") - -- Remove trailing .git if present. - if reponame:match(".%.git$") then - reponame = reponame:match("^(.+)%.git$") - end + reponame = normalise_repo_path(reponame) -- Construct the repo local repo = setmetatable({config = config, name = reponame}, repo_meta) @@ -899,12 +904,12 @@ repeat e, i = luxio.readdir(dirp) if e == 0 then - if i.d_name:find("%.git$") then + if i.d_name:match(pat.GIT_REPO_SUFFIX) then -- Might be a repo, save for later all_repos[#all_repos+1] = (util.path_join(prefix, i.d_name) ):gsub("^/", "") else - if i.d_name:find("^[^%.]") then + if not i.d_name:match(pat.DOTFILE) then recurse[#recurse+1] = i.d_name end end diff -Nru gitano-1.0/lib/gitano/supple.lua gitano-1.1/lib/gitano/supple.lua --- gitano-1.0/lib/gitano/supple.lua 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/lib/gitano/supple.lua 2017-08-03 15:11:42.000000000 +0000 @@ -36,6 +36,7 @@ local log = require 'gitano.log' local config = require 'gitano.config' local i18n = require 'gitano.i18n' +local pat = require 'gitano.patterns' local repo_proxies = {} local proxied_repo = {} @@ -112,7 +113,7 @@ end local function load_module_src(modname) - local pfx, mod = modname:match("^([^%.]+)%.(.+)$") + local pfx, mod = modname:match(pat.SUPPLE_MODULE_LOAD_MATCH) if not (pfx and mod) then error(i18n.expand("ERROR_NOT_RIGHT_NAME_FORMAT", {modname=modname})) end diff -Nru gitano-1.0/lib/gitano/usercommand.lua gitano-1.1/lib/gitano/usercommand.lua --- gitano-1.0/lib/gitano/usercommand.lua 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/lib/gitano/usercommand.lua 2017-08-03 15:11:42.000000000 +0000 @@ -34,6 +34,7 @@ local util = require 'gitano.util' local repository = require 'gitano.repository' local config = require 'gitano.config' +local pat = require 'gitano.patterns' local sio = require 'luxio.simple' local subprocess = require 'luxio.subprocess' @@ -155,7 +156,7 @@ log.error("sshkey", cmdline[2] .. ": Expected tag and no more") return false end - if not cmdline[3]:match("^[a-z][a-z0-9_-]+$") then + if not cmdline[3]:match(pat.VALID_SSHKEYNAME) then log.error("sshkey:", cmdline[3], "is not a valid tag name.") log.state("Tag names start with a letter and may contain only letters") log.state("and numbers, underscores and dashes. Tag names must be at") @@ -211,7 +212,7 @@ end elseif cmdline[2] == "add" then local sshkey = sio.stdin:read("*l") - local keytype, keydata, keytag = sshkey:match("^([^ ]+) ([^ ]+) ([^ ].*)$") + local keytype, keydata, keytag = sshkey:match(pat.SSH_KEY_CONTENTS) if not (keytype and keydata and keytag) then log.error("Unable to parse key,", filename, "did not smell like an OpenSSH v2 key") @@ -281,6 +282,7 @@ local function update_htpasswd(user, passwd) local htpasswd_path = os.getenv("HOME") .. "/htpasswd" + local lock = util.lockfile(htpasswd_path .. ".lock") local flags = io.open(htpasswd_path, "r") and "-i" or "-ic" local exit_code @@ -302,7 +304,7 @@ _, exit_code = proc:wait() end - + util.unlockfile(lock) return exit_code == 0 end diff -Nru gitano-1.0/lib/gitano/util.lua gitano-1.1/lib/gitano/util.lua --- gitano-1.0/lib/gitano/util.lua 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/lib/gitano/util.lua 2017-08-03 15:11:42.000000000 +0000 @@ -569,6 +569,21 @@ return fd, temppattern end +local function lockfile(path) + local fh = assert(sio.open(path, "cw+")) + local ok, msg = fh:lock("w", "set", 0, 0, true, false) + if not ok then + fh:close() + error(msg) + end + return fh +end + +local function unlockfile(fh) + fh:lock("", "set", 0, 0, true, false) + fh:close() +end + return { parse_cmdline = _parse_cmdline, @@ -605,4 +620,7 @@ check_password = check_password, run_command = run_command, + + lockfile = lockfile, + unlockfile = unlockfile, } diff -Nru gitano-1.0/lib/gitano.lua gitano-1.1/lib/gitano.lua --- gitano-1.0/lib/gitano.lua 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/lib/gitano.lua 2017-08-03 15:11:42.000000000 +0000 @@ -41,8 +41,10 @@ local auth = require 'gitano.auth' local plugins = require 'gitano.plugins' local i18n = require 'gitano.i18n' +local patterns = require 'gitano.patterns' +local hooks = require 'gitano.hooks' -local _VERSION = {1, 0, 0} +local _VERSION = {1, 1, 0} _VERSION.major = _VERSION[1] _VERSION.minor = _VERSION[2] _VERSION.patch = _VERSION[3] @@ -68,4 +70,6 @@ auth = auth, plugins = plugins, i18n = i18n, + patterns = patterns, + hooks = hooks, } diff -Nru gitano-1.0/.luacov gitano-1.1/.luacov --- gitano-1.0/.luacov 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/.luacov 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,23 @@ +-- -*- Lua -*- +return { + statsfile = "luacov.stats.out", + reportfile = "luacov.report.out", + tick = false, + savestepsize = 100, + runreport = false, + deletestats = false, + codefromstrings = false, + include = { }, + exclude = { + "^/usr/share", + "%.conf$", + "%.clod$", + "clod%-config", + "supple%-transfer", + "lang/[^/]+$", + "lib/gitano$", + "plugins/demo$", + "gitano/coverage$", + }, + modules = {}, +} diff -Nru gitano-1.0/Makefile gitano-1.1/Makefile --- gitano-1.0/Makefile 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/Makefile 2017-08-03 15:11:42.000000000 +0000 @@ -32,14 +32,15 @@ TEST_BIN_NAMES := gitano-test-tool -TESTS := 01-basics 02-commands-as 02-commands-config 02-commands-copy \ - 02-commands-count-objects 02-commands-create 02-commands-destroy \ - 02-commands-fsck 02-commands-gc 02-commands-graveyard \ - 02-commands-git-upload-archive 02-commands-group 02-commands-help \ - 02-commands-keyring \ - 02-commands-ls 02-commands-rename 02-commands-rsync \ - 02-commands-sshkey 02-commands-user 02-commands-whoami 03-cgit-support \ - 03-shallow-push +TESTS := 01-basics 01-hooks 02-commands-as 02-commands-config 02-commands-copy \ + 02-commands-count-objects 02-commands-create 02-commands-destroy \ + 02-commands-fsck 02-commands-gc 02-commands-graveyard \ + 02-commands-git-upload-archive 02-commands-group 02-commands-help \ + 02-commands-keyring \ + 02-commands-ls 02-commands-rename 02-commands-rsync \ + 02-commands-sshkey 02-commands-user 02-commands-whoami 03-cgit-support \ + 03-shallow-push 03-treedelta-rules 03-force-pushing \ + 03-dangling-HEAD 03-config-user-whitelist MODS := gitano \ @@ -49,7 +50,7 @@ gitano.repository gitano.supple \ gitano.command gitano.admincommand gitano.usercommand \ gitano.repocommand gitano.copycommand gitano.auth gitano.plugins\ - gitano.i18n + gitano.i18n gitano.hooks gitano.patterns SKEL_FILES := gitano-admin/rules/selfchecks.lace \ gitano-admin/rules/aschecks.lace \ @@ -61,7 +62,8 @@ gitano-admin/rules/defines.lace \ gitano-admin/rules/project.lace \ gitano-admin/rules/adminchecks.lace \ - gitano-admin/rules/createrepo.lace + gitano-admin/rules/createrepo.lace \ + gitano-admin/rules/simpleprojectauth.lace LANG_FILES := json.lua en.lua @@ -70,6 +72,18 @@ PLUGINS := rsync.lua archive.lua git-annex.lua +TEST_PLUGINS := testing-hooks.lua demo.lua + +UNSUPPORTED_PLUGINS := git-multimail.lua + +COVERAGE := no + +ifeq ($(COVERAGE),yes) + +MODS := $(MODS) gitano.coverage + +endif + MOD_DIRS := gitano MOD_FILES := $(patsubst %,%.lua,$(subst .,/,$(MODS))) SRC_MOD_FILES := $(patsubst %,lib/%,$(MOD_FILES)) @@ -86,20 +100,59 @@ GEN_BIN := utils/install-lua-bin RUN_GEN_BIN := $(LUA) $(GEN_BIN) $(LUA) + +ifeq ($(COVERAGE),yes) +define GEN_LOCAL_BIN + +$(RUN_GEN_BIN) $(shell pwd) $(shell pwd)/bin $(shell pwd)/lib $(shell pwd)/plugins $1 $2 $(shell pwd)/extras/luacov/src $(shell pwd)/.coverage/ +chmod 755 $2 + +endef +ifeq ($(COVER_GTT),yes) +define GEN_LOCAL_TESTING_BIN + +$(RUN_GEN_BIN) $(shell pwd)/testing/inst/share/gitano $(shell pwd)/testing/inst/lib/gitano/bin $(shell pwd)/testing/inst/share/lua/5.1 $(shell pwd)/testing/inst/lib/gitano/plugins $1 $2 $(shell pwd)/extras/luacov/src $(shell pwd)/.coverage/ +chmod 755 $2 + +endef +else +define GEN_LOCAL_TESTING_BIN + +$(RUN_GEN_BIN) $(shell pwd)/testing/inst/share/gitano $(shell pwd)/testing/inst/lib/gitano/bin $(shell pwd)/testing/inst/share/lua/5.1 $(shell pwd)/testing/inst/lib/gitano/plugins $1 $2 +chmod 755 $2 + +endef +endif +else define GEN_LOCAL_BIN $(RUN_GEN_BIN) $(shell pwd) $(shell pwd)/bin $(shell pwd)/lib $(shell pwd)/plugins $1 $2 chmod 755 $2 endef +define GEN_LOCAL_TESTING_BIN + +$(RUN_GEN_BIN) $(shell pwd)/testing/inst/share/gitano $(shell pwd)/testing/inst/lib/gitano/bin $(shell pwd)/testing/inst/share/lua/5.1 $(shell pwd)/testing/inst/lib/gitano/plugins $1 $2 +chmod 755 $2 +endef +endif + +ifeq ($(COVERAGE),yes) define GEN_INSTALL_BIN -$(RUN_GEN_BIN) $(SHARE_PATH) $(LIB_BIN_PATH) $(LUA_MOD_PATH) $(PLUGIN_PATH) $1 $2 +$(RUN_GEN_BIN) $(SHARE_PATH) $(LIB_BIN_PATH) $(LUA_MOD_PATH) $(PLUGIN_PATH) $1 $2 $(shell pwd)/extras/luacov/src $(shell pwd)/.coverage/ chmod 755 $2 endef +else +define GEN_INSTALL_BIN +$(RUN_GEN_BIN) $(SHARE_PATH) $(LIB_BIN_PATH) $(LUA_MOD_PATH) $(PLUGIN_PATH) $1 $2 +chmod 755 $2 + +endef +endif define GEN_INSTALL_MAN cp $1 $2 @@ -113,23 +166,53 @@ endef -local: $(LOCAL_BINS) - $(LUAC) -p $(LOCAL_BINS) +define GEN_LOCAL_MOD + +$(RUN_GEN_BIN) $(shell pwd) $(shell pwd)/bin $(shell pwd)/lib $(shell pwd)/plugins $1 $2 -clean: - @echo "CLEAN: local binaries" - @$(RM) $(LOCAL_BINS) +endef + +LOCAL_MODS := lib/gitano/coverage.lua + +local: $(LOCAL_BINS) $(LOCAL_MODS) + @$(LUAC) -p $(LOCAL_BINS) $(LOCAL_MODS) + @mkdir -p .coverage +ifeq ($(COVERAGE),yes) + @echo NOTE: Coverage gathering enabled + @if ! test -r extras/luacov/src/luacov; then \ + echo "CANNOT COVERAGE TEST, LUACOV MODULE NOT AVAILABLE"; \ + echo "Please run: git submodule init && git submodule update"; \ + fi +else + @echo NOTE: Coverage gathering is not enabled +endif + +clean: cleanbins + @echo "CLEAN: coverage stats and reports" + @$(RM) -r .coverage luacov.stats.out luacov.report.out + +cleanbins: + @echo "CLEAN: local binaries and modules" + @$(RM) $(LOCAL_BINS) $(LOCAL_MODS) @echo "CLEAN: test binaries" @$(RM) $(TEST_BINS) + @echo "CLEAN: test install" + @$(RM) -r testing/inst distclean: clean @find . -name "*~" -delete -bin/%: bin/%.in $(GEN_BIN) - $(call GEN_LOCAL_BIN,$<,$@) +bin/%: bin/%.in $(GEN_BIN) FORCE + @$(call GEN_LOCAL_BIN,$<,$@) -testing/%: testing/%.in $(GEN_BIN) - $(call GEN_LOCAL_BIN,$<,$@) +testing/%: testing/%.in $(GEN_BIN) FORCE + @$(call GEN_LOCAL_TESTING_BIN,$<,$@) + +lib/gitano/%.lua: lib/gitano/%.lua.in $(GEN_BIN) FORCE + @$(call GEN_LOCAL_MOD,$<,$@) + +FORCE: +.PHONY: FORCE install: install-bins install-lib-bins install-mods install-skel install-man install-plugins install-lang @@ -196,13 +279,22 @@ HTTP_FIRST_TEST_PORT = 8080 endif +test-install: + @$(RM) -r testing/inst + @$(MAKE) PREFIX="$(shell pwd)/testing/inst" \ + SYSCONF_DIR="$(shell pwd)/testing/inst/etc" \ + install >/dev/null + @for P in $(TEST_PLUGINS); do \ + cp plugins/$$P "$(shell pwd)/testing/inst/etc/gitano/plugins/"; \ + done + plugin-check: - $(LUAC) -p $(patsubst %,plugins/%,$(PLUGINS)) - for PLUGIN in $(patsubst %,plugins/%,$(PLUGINS)); do \ + $(LUAC) -p $(patsubst %,plugins/%,$(PLUGINS) $(TEST_PLUGINS) $(UNSUPPORTED_PLUGINS)) + for PLUGIN in $(patsubst %,plugins/%,$(PLUGINS) $(TEST_PLUGINS)); do \ env LUA_PATH="$(shell pwd)/lib/?.lua;;" $(LUA) $$PLUGIN; \ done -basictest: local plugin-check $(TEST_BINS) +basictest: local plugin-check $(TEST_BINS) test-install @echo "Running basic yarns in '$(TEST_PROTO)' mode" @echo "Set TEST_PROTO if you want to change that" @$(YARN) \ @@ -214,7 +306,7 @@ test: local plugin-check sshtests httptests -sshtests: $(TEST_BINS) +sshtests: $(TEST_BINS) test-install @echo "Running full yarns in 'ssh' mode" @$(YARN) \ --env GTT="$$(pwd)/testing/gitano-test-tool" \ @@ -224,7 +316,7 @@ testing/library.yarn $(TESTS) ifeq ($(SKIP_HTTP_TESTS),) -httptests: $(TEST_BINS) +httptests: $(TEST_BINS) test-install @echo "Running full yarns in 'http' mode" @$(YARN) \ --env GTT="$$(pwd)/testing/gitano-test-tool" \ @@ -238,7 +330,12 @@ @echo "WARNING: Cannot guarantee Gitano will work in HTTP mode." endif -testing/%: testing/%.in $(GEN_BIN) - $(call GEN_LOCAL_BIN,$<,$@) - check: test + +coverage-report: + @echo "COVERAGE: Merge stats..." + @env LUA_PATH="$(shell pwd)/extras/luacov/src/?.lua;;" $(LUA) utils/merge-luacov-stats .coverage/luacov.stats-*.out + @echo "COVERAGE: Generate report..." + @env LUA_PATH="$(shell pwd)/extras/luacov/src/?.lua;;" $(LUA) extras/luacov/src/bin/luacov + @echo "COVERAGE: Summary:" + @sed '0,/^Summary/d' < luacov.report.out | tail -n +2 | sed -e's/^/COVERAGE: /' diff -Nru gitano-1.0/NEWS gitano-1.1/NEWS --- gitano-1.0/NEWS 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/NEWS 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,39 @@ +News for Gitano +============= + +This document lists important things to know about changes to Gitano between +one release and the next. It is arranged in reverse-version-order, with the +newest NEWS at the top. + + +Changes since v1.0 +------------------ + +* A set of rules were added to the default ruleset which supports a simple + form of delegated access control by means of two lists in the project + configuration. This is currently not considered a core behaviour and + as such is subject to change. Once it is considered core, it will be + documented in the admin manual. +* A (currently unsupported) plugin for git_multimail.py integration has been + added. It is not installed by default and currently carries a separate + README alongside it. Once it is stabilised and considered core, it will + be documented in the admin manual. +* Further Yarn scenarios were added to support verification of behaviour from + 1.0 +* The test suite was improved to test HTTP more thoroughly, resulting in a + number of small tweaks and improvements being made. +* LUA_INIT is now passed through in the test suite which means increased + portability for testing on targets such as Nix. +* A 'Hook' concept has been added to Gitano's core. It is not considered + stable API at this point. This is separate from Git hooks and is an internal + implementation detail for allowing plugins to hook into certain events. +* Repositories will now be re-owned when renaming a user. +* Documentation was updated regarding 'Git hooks' +* `server-info` files are now created when repositories are created, ensuring + that the HTTP interface will work on brand new repos +* `gitano-setup` now supports reading an answer file from stdin (good for + automating setups) +* `gitano-setup` now reads all answer files supplied (this was a bug, as + previously we only read the first one) +* Fix a bug in the HTTP supple action to supply a content type in POST. +* Added a NEWS file diff -Nru gitano-1.0/plugins/git-multimail.lua gitano-1.1/plugins/git-multimail.lua --- gitano-1.0/plugins/git-multimail.lua 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/plugins/git-multimail.lua 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,220 @@ +-- Git Multimail Plugin +-- +-- This plugin enables support for git-multimail on the server-side which +-- allows for the nice(r) reporting of changes by email. +-- +-- Copyright 2017 Daniel Silverstone +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions +-- are met: +-- 1. Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- 2. Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- 3. Neither the name of the author nor the names of their contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +-- ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +-- SUCH DAMAGE. + +local gitano = require "gitano" + +local luxio = require "luxio" +local sp = require "luxio.subprocess" + +local git_multimail_py = nil + +do + local args = { + "git", "config", "--path", "gitano.multimail.location", + stdout = sp.PIPE, + cwd = luxio.getenv("HOME"), + } + local proc = sp.spawn_simple(args) + git_multimail_py = proc.stdout:read("*l") + local how, why = proc:wait() + if how ~= "exit" then + return -- don't register, something really went wrong + end + if why ~= 0 then + git_multimail_py = nil + end +end + +local GLOBAL_CONFS = { + environment = "generic", + refFilterExclusionRegex = "^refs/gitano/", + verbose = "true", +} + +local function run_multimail(confbits, env, stdin) + local args = { + env = env, + stdin = sp.PIPE, + stdout = sp.PIPE, + stderr = sp.PIPE, + git_multimail_py, + } + for k, v in pairs(confbits) do + args[#args+1] = "-c" + args[#args+1] = "multimailhook." .. k .. "=" .. v + end + gitano.log.ddebug("About to spawn git_multimail.py as...") + for _, v in ipairs(args) do + gitano.log.ddebug("=> " .. v) + end + local proc = sp.spawn_simple(args) + proc.stdin:write(stdin) + proc.stdin:close() + local outcontent = proc.stdout:read("*a") + local errcontent = proc.stderr:read("*a") + local how, why = proc:wait() + local function show(what, pfx) + if what == "" then return end + if what:sub(-1) ~= "\n" then what = what .. "\n" end + for line in what:gfind("([^\n]*)\n") do + gitano.log.state(pfx .. ": " .. line) + end + end + if outcontent ~= "" or errcontent ~= "" then + gitano.log.state("Git Multimail says:") + show(outcontent, "O") + show(errcontent, "E") + end + if (how ~= "exit") or (why ~= 0) then + gitano.log.error("git_multimail.py failed: " .. tostring(how) .. " " .. tostring(why)) + end + gitano.log.ddebug("Done with multimail") +end + +local function git_multimail_post_receive_hook(repo, updates) + -- We don't get here at all if we don't have git_multimail.py's location + -- as such, our first job is to request authorisation from the ACL + local context = { + user = "gitano/multimail", + source = "plugin_multimail", + operation = "multimail", + } + gitano.log.ddebug("Multimail: Asking " .. repo.name .. " if multimail is allowed...") + local action, reason = repo:run_lace(context) + gitano.log.ddebug(("Multimail: action=%q reason=%q"):format(action,reason)) + if action ~= "allow" then + gitano.log.info("Multimail disallowed: " .. tostring(action) .. ": " .. tostring(reason)) + return "continue" + end + + -- Okay, we're allowed to run, so let's build our config... + local confbits = gitano.util.deep_copy(GLOBAL_CONFS) + local conf = repo.config + local username = luxio.getenv("GITANO_USER") + local user = conf.users[username] or { + real_name = "Anonymous", + email_address = "postmaster@localhost.localdomain" + } + -- And we need to set 'from' + confbits.from = user.real_name .. " <" .. user.email_address .. ">" + + local function layer_confbits(fromrepo, pfx) + gitano.log.ddebug("Multimail: Attempting to layer from " .. tostring(fromrepo.name) .. " via prefix " .. pfx) + -- Note this is currently an unstable API + local from = fromrepo.project_config + for key, value in from:each(pfx) do + key = key:sub(#pfx+2,-1) + gitano.log.ddebug(("Multimail: => setting %q to %q"):format(key,value)) + confbits[key] = tostring(value) + end + end + local gitano_admin_repo = ( + gitano.repository.find(repo.config, "gitano-admin.git")) + layer_confbits(gitano_admin_repo, "defaults.multimail") + layer_confbits(repo, "multimail") + layer_confbits(gitano_admin_repo, "override.multimail") + + -- Now we run a supple hook (if necessary) which will be able to adjust + -- the config bits... + if repo:uses_hook("multimail") then + gitano.log.debug("Multimail: Configuring supple for hook") + gitano.actions.set_supple_globals("multimail") + gitano.log.info("Multimail: Running hook") + local info = { + username = username, + source = "plugin_multimail", + realname = user.real_name or "", + email = user.email_address or "", + } + local ok, msg = gitano.supple.run_hook("multimail", repo, info, updates, confbits) + if not ok then + gitano.log.error("Multimail: Hook failed: " .. tostring(msg)) + gitano.log.error("Multimail: Aborting send") + return + end + gitano.log.info("Multimail: Finished running hook, continuing...") + end + + -- Next we clean up the config bits, by copying over only what we whitelist + local unfiltered = confbits + confbits = {} + for _, key in ipairs( + { + "mailingList", "refchangeList", "announceList", "commitList", + "from", + "announceShortLog", "commitBrowseURL", "refchangeShowGraph", + "refchangeShowLog", "administrator", "emailPrefix", + "refFilterInclusionRegex", "refFilterExclusionRegex", + "refFilterDoSendRegex", "refFilterDontSendRegex", + "verbose", "stdout", + }) do + local val = unfiltered[key] + if val and val ~= "" then + gitano.log.ddebug( + ("Multimail: Permitted %q => %q"):format(key, unfiltered[key])) + confbits[key] = unfiltered[key] + end + end + + -- No matter what goes on in the clod configs, we need to set repoName + confbits.repoName = repo.name + + local fullusername = user.real_name .. " (" .. username .. ")" + local env = { + USERNAME = fullusername, + USER = fullusername, + } + + -- Prepare the stdin for git_multimail.py, in post-receive format + local stdin = {} + for ref, chg in pairs(updates) do + stdin[#stdin+1] = table.concat({chg.oldsha, chg.newsha, ref}, " ") + end + stdin = table.concat(stdin, "\n") .. "\n" + + -- Finally, if there's any address set at all, run multimail + if (confbits.mailingList or + confbits.announceList or + confbits.commitList or + confbits.refchangeList) then + run_multimail(confbits, env, stdin) + else + gitano.log.info("Multimail: Did not run, no target addresses set") + end + + return "continue" +end + +if git_multimail_py ~= nil and luxio.getenv("GITANO_USER") ~= "gitano/bypass" then + gitano.hooks.add(gitano.hooks.names.POST_RECEIVE, + 100, git_multimail_post_receive_hook) +end diff -Nru gitano-1.0/plugins/README.multimail gitano-1.1/plugins/README.multimail --- gitano-1.0/plugins/README.multimail 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/plugins/README.multimail 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,161 @@ +Using Git Multimail with Gitano +=============================== + +Gitano provides a plugin for use with `git_multimail.py`. In order to use it +a number of preparatory steps must be taken. + +Sysadmin responsibilities +------------------------- + +In order to enable the multimail capability in Gitano the plugin must be +installed (it will be, by default, but some distributions may put it in a +separate package). Also there must be a copy of `git_multimail.py` on the +system somewhere. It may be available in the contrib part of the distribution +Git package, otherwise it can be acquired from: + + https://git.kernel.org/pub/scm/git/git.git/plain/contrib/hooks/multimail/git_multimail.py?id=HEAD + +The sysadmin must become the unix user that Gitano is running as, and run the +following command to enable the plugin `git_multimail.py` + + $ git config --global gitano.multimail.location /path/to/wherever/git_multimail.py + +Without this setting, the multimail plugin will be entirely disabled. The +actual `git_multimail.py` must be executable and must have any dependencies met +on the system. + +In addition, the sysadmin will want to configure things like +`multimailhook.mailer` and associated settings such that `git_multimail.py` +will be able to send emails out. + +Gitano-Admin responsibilities +----------------------------- + +### Rules + +Once the multimail plugin is permitted to run for the site, it also needs to +use the Gitano ACL system to check for permission to run on the given +repository. Note that control over which refs it runs for is independent of +this ACL check. + +In order to check for permission, the multimail plugin will request, of the +repository in question, an ACL check of the form: + +* `source`: `plugin_multimail` +* `repository`: `path/to/repo` +* `operation`: `multimail` +* `user`: `gitano/multimail` +* The usual `config/` etc are available too + +If that ACL check results in an 'allow' then the multimail plugin will run +the `git_multimail.py` identified in the Git config from above. + +If admins do not wish to grant the capability to all repositories, they might +consider a rule (before the `include main` in `core.lace`) along the lines of: + + deny "Repositories may not authorise multimail" [operation is multimail] + +More complex rule examples may be found on the Gitano wiki. + +It's also worth noting that if you are running your instance primarily from the +gitano-admin user (which is bad and wrong, but some people do do it) then you +will need to add any denial rules very early since otherwise the allow +"Administrators can do anything" will kick in and could cause unexpected mails +to be sent. + +### Config + +In order to permit a further level of control and flexibility for the multimail +plugin, configuration is read from four places and combined in order to create +the configuration which will be passed to `git_multimail.py`. + +Firstly there are some defaults in the multimail plugin. These are there to +mostly prepare `git_multimail.py` for the fact that it is running under Gitano. + +Secondly, the `project.conf` of `gitano-admin` is consulted and anything in +a `defaults.multimail.` prefix is copied into the configuration, overriding any +defaults from the plugin itself. + +Thirdly, the `project.conf` of the repository in question is consulted, and +anything in the `multimail.` prefix is copied into the configuration, +overriding the previous two. + +Finally, the `project.conf` of `gitano-admin` is consulted a second time, and +anything in a `override.multimail.` prefix is copied into the configuration, +permitting the gitano site administrator to override anything they choose. + +This combined configuration is then passed in as additional configuration items +when `git_multimail.py` is invoked. In order to protect against wrongdoings +and potential attack vectors, the names of the configuration items (and note +the case, since that's important) which will be honoured by the config are: + +* `mailingList`: the default location for all emails unless overridden +* `refchangeList`: the location to which reference change summaries are sent +* `announceList`: the location for tag notification to be sent +* `commitList`: the location where actual commit diffs are sent + +* `from`: the address from which the mails will be sent. This takes content + of the form `Real Name ` and defaults to the real name and + email address of the entity pushing to the Gitano server. + +* `announceShortLog`: if set to `true` this means announce mails include a + short log of changes +* `commitBrowseURL`: The url format string which will be passed for configuring + the email bodies to include a URL to the commit. This is of the form + `https://some.server/somewhere/%(id)s` If the string contains the sequence + `%(repo)s` then the repository path will be substituted, allowing for site + defaults if there is a site cgit. +* `refchangeShowGraph`: if set to `true`, show the git graph in the ref change + summary mail +* `refchangeShowLog`: if set to `true`, show a patch log in the ref change + summary mail +* `administrator`: if set, this will go into the footer as the administrator + of the site. +* `emailPrefix`: if set, is a summary prefix. Using `%(repo_shortname)s` will + use a shortened repository name automatically +* `refFilterInclusionRegex`, `refFilterExclusionRegex`, `refFilterDoSendRegex`, + `refFilterDontSendRegex`: These configure the refs which `git_multimail.py` + will be sensitive to. + +* `verbose`: if set to `true` will cause `git_multimail.py` to be somewhat + verbose and say what it's up to. +* `stdout`: if set to `true`, will send the content to the output (returned to + the person pushing) instead of emailing it, so that it can be checked prior + to enabling the feature properly + +In general, see `git_multimail.py`'s documentation for further information on +the above options. + +The multimail configuration is structured as above to allow maximum flexibility +combined with the ability to control almost all aspects of the multimail +behaviour with the 'config' command (which can be ACL filtered and delegated). + +Regardless of any attempts to set it in config, the `repoName` configuration +value will be set by the plugin last and thus are not overridable. + +Repo owner responsibilities +--------------------------- + +If the gitano-admin has delegated permission to switch multimail on and off, +then the repo owner must set a Lace rule to allow it. + +In addition, any configuration for multimail which is necessary for the repo +should be set in the configuration for it. + +If nowhere sets any of the address options, then the multimail plugin will not +invoke `git_multimail.py` at all. + +Hooks +----- + +Just before the multimail plugin applies the above whitelist to the +configuration data, it will run a hook called 'multimail' whose API is: + + local repo, updates, confbits = ... + -- make changes to confbits which is a key/value table of configs + +If the hook fails then the plugin will abort. If the hook alters the +`confbits` table then the changes will be reflected in the invocation of +`git_multimail.py` and if the plugin alters the updates table, then those +changes will be reflected also. As such, the hook can choose to erase some +ref changes from the invocation if they so choose. diff -Nru gitano-1.0/plugins/testing-hooks.lua gitano-1.1/plugins/testing-hooks.lua --- gitano-1.0/plugins/testing-hooks.lua 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/plugins/testing-hooks.lua 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,76 @@ +-- Plugin for testing hooks +-- +-- This is a testing plugin which will not be installed as part of +-- Gitano. Its purpose is to allow the test suite to verify various parts +-- of the hooking system in Gitano. +-- +-- Copyright 2017 Daniel Silverstone +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions +-- are met: +-- 1. Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- 2. Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- 3. Neither the name of the author nor the names of their contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +-- ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +-- SUCH DAMAGE. + +local gitano = require "gitano" + +local function _check(what, hookname) + local aborts = "," .. (os.getenv("HOOK_"..what) or "") .. "," + return (aborts:find("," .. hookname .. ",")) +end + +local function _abort(...) return _check("ABORT", ...) end +local function _decline(...) return _check("DECLINE", ...) end + +-- This function allows us to verify behaviour of the preauth_cmdline hook +local function preauth_cmdline_hookfunc(cancel, ip, user, keytag, cmd, ...) + if _abort("PREAUTH_CMDLINE") then + return nil, "Aborted on request" + end + if _decline("PREAUTH_CMDLINE") then + return "stop", true, "Declined on request" + end + if os.getenv("PREAUTH_CMDLINE_REMOVEME") and cmd == "removeme" then + return "update", cancel, ip, user, keytag, ... + end + return "continue" +end + +gitano.hooks.add(gitano.hooks.names.PREAUTH_CMDLINE, + 10, preauth_cmdline_hookfunc) + +-- This function is meant to allow us to check that the POST_RECEIVE hook works +local function post_receive_hookfunc(repo, updates) + if _abort("POST_RECEIVE") then + return nil, "Aborted on request" + end + if _decline("POST_RECEIVE") then + io.stdout:write("HOOKFUNC_STOPPED") + io.stdout:flush() + return "stop" + end + return "continue" +end + +-- We deliberately install the hook between core behaviour and supple +gitano.hooks.add(gitano.hooks.names.POST_RECEIVE, + -100, post_receive_hookfunc) diff -Nru gitano-1.0/skel/gitano-admin/rules/core.lace gitano-1.1/skel/gitano-admin/rules/core.lace --- gitano-1.0/skel/gitano-admin/rules/core.lace 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/skel/gitano-admin/rules/core.lace 2017-08-03 15:11:42.000000000 +0000 @@ -62,6 +62,11 @@ # Now the project rules themselves include main +# Allow looking up whether the user is permitted in repository config. +# To prevent repositories from overriding project.{readers,writers} behaviour +# uncomment the code below and comment or remove the code in project.lace +#include global:simpleprojectauth + # Now, if you want to allow anonymous access if the project doesn't prevent # it, then you can uncomment the following: # allow "Anonymous access is okay" op_read !is_admin_repo diff -Nru gitano-1.0/skel/gitano-admin/rules/defines.lace gitano-1.1/skel/gitano-admin/rules/defines.lace --- gitano-1.0/skel/gitano-admin/rules/defines.lace 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/skel/gitano-admin/rules/defines.lace 2017-08-03 15:11:42.000000000 +0000 @@ -112,3 +112,7 @@ define is_admin_repo repository exact gitano-admin define is_gitano_ref ref prefix refs/gitano/ define is_admin_ref ref exact refs/gitano/admin + +# Project readers and writers +define is_project_reader config/project/readers exact ${user} +define is_project_writer config/project/writers exact ${user} diff -Nru gitano-1.0/skel/gitano-admin/rules/project.lace gitano-1.1/skel/gitano-admin/rules/project.lace --- gitano-1.0/skel/gitano-admin/rules/project.lace 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/skel/gitano-admin/rules/project.lace 2017-08-03 15:11:42.000000000 +0000 @@ -28,6 +28,11 @@ # # Core project administration rules +# Allow looking up whether the user is permitted in repository config. +# To permit repositories to override project.{readers,writers} behaviour +# comment out or delete the code below and uncomment the code in core.lace +include global:simpleprojectauth + # Admins already got allowed, so this is for non-admin users only allow "Owners can always read and write" op_is_basic is_owner diff -Nru gitano-1.0/skel/gitano-admin/rules/simpleprojectauth.lace gitano-1.1/skel/gitano-admin/rules/simpleprojectauth.lace --- gitano-1.0/skel/gitano-admin/rules/simpleprojectauth.lace 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/skel/gitano-admin/rules/simpleprojectauth.lace 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,55 @@ +# This file is part of the standard ruleset from Gitano +# Copyright 2017 Richard Maw +# Copyright 2017 Richard Ipsum +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the author nor the names of their contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# Simple deferred project authorisation +# +# This implements simple project authorisation +# by whether the user performing the access is listed in the per-repository +# configuration variables: +# +# 1. project.readers if they are performing a read operation (clone, fetch), +# 2. project.writers if they are performing a write operation (push). +# +# This is intended as a sane default for sites with small numbers of users +# and repositories, where it's feasible to authorise each repository by hand. +# +# For larger numbers of repositories and users, +# delegating permission to manage groups to users, +# and letting users grant access to repositories by group membership is better. +# +# It can be enabled before main.lace (inside gitano-admin rules/project.lace) +# to require all repositories support config variable based authorisation, +# or after main.lace (inside gitano-admin rules/core.lace) +# if projects may insist on interpreting the variables differently. + +allow "User is project reader" op_read is_project_reader + +allow "User is project writer" op_read is_project_writer +allow "User is project writer" op_write is_project_writer +allow "User is project writer" op_is_normal is_project_writer diff -Nru gitano-1.0/testing/01-hooks.yarn gitano-1.1/testing/01-hooks.yarn --- gitano-1.0/testing/01-hooks.yarn 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/01-hooks.yarn 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,74 @@ + +Basic hook support tests +======================== + +In these tests we verify the various hooks function at some basic level. + +For example, we check that we can abort some of the hooks and that we can +alter behaviour or add behaviour to certain hooks which might commonly be +used for the sorts of things plugins want. + +Preauthorization commandline hook +--------------------------------- + +The preauth_cmdline hook is used to allow plugins to adjust (or reject) the +parsed command line before Gitano even looks up what command it might be for. +This could be used to add aliases for certain commands, or just stop things +from happening... + + SCENARIO preauth_cmdline can be manipulated + ASSUMING gitano is being accessed over ssh + GIVEN a standard instance + + WHEN testinstance adminkey runs ls + THEN stdout contains gitano-admin + + GIVEN HOOK_ABORT is in the environment set to PREAUTH_CMDLINE + + WHEN testinstance adminkey, expecting failure, runs ls + THEN stderr contains Aborted on request + + GIVEN HOOK_ABORT is not in the environment + AND HOOK_DECLINE is in the environment set to PREAUTH_CMDLINE + + WHEN testinstance adminkey, expecting failure, runs ls + THEN stderr contains Declined on request + + GIVEN HOOK_DECLINE is not in the environment + AND PREAUTH_CMDLINE_REMOVEME is in the environment set to 1 + + WHEN testinstance adminkey runs removeme ls + THEN stdout contains gitano-admin + + GIVEN PREAUTH_CMDLINE_REMOVEME is not in the environment + + WHEN testinstance adminkey, expecting failure, runs removeme ls + THEN stderr contains removeme + + FINALLY the instance is torn down + +Post Receieve hook +------------------ + +The `POST_RECEIVE` hook allows plugins to perform actions during post-receive. +This is after the commits have made it into the repository, and after the refs +have been updated. The `POST_RECEIVE` hook gets given the set of updates which +were applied to the repository and it gets to take action. Generally we don't +recommend that hooks _stop_ the chain, but they can, which lets us do things +like preventing Supple running. + + SCENARIO supple isn't even considered when post_receive hooks "stop" + ASSUMING gitano is being accessed over ssh + + GIVEN a standard instance + AND testinstance using adminkey has patched gitano-admin with post-receive-alert.patch + AND HOOK_DECLINE is in the environment set to POST_RECEIVE + WHEN testinstance using adminkey clones gitano-admin.git as gitano-admin + AND testinstance using adminkey pushes an empty commit in gitano-admin + WHEN testinstance using bypasskey pushes an empty commit in gitano-admin + THEN the output does not contain PERIL + AND the output does not contain CRITICAL FAILURE + AND the output does not contain XYZZY + AND the output contains HOOKFUNC_STOPPED + + FINALLY the instance is torn down diff -Nru gitano-1.0/testing/02-commands-as.yarn gitano-1.1/testing/02-commands-as.yarn --- gitano-1.0/testing/02-commands-as.yarn 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/testing/02-commands-as.yarn 2017-08-03 15:11:42.000000000 +0000 @@ -74,3 +74,20 @@ AND stdout is empty FINALLY the instance is torn down + +As well as not leaking information, use of `as` must not thwart auditability, +so a user with elevated permissions must not be able to frame another user. + + SCENARIO Ensuring 'as' does not thwart auditability + + GIVEN a standard instance + AND testinstance has keys called other + AND testinstance has keys called sneakybackdoor + WHEN testinstance, using adminkey, adds user other, using testinstance other + AND testinstance uses their ssh public key called sneakybackdoor as stdin + AND testinstance adminkey runs as other sshkey add sneakybackdoor + AND server-side gitano-admin reads git object HEAD^{commit} + THEN stdout contains Added sneakybackdoor for other + AND stdout contains committer Administrator + + FINALLY the instance is torn down diff -Nru gitano-1.0/testing/02-commands-create.yarn gitano-1.1/testing/02-commands-create.yarn --- gitano-1.0/testing/02-commands-create.yarn 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/testing/02-commands-create.yarn 2017-08-03 15:11:42.000000000 +0000 @@ -25,3 +25,18 @@ THEN stderr contains CRIT: Repository creation is not permitted. FINALLY the instance is torn down + +When creating a repository, we update the HTTP server information file because +that way gitweb and the like don't get upset even if nothing has been pushed, +and HTTP smart access can be permitted, which might be necessary for the push +in the first place. + + SCENARIO created repositories have server info + + GIVEN a standard instance + + WHEN testinstance adminkey runs create foobar + THEN server-side foobar.git file objects/info/packs exists + AND server-side foobar.git file info/refs exists + + FINALLY the instance is torn down diff -Nru gitano-1.0/testing/02-commands-user.yarn gitano-1.1/testing/02-commands-user.yarn --- gitano-1.0/testing/02-commands-user.yarn 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/testing/02-commands-user.yarn 2017-08-03 15:11:42.000000000 +0000 @@ -74,6 +74,18 @@ THEN stdout contains ^robert THEN stdout does not contain ^bob +In addition, you might want rename a user which owns repositories. When that +is done, Gitano must re-own the repository in order that rules using the +project's owner work properly. + + WHEN testinstance adminkey runs create testrepo robert + AND testinstance adminkey runs config testrepo show project.owner + THEN stdout contains robert + + WHEN testinstance adminkey runs user rename robert bob --force + AND testinstance adminkey runs config testrepo show project.owner + THEN stdout contains bob + FINALLY the instance is torn down Deleting users diff -Nru gitano-1.0/testing/03-config-user-whitelist.yarn gitano-1.1/testing/03-config-user-whitelist.yarn --- gitano-1.0/testing/03-config-user-whitelist.yarn 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/03-config-user-whitelist.yarn 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,54 @@ +Access control by configuration keys +------------------------------------ + +While Gitano allows arbitrarily complex accss control via Lace, and supports +group and repository prefix matching to manage large projects, these approaches +are often overkill for installations with small numbers of repositories, users, +and permission grants. + +Instead, the default ruleset for Gitano also supports adding users, by name, to the +config lists "project.readers" and "project.writers" allowing a much simpler +per-repository configuration approach. + +Note: This lookup is linear time, so it won't scale to a large number of users. +Also it doesn't automatically get updated if users are added/deleted/renamed. +If any of that concerns you, take the time to use a proper group and Lace approach. + + SCENARIO Access controlled by configuration keys + + GIVEN a standard instance + AND testinstance using adminkey, adds a new user alice, with a key called main + +By default users may not read repositories they are not owners to, +so cloning fails. + + WHEN testinstance adminkey runs create testrepo + AND alice, using main, expecting failure, clones testrepo as testrepo + THEN stderr contains \(FATAL: Not authorised\|The requested URL returned error: 403\) + +When the user is added to the project.reader config then cloning works. + + WHEN testinstance adminkey runs config testrepo set project.readers.* alice + AND alice, using main, clones testrepo as testrepo + THEN alice has a clone of testrepo + +Pushing any content fails however. + + WHEN alice using main pushes an empty commit in testrepo + THEN stderr contains \(FATAL: Not authorised\|The requested URL returned error: 403\) + +Pushing works once the user is added to project.writers. + + WHEN testinstance adminkey runs config testrepo set project.writers.* alice + AND alice applies add-a-FOO.patch in testrepo + AND alice, using main, pushes testrepo to testrepo.git + AND server-side testrepo reads git object HEAD + THEN stdout contains Apply add-a-FOO.patch content change + +Being a project writer implies also being a project reader. + + WHEN testinstance adminkey runs config testrepo del project.readers.i_1 + AND alice, using main, clones testrepo as testrepo2 + THEN alice has a clone of testrepo2 + + FINALLY the instance is torn down diff -Nru gitano-1.0/testing/03-dangling-HEAD.yarn gitano-1.1/testing/03-dangling-HEAD.yarn --- gitano-1.0/testing/03-dangling-HEAD.yarn 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/03-dangling-HEAD.yarn 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,24 @@ +Being notified when a push does not fix a dangling HEAD +------------------------------------------------------- + +Things can get confusing quickly when you've got a repository +where the symbolic ref HEAD does not point to an existing branch. +This is called a "dangling HEAD". + +Since Gitano allows you to change the HEAD symbolic ref from refs/heads/master +it can be easier to accidentally leave it dangling after pushing a branch. + +To reduce the surprise involved, Gitano will provide a warning +if after a push the HEAD symbolic ref is dangling. + + SCENARIO Pushes resulting in a dangling HEAD are warned about + + GIVEN a standard instance + WHEN testinstance adminkey runs create testrepo + AND testinstance adminkey runs config testrepo set project.head refs/heads/trunk + AND testinstance, using adminkey, clones testrepo as testrepo + AND testinstance applies add-a-FOO.patch in testrepo + AND testinstance, using adminkey, pushes testrepo to testrepo.git + THEN stderr contains WARNING: HEAD remains dangling + + FINALLY the instance is torn down diff -Nru gitano-1.0/testing/03-force-pushing.yarn gitano-1.1/testing/03-force-pushing.yarn --- gitano-1.0/testing/03-force-pushing.yarn 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/03-force-pushing.yarn 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,34 @@ + + +Force Pushing +============= + +The default ruleset of Gitano denies force-pushing to everyone except the +`gitano-admin` group by default; requiring that projects explicitly enable it +if they want it. The theory being that it can be permitted for some branches +but not others, and by deny-by-default, it encourages project owners to think +hard before granting force-push. + + SCENARIO alice cannot force-push by default + + GIVEN a standard instance + AND a unix user called alice + AND alice has keys called main + + WHEN testinstance, using adminkey, adds user alice, using alice main + AND testinstance adminkey runs create testrepo alice + AND alice, using main, clones testrepo as testrepo + THEN alice testrepo has no file called FOO + + WHEN alice applies add-a-FOO.patch in testrepo + AND alice, using main, pushes testrepo to testrepo.git + THEN the output contains new branch + + WHEN alice amends testrepo with oh well never mind + AND alice, using main, expecting failure, pushes testrepo to testrepo.git + THEN the output contains non-fast-forward + + WHEN alice, using main, expecting failure, force-pushes testrepo to testrepo.git + THEN stderr contains denied action + + FINALLY the instance is torn down diff -Nru gitano-1.0/testing/03-treedelta-rules.yarn gitano-1.1/testing/03-treedelta-rules.yarn --- gitano-1.0/testing/03-treedelta-rules.yarn 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/03-treedelta-rules.yarn 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,248 @@ + + +Using the tree deltas in rules +============================== + +Gitano is able to use the tree deltas produced by changes in ref tips when +evaluating whether or not an update is permitted. Since the tree deltas can be +expensive to generate, we only trigger generation of them on demand. As such +they are an area of the code where errors could easily trickle in unless we +keep a close eye on things. + +Start and Target trees +---------------------- + +The first part of the treedelta support are the gitano/starttree and +gitano/targetttee lists. These contain the flattened entry names for +everything in the trees. + +First, let's look at what it takes to prevent the creation of files with FOO in. + + SCENARIO may not create files called FOO + GIVEN a standard instance + AND testinstance using adminkey has patched gitano-admin with no-create-FOO.patch + GIVEN a unix user called alice + AND alice has keys called main + + WHEN testinstance, using adminkey, adds user alice, using alice main + AND testinstance adminkey runs create testrepo alice + AND alice, using main, clones testrepo as testrepo + THEN alice testrepo has no file called FOO + + WHEN alice applies add-a-FOO.patch in testrepo + AND alice, using main, expecting failure, pushes testrepo to testrepo.git + THEN stderr contains No FOOs allowed + + FINALLY the instance is torn down + +Next, let's look at how we might require a FOO to be present... + + SCENARIO source must have a FOO + GIVEN a standard instance + AND a unix user called alice + AND alice has keys called main + + WHEN testinstance, using adminkey, adds user alice, using alice main + AND testinstance adminkey runs create testrepo alice + AND alice, using main, clones testrepo as testrepo + THEN alice testrepo has no file called FOO + +We now have a repo which has no FOO in it, let's first check that our admin +rule which requires `start_tree` contain a FOO by trying to push an empty tree + + GIVEN testinstance using adminkey has patched gitano-admin with must-start-with-FOO.patch + + WHEN alice, using main, expecting failure, pushes an empty commit in testrepo + THEN stderr contains Needs a FOO + +Next, let's create a FOO in our tree, prove that `start_tree` != `target_tree` + + WHEN alice applies add-a-FOO.patch in testrepo + AND alice, using main, pushes testrepo to testrepo.git + THEN stderr contains Needs a FOO + +And if we back out the rule, we can push it... + + GIVEN testinstance using adminkey has patched gitano-admin with the reverse of must-start-with-FOO.patch + + WHEN alice, using main, pushes testrepo to testrepo.git + THEN the output contains new branch + +And if we put the rule back in, an empty commit will make it through because +`start_tree` now does contain a FOO + + GIVEN testinstance using adminkey has patched gitano-admin with must-start-with-FOO.patch + + WHEN alice, using main, pushes an empty commit in testrepo + THEN the output contains master -> master + +And once again, prove `start_tree` != `target_tree` by backing out the FOO +and proving we can push that. + + WHEN alice reverts add-a-FOO.patch in testrepo + AND alice, using main, pushes testrepo to testrepo.git + THEN the output contains master -> master + +And of course, now `start_tree` does not contain a FOO, so no matter what +we do to `target_tree` we can't push... + + WHEN alice applies add-a-FOO.patch in testrepo + AND alice, using main, pushes testrepo to testrepo.git + THEN stderr contains Needs a FOO + + FINALLY the instance is torn down + +Tree deltas +----------- + +When there are trees in play, the `treediff/targets`, `treediff/added`, +`treediff/deleted`, `treediff/modified`, `treediff/renamed`, and +`treediff/renamedto` values end up set. + +The _targets_ are any name which shows up in any of _added_, _deleted_, +_modified_, _renamed_, or _renamedto_. The others are, respectively, the +names of new tree entries, removed tree entries, entries whose content has +changed, and then rename detection logic. + +> Sadly currently Gitano can't tell which rename from/to is matched with which. + +First up, let's ensure that `treediff/targets` works for the various kinds +of adding, modifying, removing, and renaming operations... + + SCENARIO any change must affect FOO + GIVEN a standard instance + AND a unix user called alice + AND alice has keys called main + + WHEN testinstance, using adminkey, adds user alice, using alice main + AND testinstance adminkey runs create testrepo alice + AND alice, using main, clones testrepo as testrepo + THEN alice testrepo has no file called FOO + + GIVEN testinstance using adminkey has patched gitano-admin with must-affect-FOO.patch + +First up, when we try an empty commit we can't push it... + + WHEN alice, using main, expecting failure, pushes an empty commit in testrepo + THEN stderr contains Needs a FOO + +Next, when a FOO is added, it should turn up in `treediff/targets` + + WHEN alice applies add-a-FOO.patch in testrepo + AND alice, using main, pushes testrepo to testrepo.git + THEN the output contains new branch + +But its mere presence in `start_tree` shouldn't allow empty commits... + + WHEN alice, using main, expecting failure, pushes an empty commit in testrepo + THEN stderr contains Needs a FOO + +Now we verify that altering the content turns up in `treediff/targets` + + WHEN alice applies change-a-FOO.patch in testrepo + AND alice, using main, pushes testrepo to testrepo.git + THEN the output contains master -> master + +Next, when the FOO gets removed, it should show in `treediff/targets` + + WHEN alice reverts change-a-FOO.patch in testrepo + AND alice reverts add-a-FOO.patch in testrepo + AND alice, using main, pushes testrepo to testrepo.git + THEN the output contains master -> master + +Next we need to rename a FOO, to do that, first add it back... + + WHEN alice applies add-a-FOO.patch in testrepo + AND alice, using main, pushes testrepo to testrepo.git + THEN the output contains master -> master + +and then check that renaming the FOO causes it to turn up in `treediff/targets` + + WHEN alice applies rename-a-FOO.patch in testrepo + AND alice, using main, pushes testrepo to testrepo.git + THEN the output contains master -> master + +and finally we ensure that renaming it *back* works too... + + WHEN alice reverts rename-a-FOO.patch in testrepo + AND alice, using main, pushes testrepo to testrepo.git + THEN the output contains master -> master + + FINALLY the instance is torn down + +Now that we know that `treediff/targets` works in all cases, we ensure that +the particular `treediff/*` element is also populated for the given activity + + SCENARIO any change must affect FOO with specificity + GIVEN a standard instance + AND a unix user called alice + AND alice has keys called main + + WHEN testinstance, using adminkey, adds user alice, using alice main + AND testinstance adminkey runs create testrepo alice + AND alice, using main, clones testrepo as testrepo + THEN alice testrepo has no file called FOO + + GIVEN testinstance using adminkey has patched gitano-admin with must-add-FOO.patch + +First up, when we try an empty commit we can't push it... + + WHEN alice, using main, expecting failure, pushes an empty commit in testrepo + THEN stderr contains Needs a FOO + +Next, when a FOO is added, it should turn up in `treediff/added` + + WHEN alice applies add-a-FOO.patch in testrepo + AND alice, using main, pushes testrepo to testrepo.git + THEN the output contains new branch + +But its mere presence in `start_tree` shouldn't allow empty commits... + + WHEN alice, using main, expecting failure, pushes an empty commit in testrepo + THEN stderr contains Needs a FOO + +Now we verify that altering the content turns up in `treediff/modified` + + GIVEN testinstance using adminkey has patched gitano-admin with the reverse of must-add-FOO.patch + AND testinstance using adminkey has patched gitano-admin with must-modify-FOO.patch + + WHEN alice applies change-a-FOO.patch in testrepo + AND alice, using main, pushes testrepo to testrepo.git + THEN the output contains master -> master + +Next, when the FOO gets removed, it should show in `treediff/deleted` + + GIVEN testinstance using adminkey has patched gitano-admin with the reverse of must-modify-FOO.patch + AND testinstance using adminkey has patched gitano-admin with must-remove-FOO.patch + + WHEN alice reverts change-a-FOO.patch in testrepo + AND alice reverts add-a-FOO.patch in testrepo + AND alice, using main, pushes testrepo to testrepo.git + THEN the output contains master -> master + +Next we need to rename a FOO, to do that, first add it back... + + GIVEN testinstance using adminkey has patched gitano-admin with the reverse of must-remove-FOO.patch + + WHEN alice applies add-a-FOO.patch in testrepo + AND alice, using main, pushes testrepo to testrepo.git + THEN the output contains master -> master + +and then check that renaming the FOO causes it to turn up in `treediff/renamed` + + GIVEN testinstance using adminkey has patched gitano-admin with must-rename-from-FOO.patch + + WHEN alice applies rename-a-FOO.patch in testrepo + AND alice, using main, pushes testrepo to testrepo.git + THEN the output contains master -> master + +and finally we ensure that renaming it *back* works too... + + GIVEN testinstance using adminkey has patched gitano-admin with the reverse of must-rename-from-FOO.patch + AND testinstance using adminkey has patched gitano-admin with must-rename-to-FOO.patch + + WHEN alice reverts rename-a-FOO.patch in testrepo + AND alice, using main, pushes testrepo to testrepo.git + THEN the output contains master -> master + + FINALLY the instance is torn down diff -Nru gitano-1.0/testing/admin-patches/must-add-FOO.patch gitano-1.1/testing/admin-patches/must-add-FOO.patch --- gitano-1.0/testing/admin-patches/must-add-FOO.patch 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/admin-patches/must-add-FOO.patch 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,13 @@ +diff --git a/rules/project.lace b/rules/project.lace +index aafa17c..388766a 100644 +--- a/rules/project.lace ++++ b/rules/project.lace +@@ -43,6 +43,8 @@ include global:remoteconfigchecks op_is_config + # Okay, if we're altering the admin ref, in we go + include global:adminchecks is_admin_ref + ++deny "Needs a FOO" op_is_normal ![treediff/added is FOO] ++ + # Now we're into branch operations. Owners can do any normal operation + # Normal ops are create/delete/fastforward on refs + allow "Owners can create refs" op_is_normal is_owner diff -Nru gitano-1.0/testing/admin-patches/must-affect-FOO.patch gitano-1.1/testing/admin-patches/must-affect-FOO.patch --- gitano-1.0/testing/admin-patches/must-affect-FOO.patch 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/admin-patches/must-affect-FOO.patch 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,13 @@ +diff --git a/rules/project.lace b/rules/project.lace +index aafa17c..388766a 100644 +--- a/rules/project.lace ++++ b/rules/project.lace +@@ -43,6 +43,8 @@ include global:remoteconfigchecks op_is_config + # Okay, if we're altering the admin ref, in we go + include global:adminchecks is_admin_ref + ++deny "Needs a FOO" op_is_normal ![treediff/targets is FOO] ++ + # Now we're into branch operations. Owners can do any normal operation + # Normal ops are create/delete/fastforward on refs + allow "Owners can create refs" op_is_normal is_owner diff -Nru gitano-1.0/testing/admin-patches/must-modify-FOO.patch gitano-1.1/testing/admin-patches/must-modify-FOO.patch --- gitano-1.0/testing/admin-patches/must-modify-FOO.patch 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/admin-patches/must-modify-FOO.patch 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,13 @@ +diff --git a/rules/project.lace b/rules/project.lace +index aafa17c..388766a 100644 +--- a/rules/project.lace ++++ b/rules/project.lace +@@ -43,6 +43,8 @@ include global:remoteconfigchecks op_is_config + # Okay, if we're altering the admin ref, in we go + include global:adminchecks is_admin_ref + ++deny "Needs a FOO" op_is_normal ![treediff/modified is FOO] ++ + # Now we're into branch operations. Owners can do any normal operation + # Normal ops are create/delete/fastforward on refs + allow "Owners can create refs" op_is_normal is_owner diff -Nru gitano-1.0/testing/admin-patches/must-remove-FOO.patch gitano-1.1/testing/admin-patches/must-remove-FOO.patch --- gitano-1.0/testing/admin-patches/must-remove-FOO.patch 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/admin-patches/must-remove-FOO.patch 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,13 @@ +diff --git a/rules/project.lace b/rules/project.lace +index aafa17c..388766a 100644 +--- a/rules/project.lace ++++ b/rules/project.lace +@@ -43,6 +43,8 @@ include global:remoteconfigchecks op_is_config + # Okay, if we're altering the admin ref, in we go + include global:adminchecks is_admin_ref + ++deny "Needs a FOO" op_is_normal ![treediff/deleted is FOO] ++ + # Now we're into branch operations. Owners can do any normal operation + # Normal ops are create/delete/fastforward on refs + allow "Owners can create refs" op_is_normal is_owner diff -Nru gitano-1.0/testing/admin-patches/must-rename-from-FOO.patch gitano-1.1/testing/admin-patches/must-rename-from-FOO.patch --- gitano-1.0/testing/admin-patches/must-rename-from-FOO.patch 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/admin-patches/must-rename-from-FOO.patch 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,13 @@ +diff --git a/rules/project.lace b/rules/project.lace +index aafa17c..388766a 100644 +--- a/rules/project.lace ++++ b/rules/project.lace +@@ -43,6 +43,8 @@ include global:remoteconfigchecks op_is_config + # Okay, if we're altering the admin ref, in we go + include global:adminchecks is_admin_ref + ++deny "Needs a FOO" op_is_normal ![treediff/renamed is FOO] ++ + # Now we're into branch operations. Owners can do any normal operation + # Normal ops are create/delete/fastforward on refs + allow "Owners can create refs" op_is_normal is_owner diff -Nru gitano-1.0/testing/admin-patches/must-rename-to-FOO.patch gitano-1.1/testing/admin-patches/must-rename-to-FOO.patch --- gitano-1.0/testing/admin-patches/must-rename-to-FOO.patch 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/admin-patches/must-rename-to-FOO.patch 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,13 @@ +diff --git a/rules/project.lace b/rules/project.lace +index aafa17c..388766a 100644 +--- a/rules/project.lace ++++ b/rules/project.lace +@@ -43,6 +43,8 @@ include global:remoteconfigchecks op_is_config + # Okay, if we're altering the admin ref, in we go + include global:adminchecks is_admin_ref + ++deny "Needs a FOO" op_is_normal ![treediff/renamedto is FOO] ++ + # Now we're into branch operations. Owners can do any normal operation + # Normal ops are create/delete/fastforward on refs + allow "Owners can create refs" op_is_normal is_owner diff -Nru gitano-1.0/testing/admin-patches/must-start-with-FOO.patch gitano-1.1/testing/admin-patches/must-start-with-FOO.patch --- gitano-1.0/testing/admin-patches/must-start-with-FOO.patch 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/admin-patches/must-start-with-FOO.patch 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,13 @@ +diff --git a/rules/project.lace b/rules/project.lace +index aafa17c..388766a 100644 +--- a/rules/project.lace ++++ b/rules/project.lace +@@ -43,6 +43,8 @@ include global:remoteconfigchecks op_is_config + # Okay, if we're altering the admin ref, in we go + include global:adminchecks is_admin_ref + ++deny "Needs a FOO" op_is_normal ![start_tree is FOO] ++ + # Now we're into branch operations. Owners can do any normal operation + # Normal ops are create/delete/fastforward on refs + allow "Owners can create refs" op_is_normal is_owner diff -Nru gitano-1.0/testing/admin-patches/no-create-FOO.patch gitano-1.1/testing/admin-patches/no-create-FOO.patch --- gitano-1.0/testing/admin-patches/no-create-FOO.patch 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/admin-patches/no-create-FOO.patch 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,13 @@ +diff --git a/rules/project.lace b/rules/project.lace +index aafa17c..388766a 100644 +--- a/rules/project.lace ++++ b/rules/project.lace +@@ -43,6 +43,8 @@ include global:remoteconfigchecks op_is_config + # Okay, if we're altering the admin ref, in we go + include global:adminchecks is_admin_ref + ++deny "No FOOs allowed" op_is_normal [target_tree is FOO] ++ + # Now we're into branch operations. Owners can do any normal operation + # Normal ops are create/delete/fastforward on refs + allow "Owners can create refs" op_is_normal is_owner diff -Nru gitano-1.0/testing/content-patches/add-a-FOO.patch gitano-1.1/testing/content-patches/add-a-FOO.patch --- gitano-1.0/testing/content-patches/add-a-FOO.patch 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/content-patches/add-a-FOO.patch 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,7 @@ +diff --git a/FOO b/FOO +new file mode 100644 +index 0000000..ee5a5f2 +--- /dev/null ++++ b/FOO +@@ -0,0 +1 @@ ++This is a FOO diff -Nru gitano-1.0/testing/content-patches/change-a-FOO.patch gitano-1.1/testing/content-patches/change-a-FOO.patch --- gitano-1.0/testing/content-patches/change-a-FOO.patch 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/content-patches/change-a-FOO.patch 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,6 @@ +diff -urN a/FOO b/FOO +--- a/FOO 2017-03-04 10:49:13.351217763 +0000 ++++ b/FOO 2017-03-04 10:49:36.467415444 +0000 +@@ -1 +1,2 @@ + This is a FOO ++FOO has changed diff -Nru gitano-1.0/testing/content-patches/rename-a-FOO.patch gitano-1.1/testing/content-patches/rename-a-FOO.patch --- gitano-1.0/testing/content-patches/rename-a-FOO.patch 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/content-patches/rename-a-FOO.patch 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,10 @@ +diff -urN a/BAR b/BAR +--- a/BAR 1970-01-01 01:00:00.000000000 +0100 ++++ b/BAR 2017-03-04 10:59:53.488578394 +0000 +@@ -0,0 +1 @@ ++This is a FOO +diff -urN a/FOO b/FOO +--- a/FOO 2017-03-04 10:59:53.488578394 +0000 ++++ b/FOO 1970-01-01 01:00:00.000000000 +0100 +@@ -1 +0,0 @@ +-This is a FOO diff -Nru gitano-1.0/testing/gitano-test-tool.in gitano-1.1/testing/gitano-test-tool.in --- gitano-1.0/testing/gitano-test-tool.in 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/testing/gitano-test-tool.in 2017-08-03 15:11:42.000000000 +0000 @@ -70,10 +70,28 @@ end end +local function load_env(into) + local f, msg = loadfile(basedir .. ".gtt-env") + if f then + setfenv(f, into) + f() + end +end + +local function save_env(env) + local f = io.open(basedir .. ".gtt-env", "w") + for k, v in pairs(env) do + f:write(("%s = %q\n"):format(k, v)) + end + f:close() +end + local function run_program(t) - if t.env and os.getenv("GITANO_DUMP_VARIABLE_FILE") then + t.env = (t.env or {}) + if os.getenv("GITANO_DUMP_VARIABLE_FILE") then t.env["GITANO_DUMP_VARIABLE_FILE"] = os.getenv("GITANO_DUMP_VARIABLE_FILE") end + load_env(t.env) local f = io.open(basedir .. "last-program", "w") local function print (...) f:write(...) @@ -145,6 +163,19 @@ return ret end +local function set_stored_password(user, pass) + local fh = assert(io.open(user_home(user) .. "/passwd", "w")) + fh:write(pass .. "\n") + fh:close() +end + +local function load_stored_password(user) + local fh = assert(io.open(user_home(user) .. "/passwd", "r")) + local pass = fh:read("*l") + fh:close() + return pass +end + local function generate_exturl(user, key, repo) local authkeys = load_auth(ssh_key_file("testinstance", "authorized_keys")) local pubkey = (sio.open(ssh_key_file(user, key) .. ".pub", "r")):read("*l") @@ -161,7 +192,7 @@ esc(authline.user), esc(authline.keyset)) end -local function generate_httpurl(user, key, repo) +local function generate_httpurl(user, key, path) local authkeys = load_auth(ssh_key_file("testinstance", "authorized_keys")) local pubkey = (sio.open(ssh_key_file(user, key) .. ".pub", "r")):read("*l") local authline = assert(authkeys[pubkey]) @@ -171,7 +202,7 @@ local port = tonumber(fh:read()) fh:close() - return ("http://%s:%s@localhost:%d/git/%s"):format(authline.user, authline.user, port, repo) + return ("http://%s:%s@localhost:%d/%s"):format(authline.user, load_stored_password(user), port, path) end function cmd_setgitconfig(username, key, value) @@ -180,12 +211,36 @@ } end +function cmd_setenv(key, value) + local t = {} + load_env(t) + t[key] = value + save_env(t) +end + +function cmd_unsetenv(key) + local t = {} + load_env(t) + t[key] = nil + save_env(t) +end + function cmd_createunixuser(username) assert(sio.mkdir(user_home(username), "0755")) assert(sio.mkdir(ssh_base(username), "0755")) cmd_setgitconfig(username, "user.name", username) cmd_setgitconfig(username, "user.email", username.."@example.com") cmd_setgitconfig(username, "push.default", "simple") + cmd_setgitconfig(username, "protocol.ext.allow", "always") + set_stored_password(username, username) +end + +function cmd_getpasswd(username) + print(load_stored_password(username)) +end + +function cmd_setpasswd(username, passwd) + set_stored_password(username, passwd) end function cmd_createsshkey(username, keyname, optionaltype) @@ -231,11 +286,12 @@ fh:close() run_program { "gitano-setup", clodname, - exe = gitano.config.lib_bin_path() .. "/gitano-setup", + exe = gitano.config.lib_bin_path() .. "/../../../bin/gitano-setup", env = { HOME = user_home(owning_user) } } if os.getenv("GTT_PROTO") == "http" then -- setup lighttpd + local lua_init = os.getenv("LUA_INIT") or "" local htpasswd = user_home(owning_user) .. "/htpasswd" local pid_file = basedir .. "lighttpd.pid" local port_file = basedir .. "lighttpd.port" @@ -250,9 +306,14 @@ fh:write(('server.port = %d\n'):format(port)) fh:write('server.modules = ( "mod_auth", "mod_alias", "mod_cgi", "mod_setenv" )\n') fh:write(([[ -$HTTP["url"] =~ ".*/gitano-command.cgi$" { +$HTTP["url"] =~ "/gitano-command.cgi$" { + alias.url += ( "/gitano-command.cgi" => %q ) + + cgi.assign = ("" => "") + setenv.add-environment = ( "HOME" => %q, + "LUA_INIT" => %q, "GITANO_ROOT" => %q ) @@ -276,6 +337,7 @@ "GIT_HTTP_EXPORT_ALL" => "", "GIT_PROJECT_ROOT" => %q, "HOME" => %q, + "LUA_INIT" => %q, "GITANO_ROOT" => %q ) @@ -290,9 +352,10 @@ auth.backend = "htpasswd" auth.backend.htpasswd.userfile = %q } -]]):format(user_home(owning_user), repo_path, htpasswd, +]]):format(gitano.config.lib_bin_path() .. "/gitano-command.cgi", + user_home(owning_user), lua_init, repo_path, htpasswd, gitano.config.lib_bin_path() .. "/gitano-smart-http.cgi", repo_path, - user_home(owning_user), repo_path, htpasswd)) + user_home(owning_user), lua_init, repo_path, htpasswd)) fh:close() @@ -331,7 +394,7 @@ function cmd_clone(user, key, repo, localname, ...) local url if os.getenv("GTT_PROTO") == "http" then - url = generate_httpurl(user, key, repo) + url = generate_httpurl(user, key, "git/" .. repo) end if os.getenv("GTT_PROTO") == "ssh" then url = generate_exturl(user, key, repo) @@ -345,7 +408,7 @@ function cmd_push(user, key, localname, repo, ...) local url if os.getenv("GTT_PROTO") == "http" then - url = generate_httpurl(user, key, repo) + url = generate_httpurl(user, key, "git/" .. repo) end if os.getenv("GTT_PROTO") == "ssh" then url = generate_exturl(user, key, repo) @@ -368,6 +431,7 @@ function cmd_gitarchive(user, key, repo, ref) local exturl = generate_exturl(user, key, repo) run_program { + env = { HOME = user_home(user) }, "git", "archive", "--remote", exturl, ref, } end @@ -398,30 +462,22 @@ end function cmd_runcommand_http(user, key, ...) - local authkeys = load_auth(ssh_key_file("testinstance", "authorized_keys")) - local pubkey = (sio.open(ssh_key_file(user, key) .. ".pub", "r")):read("*l") - local authline = assert(authkeys[pubkey]) - local cmdline = { - "testing/http-unwrap", - gitano.config.lib_bin_path() .. "/gitano-command.cgi", - env = { - HOME = user_home("testinstance"), - REMOTE_USER = authline.user, - REMOTE_ADDR = "10.0.0.1", - GITANO_ROOT = authline.repopath, - } - } + local httpurl = generate_httpurl(user, key, "gitano-command.cgi") local elems = esc_quote_all_({...}) - local function escape (str) - str = string.gsub (str, "([^0-9a-zA-Z !'()*._~-])", -- locale independent - function (c) return string.format ("%%%02X", string.byte(c)) end) - str = string.gsub (str, " ", "+") + local function escape(str) + str = string.gsub(str, "([^0-9a-zA-Z !'()*._~-])", -- locale independent + function (c) return string.format ("%%%02X", string.byte(c)) end) + str = string.gsub(str, " ", "+") return str end for i = 1, #elems do elems[i] = escape(elems[i]) end - cmdline.env.QUERY_STRING = "cmd=" .. table.concat(elems, "+") + httpurl = httpurl .. "?cmd=" .. table.concat(elems, "+") + local cmdline = { + "testing/http-unwrap", + httpurl + } run_program(cmdline) end diff -Nru gitano-1.0/testing/http-unwrap gitano-1.1/testing/http-unwrap --- gitano-1.0/testing/http-unwrap 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/testing/http-unwrap 2017-08-03 15:11:42.000000000 +0000 @@ -27,18 +27,26 @@ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. -"$@" | ( - OIFS="$IFS"; - IFS=" -"; - read STATUSLINE; - read BLANKLINE; - IFS="$OIFS"; - OKLINE=${STATUSLINE##Status: 200} - if test x"$OKLINE" = x"$STATUSLINE"; then - cat >&2 - exit 1 - else - cat - fi -) +INFILE=$(mktemp -p "$DATADIR") +OUTFILE=$(mktemp -p "$DATADIR") +cleanup () { + rm -f "$OUTFILE" + rm -f "$INFILE" +} +trap cleanup 0 + +cat > "$INFILE" + +if test $(stat -c %s "$INFILE") != "0"; then + wget -q -O- --content-on-error --auth-no-challenge --post-file="$INFILE" "$@" > "$OUTFILE" 2>&1 +else + wget -q -O- --content-on-error --auth-no-challenge "$@" > "$OUTFILE" 2>&1 +fi +RES="$?" +if test "$RES" = "0"; then + cat "$OUTFILE" + exit 0 +else + cat >&2 "$OUTFILE" + exit 1 +fi diff -Nru gitano-1.0/testing/keys/testinstance@sneakybackdoor_rsa gitano-1.1/testing/keys/testinstance@sneakybackdoor_rsa --- gitano-1.0/testing/keys/testinstance@sneakybackdoor_rsa 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/keys/testinstance@sneakybackdoor_rsa 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAtJpcf1ETrcTKQA/wdWW14l6YPR7VyqcjlXWJEMPN+derw1Qg +rXC24If8bl89k8+pJFQBZ0EGjBWOX/39f9WhLt67Qyhg94tAZ2hOJWL5u4Qbs7Ed +zKB1+Em2l7bKwRo6OeYMQdO4wVVLyeKitfO5A0DvWobhk+c6P/He7J7jEB4zRZc+ +/0eSRP5UgEpeATvu9pdn3B9Yf8LJWL7/VEi67uTaxrydpuNqeDHSrGM/xGhlZ4m2 +RDCAT9BWTLFgKmV7MfxVLxpIkZmjbcy+QSWVoiuH/nanhRZL4tvNW4b7bLkGz3gQ +6gsoi/EhDa6UAgTYrSr1zqlILt1mqIV4vXzEKwIDAQABAoIBAHOW7CaMZLTt46hW +y0bH/z5P3s4XoyueB7dLz5sMRxNmBsfwWy3GmzVfs5+Mk2O8H/xhS7ijNKaJ0WL8 +s7eSqiPOaDoWaOFmnaTRbFqfW0i5x+UdMf5aoMZ1n0jAtEodGDEgXK3w7SnIBsbC +p/Med0Zu1AKzm0LHVk/A5TW6h4czSUGNzdJl4l58rZRNX3w/WnRmjCKEsQvftqIA +GITxXMqcW38TrvOYmesAR5/ukdii+/aE9WTofq2qZR1IZfzwc/0XOWX3fTjDhIF8 +aE5xUb/teDbPnc6hVEU6ik6yZ/UnNU4X6mqRQrphdWdVZKXfhSwr+bLSErMzzhMN +i6pXtOECgYEA5Qal8EYQszmCarSvfL1p6AGalB/LDkNz+zFDqEYIXGtHtJJLFwqc +axIM/V4mGmprDrZq0lMstE/CA6h0WZydRNN40uGlOP3A0YaYQRtiSz8nCDFqazNH +tRA2BnIQZvDhdlGyJwqsoCfdprGh6L8dUTbkzFaDRAF30LfPr9hsxJsCgYEAyd+2 +qlZ9gvBReiMIuJhmZHXA6I2qPpMwVCoja3uiXtHG60moPblWaUlPnoDgTH8S6O81 +lS4aTVCCHumcVP2Q75Six2oVBO6URE1UtxYd0ADYhLsM5ziAjcliTwGm7QH4w2DR +akBEIN6/+AIPACMpFWs+oBMcZPqe93JEBCInT7ECgYAC0fUjI0m7Wz7u33C1wYNX +VwW3Qzj14QDBnBawMMSTlsKYR6DjFL9eVieQyyL++kZ9NOPV2S5Yvg6uitl77QDG +wy/esOae8Aj6y4R+cL7iHFH3uNwNm+ELKrrvk2H+UoMEOPdPocMEadlB3zgWLJxI +zrs8hOgy4y29hTXqfWjBdwKBgDefxutEjazonuqygJKsm3oO4Cqz7jbzw5tNSRky +pdjOoKrwTsVDLkYwhxm7lRI+6Wz5jKAgZerrxg7Se9sHS0pYgEnGNyh2vK/dRvxz +wZ8wvHhGOhX0AagP12DBqccghfT/1nQaZStRdT/XAV8eURGvzT+6RFamn+q6t3cU +GhThAoGABgren8o1Ymr56nfy/SI9+OBWB4G8347/XeLs2IQT2389rY1egW7Pra7N +eoxt/nacGwHIMbNlFPw1UQQ+LvsQbFr3rNZ4dV+MOFLs4TpmEjd71KikYIjLLV2r +EHG9NGK0ZY20SNj21FMX7lN2MDr903qSq1z8EMG9woWKCTayIIA= +-----END RSA PRIVATE KEY----- diff -Nru gitano-1.0/testing/keys/testinstance@sneakybackdoor_rsa.pub gitano-1.1/testing/keys/testinstance@sneakybackdoor_rsa.pub --- gitano-1.0/testing/keys/testinstance@sneakybackdoor_rsa.pub 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/testing/keys/testinstance@sneakybackdoor_rsa.pub 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0mlx/UROtxMpAD/B1ZbXiXpg9HtXKpyOVdYkQw83516vDVCCtcLbgh/xuXz2Tz6kkVAFnQQaMFY5f/f1/1aEu3rtDKGD3i0BnaE4lYvm7hBuzsR3MoHX4SbaXtsrBGjo55gxB07jBVUvJ4qK187kDQO9ahuGT5zo/8d7snuMQHjNFlz7/R5JE/lSASl4BO+72l2fcH1h/wslYvv9USLru5NrGvJ2m42p4MdKsYz/EaGVnibZEMIBP0FZMsWAqZXsx/FUvGkiRmaNtzL5BJZWiK4f+dqeFFkvi281bhvtsuQbPeBDqCyiL8SENrpQCBNitKvXOqUgu3WaohXi9fMQr testinstance-rsa@sneakybackdoor diff -Nru gitano-1.0/testing/library.yarn gitano-1.1/testing/library.yarn --- gitano-1.0/testing/library.yarn 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/testing/library.yarn 2017-08-03 15:11:42.000000000 +0000 @@ -23,12 +23,12 @@ $GTT createsshkey testinstance bypasskey $GTT setupstandard testinstance adminkey bypasskey if [ "$GTT_PROTO" = http ]; then - printf "%s" admin | GTT_PROTO=ssh $GTT runcommand \ + printf "%s" $($GTT getpasswd testinstance) | GTT_PROTO=ssh $GTT runcommand \ testinstance adminkey as admin passwd \ - >> $DATADIR/stdout 2>> $DATADIR/stderr - printf "%s" gitano-bypass | GTT_PROTO=ssh $GTT runcommand \ + >> "$DATADIR/stdout" 2>> "$DATADIR/stderr" + printf "%s" $($GTT getpasswd testinstance) | GTT_PROTO=ssh $GTT runcommand \ testinstance bypasskey as gitano-bypass passwd \ - >> $DATADIR/stdout 2>> $DATADIR/stderr + >> "$DATADIR/stdout" 2>> "$DATADIR/stderr" fi IMPLEMENTS FINALLY the instance is torn down @@ -41,10 +41,10 @@ SSH keys. Sometimes it's helpful to be able to work with these... IMPLEMENTS GIVEN a unix user called ([a-z][a-z0-9]*) - $GTT createunixuser $MATCH_1 + $GTT createunixuser "$MATCH_1" IMPLEMENTS GIVEN ([a-z][a-z0-9]*) has keys called ([a-z][a-z0-9]*) - $GTT createsshkey $MATCH_1 $MATCH_2 + $GTT createsshkey "$MATCH_1" "$MATCH_2" IMPLEMENTS WHEN ([a-z][a-z0-9]*) uses their ssh public key called ([a-z][a-z0-9]*) as stdin cp "$DATADIR/user-home-$MATCH_1/.ssh/$MATCH_2.pub" "$DATADIR/stdin" @@ -54,34 +54,39 @@ of the user inside Gitano. IMPLEMENTS GIVEN ([a-z][a-z0-9]*),? using ([a-z][a-z0-9_-]*),? adds a new user ([a-z][a-z0-9_-]*), with a key called ([a-z][a-z0-9_-]*) - $GTT createunixuser $MATCH_3 - $GTT createsshkey $MATCH_3 $MATCH_4 - $GTT runcommand $MATCH_1 $MATCH_2 \ - user add $MATCH_3 $MATCH_3@testinstance "$MATCH_3's real name" > $DATADIR/stdout 2> $DATADIR/stderr - $GTT runcommand $MATCH_1 $MATCH_2 \ - as $MATCH_3 sshkey add default < \ - $($GTT pubkeyfilename $MATCH_3 $MATCH_4) >> $DATADIR/stdout 2>> $DATADIR/stderr + $GTT createunixuser "$MATCH_3" + $GTT createsshkey "$MATCH_3" "$MATCH_4" + $GTT runcommand "$MATCH_1" "$MATCH_2" \ + user add "$MATCH_3" "$MATCH_3"@testinstance "$MATCH_3's real name" < "/dev/null" > "$DATADIR/stdout" 2> "$DATADIR/stderr" + $GTT runcommand "$MATCH_1" "$MATCH_2" \ + as "$MATCH_3" sshkey add default < \ + "$($GTT pubkeyfilename "$MATCH_3" "$MATCH_4")" >> "$DATADIR/stdout" 2>> "$DATADIR/stderr" if [ "$GTT_PROTO" = http ]; then - printf "%s" "$MATCH_3" | GTT_PROTO=ssh $GTT runcommand \ - $MATCH_1 $MATCH_2 as $MATCH_3 passwd \ - >> $DATADIR/stdout 2>> $DATADIR/stderr + printf "%s" $($GTT getpasswd "$MATCH_3") | GTT_PROTO=ssh $GTT runcommand \ + "$MATCH_1" "$MATCH_2" as "$MATCH_3" passwd \ + >> "$DATADIR/stdout" 2>> "$DATADIR/stderr" fi Repository access ----------------- - IMPLEMENTS WHEN ([a-z][a-z0-9]*),? using ([a-z][a-z0-9]*),? clones ([^ ]+) as ([^ ]+)( with depth (\d+))? - $GTT clone $MATCH_1 $MATCH_2 "$MATCH_3" "$MATCH_4" ${MATCH_5:+ --no-local --depth="$MATCH_6"} \ - >$DATADIR/stdout 2>$DATADIR/stderr + IMPLEMENTS WHEN ([a-z][a-z0-9]*),? using ([a-z][a-z0-9]*),? (expecting failure,? )?clones ([^ ]+) as ([^ ]+)( with depth (\d+))? + if $GTT clone "$MATCH_1" "$MATCH_2" "$MATCH_4" "$MATCH_5" ${MATCH_6:+ --no-local --depth="$MATCH_7"} \ + >"$DATADIR/stdout" 2>"$DATADIR/stderr"; then + test "$MATCH_3" = "" + fi - IMPLEMENTS WHEN ([a-z][a-z0-9]*),? using ([a-z][a-z0-9]*),? pushes ([^ ]+) to ([^ ]+) - $GTT push $MATCH_1 $MATCH_2 "$MATCH_3" "$MATCH_4" + IMPLEMENTS WHEN ([a-z][a-z0-9]*),? using ([a-z][a-z0-9]*),? (expecting failure,? )?(force.)?pushes ([^ ]+) to ([^ ]+) + if $GTT push "$MATCH_1" "$MATCH_2" "$MATCH_5" "$MATCH_6" ${MATCH_4:+--force} \ + >"$DATADIR/stdout" 2>"$DATADIR/stderr"; then + test "$MATCH_3" = "" + fi IMPLEMENTS WHEN ([a-z][a-z0-9]*),? using ([a-z][a-z0-9]*),? (expecting failure,? )?destroys ([^ ]+) using the (.+) token - if $GTT runcommand $MATCH_1 $MATCH_2 \ - destroy $MATCH_4 "$(cat "$DATADIR/saved-tokens/$MATCH_5")" \ - >$DATADIR/stdout 2>$DATADIR/stderr; then + if $GTT runcommand "$MATCH_1" "$MATCH_2" \ + destroy "$MATCH_4" "$(cat "$DATADIR/saved-tokens/$MATCH_5")" \ + < /dev/null >"$DATADIR/stdout" 2>"$DATADIR/stderr"; then test "$MATCH_3" = "" fi @@ -89,42 +94,45 @@ ---------------------------------------------------------- IMPLEMENTS THEN server-side ([^ ]+) file ([^ ]+) contains (.+) - cd "$($GTT serverlocation $MATCH_1)" + cd "$($GTT serverlocation "$MATCH_1")" grep -q "$MATCH_3" "$MATCH_2" IMPLEMENTS THEN server-side ([^ ]+) file ([^ ]+) exists - cd "$($GTT serverlocation $MATCH_1)" + cd "$($GTT serverlocation "$MATCH_1")" test -e "$MATCH_2" IMPLEMENTS THEN server-side ([^ ]+) has identical refs to ([^ ]+) bash -c 'diff -u <(git ls-remote -ht "$($GTT serverlocation "$MATCH_1")" | sort -k2) <(git ls-remote -ht "$($GTT serverlocation "$MATCH_2")" | sort -k2)' IMPLEMENTS THEN server-side ([^ ]+) has no missing objects - cd "$($GTT serverlocation $MATCH_1)".git + cd "$($GTT serverlocation "$MATCH_1")".git git fsck + IMPLEMENTS WHEN server-side ([^ ]+) reads git object (.+) + cd "$($GTT serverlocation "$MATCH_1")".git + git cat-file -p "$MATCH_2" >"$DATADIR/stdout" 2>"$DATADIR/stderr" + Clone manipulation ------------------ IMPLEMENTS THEN ([a-z][a-z0-9]*) has a clone of ([^ ]+) - $GTT cloneexists $MATCH_1 "$MATCH_2" + $GTT cloneexists "$MATCH_1" "$MATCH_2" IMPLEMENTS WHEN git pull happens in ([a-z][a-z0-9]*) ([^ ]+) - cd "$($GTT clonelocation $MATCH_1 "$MATCH_2")" - git pull + $GTT rungit "$MATCH_1" "$MATCH_2" pull IMPLEMENTS THEN ([a-z][a-z0-9]*) ([^ ]+) has a file called (.+) - cd "$($GTT clonelocation $MATCH_1 "$MATCH_2")" + cd "$($GTT clonelocation "$MATCH_1" "$MATCH_2")" test -r "$MATCH_3" IMPLEMENTS THEN ([a-z][a-z0-9]*) ([^ ]+) has no file called (.+) set -x - cd "$($GTT clonelocation $MATCH_1 "$MATCH_2")" + cd "$($GTT clonelocation "$MATCH_1" "$MATCH_2")" if test -r "$MATCH_3"; then false; else true; fi IMPLEMENTS WHEN ([a-z][a-z0-9]*) ([a-z][a-z0-9]*) uses git archive to extract the tree of ([a-z][a-z0-9-]*) ([a-z][a-z0-9]*) to ([a-z][a-z0-9]*) mkdir -p "$DATADIR/$MATCH_5" - $GTT gitarchive $MATCH_1 $MATCH_2 $MATCH_3 $MATCH_4 | tar -C "$DATADIR/$MATCH_5" -x + $GTT gitarchive "$MATCH_1" "$MATCH_2" "$MATCH_3" "$MATCH_4" | tar -C "$DATADIR/$MATCH_5" -x rsync manipulation ------------------ @@ -133,112 +141,144 @@ test "x$GTT_PROTO" = "xssh" IMPLEMENTS WHEN ([a-z][a-z0-9]*) ([a-z][a-z0-9]*) rsync'?s (.*) to (.*) - rsync -I --rsh="$GTT rsh $MATCH_1 $MATCH_2" "$DATADIR/$MATCH_3" "dummy:$MATCH_4/$MATCH_3" + rsync -I --rsh="$GTT rsh \"$MATCH_1\" \"$MATCH_2\"" "$DATADIR/$MATCH_3" "dummy:$MATCH_4/$MATCH_3" IMPLEMENTS WHEN ([a-z][a-z0-9]*) ([a-z][a-z0-9]*) rsync'?s (.*) from (.*) - rsync -I --rsh="$GTT rsh $MATCH_1 $MATCH_2" "dummy:$MATCH_4/$MATCH_3" "$DATADIR/$MATCH_3" + rsync -I --rsh="$GTT rsh \"$MATCH_1\" \"$MATCH_2\"" "dummy:$MATCH_4/$MATCH_3" "$DATADIR/$MATCH_3" Admin repo manipulation ----------------------- IMPLEMENTS WHEN ([a-z][a-z0-9]*),? using ([a-z][a-z0-9]*),? adds user ([a-z][a-z0-9]*),? using ([a-z][a-z0-9]*) ([a-z][a-z0-9]*) - $GTT runcommand $MATCH_1 $MATCH_2 \ - user add $MATCH_3 $MATCH_3@testinstance "$MATCH_3's real name" > $DATADIR/stdout 2> $DATADIR/stderr - $GTT runcommand $MATCH_1 $MATCH_2 \ - as $MATCH_3 sshkey add default < \ - $($GTT pubkeyfilename $MATCH_4 $MATCH_5) >> $DATADIR/stdout 2>> $DATADIR/stderr + $GTT runcommand "$MATCH_1" "$MATCH_2" \ + user add "$MATCH_3" "$MATCH_3"@testinstance "$MATCH_3's real name" < "/dev/null" > "$DATADIR/stdout" 2> "$DATADIR/stderr" + $GTT runcommand "$MATCH_1" "$MATCH_2" \ + as "$MATCH_3" sshkey add default < \ + "$($GTT pubkeyfilename "$MATCH_4" "$MATCH_5")" >> "$DATADIR/stdout" 2>> "$DATADIR/stderr" if [ "$GTT_PROTO" = http ]; then - printf "%s" "$MATCH_3" | GTT_PROTO=ssh $GTT runcommand \ - $MATCH_1 $MATCH_2 as $MATCH_3 passwd \ - >> $DATADIR/stdout 2>> $DATADIR/stderr + printf "%s" $($GTT getpasswd "$MATCH_4") | GTT_PROTO=ssh $GTT runcommand \ + "$MATCH_1" "$MATCH_2" as "$MATCH_3" passwd \ + >> "$DATADIR/stdout" 2>> "$DATADIR/stderr" fi IMPLEMENTS WHEN ([a-z][a-z0-9]*),? using ([a-z][a-z0-9]*),? deletes user ([a-z][a-z0-9]*) - TOKEN=$($GTT runcommand $MATCH_1 $MATCH_2 user del $MATCH_3 2>&1 | $GTT findtoken) - $GTT runcommand $MATCH_1 $MATCH_2 user del $MATCH_3 $TOKEN + TOKEN="$($GTT runcommand "$MATCH_1" "$MATCH_2" user del "$MATCH_3" 2>&1 < /dev/null | $GTT findtoken)" + $GTT runcommand "$MATCH_1" "$MATCH_2" user del "$MATCH_3" $TOKEN "$DATADIR/stdout" 2>"$DATADIR/stderr" + + IMPLEMENTS GIVEN ([a-z][a-z0-9]*),? using ([a-z][a-z0-9]*),? has patched gitano-admin with (the reverse of )?([^ ]+) + $GTT clone "$MATCH_1" "$MATCH_2" gitano-admin.git tmp-adminpatch \ + >"$DATADIR/stdout" 2>"$DATADIR/stderr" + if test "$MATCH_3" = ""; then + $GTT rungit "$MATCH_1" tmp-adminpatch apply -v --cached - <"testing/admin-patches/$MATCH_4" \ + >>"$DATADIR/stdout" 2>>"$DATADIR/stderr" + else + $GTT rungit "$MATCH_1" tmp-adminpatch apply -v --cached --reverse - <"testing/admin-patches/$MATCH_4" \ + >>"$DATADIR/stdout" 2>>"$DATADIR/stderr" + fi + $GTT rungit "$MATCH_1" tmp-adminpatch diff --cached \ + >>"$DATADIR/stdout" 2>>"$DATADIR/stderr" + $GTT rungit "$MATCH_1" tmp-adminpatch commit --allow-empty -m "Apply $MATCH_4 rules change" \ + >>"$DATADIR/stdout" 2>>"$DATADIR/stderr" + $GTT rungit "$MATCH_1" tmp-adminpatch show HEAD \ + >>"$DATADIR/stdout" 2>>"$DATADIR/stderr" + $GTT push "$MATCH_1" "$MATCH_2" tmp-adminpatch gitano-admin.git \ + >>"$DATADIR/stdout" 2>>"$DATADIR/stderr" + rm -r "$($GTT clonelocation "$MATCH_1" tmp-adminpatch)" \ + >>"$DATADIR/stdout" 2>>"$DATADIR/stderr" + + IMPLEMENTS WHEN ([a-z][a-z0-9]*),? using ([a-z][a-z0-9]*),? (expecting failure,? )?pushes an empty commit in ([^ ]+) + $GTT rungit "$MATCH_1" "$MATCH_4" commit --allow-empty -m "Make an empty commit" + if $GTT push "$MATCH_1" "$MATCH_2" "$MATCH_4" "$MATCH_4".git > "$DATADIR/stdout" 2>"$DATADIR/stderr"; then + test "$MATCH_3" = "" + fi + + IMPLEMENTS WHEN ([a-z][a-z0-9]*) (applies|reverts) ([^ ]+) in ([^ ]+) + if test "$MATCH_2" = "applies"; then + $GTT rungit "$MATCH_1" "$MATCH_4" apply --cached - <"testing/content-patches/$MATCH_3" \ + >"$DATADIR/stdout" 2>"$DATADIR/stderr" + else + $GTT rungit "$MATCH_1" "$MATCH_4" apply --cached --reverse - <"testing/content-patches/$MATCH_3" \ + >"$DATADIR/stdout" 2>"$DATADIR/stderr" + fi + $GTT rungit "$MATCH_1" "$MATCH_4" commit --allow-empty -m "Apply $MATCH_3 content change" \ + >>"$DATADIR/stdout" 2>>"$DATADIR/stderr" - IMPLEMENTS GIVEN ([a-z][a-z0-9]*),? using ([a-z][a-z0-9]*),? has patched gitano-admin with ([^ ]+) - $GTT clone $MATCH_1 $MATCH_2 gitano-admin.git tmp-adminpatch - $GTT rungit $MATCH_1 tmp-adminpatch apply --cached - <"testing/admin-patches/$MATCH_3" - $GTT rungit $MATCH_1 tmp-adminpatch commit --allow-empty -m "Apply $MATCH_3 rules change" - $GTT push $MATCH_1 $MATCH_2 tmp-adminpatch gitano-admin.git - rm -r "$($GTT clonelocation $MATCH_1 tmp-adminpatch)" - - IMPLEMENTS WHEN ([a-z][a-z0-9]*),? using ([a-z][a-z0-9]*),? pushes an empty commit in ([^ ]+) - $GTT rungit $MATCH_1 $MATCH_3 commit --allow-empty -m "Make an empty commit" - $GTT push $MATCH_1 $MATCH_2 $MATCH_3 gitano-admin.git > $DATADIR/stdout 2>$DATADIR/stderr + IMPLEMENTS WHEN ([a-z][a-z0-9]*) amends ([^ ]+) with (.+) + $GTT rungit "$MATCH_1" "$MATCH_2" commit --amend -m "$MATCH_3" Specific commands ----------------- IMPLEMENTS GIVEN ([a-z][a-z0-9]*) ([a-z][a-z0-9]*) has copied ([^ ]+) to ([^ ]+) - $GTT runcommand $MATCH_1 $MATCH_2 copy $MATCH_3 $MATCH_4 + $GTT runcommand "$MATCH_1" "$MATCH_2" copy "$MATCH_3" "$MATCH_4" "$DATADIR/stdout" 2>"$DATADIR/stderr" IMPLEMENTS GIVEN ([a-z][a-z0-9]*) ([a-z][a-z0-9]*) has set the owner of ([^ ]+) to ([a-z][a-z0-9]*) - $GTT runcommand $MATCH_1 $MATCH_2 config $MATCH_3 set project.owner $MATCH_4 + $GTT runcommand "$MATCH_1" "$MATCH_2" config "$MATCH_3" set project.owner "$MATCH_4" "$DATADIR/stdout" 2>"$DATADIR/stderr" IMPLEMENTS WHEN ([a-z][a-z0-9]*) ([a-z][a-z0-9]*) restores the latest deletion to (.+) - $GTT runcommand $MATCH_1 $MATCH_2 graveyard list >$DATADIR/stdout 2>$DATADIR/stderr - reponame="$(head -n1 $DATADIR/stdout | cut -d' ' -f2)" - $GTT runcommand $MATCH_1 $MATCH_2 graveyard restore "$reponame" "$MATCH_3" + $GTT runcommand "$MATCH_1" "$MATCH_2" graveyard list "$DATADIR/stdout" 2>"$DATADIR/stderr" + reponame="$(head -n1 "$DATADIR/stdout" | cut -d' ' -f2)" + $GTT runcommand "$MATCH_1" "$MATCH_2" graveyard restore "$reponame" "$MATCH_3" $DATADIR/stdout 2>$DATADIR/stderr - reponame="$(head -n1 $DATADIR/stderr | cut -d' ' -f2)" - $GTT runcommand $MATCH_1 $MATCH_2 graveyard purge "$reponame" + $GTT runcommand "$MATCH_1" "$MATCH_2" graveyard list "$DATADIR/stdout" 2>"$DATADIR/stderr" + reponame="$(head -n1 "$DATADIR/stderr" | cut -d' ' -f2)" + $GTT runcommand "$MATCH_1" "$MATCH_2" graveyard purge "$reponame" $DATADIR/stdout 2> $DATADIR/stderr - rm -f $DATADIR/stdin + if ! test -e "$DATADIR/stdin"; then touch "$DATADIR/stdin"; fi + $GTT runcommand "$MATCH_1" "$MATCH_2" $MATCH_3 < "$DATADIR/stdin" > "$DATADIR/stdout" 2> "$DATADIR/stderr" + rm -f "$DATADIR/stdin" IMPLEMENTS WHEN ([a-z][a-z0-9]*) ([a-z][a-z0-9]*),? expecting failure,? runs ?(.*) - if ! test -e $DATADIR/stdin; then touch $DATADIR/stdin; fi - if $GTT runcommand $MATCH_1 $MATCH_2 $MATCH_3 > $DATADIR/stdout 2> $DATADIR/stderr; then + if ! test -e "$DATADIR/stdin"; then touch "$DATADIR/stdin"; fi + if $GTT runcommand "$MATCH_1" "$MATCH_2" $MATCH_3 < "$DATADIR/stdin" > "$DATADIR/stdout" 2> "$DATADIR/stderr"; then false fi - rm -f $DATADIR/stdin + rm -f "$DATADIR/stdin" IMPLEMENTS GIVEN ([^ ]+) contains (.+) printf %s "$MATCH_2" >"$DATADIR/$MATCH_1" IMPLEMENTS THEN ([^ ]+) contains (.+) - grep -q "$MATCH_2" $DATADIR/"$MATCH_1" + grep -q "$MATCH_2" "$DATADIR/$MATCH_1" IMPLEMENTS THEN the output contains (.+) - grep -q "$MATCH_1" $DATADIR/stdout $DATADIR/stderr + grep -q "$MATCH_1" "$DATADIR/stdout" "$DATADIR/stderr" IMPLEMENTS THEN ([^ ]+) does not contain (.+) - if grep -q "$MATCH_2" $DATADIR/"$MATCH_1"; then false; else true; fi + if grep -q "$MATCH_2" "$DATADIR/$MATCH_1"; then false; else true; fi IMPLEMENTS THEN the output does not contain (.+) - if grep -q "$MATCH_1" $DATADIR/stdout $DATADIR/stderr; then false; else true; fi + if grep -q "$MATCH_1" "$DATADIR/stdout" "$DATADIR/stderr"; then false; else true; fi IMPLEMENTS THEN ([^ ]+) is empty - if grep -q . $DATADIR/"$MATCH_1"; then false; fi + if grep -q . "$DATADIR/$MATCH_1"; then false; fi IMPLEMENTS THEN ([^ ]+) is not empty - grep -q . $DATADIR/"$MATCH_1" + grep -q . "$DATADIR/$MATCH_1" IMPLEMENTS THEN the output is empty - if grep -q . $DATADIR/stdout $DATADIR/stderr; then false; else true; fi + if grep -q . "$DATADIR/stdout" "$DATADIR/stderr"; then false; else true; fi IMPLEMENTS THEN the output is not empty - grep -q . $DATADIR/stdout $DATADIR/stderr + grep -q . "$DATADIR/stdout" "$DATADIR/stderr" IMPLEMENTS GIVEN the token is saved as (.+) mkdir -p "$DATADIR/saved-tokens" cat "$DATADIR/stdout" "$DATADIR/stderr" | $GTT findtoken >"$DATADIR/saved-tokens/$MATCH_1" IMPLEMENTS THEN failure ensues - cd $DATADIR + cd "$DATADIR" echo "FIND:" find . echo "KEYS:" cat user-home-testinstance/.ssh/authorized_keys + echo "IN": + if test -r stdin; then cat stdin; fi echo "OUT": if test -r stdout; then cat stdout; fi echo "ERR": @@ -250,6 +290,12 @@ IMPLEMENTS ASSUMING gitano is being accessed over ([^ ]+) test "$GTT_PROTO" = "$MATCH_1" + IMPLEMENTS GIVEN ([^ ]+) is in the environment set to (.+) + $GTT setenv "$MATCH_1" "$MATCH_2" + + IMPLEMENTS GIVEN ([^ ]+) is not in the environment + $GTT unsetenv "$MATCH_1" + GPG Keyring related helpers --------------------------- @@ -258,4 +304,4 @@ `DATADIR`. IMPLEMENTS GIVEN gpg key ([0-9A-Fa-f]+) on stdin - $GTT gpg --armor --export $MATCH_1 > $DATADIR/stdin + $GTT gpg --armor --export "$MATCH_1" > "$DATADIR/stdin" diff -Nru gitano-1.0/TESTING gitano-1.1/TESTING --- gitano-1.0/TESTING 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/TESTING 2017-08-03 15:11:42.000000000 +0000 @@ -104,7 +104,7 @@ * `admin-patches/`: This directory contains patches to the gitano-admin.git which can be applied during tests. If you change the skeleton ruleset (or otherwise) for Gitano, you may need to refresh these patches. - + * `library.yarn`: This contains the implementations for the test suite yarn statements. @@ -113,7 +113,7 @@ * `02_*.yarn`: These tests are focussed around validating the commands specifically. The test might not use only the command in question, but that is their goal. - + * `03_*.yarn` These tests are more integration tests which look to simulate more complex user use-cases. @@ -274,3 +274,50 @@ and depends on the test suite coverage to produce sufficient results. Making use of this also requires the penlight library to be installed. + +Obtaining coverage statistics +============================= + +When writing tests, one often wishes to know that ones test has improved the +coverage of the codebase. To that end, the build system for Gitano has support +for using luacov to generate and report coverage statistics. + +In order to use this, you must first ensure that you have the submodules +present: + + $ git submodule init + $ git submodule update + +Next, to gather coverage data run: + + $ make COVERAGE=yes test + +You can specify `SCENARIO=blahfoo`, or `YARN_ARGS=...` etc, on the command line +if you wish. If you do not specify `COVERAGE=yes` then the tests will be run +without augmenting the coverage data. + +Coverage data is aggregated across multiple runs. This allows you to run only +specific scenarios, and still see aggregated coverage across the codebase. + +To generate the coverage report, run: + + $ make coverage-report + +This will merge all the aggregated coverage data into a single file, and then +will run the luacov tool to generate `luacov.report.out` which contains the +report. Finally it will display a summary of the coverage for the relevant +parts of the codebase. Some of the codebase, as well as anything in +`/usr/share` will be ignored by the report generator. + +If you find that something which should be included is not, or something which +should be excluded is not, then adjust the `.luacov` file which is the +configuration for the coverage tool. This is also loaded by the internal +in-tree-only module `lib/gitano/coverage.lua` which is generated at build time +from `lib/gitano/coverage.lua.in`. + +If you are trying to get coverage for the `gitano-test-tool` binary in the +`testing` directory then you will need to **also** pass `COVER_GTT=yes` when +running the test suite. This distinction is present since around two thirds of +all invocations of coverable code in the test suite uses the test tool to +perform some related task, meaning that covering it causes a significant +slowdown in testing, and increases the cost of generating the coverage report. diff -Nru gitano-1.0/utils/install-lua-bin gitano-1.1/utils/install-lua-bin --- gitano-1.0/utils/install-lua-bin 2017-01-15 16:18:19.000000000 +0000 +++ gitano-1.1/utils/install-lua-bin 2017-08-03 15:11:42.000000000 +0000 @@ -27,7 +27,7 @@ -- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -- SUCH DAMAGE. -local lua_bin, inst_share_path, inst_bin_path, inst_mod_path, inst_plugin_path, input_name, output_name = ... +local lua_bin, inst_share_path, inst_bin_path, inst_mod_path, inst_plugin_path, input_name, output_name, coverage_path, report_path = ... local input_fh = assert(io.open(input_name, "r")) local output_fh = assert(io.open(output_name, "w")) @@ -39,6 +39,22 @@ inst_mod_path = inst_mod_path:gsub("/+", "/") end +if coverage_path and coverage_path == "" then + coverage_path = nil +end + +if coverage_path and not coverage_path:match("%?") then + coverage_path = coverage_path .. "/?.lua" + coverage_path = coverage_path:gsub("/+", "/") +end + +local coverage_part +if coverage_path then + -- We are generating coverage, the part is the leafname of the + -- output binary + coverage_part = output_name:gsub("^.+/", "") +end + local mod_path_present = false for path_elem in package.path:gmatch("([^;]+)") do if path_elem == inst_mod_path then @@ -65,12 +81,20 @@ elseif token == "GITANO_LUA_PATH" then if not mod_path_present then output_fh:write(("package.path = ('%%s;%%s'):" .. - "format(%q, package.path)" .. - "\n"):format(inst_mod_path)) + "format(%q, package.path)" + ):format(inst_mod_path)) else - output_fh:write("-- Gitano modules installed into " .. - inst_mod_path .. "\n") + output_fh:write("--[[Gitano modules installed into " .. + inst_mod_path .. "]]") + end + if coverage_path then + output_fh:write((" package.path = ('%%s;%%s'):" .. + "format(%q, package.path)" + ):format(coverage_path)) + output_fh:write((" require('gitano.coverage').begin(%q, %q)" + ):format(report_path, coverage_part)) end + output_fh:write("\n") elseif token == "GITANO_BIN_PATH" then output_fh:write(("gitano.config.lib_bin_path(%q)\n"):format(inst_bin_path)) elseif token == "GITANO_SHARE_PATH" then @@ -82,6 +106,11 @@ else output_fh:write("-- Unknown token: " .. token .. "\n") end + elseif line:match("@@%.luacov@@") then + local handle = io.popen('pwd') + local cwd = handle:read("*all"):sub(0, -2) + output_fh:write(line:gsub("@@%.luacov@@", cwd .. "/.luacov") .. "\n") + handle:close() else output_fh:write(line .. "\n") end diff -Nru gitano-1.0/utils/merge-luacov-stats gitano-1.1/utils/merge-luacov-stats --- gitano-1.0/utils/merge-luacov-stats 1970-01-01 00:00:00.000000000 +0000 +++ gitano-1.1/utils/merge-luacov-stats 2017-08-03 15:11:42.000000000 +0000 @@ -0,0 +1,104 @@ +-- Run this explicitly through -*- Lua -*- + +-- Copyright 2017 Daniel Silverstone +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions +-- are met: +-- 1. Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- 2. Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- 3. Neither the name of the author nor the names of their contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +-- ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +-- SUCH DAMAGE. + +local infiles = {...} + +local stats = require("luacov.stats") + +local instpfx = assert(io.popen("pwd", "r")):read("*l") +if instpfx:sub(-1) ~= "/" then instpfx = instpfx .. "/" end +instpfx = instpfx .. "testing/inst/" + +local inst_prefix_map = { + { "etc/gitano/plugins/", "plugins/" }, + { "lib/gitano/plugins/", "plugins/" }, + { "lib/gitano/bin/../../../bin/", "bin/", ".in" }, + { "lib/gitano/bin/", "bin/", ".in" }, + { "share/lua/5.1/", "lib/" }, +} + +local noninst_prefix_map = { + { "hooks/", "bin/gitano-", "-hook.in" }, +} + +local function remap_prefixes(fname, map) + for _, mapentry in ipairs(map) do + local pfx, replace, suffix = mapentry[1], mapentry[2], mapentry[3] + if fname:sub(1, #pfx) == pfx then + fname = replace .. fname:sub(#pfx+1, -1) + if suffix then + fname = fname .. suffix + end + end + end + return fname +end + +local function remap_fname(fname) + if fname:sub(1, #instpfx) ~= instpfx then + return remap_prefixes(fname, noninst_prefix_map) + end + fname = fname:sub(#instpfx+1, -1) + return remap_prefixes(fname, inst_prefix_map) +end + +-- Step 1, merge all the coverage files together into a single one... + +local merged = {} + +function merge_data(fname) + local subdata = stats.load(fname) + for fname, fdat in pairs(subdata) do + fname = remap_fname(fname) + if not merged[fname] then + merged[fname] = fdat + else + for i = 1, fdat.max do + merged[fname][i] = (merged[fname][i] or 0) + (fdat[i] or 0) + end + end + end +end + +local percent = 0 +for i = 1, #infiles do + merge_data(infiles[i]) + local newpercent = math.floor((i*100) / #infiles) + if (newpercent - percent > 0) or (newpercent == 100) then + io.stdout:write(("COVERAGE: %3d%% merged.\r"):format(newpercent)) + percent = newpercent + io.stdout:flush(); + end +end +io.stdout:write("\n"); + +-- Step 2, write it all out + +print("Saving merged stats") +stats.save("luacov.stats.out", merged)