diff options
-rw-r--r-- | Makefile | 14 | ||||
-rw-r--r-- | bin/gitano-post-receive-hook.in | 130 | ||||
-rw-r--r-- | lang/en.lua | 3 | ||||
-rw-r--r-- | lib/gitano.lua | 7 | ||||
-rw-r--r-- | lib/gitano/auth.lua | 18 | ||||
-rw-r--r-- | lib/gitano/hooks.lua | 137 | ||||
-rw-r--r-- | plugins/testing-hooks.lua | 76 | ||||
-rw-r--r-- | testing/01-hooks.yarn | 73 | ||||
-rw-r--r-- | testing/gitano-test-tool.in | 34 | ||||
-rw-r--r-- | testing/library.yarn | 6 |
10 files changed, 429 insertions, 69 deletions
@@ -32,13 +32,13 @@ BINS := gitano-setup 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 \ +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 diff --git a/bin/gitano-post-receive-hook.in b/bin/gitano-post-receive-hook.in index ad77da2..cb77e06 100644 --- a/bin/gitano-post-receive-hook.in +++ b/bin/gitano-post-receive-hook.in @@ -141,71 +141,83 @@ local function report_repo(reponame, repo, msg) 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 - - 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) +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 - 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) +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 -if repo:uses_hook("post-receive") then - gitano.log.debug("Configuring for post-receive hook") - gitano.actions.set_supple_globals("post-receive") +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) - 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 --git a/lang/en.lua b/lang/en.lua index ff69968..96af43b 100644 --- a/lang/en.lua +++ b/lang/en.lua @@ -162,7 +162,8 @@ example administration repository rules and an admin user and group. 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 --git a/lib/gitano.lua b/lib/gitano.lua index 38c22ef..5354a0f 100644 --- a/lib/gitano.lua +++ b/lib/gitano.lua @@ -41,9 +41,13 @@ local supple = require 'gitano.supple' local auth = require 'gitano.auth' local plugins = require 'gitano.plugins' local i18n = require 'gitano.i18n' +<<<<<<< HEAD local patterns = require 'gitano.patterns' +======= +local hooks = require 'gitano.hooks' +>>>>>>> dsilvers/hooks -local _VERSION = {1, 0, 0} +local _VERSION = {1, 1, 0} _VERSION.major = _VERSION[1] _VERSION.minor = _VERSION[2] _VERSION.patch = _VERSION[3] @@ -70,4 +74,5 @@ return { plugins = plugins, i18n = i18n, patterns = patterns, + hooks = hooks, } diff --git a/lib/gitano/auth.lua b/lib/gitano/auth.lua index c5a1095..bf3260f 100644 --- a/lib/gitano/auth.lua +++ b/lib/gitano/auth.lua @@ -37,6 +37,7 @@ local log = require 'gitano.log' 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 @@ local function is_authorized(user, source, cmdline, repo_root, 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 --git a/lib/gitano/hooks.lua b/lib/gitano/hooks.lua new file mode 100644 index 0000000..0c3cba9 --- /dev/null +++ b/lib/gitano/hooks.lua @@ -0,0 +1,137 @@ +-- gitano.hooks +-- +-- Hook management routines for Gitano +-- +-- Copyright 2017 Daniel Silverstone <dsilvers@digital-scurf.org> +-- 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 --git a/plugins/testing-hooks.lua b/plugins/testing-hooks.lua new file mode 100644 index 0000000..f428001 --- /dev/null +++ b/plugins/testing-hooks.lua @@ -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 <daniel.silverstone@digital-scurf.org> +-- 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 --git a/testing/01-hooks.yarn b/testing/01-hooks.yarn new file mode 100644 index 0000000..b3e4413 --- /dev/null +++ b/testing/01-hooks.yarn @@ -0,0 +1,73 @@ +<!-- -*- markdown -*- --> +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 + 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 --git a/testing/gitano-test-tool.in b/testing/gitano-test-tool.in index f467918..1fd39b0 100644 --- a/testing/gitano-test-tool.in +++ b/testing/gitano-test-tool.in @@ -70,10 +70,28 @@ local function unix_assert(ret, errno) 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(...) @@ -180,6 +198,20 @@ function cmd_setgitconfig(username, key, value) } 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")) diff --git a/testing/library.yarn b/testing/library.yarn index 6facf2a..a370bfe 100644 --- a/testing/library.yarn +++ b/testing/library.yarn @@ -283,6 +283,12 @@ Generic utility methods 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 --------------------------- |