-- Copyright (C) 2003 Graydon Hoare <graydon@pobox.com>
--
-- This program is made available under the GNU GPL version 2.0 or
-- greater. See the accompanying file COPYING for details.
--
-- This program is distributed WITHOUT ANY WARRANTY; without even the
-- implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
-- PURPOSE.

-- this is the standard set of lua hooks for monotone;
-- user-provided files can override it or add to it.

-- Since Lua 5.2, unpack and loadstrings are deprecated and are either moved
-- to table.unpack() or replaced by load(). If lua was compiled without
-- LUA_COMPAT_UNPACK and/or LUA_COMPAT_LOADSTRING, these two are not
-- available and we add a similar compatibility layer, ourselves.
unpack = unpack or table.unpack
loadstring = loadstring or load

function temp_file(namehint, filemodehint)
   local tdir
   tdir = os.getenv("TMPDIR")
   if tdir == nil then tdir = os.getenv("TMP") end
   if tdir == nil then tdir = os.getenv("TEMP") end
   if tdir == nil then tdir = "/tmp" end
   local filename
   if namehint == nil then
      filename = string.format("%s/mtn.XXXXXX", tdir)
   else
      filename = string.format("%s/mtn.%s.XXXXXX", tdir, namehint)
   end
   local filemode
   if filemodehint == nil then
      filemode = "r+"
   else
      filemode = filemodehint
   end
   local name = mkstemp(filename)
   local file = io.open(name, filemode)
   return file, name
end

function execute(path, ...)
   local pid
   local ret = -1
   pid = spawn(path, ...)
   if (pid ~= -1) then ret, pid = wait(pid) end
   return ret
end

function execute_redirected(stdin, stdout, stderr, path, ...)
   local pid
   local ret = -1
   io.flush();
   pid = spawn_redirected(stdin, stdout, stderr, path, ...)
   if (pid ~= -1) then ret, pid = wait(pid) end
   return ret
end

-- Wrapper around execute to let user confirm in the case where a subprocess
-- returns immediately
-- This is needed to work around some brokenness with some merge tools
-- (e.g. on OS X)
function execute_confirm(path, ...)
   ret = execute(path, ...)

   if (ret ~= 0)
   then
      print(gettext("Press enter"))
   else
      print(gettext("Press enter when the subprocess has completed"))
   end
   io.read()
   return ret
end

-- attributes are persistent metadata about files (such as execute
-- bit, ACLs, various special flags) which we want to have set and
-- re-set any time the files are modified. the attributes themselves
-- are stored in the roster associated with the revision. each (f,k,v)
-- attribute triple turns into a call to attr_functions[k](f,v) in lua.

if (attr_init_functions == nil) then
   attr_init_functions = {}
end

attr_init_functions["mtn:execute"] =
   function(filename)
      if (is_executable(filename)) then
        return "true"
      else
        return nil
      end
   end

attr_init_functions["mtn:manual_merge"] =
   function(filename)
      if (binary_file(filename)) then
        return "true" -- binary files must be merged manually
      else
        return nil
      end
   end

if (attr_functions == nil) then
   attr_functions = {}
end

attr_functions["mtn:execute"] =
   function(filename, value)
      if (value == "true") then
         set_executable(filename)
      else
         clear_executable(filename)
      end
   end

