-- gitano.command
--
-- Gitano command processing
--
-- Copyright 2012-2019 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 log = require 'gitano.log'
local util = require 'gitano.util'
local repository = require 'gitano.repository'
local pattern = require 'gitano.patterns'

local sio = require "luxio.simple"

local cmds = {}

local function default_detect_repo(config, parsed_cmdline)
   local repo, msg
   if #parsed_cmdline > 1 then
      -- Acquire the repository object for the target repo from arg 2
      repo, msg = repository.find(config, parsed_cmdline[2])
      if not repo then
         log.critical("Unable to locate repository.")
         log.critical("  * " .. (tostring(msg)))
         return nil, nil
      end

      if repo.is_nascent then
         log.info("Repository " .. repo.name .. " is nascent")
      end
   else
      log.critical("No repository provided.")
      return nil, nil
   end
   return repo, parsed_cmdline
end

local function register_cmd(cmdname, short, helptext,
                            validate_fn, prep_fn, run_fn,
                            takes_repo, hidden, is_admin, detect_repo,
                            suppress_error_msgs)
   --[[
      log.ddebug("Register command", cmdname)
      if takes_repo then
      log.ddebug("  =>  Takes a repo")
      end
   --]]
   if cmds[cmdname] then
      log.warn("Attempt to double-register", cmdname)
      return false, "Attempt to double-register " .. cmdname
   end
   cmds[cmdname] = {
      name = cmdname,
      validate = validate_fn,
      prep = prep_fn,
      run = run_fn,
      takes_repo = takes_repo,
      hidden = hidden,
      admin = is_admin,
      short = short,
      helptext = helptext,
      suppress_error_msgs = suppress_error_msgs,
      detect_repo = detect_repo or default_detect_repo
   }
   cmds[#cmds+1] = cmdname
   table.sort(cmds)
   return true
end

local function get_cmd(cmdname)
   local cmd = cmds[cmdname]
   if not cmd then
      return nil, "Unknown command " .. cmdname
   end
   return {
      validate = cmd.validate,
      prep = cmd.prep,
      run = cmd.run,
      takes_repo = cmd.takes_repo,
      detect_repo = cmd.detect_repo,
      suppress_error_msgs = cmd.suppress_error_msgs,
      name = cmd.name,
   }
end

local builtin_version_short = "Get the Gitano version"
local builtin_version_helptext = [[
usage: version [machine]

Without an argument, display the version and basic copyright information for
Gitano in a form which a human can parse.

With the 'machine' command, display the Gitano version as a machine-parseable
list of values instead.

**NOTE**: the format of the machine-parseable output is not guaranteed stable
at this time.
]]

local function builtin_version_validate(config, _, cmdline)
   if #cmdline > 2 then
      log.error("usage: version [machine]")
      return false
   end
   if #cmdline == 2 and cmdline[2] ~= "machine" then
      log.error("usage: version [machine]")
      return false
   end
   return true
end

local function builtin_version_prep(config, _, cmdline, context)
   return "allow", "Always allowed to ask for the version"
end

local function builtin_version_run(config, _, cmdline, env)
   -- We do this lazily because otherwise circular includes get in our way
   local _gitano = require "gitano"
   if #cmdline == 1 then
      log.state(_gitano.VERSION_STRING)
      log.state(_gitano.COPYRIGHT)
   else
      -- Machine-parseable version
      log.state(("VERSION:%d.%d.%d"):format(
	    _gitano.VERSION.major,
	    _gitano.VERSION.minor,
	    _gitano.VERSION.patch))
      log.state(("MAJOR:%d"):format(_gitano.VERSION.major))
      log.state(("MINOR:%d"):format(_gitano.VERSION.minor))
      log.state(("PATCH:%d"):format(_gitano.VERSION.patch))
   end
   return "exit", 0
end

assert(register_cmd("version", builtin_version_short, builtin_version_helptext,
                    builtin_version_validate, builtin_version_prep,
                    builtin_version_run, false, false))

local builtin_help_short = "Ask for help"
local builtin_help_helptext = [[
usage: help [admin|command]

Without the command argument, lists all visible commands.  With the
command argument, provides detailed help about the given command.

If the command argument is specifically 'admin' then list the
admin commands instead of the normal commands.  If it is 'all' then
list all the commands, even the hidden commands.
]]

local function builtin_help_validate(config, repo, cmdline)
   if #cmdline > 2 then
      log.error("usage: help [admin|command]")
      return false
   end
   if #cmdline == 2 then
      if cmdline[2] == "all" or cmdline[2] == "admin" then
         return true
      end
      if not cmds[cmdline[2]] then
         log.error("Unknown command:", help)
         return false
      end
   end
   return true
end

local function builtin_help_prep(config, repo, cmdline, context)
   return "allow", "Always allowed to ask for help"
end

local function builtin_help_run(config, repo, cmdline, env)
   local function do_want(cmd)
      if cmdline[2] == "all" then
         return true
      end
      if cmdline[2] == "admin" then
         return cmd.admin
      end
      return not (cmd.hidden or cmd.admin)
   end
   local function do_sep(cmd)
      local first = cmd.hidden and "-H" or "--"
      local second = cmd.admin and "A-" or "--"
      return first .. second
   end
   if #cmdline == 1 or cmdline[2] == "admin" or cmdline[2] == "all" then
      -- List all commands
      local maxcmdn = 0
      for i = 1, #cmds do
         local cmd = cmds[cmds[i]]
         local wanted = do_want(cmd)
         if wanted then
            if #cmd.name > maxcmdn then
               maxcmdn = #cmd.name
            end
         end
      end
      for i = 1, #cmds do
         local cmd = cmds[cmds[i]]
         local wanted = do_want(cmd)
         if wanted then
            local gap = (" "):rep(maxcmdn - #cmd.name)
            local desc = (cmd.short or "No description")
            if cmd.takes_repo then
               desc = desc .. " (Takes a repo)"
            end
            log.state(gap .. cmd.name, do_sep(cmd), desc)
         end
      end
   else
      local cmd = cmds[cmdline[2]]
      local desc = (cmd.short or "No description")
      if cmd.takes_repo then
         desc = desc .. " (Takes a repo)"
      end
      log.state(cmd.name, do_sep(cmd), desc)
      if cmd.helptext then
         log.state("")
         for line in (cmd.helptext):gmatch(pattern.TEXT_LINE) do
            log.state("=>", line)
         end
      end
   end
   return "exit", 0
end

assert(register_cmd("help", builtin_help_short, builtin_help_helptext,
                    builtin_help_validate, builtin_help_prep,
                    builtin_help_run, false, false))

local function builtin_upload_pack_validate(config, repo, cmdline)
   -- git-upload-pack repo
   if #cmdline > 2 then
      return false
   end
   cmdline[2] = repo:fs_path()
   return true
end

local function builtin_upload_pack_prep(config, repo, cmdline, context)
   if repo.is_nascent then
      return "deny", "Repository " .. repo.name .. " does not exist"
   end
   -- git-upload-pack is always a read operation
   context.operation = "read"
   return repo:run_lace(context)
end

local function builtin_upload_pack_run(config, repo, cmdline, env)
   local cmdcopy = {"upload-pack", env=env}
   for i = 2, #cmdline do cmdcopy[i] = cmdline[i] end
   return repo:git_command(cmdcopy)
end

assert(register_cmd("git-upload-pack", nil, nil,
                    builtin_upload_pack_validate,
                    builtin_upload_pack_prep,
                    builtin_upload_pack_run,
                    true, true))

local function builtin_receive_pack_validate(config, repo, cmdline)
   -- git-receive-pack repo
   if #cmdline > 2 then
      return false
   end
   cmdline[2] = repo:fs_path()
   return true
end

local function builtin_receive_pack_prep(config, repo, cmdline, context)
   if repo.is_nascent then
      return "deny", "Repository " .. repo.name .. " does not exist"
   end
   -- git-receive-pack is always a simple write operation
   context.operation = "write"
   return repo:run_lace(context)
end

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
   return repo:git_command(cmdcopy)
end

assert(register_cmd("git-receive-pack", nil, nil,
                    builtin_receive_pack_validate,
                    builtin_receive_pack_prep,
                    builtin_receive_pack_run,
                    true, true))

local builtin_create_short = "Create a new repository"
local builtin_create_helptext = [[
usage: create <reponame> [<owner>]

Create a new repository, optionally setting its owner directly.

In order to create a repository, the site administrators must grant you the
ability in some part of the namespace.  Specifying an owner is equivalent
to creating the repository and then calling config set project.owner to
re-assign it.
]]

local function builtin_create_validate(config, repo, cmdline)
   -- create reponame
   if #cmdline > 3 then
      log.error("usage: create <reponame> [<owner>]")
      return false
   end
   if not repo then
      log.error("No repository?")
      return false
   end
   if repo and not repo.is_nascent then
      log.error("Repository", repo.name, "already exists")
      return false
   end
   return true
end

local function builtin_create_prep(config, repo, cmdline, context)
   context.operation = "createrepo"
   return repo:run_lace(context)
end

local function builtin_create_run(config, repo, cmdline, env)
   -- Realise the repository
   log.chat("Creating repository:", repo.name)
   local ok, msg = repo:realise()
   if not ok then
      log.error(msg)
      return "exit", 1
   end
   local owner = cmdline[3] or env["GITANO_USER"]
   log.chat("Setting repository owner to", owner)
   ok, msg = repo:conf_set_and_save("project.owner", owner,
                                    env.GITANO_USER, env.GITANO_ORIG_USER)
   if not ok then
      log.error(msg)
      return "exit", 1
   end
   log.chat("Running checks to ensure hooks etc are configured")
   ok, msg = repo:run_checks()
   if not ok then
      log.error(msg)
      return "exit", 1
   end
   log.state("Repository", repo.name,
             "created ok.  Remember to configure rules etc.")
   return "exit", 0
end

assert(register_cmd("create", builtin_create_short, builtin_create_helptext,
                    builtin_create_validate, builtin_create_prep,
                    builtin_create_run, true, false))

local builtin_config_short = "View and change configuration for a repository"
local builtin_config_helptext = [[
usage: config <reponame> <cmd> [args...]

View and manipulate the configuration of a repository.

* config <reponame> show [<filter>...]
  List all configuration variables in <reponame> which match any of
  the filters provided.  The filters are prefixes which are matched
  against the keys of the configuration variables.

  For example: `config sampler list project` will list all the
  project configuration entries for the sampler.git repository.

  Keys which represent lists are shown as `foo.*`  If you wish to
  show the detailed key, showing the index of the entry in the list
  then you should set the filter exactly to `foo.*` which will cause
  the show command to expand list keys into the form `foo.i_N` where N
  is the index in the list.

  For example: `config sampler set project.readers.* alice`,
  and `config sampler set project.readers.* bob` will add users alice and
  bob to the `project.readers` list for the sampler.git repository.

* config <reponame> set key value
  Set the given configuration key to the given value.  If the key ends
  in `.*` then the system will add the given value to the end of the
  list represented by the key.  To replace a specific entry, set the
  specific `i_N` entry to the value you want to replace it.

* config <reponame> {del,delete,rm} key
  Removes the given key from the configuration set.  If the key ends in `.*`
  then the system will remove all configuration values below that prefix.
  To remove a specific element of a list, instead, be sure to delete
  the `i_N` entry instead.
]]
local function builtin_config_validate(conf, repo, cmdline)
   if not repo or repo.is_nascent then
      log.error("Cannot access configuration of a nascent repository")
      return false
   end
   if #cmdline < 3 then
      cmdline[3] = "show"
   end
   if cmdline[3] == "show" then
      -- No validation to do yet
   elseif cmdline[3] == "set" then
      if #cmdline < 5 then
         log.error("config <repo> set: takes a key and a value to set")
         return false
      end
      if #cmdline > 5 then
         local cpy = {}
         for i = #cmdline, 5, -1 do
            table.insert(cpy, 1, cmdline[i])
            cmdline[i] = nil
         end
         cmdline[5] = table.concat(cpy, " ")
      end
      if cmdline[4] == "project.owner" then
         -- Verify that the new owner is a valid user
         if not conf.users[cmdline[5]] then
            log.error("Unknown user: " .. cmdline[5])
            return false
         end
      end
   elseif cmdline[3] == "del" or cmdline[3] == "delete" or
   cmdline[3] == "rm" then
      cmdline[3] = "del"
      if #cmdline ~= 4 then
         log.error("config <repo> del: takes a key and nothing more")
         return false
      end
      cmdline.orig_key = cmdline[4]
      if cmdline[4]:match(pattern.CONF_ENDS_WILDCARD) then
         -- Doing a wild removal, expand it now
         local prefix = cmdline[4]:match(pattern.CONF_WILDCARD)
         cmdline[4] = nil
         for k in repo.project_config:each(prefix) do
            cmdline[#cmdline+1] = k
         end
      end
   else
      log.error("Unknown subcommand <" .. cmdline[3] .. "> for config.")
      log.info("Valid subcommands are <show> <set> and <del>")
      return false
   end
   return true
end

local function builtin_config_prep(conf, repo, cmdline, context)
   if cmdline[3] == "show" then
      context.operation = "config_show"
      for i = 4, #cmdline do
         local cpy = util.deep_copy(context)
         cpy.key = cmdline[i]
         local action, reason = repo:run_lace(cpy)
         if action ~= "allow" then
            return action, reason
         end
      end
      return "allow", "Show not denied"
   elseif cmdline[3] == "set" then
      context.operation = "config_set"
      context.key = cmdline[4]
      context.value = cmdline[5]
      return repo:run_lace(context)
   elseif cmdline[3] == "del" then
      context.operation = "config_del"
      for i = 4, #cmdline do
         local cpy = util.deep_copy(context)
         cpy.key = cmdline[i]
         local action, reason = repo:run_lace(cpy)
         if action ~= "allow" then
            return action, reason
         end
      end
      return "allow", "Delete not denied"
   end
   return "deny", "Unknown sub command slipped through"
end

local function builtin_config_run(conf, repo, cmdline, env)
   if cmdline[3] == "show" then
      local all_keys = {}
      if #cmdline == 3 then
         for k in repo.project_config:each() do
            all_keys[k] = true
         end
      else
         for i = 4, #cmdline do
            for k in repo.project_config:each(cmdline[i]) do
               all_keys[k] = true
            end
         end
      end
      -- Transform the all_keys set into a sorted list
      local slist = {}
      for k in pairs(all_keys) do
         slist[#slist+1] = k
      end
      -- TODO: Fix this sort to cope with .i_N keys neatly
      table.sort(slist)
      for i = 1, #slist do
         local key = slist[i]
         local value = repo.project_config.settings[key]
         local prefix = key:match(pattern.CONF_ARRAY_INDEX)
         if prefix then
            local neatkey = prefix .. ".*"
            for i = 4, #cmdline do
               if cmdline[i] == neatkey then
                  neatkey = key
                  break
               end
            end
         end
         log.stdout(key .. ": " .. value)
      end
      if #slist == 0 then
         local keys = {}
         for i = 4, #cmdline do
            keys[#keys+1] = "'" .. cmdline[i] .. "'"
         end
         keys = table.concat(keys, ", ")
         log.error("Configuration keys not found: " .. keys)
         return "exit", 1
      end
   elseif cmdline[3] == "set" then
      local key, value = cmdline[4], cmdline[5]
      local vtype, rest = value:match(pattern.CONF_SET_TYPE_PREFIX)
      if vtype then
         if vtype == "s" then
            value = rest
         end
         if vtype == "i" then
            value = tonumber(rest)
         end
         if vtype == "b" then
            value = ((rest:lower() == "true") or (rest == "1") or
                  (rest:lower() == "on") or (rest:lower() == "yes"))
         end
      end
      repo.project_config.settings[key] = value
      local ok, msg = repo:save_admin("Changed project setting: " .. key,
                                      env.GITANO_USER, env.GITANO_ORIG_USER)
      if not ok then
         log.error(msg)
         return "exit", 2
      end
   elseif cmdline[3] == "del" then
      local key = cmdline.orig_key
      for i = 4, #cmdline do
         repo.project_config.settings[cmdline[4]] = nil
      end
      local ok, msg = repo:save_admin("Deleted project setting: " .. key,
                                      env.GITANO_USER, env.GITANO_ORIG_USER)
      if not ok then
         log.error(msg)
         return "exit", 2
      end
   else
      log.error("Unknown sub command slipped through")
      return "exit", 1
   end
   return "exit", 0
end

assert(register_cmd("config", builtin_config_short,
                    builtin_config_helptext,
                    builtin_config_validate, builtin_config_prep,
                    builtin_config_run, true, false))

local builtin_destroy_short = "Destroy (delete) a repository"
local builtin_destroy_helptext = [[
usage: destroy <repo> [confirmtoken]

This command destroys a repository.  Run without a confirmation token
it will tell the caller what the confirmation token is for that repository.
The caller will then run the destroy command again with the confirmation
token if they really do wish to destroy the repository.

Optionally, you can supply '--force' as the token to bypass the confirmation
stage.  It is not recommended to use this outside of automation.
]]

local function builtin_destroy_validate(config, repo, cmdline)
   if #cmdline < 2 or #cmdline > 3 then
      log.error("Destroy takes a repository and a (optional) confirmation token")
      return false
   end
   if not repo or repo.is_nascent then
      log.error("Cannot destroy a repository which does not exist")
      return false
   end
   return true
end

local function builtin_destroy_prep(config, repo, cmdline, context)
   context.operation = "destroyrepo"
   return repo:run_lace(context)
end

local function builtin_destroy_run(config, repo, cmdline, env)
   local token = repo:generate_confirmation("destroy repo " .. repo.name)
   if #cmdline == 2 then
      -- Generate the confirmation token
      log.stdout("")
      log.stdout("If you are *certain* you wish to destroy this repository")
      log.stdout("Then re-run your command with the following confirmation token:")
      log.stdout("")
      log.stdout("   ", token)
   else
      if cmdline[3] ~= token and cmdline[3] ~= '--force' then
         log.error("Confirmation token does not match, refusing to destroy")
         return "exit", 1
      end
      -- Tokens match, ask the repo to destroy itself
      local nowstamp = os.date("!%Y-%m-%d.%H:%M:%S.UTC")
      local ok, msg = repo:destroy_self(nowstamp .. "." ..
                                           (repo.name:gsub("[^A-Za-z0-9_%.%-]",
                                                           "_")) ..
                                           "." .. token .. ".destroyed")
      if not ok then
         log.error(msg)
         return "exit", 1
      end
      log.stdout("Should you need to recover the repository you just destroyed")
      log.stdout("then you will need to speak with an admin as soon as possible")
      log.stdout("")
      log.stdout("When you do, be sure to include the current time (" ..
                    nowstamp .. ").")
      log.stdout("It may also help if you include your token:")
      log.stdout("  ", token)
      log.stdout("")
      log.stdout("Successfully destroyed", repo.name)
   end
   return "exit", 0
end

assert(register_cmd("destroy", builtin_destroy_short,
                    builtin_destroy_helptext, builtin_destroy_validate,
                    builtin_destroy_prep, builtin_destroy_run,
                    true, false))

local builtin_rename_short = "Rename a repository"
local builtin_rename_helptext = [[
usage: rename <repo> <newname>

Renames a repository to the given new name.  In order to do this, you must
have the ability to create repositories at the new name, the ability to
read the current repository and the ability to rename the current repository.
]]

local function builtin_rename_validate(config, repo, cmdline)
   if #cmdline ~= 3 then
      log.error("Rename takes a repository and a new name for it")
      return false
   end
   return true
end

local function builtin_rename_prep(config, repo, cmdline, context)
   local ctx, action, reason
   -- Check 1, read current repo
   ctx = util.deep_copy(context)
   ctx.operation = "read"
   action, reason = repo:run_lace(ctx)
   if action ~= "allow" then
      return action, reason
   end
   -- Check 2, is the current repo nascent
   if repo.is_nascent then
      return "deny", "Cannot rename a repository which does not exist"
   end
   -- Check 3, rename current repo
   ctx = util.deep_copy(context)
   ctx.operation = "renamerepo"
   action, reason = repo:run_lace(ctx)
   if action ~= "allow" then
      return action, reason
   end
   -- Check 4, create new repo
   ctx = util.deep_copy(context)
   local newrepo, msg = repository.find(config, cmdline[3])
   if not newrepo then
      return "deny", msg
   end
   ctx.operation="createrepo"
   action, reason = newrepo:run_lace(ctx)
   if action ~= "allow" then
      return action, reason
   end
   -- Check 5, does new repo already exist?
   if not newrepo.is_nascent then
      return "deny", "Destination location is in use"
   end
   -- Okay, we could create, read, and destroy
   -- thus we can rename
   return "allow", "Passed all checks, can rename"
end

local function builtin_rename_run(config, repo, cmdline, env)
   local ok, msg = repo:rename_to(cmdline[3])
   if not ok then
      log.error(msg)
      return "exit", 1
   end
   log.state("Renamed", cmdline[2], "to", cmdline[3])
   return "exit", 0
end

assert(register_cmd("rename", builtin_rename_short, builtin_rename_helptext,
                    builtin_rename_validate, builtin_rename_prep,
                    builtin_rename_run, true, false))

local builtin_ls_short = "List repositories on the server"
local builtin_ls_helptext = [[
usage: ls [--verbose|-v] [--all|-a] [<pattern>...]

List repositories on the server.  If you do not provide a pattern then all
repositories are considered, otherwise only ones which match the given patterns
will be considered.  If you specify --verbose then the head ref name and the
description will be provided in addition to the access rights and repository
name, separated by tabs.  If you specify --all then repositories which you
have access to, but have been marked as archived will also be displayed.

Patterns are a type of extended glob style syntax:

      ? == any one character except /
      * == zero or more characters except /
     ** == zero or more characters including /

Any other characters are "as-is" except \ which escapes the next character.

If your pattern contains no / and no ** then it will be matched against
leafnames of repositories.

Note, this means that if you run `ls foo` then the server is going to look
for repositories called `foo.git` rather than look in side a folder called
`foo/`.  For the latter, do `ls foo/` instead.
]]

local function builtin_ls_validate(config, _, cmdline)
   -- For now, anything will do
   return true
end

local function builtin_ls_prep(config, _, cmdline, ctx)
   -- We cheat and store the context for later because we'll need it
   cmdline._ctx = ctx
   return "allow", "You can always try and get a listing"
end

local builtin_ls_special = {}
do
   local builtin_ls_special_chrs = "*?.[]()+-%^$"
   for c in builtin_ls_special_chrs:gmatch(".") do
      builtin_ls_special[c] = true
   end
end

local lsargs = {
   ["--verbose"] = "verbose",
   ["-v"] = "verbose",
   ["--all"] = "all",
   ["-a"] = "all",
}

local function builtin_ls_run(config, _, cmdline, env)
   -- Step one, parse each pattern into a lua pattern
   local pats = {}
   local firstpat, verbose, all = 2, false, false
   while lsargs[cmdline[firstpat]] do
      if lsargs[cmdline[firstpat]] == "verbose" then
         firstpat, verbose = firstpat + 1, true
      end
      if lsargs[cmdline[firstpat]] == "all" then
         firstpat, all = firstpat + 1, true
      end
   end
   for i = firstpat, #cmdline do
      local pat, c, input = "", "", cmdline[i]
      local escaping, star, used_evil = false, false, false
      c, input = input:match("^(.)(.*)$")
      while c and c ~= "" do
         if escaping then
            pat = pat .. (builtin_ls_special[c] and "%" or "") .. c
            if c == "/" then
               used_evil = true
            end
            escaping = false
         else
            if c == "*" then
               if star then
                  -- **
                  pat = pat .. ".*"
                  used_evil = true
                  star = false
               else
                  star = true
               end
            else
               if star then
                  -- *
                  pat = pat .. "[^/]*"
                  star = false
               end
               if c == "?" then
                  pat = pat .. "[^/]"
               elseif c == "\\" then
                  escaping = true
               else
                  pat = pat .. (builtin_ls_special[c] and "%" or "") .. c
                  if c == "/" then
                     used_evil = true
                  end
               end
            end
         end
         c, input = input:match("^(.)(.*)$")
      end
      if star then
         -- spare star
         pat = pat .. "[^/]*"
      end
      if cmdline[i]:match("/$") then
         pat = pat .. ".*"
      end
      if used_evil then
         pat = "^/" .. pat .. pattern.GIT_REPO_SUFFIX
      else
         pat = "/" .. pat .. pattern.GIT_REPO_SUFFIX
      end
      log.debug("PAT:", pat)
      pats[#pats+1] = pat
   end
   if #pats == 0 then
      pats[1] = "."
   end
   -- Now we iterate all the repositories, listing them if (a) they pass a
   -- pattern and (b) they allow the current user to read.
   local _ctx = cmdline._ctx
   local function filter_callback(name)
      for i = 1, #pats do
         if ("/" .. name):match(pats[i]) then
            return true
         end
      end
   end
   local function callback(reponame, repo, msg)
      if repo then
         local archived = repo:conf_get("project.archived")
         if archived and not all then return end
         local ctx = util.deep_copy(_ctx)
         ctx.operation = "read"
         local action, reason = repo:run_lace(ctx)
         if action == "allow" then
            ctx = util.deep_copy(_ctx)
            ctx.operation = "write"
            action, reason = repo:run_lace(ctx)
            local tail = ""
            if verbose then
               local desc = repo:conf_get("project.description")
               desc = desc:gsub("\n.*", "")
               tail = " " .. repo:conf_get("project.head") .. " " .. desc
            end
            log.stdout((action == "allow" and "RW" or "R ") ..
                  (archived and "A" or " "), repo.name .. tail)
         end
      end
   end
   repository.foreach(config, callback, filter_callback)
   return "exit", 0
end

assert(register_cmd("ls", builtin_ls_short, builtin_ls_helptext,
                    builtin_ls_validate, builtin_ls_prep, builtin_ls_run,
                    false, false))


local usercmds = require 'gitano.usercommand'
usercmds.register(register_cmd)
local admincmds = require 'gitano.admincommand'
admincmds.register(register_cmd)
local repocmds = require 'gitano.repocommand'
repocmds.register(register_cmd)
local copycmds = require 'gitano.copycommand'
copycmds.register(register_cmd)

return {
   register = register_cmd,
   get = get_cmd,
}