function dir_matches(name, dir)
   -- helper for ignore_file, matching files within dir, or dir itself.
   -- eg for dir of 'CVS', matches CVS/, CVS/*, */CVS/ and */CVS/*
   if (string.find(name, "^" .. dir .. "/")) then return true end
   if (string.find(name, "^" .. dir .. "$")) then return true end
   if (string.find(name, "/" .. dir .. "/")) then return true end
   if (string.find(name, "/" .. dir .. "$")) then return true end
   return false
end

function portable_readline(f)
    line = f:read()
    if line ~= nil then
        line = string.gsub(line, "\r$","") -- strip possible \r left from windows editing
    end
    return line
end

function ignore_file(name)
   -- project specific
   if (ignored_files == nil) then
      ignored_files = {}
      local ignfile = io.open(".mtn-ignore", "r")
      if (ignfile ~= nil) then
         local line = portable_readline(ignfile)
         while (line ~= nil) do
            if line ~= "" then
                table.insert(ignored_files, line)
            end
            line = portable_readline(ignfile)
         end
         io.close(ignfile)
      end
   end

   local warn_reported_file = false
   for i, line in pairs(ignored_files)
   do
      if (line ~= nil) then
         local pcallstatus, result = pcall(function()
        return regex.search(line, name)
     end)
         if pcallstatus == true then
            -- no error from the regex.search call
            if result == true then return true end
         else
            -- regex.search had a problem, warn the user their
            -- .mtn-ignore file syntax is wrong
        if not warn_reported_file then
           io.stderr:write("mtn: warning: while matching file '"
                       .. name .. "':\n")
           warn_reported_file = true
        end
        local prefix = ".mtn-ignore:" .. i .. ": warning: "
            io.stderr:write(prefix
                            .. string.gsub(result, "\n", "\n" .. prefix)
                               .. "\n\t- skipping this regex for "
                               .. "all remaining files.\n")
            ignored_files[i] = nil
         end
      end
   end

   local file_pats = {
      -- c/c++
      "%.a$", "%.so$", "%.o$", "%.la$", "%.lo$", "^core$",
      "/core$", "/core%.%d+$",
      -- java
      "%.class$",
      -- python
      "%.pyc$", "%.pyo$",
      -- gettext
      "%.g?mo$",
      -- intltool
      "%.intltool%-merge%-cache$",
      -- TeX
      "%.aux$",
      -- backup files
      "%.bak$", "%.orig$", "%.rej$", "%~$",
      -- vim creates .foo.swp files
      "%.[^/]*%.swp$",
      -- emacs creates #foo# files
      "%#[^/]*%#$",
      -- other VCSes (where metadata is stored in named files):
      "%.scc$",
      -- desktop/directory configuration metadata
      "^%.DS_Store$", "/%.DS_Store$", "^desktop%.ini$", "/desktop%.ini$"
   }

   local dir_pats = {
      -- autotools detritus:
      "autom4te%.cache", "%.deps", "%.libs",
      -- Cons/SCons detritus:
      "%.consign", "%.sconsign",
      -- other VCSes (where metadata is stored in named dirs):
      "CVS", "%.svn", "SCCS", "_darcs", "%.cdv", "%.git", "%.bzr", "%.hg"
   }

   for _, pat in ipairs(file_pats) do
      if string.find(name, pat) then return true end
   end
   for _, pat in ipairs(dir_pats) do
      if dir_matches(name, pat) then return true end
   end

   return false;
end

-- return true means "binary", false means "text",
-- nil means "unknown, try to guess"
function binary_file(name)
   -- some known binaries, return true
   local bin_pats = {
      "%.gif$", "%.jpe?g$", "%.png$", "%.bz2$", "%.gz$", "%.zip$",
      "%.class$", "%.jar$", "%.war$", "%.ear$"
   }

   -- some known text, return false
   local txt_pats = {
      "%.cc?$", "%.cxx$", "%.hh?$", "%.hxx$", "%.cpp$", "%.hpp$",
      "%.lua$", "%.texi$", "%.sql$", "%.java$"
   }

   local lowname=string.lower(name)
   for _, pat in ipairs(bin_pats) do
      if string.find(lowname, pat) then return true end
   end
   for _, pat in ipairs(txt_pats) do
      if string.find(lowname, pat) then return false end
   end

   -- unknown - read file and use the guess-binary
   -- monotone built-in function
   return guess_binary_file_contents(name)
end

-- given a file name, return a regular expression which will match
-- lines that name top-level constructs in that file, or "", to disable
-- matching.
function get_encloser_pattern(name)
   -- texinfo has special sectioning commands
   if (string.find(name, "%.texi$")) then
      -- sectioning commands in texinfo: @node, @chapter, @top,
      -- @((sub)?sub)?section, @unnumbered(((sub)?sub)?sec)?,
      -- @appendix(((sub)?sub)?sec)?, @(|major|chap|sub(sub)?)heading
      return ("^@("
              .. "node|chapter|top"
              .. "|((sub)?sub)?section"
              .. "|(unnumbered|appendix)(((sub)?sub)?sec)?"
              .. "|(major|chap|sub(sub)?)?heading"
              .. ")")
   end
   -- LaTeX has special sectioning commands.  This rule is applied to ordinary
   -- .tex files too, since there's no reliable way to distinguish those from
   -- latex files anyway, and there's no good pattern we could use for
   -- arbitrary plain TeX anyway.
   if (string.find(name, "%.tex$")
       or string.find(name, "%.ltx$")
       or string.find(name, "%.latex$")) then
      return ("\\\\("
              .. "part|chapter|paragraph|subparagraph"
              .. "|((sub)?sub)?section"
              .. ")")
   end
   -- There's no good way to find section headings in raw text, and trying
   -- just gives distracting output, so don't even try.
   if (string.find(name, "%.txt$")
       or string.upper(name) == "README") then
      return ""
   end
   -- This default is correct surprisingly often -- in pretty much any text
   -- written with code-like indentation.
   return "^[[:alnum:]$_]"
end

function edit_comment(user_log_message)
   local exe = nil

   -- top priority is VISUAL, then EDITOR, then a series of hardcoded
   -- defaults, if available.

   local visual = os.getenv("VISUAL")
   local editor = os.getenv("EDITOR")
   if (visual ~= nil) then exe = visual
   elseif (editor ~= nil) then exe = editor
   elseif (program_exists_in_path("editor")) then exe = "editor"
   elseif (program_exists_in_path("vi")) then exe = "vi"
   elseif (string.sub(get_ostype(), 1, 6) ~= "CYGWIN" and
       program_exists_in_path("notepad.exe")) then exe = "notepad"
   else
      io.write(gettext("Could not find editor to enter commit message\n"
               .. "Try setting the environment variable EDITOR\n"))
      return nil
   end

   local tmp, tname = temp_file()
   if (tmp == nil) then return nil end
   tmp:write(user_log_message)
   if user_log_message == "" or string.sub(user_log_message, -1) ~= "\n" then
      tmp:write("\n")
   end
   io.close(tmp)

   -- By historical convention, VISUAL and EDITOR can contain arguments
   -- (and, in fact, arbitrarily complicated shell constructs).  Since Lua
   -- has no word-splitting functionality, we invoke the shell to deal with
   -- anything more complicated than a single word with no metacharacters.
   -- This, unfortunately, means we have to quote the file argument.

   if (not string.find(exe, "[^%w_.+-]")) then
      -- safe to call spawn directly
      if (execute(exe, tname) ~= 0) then
         io.write(string.format(gettext("Error running editor '%s' "..
                                        "to enter log message\n"),
                                exe))
         os.remove(tname)
         return nil
      end
   else
      -- must use shell
      local shell = os.getenv("SHELL")
      if (shell == nil) then shell = "sh" end
      if (not program_exists_in_path(shell)) then
         io.write(string.format(gettext("Editor command '%s' needs a shell, "..
                                        "but '%s' is not to be found"),
                                exe, shell))
         os.remove(tname)
         return nil
      end

      -- Single-quoted strings in both Bourne shell and csh can contain
      -- anything but a single quote.
      local safe_tname = " '" .. string.gsub(tname, "'", "'\\''") .. "'"

      if (execute(shell, "-c", editor .. safe_tname) ~= 0) then
         io.write(string.format(gettext("Error running editor '%s' "..
                                        "to enter log message\n"),
                                exe))
         os.remove(tname)
         return nil
      end
   end

   tmp = io.open(tname, "r")
   if (tmp == nil) then os.remove(tname); return nil end
   local res = tmp:read("*a")
   io.close(tmp)
   os.remove(tname)
   return res
end


function get_local_key_name(key_identity)
   return key_identity.given_name
end


function persist_phrase_ok()
   return true
end


function use_inodeprints()
   return false
end

function get_date_format_spec(wanted)
   -- Return the strftime(3) specification to be used to print dates
   -- in human-readable format after conversion to the local timezone.
   -- The default uses the preferred date and time representation for
   -- the current locale, e.g. the output looks like this: "09/08/2009
   -- 06:49:26 PM" for en_US and "date_time_long", or "08.09.2009"
   -- for de_DE and "date_short"
   --
   -- A sampling of other possible formats you might want:
   --   default for your locale: "%c" (may include a confusing timezone label)
   --   12 hour format: "%d %b %Y, %I:%M:%S %p"
   --   like ctime(3):  "%a %b %d %H:%M:%S %Y"
   --   email style:    "%a, %d %b %Y %H:%M:%S"
   --   ISO 8601:       "%Y-%m-%d %H:%M:%S" or "%Y-%m-%dT%H:%M:%S"
   --
   --   ISO 8601, no timezone conversion: ""
   --.
   if (wanted == "date_long" or wanted == "date_short") then
       return "%x"
   end
   if (wanted == "time_long" or wanted == "time_short") then
       return "%X"
   end
   return "%x %X"
end

-- trust evaluation hooks

function intersection(a,b)
   local s={}
   local t={}
   for k,v in pairs(a) do s[v.name] = 1 end
   for k,v in pairs(b) do if s[v] ~= nil then table.insert(t,v) end end
   return t
end

function get_revision_cert_trust(signers, id, name, val)
   return true
end

-- This is only used by migration from old manifest-style ancestry
function get_manifest_cert_trust(signers, id, name, val)
   return true
end

-- http://snippets.luacode.org/?p=snippets/String_to_Hex_String_68
function hex_dump(str,spacer)
   return (string.gsub(str,"(.)",
      function (c)
         return string.format("%02x%s",string.byte(c), spacer or "")
      end)
   )
end

function accept_testresult_change_hex(old_results, new_results)
   local reqfile = io.open("_MTN/wanted-testresults", "r")
   if (reqfile == nil) then return true end
   local line = reqfile:read()
   local required = {}
   while (line ~= nil)
   do
      required[line] = true
      line = reqfile:read()
   end
   io.close(reqfile)
   for test, res in pairs(required)
   do
      if old_results[test] == true and new_results[test] ~= true
      then
         return false
      end
   end
   return true
end

function accept_testresult_change(old_results, new_results)
   -- Hex encode each of the key hashes to match those in 'wanted-testresults'
   local old_results_hex = {}
   for k, v in pairs(old_results) do
	old_results_hex[hex_dump(k)] = v
   end

   local new_results_hex = {}
   for k, v in pairs(new_results) do
      new_results_hex[hex_dump(k)] = v
   end

   return accept_testresult_change_hex(old_results_hex, new_results_hex)
end

-- merger support

-- Fields in the mergers structure:
-- cmd       : a function that performs the merge operation using the chosen
--             program, best try.
-- available : a function that checks that the needed program is installed and
--             in $PATH
-- wanted    : a function that checks if the user doesn't want to use this
--             method, and returns false if so.  This should normally return
--             true, but in some cases, especially when the merger is really
--             an editor, the user might have a preference in EDITOR and we
--             need to respect that.
--             NOTE: wanted is only used when the user has NOT defined the
--             `merger' variable or the MTN_MERGE environment variable.
mergers = {}

-- This merger is designed to fail if there are any conflicts without trying to resolve them
mergers.fail = {
   cmd = function (tbl) return false end,
   available = function () return true end,
   wanted = function () return true end
}

mergers.meld = {
   cmd = function (tbl)
      io.write(string.format(
        "\nWARNING: 'meld' was chosen to perform an external 3-way merge.\n"..
        "You must merge all changes to the *CENTER* file.\n\n"
      ))
      local path = "meld"
      local ret = execute(path, tbl.lfile, tbl.afile, tbl.rfile)
      if (ret ~= 0) then
         io.write(string.format(gettext("Error running merger '%s'\n"), path))
         return false
      end
      return tbl.afile
   end ,
   available = function () return program_exists_in_path("meld") end,
   wanted = function () return true end
}

mergers.diffuse = {
   cmd = function (tbl)
      io.write(string.format(
        "\nWARNING: 'diffuse' was chosen to perform an external 3-way merge.\n"..
        "You must merge all changes to the *CENTER* file.\n\n"
      ))
      local path = "diffuse"
      local ret = execute(path, tbl.lfile, tbl.afile, tbl.rfile)
      if (ret ~= 0) then
         io.write(string.format(gettext("Error running merger '%s'\n"), path))
         return false
      end
      return tbl.afile
   end ,
   available = function () return program_exists_in_path("diffuse") end,
   wanted = function () return true end
}

mergers.tortoise = {
   cmd = function (tbl)
      local path = "tortoisemerge"
      local ret = execute(path,
                          string.format("/base:%s", tbl.afile),
                          string.format("/theirs:%s", tbl.lfile),
                          string.format("/mine:%s", tbl.rfile),
                          string.format("/merged:%s", tbl.outfile))
      if (ret ~= 0) then
         io.write(string.format(gettext("Error running merger '%s'\n"), path))
         return false
      end
      return tbl.outfile
   end ,
   available = function() return program_exists_in_path ("tortoisemerge") end,
   wanted = function () return true end
}

mergers.vim = {
   cmd = function (tbl)
      function execute_diff3(mine, yours, out)
     local diff3_args = {
        "diff3",
        "--merge",
        "--easy-only",
     }
     table.insert(diff3_args, string.gsub(mine, "\\", "/") .. "")
     table.insert(diff3_args, string.gsub(tbl.afile, "\\", "/") .. "")
     table.insert(diff3_args, string.gsub(yours, "\\", "/") .. "")

     return execute_redirected("", string.gsub(out, "\\", "/"), "", unpack(diff3_args))
      end

      io.write (string.format("\nWARNING: 'vim' was chosen to perform "..
                  "an external 3-way merge.\n"..
                  "You must merge all changes to the "..
                  "*LEFT* file.\n"))

      local vim
      if os.getenv ("DISPLAY") ~= nil and program_exists_in_path ("gvim") then
     vim = "gvim"
      else
     vim = "vim"
      end

      local lfile_merged = tbl.lfile .. ".merged"
      local rfile_merged = tbl.rfile .. ".merged"

      -- first merge lfile using diff3
      local ret = execute_diff3(tbl.lfile, tbl.rfile, lfile_merged)
      if ret == 2 then
         io.write(string.format(gettext("Error running diff3 for merger '%s'\n"), vim))
         os.remove(lfile_merged)
     return false
      end

      -- now merge rfile using diff3
      ret = execute_diff3(tbl.rfile, tbl.lfile, rfile_merged)
      if ret == 2 then
         io.write(string.format(gettext("Error running diff3 for merger '%s'\n"), vim))
         os.remove(lfile_merged)
         os.remove(rfile_merged)
     return false
      end

      os.rename(lfile_merged, tbl.lfile)
      os.rename(rfile_merged, tbl.rfile)

      local ret = execute(vim, "-f", "-d", "-c", string.format("silent file %s", tbl.outfile),
                          tbl.lfile, tbl.rfile)
      if (ret ~= 0) then
         io.write(string.format(gettext("Error running merger '%s'\n"), vim))
         return false
      end
      return tbl.outfile
   end ,
   available =
      function ()
     return program_exists_in_path("diff3") and
            (program_exists_in_path("vim") or
        program_exists_in_path("gvim"))
      end ,
   wanted =
      function ()
     local editor = os.getenv("EDITOR")
     if editor and
        not (string.find(editor, "vim") or
         string.find(editor, "gvim")) then
        return false
     end
     return true
      end
}

mergers.rcsmerge = {
   cmd = function (tbl)
      -- XXX: This is tough - should we check if conflict markers stay or not?
      -- If so, we should certainly give the user some way to still force
      -- the merge to proceed since they can appear in the files (and I saw
      -- that). --pasky
      local merge = os.getenv("MTN_RCSMERGE")
      if execute(merge, tbl.lfile, tbl.afile, tbl.rfile) == 0 then
         copy_text_file(tbl.lfile, tbl.outfile);
         return tbl.outfile
      end
      local ret = execute("vim", "-f", "-c", string.format("file %s", tbl.outfile
),
                          tbl.lfile)
      if (ret ~= 0) then
         io.write(string.format(gettext("Error running merger '%s'\n"), "vim"))
         return false
      end
      return tbl.outfile
   end,
   available =
      function ()
     local merge = os.getenv("MTN_RCSMERGE")
     return merge and
        program_exists_in_path(merge) and program_exists_in_path("vim")
      end ,
   wanted = function () return os.getenv("MTN_RCSMERGE") ~= nil end
}

--  GNU diffutils based merging
mergers.diffutils = {
    --  merge procedure execution
    cmd = function (tbl)
        --  parse options
        local option = {}
        option.partial = false
        option.diff3opts = ""
        option.sdiffopts = ""
        local options = os.getenv("MTN_MERGE_DIFFUTILS")
        if options ~= nil then
            for spec in string.gmatch(options, "%s*(%w[^,]*)%s*,?") do
                local name, value = string.match(spec, "^(%w+)=([^,]*)")
                if name == nil then
                    name = spec
                    value = true
                end
                if type(option[name]) == "nil" then
                    io.write("mtn: " .. string.format(gettext("invalid \"diffutils\" merger option \"%s\""), name) .. "\n")
                    return false
                end
                option[name] = value
            end
        end

        --  determine the diff3(1) command
        local diff3 = {
            "diff3",
            "--merge",
            "--label", string.format("%s [left]",     tbl.left_path ),
            "--label", string.format("%s [ancestor]", tbl.anc_path  ),
            "--label", string.format("%s [right]",    tbl.right_path),
        }
        if option.diff3opts ~= "" then
            for opt in string.gmatch(option.diff3opts, "%s*([^%s]+)%s*") do
                table.insert(diff3, opt)
            end
        end
        table.insert(diff3, string.gsub(tbl.lfile, "\\", "/") .. "")
        table.insert(diff3, string.gsub(tbl.afile, "\\", "/") .. "")
        table.insert(diff3, string.gsub(tbl.rfile, "\\", "/") .. "")

        --  dispatch according to major operation mode
        if option.partial then
            --  partial batch/non-modal 3-way merge "resolution":
            --  simply merge content with help of conflict markers
            io.write("mtn: " .. gettext("3-way merge via GNU diffutils, resolving conflicts via conflict markers") .. "\n")
            local ret = execute_redirected("", string.gsub(tbl.outfile, "\\", "/"), "", unpack(diff3))
            if ret == 2 then
                io.write("mtn: " .. gettext("error running GNU diffutils 3-way difference/merge tool \"diff3\"") .. "\n")
                return false
            end
            return tbl.outfile
        else
            --  real interactive/modal 3/2-way merge resolution:
            --  display 3-way merge conflict and perform 2-way merge resolution
            io.write("mtn: " .. gettext("3-way merge via GNU diffutils, resolving conflicts via interactive prompt") .. "\n")

            --  display 3-way merge conflict (batch)
            io.write("\n")
            io.write("mtn: " .. gettext("---- CONFLICT SUMMARY ------------------------------------------------") .. "\n")
            local ret = execute(unpack(diff3))
            if ret == 2 then
                io.write("mtn: " .. gettext("error running GNU diffutils 3-way difference/merge tool \"diff3\"") .. "\n")
                return false
            end

            --  perform 2-way merge resolution (interactive)
            io.write("\n")
            io.write("mtn: " .. gettext("---- CONFLICT RESOLUTION ---------------------------------------------") .. "\n")
            local sdiff = {
                "sdiff",
                "--diff-program=diff",
                "--suppress-common-lines",
                "--minimal",
                "--output=" .. string.gsub(tbl.outfile, "\\", "/")
            }
            if option.sdiffopts ~= "" then
                for opt in string.gmatch(option.sdiffopts, "%s*([^%s]+)%s*") do
                    table.insert(sdiff, opt)
                end
            end
            table.insert(sdiff, string.gsub(tbl.lfile, "\\", "/") .. "")
            table.insert(sdiff, string.gsub(tbl.rfile, "\\", "/") .. "")
            local ret = execute(unpack(sdiff))
            if ret == 2 then
                io.write("mtn: " .. gettext("error running GNU diffutils 2-way merging tool \"sdiff\"") .. "\n")
                return false
            end
            return tbl.outfile
        end
    end,

    --  merge procedure availability check
    available = function ()
        --  make sure the GNU diffutils tools are available
        return program_exists_in_path("diff3") and
               program_exists_in_path("sdiff") and
               program_exists_in_path("diff");
    end,

    --  merge procedure request check
    wanted = function ()
        --  assume it is requested (if it is available at all)
        return true
    end
}

mergers.emacs = {
   cmd = function (tbl)
      local emacs
      if program_exists_in_path("xemacs") then
         emacs = "xemacs"
      else
         emacs = "emacs"
      end
      local elisp = "(ediff-merge-files-with-ancestor \"%s\" \"%s\" \"%s\" nil \"%s\")"
      -- Converting backslashes is necessary on Win32 MinGW; emacs
      -- lisp string syntax says '\' is an escape.
      local ret = execute(emacs, "--eval",
                          string.format(elisp,
                          string.gsub (tbl.lfile, "\\", "/"),
                          string.gsub (tbl.rfile, "\\", "/"),
                          string.gsub (tbl.afile, "\\", "/"),
                          string.gsub (tbl.outfile, "\\", "/")))
      if (ret ~= 0) then
         io.write(string.format(gettext("Error running merger '%s'\n"), emacs))
         return false
      end
      return tbl.outfile
   end,
   available =
      function ()
     return program_exists_in_path("xemacs") or
        program_exists_in_path("emacs")
      end ,
   wanted =
      function ()
     local editor = os.getenv("EDITOR")
     if editor and
        not (string.find(editor, "emacs") or
         string.find(editor, "gnu")) then
        return false
     end
     return true
      end
}

mergers.xxdiff = {
   cmd = function (tbl)
      local path = "xxdiff"
      local ret = execute(path,
                        "--title1", tbl.left_path,
                        "--title2", tbl.right_path,
                        "--title3", tbl.merged_path,
                        tbl.lfile, tbl.afile, tbl.rfile,
                        "--merge",
                        "--merged-filename", tbl.outfile,
                        "--exit-with-merge-status")
      if (ret ~= 0) then
         io.write(string.format(gettext("Error running merger '%s'\n"), path))
         return false
      end
      return tbl.outfile
   end,
   available = function () return program_exists_in_path("xxdiff") end,
   wanted = function () return true end
}

mergers.kdiff3 = {
   cmd = function (tbl)
      local path = "kdiff3"
      local ret = execute(path,
                          "--L1", tbl.anc_path,
                          "--L2", tbl.left_path,
                          "--L3", tbl.right_path,
                          tbl.afile, tbl.lfile, tbl.rfile,
                          "--merge",
                          "--o", tbl.outfile)
      if (ret ~= 0) then
         io.write(string.format(gettext("Error running merger '%s'\n"), path))
         return false
      end
      return tbl.outfile
   end,
   available = function () return program_exists_in_path("kdiff3") end,
   wanted = function () return true end
}

mergers.opendiff = {
   cmd = function (tbl)
      local path = "opendiff"
      -- As opendiff immediately returns, let user confirm manually
      local ret = execute_confirm(path,
                                  tbl.lfile,tbl.rfile,
                                  "-ancestor",tbl.afile,
                                  "-merge",tbl.outfile)
      if (ret ~= 0) then
         io.write(string.format(gettext("Error running merger '%s'\n"), path))
         return false
      end
      return tbl.outfile
   end,
   available = function () return program_exists_in_path("opendiff") end,
   wanted = function () return true end
}

function write_to_temporary_file(data, namehint, filemodehint)
   tmp, filename = temp_file(namehint, filemodehint)
   if (tmp == nil) then
      return nil
   end;
   tmp:write(data)
   io.close(tmp)
   return filename
end

function copy_text_file(srcname, destname)
   src = io.open(srcname, "r")
   if (src == nil) then return nil end
   dest = io.open(destname, "w")
   if (dest == nil) then return nil end

   while true do
      local line = src:read()
      if line == nil then break end
      dest:write(line, "\n")
   end

   io.close(dest)
   io.close(src)
end

function read_contents_of_file(filename, mode)
   tmp = io.open(filename, mode)
   if (tmp == nil) then
      return nil
   end
   local data = tmp:read("*a")
   io.close(tmp)
   return data
end

function program_exists_in_path(program)
   return existsonpath(program) == 0
end

function get_preferred_merge3_command (tbl)
   local default_order = {"diffuse", "kdiff3", "xxdiff", "opendiff",
                          "tortoise", "emacs", "vim", "meld", "diffutils"}
   local function existmerger(name)
      local m = mergers[name]
      if type(m) == "table" and m.available(tbl) then
         return m.cmd
      end
      return nil
   end
   local function trymerger(name)
      local m = mergers[name]
      if type(m) == "table" and m.available(tbl) and m.wanted(tbl) then
         return m.cmd
      end
      return nil
   end
   -- Check if there's a merger given by the user.
   local mkey = os.getenv("MTN_MERGE")
   if not mkey then mkey = merger end
   if not mkey and os.getenv("MTN_RCSMERGE") then mkey = "rcsmerge" end
   -- If there was a user-given merger, see if it exists.  If it does, return
   -- the cmd function.  If not, return nil.
   local c
   if mkey then c = existmerger(mkey) end
   if c then return c,mkey end
   if mkey then return nil,mkey end
   -- If there wasn't any user-given merger, take the first that's available
   -- and wanted.
   for _,mkey in ipairs(default_order) do
      c = trymerger(mkey) ; if c then return c,mkey end
   end
end

function merge3 (anc_path, left_path, right_path, merged_path, ancestor, left, right)
   local ret = nil
   local tbl = {}

   tbl.anc_path = anc_path
   tbl.left_path = left_path
   tbl.right_path = right_path

   tbl.merged_path = merged_path
   tbl.afile = nil
   tbl.lfile = nil
   tbl.rfile = nil
   tbl.outfile = nil
   tbl.meld_exists = false
   tbl.lfile = write_to_temporary_file (left, "left", "r+b")
   tbl.afile = write_to_temporary_file (ancestor, "ancestor", "r+b")
   tbl.rfile = write_to_temporary_file (right, "right", "r+b")
   tbl.outfile = write_to_temporary_file ("", "merged", "r+b")

   if tbl.lfile ~= nil and tbl.rfile ~= nil and tbl.afile ~= nil and tbl.outfile ~= nil
   then
      local cmd,mkey = get_preferred_merge3_command (tbl)
      if cmd ~=nil
      then
         io.write ("mtn: " .. string.format(gettext("executing external 3-way merge via \"%s\" merger\n"), mkey))
         ret = cmd (tbl)
         if not ret then
            ret = nil
         else
            ret = read_contents_of_file (ret, "rb")
            if string.len (ret) == 0
            then
               ret = nil
            end
         end
      else
     if mkey then
        io.write (string.format("The possible commands for the "..mkey.." merger aren't available.\n"..
                "You may want to check that $MTN_MERGE or the lua variable `merger' is set\n"..
                "to something available.  If you want to use vim or emacs, you can also\n"..
        "set $EDITOR to something appropriate.\n"))
     else
        io.write (string.format("No external 3-way merge command found.\n"..
                "You may want to check that $EDITOR is set to an editor that supports 3-way\n"..
                "merge, set this explicitly in your get_preferred_merge3_command hook,\n"..
                "or add a 3-way merge program to your path.\n"))
     end
      end
   end

   os.remove (tbl.lfile)
   os.remove (tbl.rfile)
   os.remove (tbl.afile)
   os.remove (tbl.outfile)

   return ret
end

-- expansion of values used in selector completion

function expand_selector(str)

   -- something which looks like a generic cert pattern
   if string.find(str, "^[^=]*=.*$")
   then
      return ("c:" .. str)
   end

   -- something which looks like an email address
   if string.find(str, "[%w%-_]+@[%w%-_]+")
   then
      return ("a:" .. str)
   end

   -- something which looks like a branch name
   if string.find(str, "[%w%-]+%.[%w%-]+")
   then
      return ("b:" .. str)
   end

   -- a sequence of nothing but hex digits
   if string.find(str, "^%x+$")
   then
      return ("i:" .. str)
   end

   -- tries to expand as a date
   local dtstr = expand_date(str)
   if  dtstr ~= nil
   then
      return ("d:" .. dtstr)
   end

   return nil
end

-- expansion of a date expression
function expand_date(str)
   -- simple date patterns
   if string.find(str, "^19%d%d%-%d%d")
      or string.find(str, "^20%d%d%-%d%d")
   then
      return (str)
   end

   -- "now"
   if str == "now"
   then
      local t = os.time(os.date('!*t'))
      return os.date("!%Y-%m-%dT%H:%M:%S", t)
   end

   -- today don't uses the time         # for xgettext's sake, an extra quote
   if str == "today"
   then
      local t = os.time(os.date('!*t'))
      return os.date("!%Y-%m-%d", t)
   end

   -- "yesterday", the source of all hangovers
   if str == "yesterday"
   then
      local t = os.time(os.date('!*t'))
      return os.date("!%Y-%m-%d", t - 86400)
   end

   -- "CVS style" relative dates such as "3 weeks ago"
   local trans = {
      minute = 60;
      hour = 3600;
      day = 86400;
      week = 604800;
      month = 2678400;
      year = 31536000
   }
   local pos, len, n, type = string.find(str, "(%d+) ([minutehordaywk]+)s? ago")
   if trans[type] ~= nil
   then
      local t = os.time(os.date('!*t'))
      if trans[type] <= 3600
      then
        return os.date("!%Y-%m-%dT%H:%M:%S", t - (n * trans[type]))
      else
        return os.date("!%Y-%m-%d", t - (n * trans[type]))
      end
   end

   return nil
end


external_diff_default_args = "-u"

-- default external diff, works for gnu diff
function external_diff(file_path, data_old, data_new, is_binary, diff_args, rev_old, rev_new)
   local old_file = write_to_temporary_file(data_old, nil, "r+b");
   local new_file = write_to_temporary_file(data_new, nil, "r+b");

   if diff_args == nil then diff_args = external_diff_default_args end
   execute("diff", diff_args, "--label", file_path .. "\told", old_file, "--label", file_path .. "\tnew", new_file);

   os.remove (old_file);
   os.remove (new_file);
end

-- netsync permissions hooks (and helper)

function globish_match(glob, str)
      local pcallstatus, result = pcall(function() if (globish.match(glob, str)) then return true else return false end end)
      if pcallstatus == true then
          -- no error
          return result
      else
          -- globish.match had a problem
          return nil
      end
end

function _get_netsync_read_permitted(branch, ident, permfilename, state)
   if not exists(permfilename) or isdir(permfilename) then
      return false
   end
   local permfile = io.open(permfilename, "r")
   if (permfile == nil) then return false end
   local dat = permfile:read("*a")
   io.close(permfile)
   local res = parse_basic_io(dat)
   if res == nil then
      io.stderr:write("file "..permfilename.." cannot be parsed\n")
      return false,"continue"
   end
   state["matches"] = state["matches"] or false
   state["cont"] = state["cont"] or false
   for i, item in pairs(res)
   do
      -- legal names: pattern, allow, deny, continue
      if item.name == "pattern" then
         if state["matches"] and not state["cont"] then return false end
         state["matches"] = false
         state["cont"] = false
         for j, val in pairs(item.values) do
            if globish_match(val, branch) then state["matches"] = true end
         end
      elseif item.name == "allow" then if state["matches"] then
         for j, val in pairs(item.values) do
            if val == "*" then return true end
            if val == "" and ident == nil then return true end
            if ident ~= nil and val == ident.id then return true end
            if ident ~= nil and globish_match(val, ident.name) then return true end
         end
      end elseif item.name == "deny" then if state["matches"] then
         for j, val in pairs(item.values) do
            if val == "*" then return false end
            if val == "" and ident == nil then return false end
            if ident ~= nil and val == ident.id then return false end
            if ident ~= nil and globish_match(val, ident.name) then return false end
         end
      end elseif item.name == "continue" then if state["matches"] then
         state["cont"] = true
         for j, val in pairs(item.values) do
            if val == "false" or val == "no" then
              state["cont"] = false
            end
         end
      end elseif item.name ~= "comment" then
         io.stderr:write("unknown symbol in read-permissions: " .. item.name .. "\n")
         return false
      end
   end
   return false
end

function get_netsync_read_permitted(branch, ident)
   local permfilename = get_confdir() .. "/read-permissions"
   local permdirname = permfilename .. ".d"
   local state = {}
   if _get_netsync_read_permitted(branch, ident, permfilename, state) then
      return true
   end
   if isdir(permdirname) then
      local files = read_directory(permdirname)
      table.sort(files)
      for _,f in ipairs(files) do
        pf = permdirname.."/"..f
        if _get_netsync_read_permitted(branch, ident, pf, state) then
          return true
        end
      end
   end
   return false
end

function _get_netsync_write_permitted(ident, permfilename)
   if not exists(permfilename) or isdir(permfilename) then return false end
   local permfile = io.open(permfilename, "r")
   if (permfile == nil) then
      return false
   end
   local matches = false
   local line = permfile:read()
   while (not matches and line ~= nil) do
      local _, _, ln = string.find(line, "%s*([^%s]*)%s*")
      if ln == "*" then matches = true end
      if ln == ident.id then matches = true end
      if globish_match(ln, ident.name) then matches = true end
      line = permfile:read()
   end
   io.close(permfile)
   return matches
end

function get_netsync_write_permitted(ident)
   local permfilename = get_confdir() .. "/write-permissions"
   local permdirname = permfilename .. ".d"
   if _get_netsync_write_permitted(ident, permfilename) then return true end
   if isdir(permdirname) then
      local files = read_directory(permdirname)
      table.sort(files)
      for _,f in ipairs(files) do
        pf = permdirname.."/"..f
        if _get_netsync_write_permitted(ident, pf) then return true end
      end
   end
   return false
end

-- This is a simple function which assumes you're going to be spawning
-- a copy of mtn, so reuses a common bit at the end for converting
-- local args into remote args. You might need to massage the logic a
-- bit if this doesn't fit your assumptions.

function get_netsync_connect_command(uri, args)

        local argv = nil

        if uri["scheme"] == "ssh"
                and uri["host"]
                and uri["path"] then

                argv = { "ssh" }
                if uri["user"] then
                        table.insert(argv, "-l")
                        table.insert(argv, uri["user"])
                end
                if uri["port"] then
                        table.insert(argv, "-p")
                        table.insert(argv, uri["port"])
                end

                -- ssh://host/~/dir/file.mtn or
                -- ssh://host/~user/dir/file.mtn should be home-relative
                if string.find(uri["path"], "^/~") then
                        uri["path"] = string.sub(uri["path"], 2)
                end

                table.insert(argv, uri["host"])
        end

        if uri["scheme"] == "file" and uri["path"] then
                argv = { }
        end

        if uri["scheme"] == "ssh+ux"
                and uri["host"]
                and uri["path"] then

                argv = { "ssh" }
                if uri["user"] then
                        table.insert(argv, "-l")
                        table.insert(argv, uri["user"])
                end
                if uri["port"] then
                        table.insert(argv, "-p")
                        table.insert(argv, uri["port"])
                end

                -- ssh://host/~/dir/file.mtn or
                -- ssh://host/~user/dir/file.mtn should be home-relative
                if string.find(uri["path"], "^/~") then
                        uri["path"] = string.sub(uri["path"], 2)
                end

                table.insert(argv, uri["host"])
                table.insert(argv, get_remote_unix_socket_command(uri["host"]))
                table.insert(argv, "-")
                table.insert(argv, "UNIX-CONNECT:" .. uri["path"])
        else
            if argv then
                    -- start remote monotone process

                    table.insert(argv, get_mtn_command(uri["host"]))

                    if args["debug"] then
                            table.insert(argv, "--verbose")
                    else
                            table.insert(argv, "--quiet")
                    end

                    table.insert(argv, "--db")
                    table.insert(argv, uri["path"])
                    table.insert(argv, "serve")
                    table.insert(argv, "--stdio")
                    table.insert(argv, "--no-transport-auth")

            -- else scheme does not require starting a new remote
            -- process (ie mtn:)
            end
        end
        return argv
end

function use_transport_auth(uri)
        if uri["scheme"] == "ssh"
        or uri["scheme"] == "ssh+ux"
        or uri["scheme"] == "file" then
                return false
        else
                return true
        end
end

function get_mtn_command(host)
        return "mtn"
end

function get_remote_unix_socket_command(host)
    return "socat"
end

function get_default_command_options(command)
    local default_args = {}
    return default_args
end

function get_default_database_alias()
    return ":default.mtn"
end

function get_default_database_locations()
    local paths = {}
    table.insert(paths, get_confdir() .. "/databases")
    return paths
end

function get_default_database_glob()
    return "*.{mtn,db}"
end

hook_wrapper_dump                = {}
hook_wrapper_dump.depth          = 0
hook_wrapper_dump._string        = function(s) return string.format("%q", s) end
hook_wrapper_dump._number        = function(n) return tostring(n) end
hook_wrapper_dump._boolean       = function(b) if (b) then return "true" end return "false" end
hook_wrapper_dump._userdata      = function(u) return "nil --[[userdata]]" end
-- if we really need to return / serialize functions we could do it
-- like cbreak@irc.freenode.net did here: http://lua-users.org/wiki/TablePersistence
hook_wrapper_dump._function      = function(f) return "nil --[[function]]" end
hook_wrapper_dump._nil           = function(n) return "nil" end
hook_wrapper_dump._thread        = function(t) return "nil --[[thread]]" end
hook_wrapper_dump._lightuserdata = function(l) return "nil --[[lightuserdata]]" end

hook_wrapper_dump._table = function(t)
    local buf = ''
    if (hook_wrapper_dump.depth > 0) then
        buf = buf .. '{\n'
    end
    hook_wrapper_dump.depth = hook_wrapper_dump.depth + 1;
    for k,v in pairs(t) do
        buf = buf..string.format('%s[%s] = %s;\n',
              string.rep("\t", hook_wrapper_dump.depth - 1),
              hook_wrapper_dump["_" .. type(k)](k),
              hook_wrapper_dump["_" .. type(v)](v))
    end
    hook_wrapper_dump.depth = hook_wrapper_dump.depth - 1;
    if (hook_wrapper_dump.depth > 0) then
        buf = buf .. string.rep("\t", hook_wrapper_dump.depth - 1) .. '}'
    end
    return buf
end

function hook_wrapper(func_name, ...)
    -- we have to ensure that nil arguments are restored properly for the
    -- function call, see http://lua-users.org/wiki/StoringNilsInTables
    local args = { n=select('#', ...), ... }
    for i=1,args.n do
        local val = assert(loadstring("return " .. args[i]),
                         "argument "..args[i].." could not be evaluated")()
        assert(val ~= nil or args[i] == "nil",
               "argument "..args[i].." was evaluated to nil")
        args[i] = val
    end
    local res = { _G[func_name](unpack(args, 1, args.n)) }
    return hook_wrapper_dump._table(res)
end

do
   -- Hook functions are tables containing any of the following 6 items
   -- with associated functions:
   --
   --   startup			Corresponds to note_mtn_startup()
   --   start			Corresponds to note_netsync_start()
   --   revision_received	Corresponds to note_netsync_revision_received()
   --   revision_sent		Corresponds to note_netsync_revision_sent()
   --   cert_received		Corresponds to note_netsync_cert_received()
   --   cert_sent		Corresponds to note_netsync_cert_sent()
   --   pubkey_received		Corresponds to note_netsync_pubkey_received()
   --   pubkey_sent		Corresponds to note_netsync_pubkey_sent()
   --   end			Corresponds to note_netsync_end()
   --
   -- Those functions take exactly the same arguments as the corresponding
   -- global functions, but return a different kind of value, a tuple
   -- composed of a return code and a value to be returned back to monotone.
   -- The codes are strings:
   -- "continue" and "stop"
   -- When the code "continue" is returned and there's another notifier, the
   -- second value is ignored and the next notifier is called.  Otherwise,
   -- the second value is returned immediately.
   local hook_functions = {}
   local supported_items = {
      "startup",
      "start", "revision_received", "revision_sent", "cert_received", "cert_sent",
      "pubkey_received", "pubkey_sent", "end"
   }

   function _hook_functions_helper(f,...)
      local s = "continue"
      local v = nil
      for _,n in pairs(hook_functions) do
         if n[f] then
            s,v = n[f](...)
         end
         if s ~= "continue" then
            break
         end
      end
      return v
   end
   function note_mtn_startup(...)
      return _hook_functions_helper("startup",...)
   end
   function note_netsync_start(...)
      return _hook_functions_helper("start",...)
   end
   function note_netsync_revision_received(...)
      return _hook_functions_helper("revision_received",...)
   end
   function note_netsync_revision_sent(...)
      return _hook_functions_helper("revision_sent",...)
   end
   function note_netsync_cert_received(...)
      return _hook_functions_helper("cert_received",...)
   end
   function note_netsync_cert_sent(...)
      return _hook_functions_helper("cert_sent",...)
   end
   function note_netsync_pubkey_received(...)
      return _hook_functions_helper("pubkey_received",...)
   end
   function note_netsync_pubkey_sent(...)
      return _hook_functions_helper("pubkey_sent",...)
   end
   function note_netsync_end(...)
      return _hook_functions_helper("end",...)
   end

   function add_hook_functions(functions, precedence)
      if type(functions) ~= "table" or type(precedence) ~= "number" then
         return false, "Invalid type"
      end
      if hook_functions[precedence] then
         return false, "Precedence already taken"
      end

      local unknown_items = ""
      local warning = nil
      local is_member =
         function (s,t)
            for k,v in pairs(t) do if s == v then return true end end
            return false
         end

      for n,f in pairs(functions) do
         if type(n) == "string" then
            if not is_member(n, supported_items) then
               if unknown_items ~= "" then
                  unknown_items = unknown_items .. ","
               end
               unknown_items = unknown_items .. n
            end
            if type(f) ~= "function" then
               return false, "Value for functions item "..n.." isn't a function"
            end
         else
            warning = "Non-string item keys found in functions table"
         end
      end

      if warning == nil and unknown_items ~= "" then
         warning = "Unknown item(s) " .. unknown_items .. " in functions table"
      end

      hook_functions[precedence] = functions
      return true, warning
   end
   function push_hook_functions(functions)
      local n = #hook_functions + 1
      return add_hook_functions(functions, n)
   end

   -- Kept for backward compatibility
   function add_netsync_notifier(notifier, precedence)
      return add_hook_functions(notifier, precedence)
   end
   function push_netsync_notifier(notifier)
      return push_hook_functions(notifier)
   end
end

-- to ensure only mapped authors are allowed through
-- return "" from unmapped_git_author
-- and validate_git_author will fail

function unmapped_git_author(author)
   -- replace "foo@bar" with "foo <foo@bar>"
   name = author:match("^([^<>]+)@[^<>]+$")
   if name then
      return name .. " <" .. author .. ">"
   end

   -- replace "<foo@bar>" with "foo <foo@bar>"
   name = author:match("^<([^<>]+)@[^<>]+>$")
   if name then
      return name .. " " .. author
   end

   -- replace "foo" with "foo <foo>"
   name = author:match("^[^<>@]+$")
   if name then
      return name .. " <" .. name .. ">"
   end

   return author -- unchanged
end

function validate_git_author(author)
   -- ensure author matches the "Name <email>" format git expects
   if author:match("^[^<]+ <[^>]*>$") then
      return true
   end

   return false
end

function get_man_page_formatter_command()
   local term_width = guess_terminal_width() - 2
   -- The string returned is run in a process created with 'popen'
   -- (see cmd.cc manpage).
   --
   -- On Unix (and POSIX compliant systems), 'popen' runs 'sh' with
   -- the inherited path.
   --
   -- On MinGW, 'popen' runs 'cmd.exe' with the inherited path. MinGW
   -- does not (currently) provide nroff or equivalent. So we assume
   -- sh, nroff, locale and less are also installed, from Cygwin or
   -- some other toolset.
   --
   -- GROFF_ENCODING is an environment variable that, when set, tells
   -- groff (called by nroff where applicable) to use preconv to convert
   -- the input from the given encoding to something groff understands.
   -- For example, groff doesn NOT understand raw UTF-8 as input, but
   -- it does understand unicode, which preconv will happily provide.
   -- This doesn't help people that don't use groff, unfortunately.
   -- Patches are welcome!
   if string.sub(get_ostype(), 1, 7) == "Windows" then
      return string.format("sh -c 'GROFF_ENCODING=`locale charmap` nroff -man -rLL=%dn' | less -R", term_width)
   else
      return string.format("GROFF_ENCODING=`locale charmap` nroff -man -rLL=%dn | less -R", term_width)
   end
end

function get_output_color(purpose)
   -- Returns a triple containing the fore color, background color and
   -- style to use for formatting the output.
   --
   -- The fore color and background color can be any of the following:
   -- red, green, blue, yellow, cyan, magenta, black, white.
   -- Alternatively, they can be the empty string and Monotone will
   -- decide.
   --
   -- Valid values for style are: none, bold, italic, underline.
   -- Alternatively, it can be the empty string and Monotone will
   -- decide.

   local default_color = { fg = "", bg = "", style = "" }
   local color_table = 
   {
      normal = default_color,

      -- added lines in 'diff' output and added files/dirs in 'log'
      add = { fg = "green", bg = "", style = "" },

	  -- branch names
	  branch = { fg = "yellow", bg = "", style = ""},

      -- changed lines in 'diff' output and modified files in 'log'
      change = { fg = "cyan", bg = "", style = "" },

	  -- comment lines in 'diff' output
      comment = { fg = "yellow", bg = "", style = "" },

      -- encloser hint (i.e. function header) in 'diff'
      encloser = { fg = "cyan", bg = "", style = "bold" },

	  -- asciik graphs
      graph = { fg = "yellow", bg = "", style = "" },

      -- hints in 'status'
      hint = { fg = "", bg = "", style = "italic" },

	  -- currently unused!
      important = { fg = "white", bg = "red", style = "bold" },

	  -- deleted lines in 'diff' output and dropped files/dirs in 'log'
      remove = { fg = "red", bg = "", style = "" },

      -- renamed files or dirs in cset summaries
      rename = { fg = "yellow", bg = "", style = "" },

	  -- revision header fields in 'log' output
      rev_header = { fg = "", bg = "", style = "bold" },

	  -- revision ids in 'log', 'diff' and 'status'
      rev_id = { fg = "magenta", bg = "", style = "" },

	  -- separator between sections of different files in 'diff' and between
	  -- revisions in 'log' output
      separator = { fg = "yellow", bg = "", style = "bold" },

      -- attributes set in cset summaries
      set = { fg = "green", bg = "", style = "" },

	  -- attributes cleared in cset summaries
      unset = { fg = "red", bg = "", style = "" }
   }

   local chosen_color = color_table[purpose]
	
   if chosen_color == nil then
      return default_color
   else
      return chosen_color.fg, chosen_color.bg, chosen_color.style
   end
end
