Below is the file 'commands.cc' from this revision. You can also download the file.

// -*- mode: C++; c-file-style: "gnu"; indent-tabs-mode: nil -*-
// copyright (C) 2002, 2003 graydon hoare <graydon@pobox.com>
// all rights reserved.
// licensed to the public under the terms of the GNU GPL (>= 2)
// see the file COPYING for details

#include <map>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <set>
#include <vector>
#include <algorithm>
#include <iterator>
#include <fstream>
#include <boost/lexical_cast.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/tokenizer.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>

#include "commands.hh"
#include "constants.hh"

#include "app_state.hh"
#include "automate.hh"
#include "basic_io.hh"
#include "cert.hh"
#include "database_check.hh"
#include "diff_patch.hh"
#include "file_io.hh"
#include "keys.hh"
#include "manifest.hh"
#include "netsync.hh"
#include "packet.hh"
#include "rcs_import.hh"
#include "restrictions.hh"
#include "sanity.hh"
#include "transforms.hh"
#include "ui.hh"
#include "update.hh"
#include "vocab.hh"
#include "work.hh"
#include "automate.hh"
#include "inodeprint.hh"
#include "platform.hh"
#include "selectors.hh"
#include "annotate.hh"
#include "options.hh"
#include "globish.hh"
#include "paths.hh"

//
// this file defines the task-oriented "top level" commands which can be
// issued as part of a monotone command line. the command line can only
// have one such command on it, followed by a vector of strings which are its
// arguments. all --options will be processed by the main program *before*
// calling a command
//
// we might expose this blunt command interface to scripting someday. but
// not today.

namespace commands
{
  struct command;
  bool operator<(command const & self, command const & other);
};

namespace std
{
  template <>
  struct greater<commands::command *>
  {
    bool operator()(commands::command const * a, commands::command const * b)
    {
      return *a < *b;
    }
  };
};

namespace commands
{
  using namespace std;

  struct command;

  static map<string,command *> cmds;

  struct no_opts {};

  struct command_opts
  {
    set<int> opts;
    command_opts() {}
    command_opts & operator%(int o)
    { opts.insert(o); return *this; }
    command_opts & operator%(no_opts o)
    { return *this; }
    command_opts & operator%(command_opts const &o)
    { opts.insert(o.opts.begin(), o.opts.end()); return *this; }
  };

  struct command
  {
    // NB: these strings are stred _un_translated
    // because we cannot translate them until after main starts, by which time
    // the command objects have all been constructed.
    string name;
    string cmdgroup;
    string params;
    string desc;
    command_opts options;
    command(string const & n,
            string const & g,
            string const & p,
            string const & d,
            command_opts const & o)
      : name(n), cmdgroup(g), params(p), desc(d), options(o)
    { cmds[n] = this; }
    virtual ~command() {}
    virtual void exec(app_state & app, vector<utf8> const & args) = 0;
  };

  bool operator<(command const & self, command const & other)
  {
    // *twitch*
    return ((std::string(_(self.cmdgroup.c_str())) < std::string(_(other.cmdgroup.c_str())))
            || ((self.cmdgroup == other.cmdgroup)
                && (std::string(_(self.name.c_str())) < (std::string(_(other.name.c_str()))))));
  }


  string complete_command(string const & cmd)
  {
    if (cmd.length() == 0 || cmds.find(cmd) != cmds.end()) return cmd;

    L(F("expanding command '%s'\n") % cmd);

    vector<string> matched;

    for (map<string,command *>::const_iterator i = cmds.begin();
         i != cmds.end(); ++i)
      {
        if (cmd.length() < i->first.length())
          {
            string prefix(i->first, 0, cmd.length());
            if (cmd == prefix) matched.push_back(i->first);
          }
      }

    if (matched.size() == 1)
      {
      string completed = *matched.begin();
      L(F("expanded command to '%s'\n") %  completed);
      return completed;
      }
    else if (matched.size() > 1)
      {
      string err = (F("command '%s' has multiple ambiguous expansions:\n") % cmd).str();
      for (vector<string>::iterator i = matched.begin();
           i != matched.end(); ++i)
        err += (*i + "\n");
      W(boost::format(err));
    }

    return cmd;
  }

  const char * safe_gettext(const char * msgid)
  {
    if (strlen(msgid) == 0)
      return msgid;

    return _(msgid);
  }

  void explain_usage(string const & cmd, ostream & out)
  {
    map<string,command *>::const_iterator i;

    // try to get help on a specific command

    i = cmds.find(cmd);

    if (i != cmds.end())
      {
        string params = safe_gettext(i->second->params.c_str());
        vector<string> lines;
        split_into_lines(params, lines);
        for (vector<string>::const_iterator j = lines.begin();
             j != lines.end(); ++j)
          out << "     " << i->second->name << " " << *j << endl;
        split_into_lines(safe_gettext(i->second->desc.c_str()), lines);
        for (vector<string>::const_iterator j = lines.begin();
             j != lines.end(); ++j)
          out << "       " << *j << endl;
        out << endl;
        return;
      }

    vector<command *> sorted;
    out << _("commands:") << endl;
    for (i = cmds.begin(); i != cmds.end(); ++i)
      {
        sorted.push_back(i->second);
      }

    sort(sorted.begin(), sorted.end(), std::greater<command *>());

    string curr_group;
    size_t col = 0;
    size_t col2 = 0;
    for (size_t i = 0; i < sorted.size(); ++i)
      {
        col2 = col2 > idx(sorted, i)->cmdgroup.size() ? col2 : idx(sorted, i)->cmdgroup.size();
      }

    for (size_t i = 0; i < sorted.size(); ++i)
      {
        if (idx(sorted, i)->cmdgroup != curr_group)
          {
            curr_group = idx(sorted, i)->cmdgroup;
            out << endl;
            out << "  " << safe_gettext(idx(sorted, i)->cmdgroup.c_str());
            col = idx(sorted, i)->cmdgroup.size() + 2;
            while (col++ < (col2 + 3))
              out << ' ';
          }
        out << " " << idx(sorted, i)->name;
        col += idx(sorted, i)->name.size() + 1;
        if (col >= 70)
          {
            out << endl;
            col = 0;
            while (col++ < (col2 + 3))
              out << ' ';
          }
      }
    out << endl << endl;
  }

  int process(app_state & app, string const & cmd, vector<utf8> const & args)
  {
    if (cmds.find(cmd) != cmds.end())
      {
        L(F("executing command '%s'\n") % cmd);
        cmds[cmd]->exec(app, args);
        return 0;
      }
    else
      {
        ui.inform(F("unknown command '%s'\n") % cmd);
        return 1;
      }
  }

  set<int> command_options(string const & cmd)
  {
    if (cmds.find(cmd) != cmds.end())
      {
        return cmds[cmd]->options.opts;
      }
    else
      {
        return set<int>();
      }
  }

static const no_opts OPT_NONE = no_opts();

#define CMD(C, group, params, desc, opts)                            \
struct cmd_ ## C : public command                                    \
{                                                                    \
  cmd_ ## C() : command(#C, group, params, desc,                     \
                        command_opts() % opts)                       \
  {}                                                                 \
  virtual void exec(app_state & app,                                 \
                    vector<utf8> const & args);                      \
};                                                                   \
static cmd_ ## C C ## _cmd;                                          \
void cmd_ ## C::exec(app_state & app,                                \
                     vector<utf8> const & args)                      \

#define ALIAS(C, realcommand)                                        \
CMD(C, realcommand##_cmd.cmdgroup, realcommand##_cmd.params,         \
    realcommand##_cmd.desc + "\nAlias for " #realcommand,            \
    realcommand##_cmd.options)                                       \
{                                                                    \
  process(app, string(#realcommand), args);                          \
}

struct pid_file
{
  explicit pid_file(system_path const & p)
    : path(p)
  {
    if (path.empty())
      return;
    require_path_is_nonexistent(path, F("pid file '%s' already exists") % path);
    file.open(path.as_external().c_str());
    file << get_process_id();
    file.flush();
  }

  ~pid_file()
  {
    if (path.empty())
      return;
    pid_t pid;
    std::ifstream(path.as_external().c_str()) >> pid;
    if (pid == get_process_id()) {
      file.close();
      delete_file(path);
    }
  }

private:
  std::ofstream file;
  system_path path;
};

static void
maybe_update_inodeprints(app_state & app)
{
  if (!in_inodeprints_mode())
    return;
  inodeprint_map ipm_new;
  revision_set rev;
  manifest_map man_old, man_new;
  calculate_unrestricted_revision(app, rev, man_old, man_new);
  for (manifest_map::const_iterator i = man_new.begin(); i != man_new.end(); ++i)
    {
      manifest_map::const_iterator o = man_old.find(i->first);
      if (o != man_old.end() && o->second == i->second)
        {
          hexenc<inodeprint> ip;
          if (inodeprint_file(i->first, ip))
            ipm_new.insert(inodeprint_entry(i->first, ip));
        }
    }
  data dat;
  write_inodeprint_map(ipm_new, dat);
  write_inodeprints(dat);
}

static string
get_stdin()
{
  char buf[constants::bufsz];
  string tmp;
  while(cin)
    {
      cin.read(buf, constants::bufsz);
      tmp.append(buf, cin.gcount());
    }
  return tmp;
}

static void
get_log_message(revision_set const & cs,
                app_state & app,
                string & log_message)
{
  string commentary;
  data summary, user_log_message;
  write_revision_set(cs, summary);
  read_user_log(user_log_message);
  commentary += "----------------------------------------------------------------------\n";
  commentary += _("Enter a description of this change.\n"
                  "Lines beginning with `MT:' are removed automatically.\n");
  commentary += "\n";
  commentary += summary();
  commentary += "----------------------------------------------------------------------\n";

  N(app.lua.hook_edit_comment(commentary, user_log_message(), log_message),
    F("edit of log message failed"));
}

static void
notify_if_multiple_heads(app_state & app) {
  set<revision_id> heads;
  get_branch_heads(app.branch_name(), app, heads);
  if (heads.size() > 1) {
    std::string prefixedline;
    prefix_lines_with(_("note: "),
                      _("branch '%s' has multiple heads\n"
                        "perhaps consider 'monotone merge'"),
                      prefixedline);
    P(boost::format(prefixedline) % app.branch_name);
  }
}

static string
describe_revision(app_state & app, revision_id const & id)
{
  cert_name author_name(author_cert_name);
  cert_name date_name(date_cert_name);

  string description;

  description += id.inner()();

  // append authors and date of this revision
  vector< revision<cert> > tmp;
  app.db.get_revision_certs(id, author_name, tmp);
  erase_bogus_certs(tmp, app);
  for (vector< revision<cert> >::const_iterator i = tmp.begin();
       i != tmp.end(); ++i)
    {
      cert_value tv;
      decode_base64(i->inner().value, tv);
      description += " ";
      description += tv();
    }
  app.db.get_revision_certs(id, date_name, tmp);
  erase_bogus_certs(tmp, app);
  for (vector< revision<cert> >::const_iterator i = tmp.begin();
       i != tmp.end(); ++i)
    {
      cert_value tv;
      decode_base64(i->inner().value, tv);
      description += " ";
      description += tv();
    }

  return description;
}

static void
complete(app_state & app,
         string const & str,
         revision_id & completion,
         bool must_exist=true)
{
  // This copies the start of selectors::parse_selector().to avoid
  // getting a log when there's no expansion happening...:
  //
  // this rule should always be enabled, even if the user specifies
  // --norc: if you provide a revision id, you get a revision id.
  if (str.find_first_not_of(constants::legal_id_bytes) == string::npos
      && str.size() == constants::idlen)
    {
      completion = revision_id(str);
      if (must_exist)
        N(app.db.revision_exists(completion),
          F("no such revision '%s'") % completion);
      return;
    }

  vector<pair<selectors::selector_type, string> >
    sels(selectors::parse_selector(str, app));

  P(F("expanding selection '%s'\n") % str);

  // we jam through an "empty" selection on sel_ident type
  set<string> completions;
  selectors::selector_type ty = selectors::sel_ident;
  selectors::complete_selector("", sels, ty, completions, app);

  N(completions.size() != 0,
    F("no match for selection '%s'") % str);
  if (completions.size() > 1)
    {
      string err = (F("selection '%s' has multiple ambiguous expansions: \n") % str).str();
      for (set<string>::const_iterator i = completions.begin();
           i != completions.end(); ++i)
        err += (describe_revision(app, revision_id(*i)) + "\n");
      N(completions.size() == 1, boost::format(err));
    }
  completion = revision_id(*(completions.begin()));
  P(F("expanded to '%s'\n") %  completion);
}


template<typename ID>
static void
complete(app_state & app,
         string const & str,
         ID & completion)
{
  N(str.find_first_not_of(constants::legal_id_bytes) == string::npos,
    F("non-hex digits in id"));
  if (str.size() == constants::idlen)
    {
      completion = ID(str);
      return;
    }
  set<ID> completions;
  app.db.complete(str, completions);
  N(completions.size() != 0,
    F("partial id '%s' does not have an expansion") % str);
  if (completions.size() > 1)
    {
      string err = (F("partial id '%s' has multiple ambiguous expansions:\n") % str).str();
      for (typename set<ID>::const_iterator i = completions.begin();
           i != completions.end(); ++i)
        err += (i->inner()() + "\n");
      N(completions.size() == 1, boost::format(err));
    }
  completion = *(completions.begin());
  P(F("expanded partial id '%s' to '%s'\n")
    % str % completion);
}

static void
ls_certs(string const & name, app_state & app, vector<utf8> const & args)
{
  if (args.size() != 1)
    throw usage(name);

  vector<cert> certs;

  transaction_guard guard(app.db);

  revision_id ident;
  complete(app, idx(args, 0)(), ident);
  vector< revision<cert> > ts;
  app.db.get_revision_certs(ident, ts);
  for (size_t i = 0; i < ts.size(); ++i)
    certs.push_back(idx(ts, i).inner());

  {
    set<rsa_keypair_id> checked;
    for (size_t i = 0; i < certs.size(); ++i)
      {
        if (checked.find(idx(certs, i).key) == checked.end() &&
            !app.db.public_key_exists(idx(certs, i).key))
          P(F("no public key '%s' found in database")
            % idx(certs, i).key);
        checked.insert(idx(certs, i).key);
      }
  }

  // Make the output deterministic; this is useful for the test suite, in
  // particular.
  sort(certs.begin(), certs.end());

  string str     = _("Key   : %s\n"
                     "Sig   : %s\n"
                     "Name  : %s\n"
                     "Value : %s\n");
  string extra_str = "      : %s\n";

  string::size_type colon_pos = str.find(':');

  if (colon_pos != string::npos)
    {
      string substr(str, 0, colon_pos);
      colon_pos = length(substr);
      extra_str = string(colon_pos, ' ') + ": %s\n";
    }

  for (size_t i = 0; i < certs.size(); ++i)
    {
      cert_status status = check_cert(app, idx(certs, i));
      cert_value tv;
      decode_base64(idx(certs, i).value, tv);
      string washed;
      if (guess_binary(tv()))
        {
          washed = "<binary data>";
        }
      else
        {
          washed = tv();
        }

      string stat;
      switch (status)
        {
        case cert_ok:
          stat = _("ok");
          break;
        case cert_bad:
          stat = _("bad");
          break;
        case cert_unknown:
          stat = _("unknown");
          break;
        }

      vector<string> lines;
      split_into_lines(washed, lines);
      I(lines.size() > 0);

      cout << std::string(guess_terminal_width(), '-') << '\n'
           << boost::format(str)
        % idx(certs, i).key()
        % stat
        % idx(certs, i).name()
        % idx(lines, 0);

      for (size_t i = 1; i < lines.size(); ++i)
        cout << boost::format(extra_str) % idx(lines, i);
    }

  if (certs.size() > 0)
    cout << endl;

  guard.commit();
}

static void
ls_keys(string const & name, app_state & app, vector<utf8> const & args)
{
  vector<rsa_keypair_id> pubkeys;
  vector<rsa_keypair_id> privkeys;

  transaction_guard guard(app.db);

  if (args.size() == 0)
    app.db.get_key_ids("", pubkeys, privkeys);
  else if (args.size() == 1)
    app.db.get_key_ids(idx(args, 0)(), pubkeys, privkeys);
  else
    throw usage(name);

  if (pubkeys.size() > 0)
    {
      cout << endl << "[public keys]" << endl;
      for (size_t i = 0; i < pubkeys.size(); ++i)
        {
          rsa_keypair_id keyid = idx(pubkeys, i)();
          base64<rsa_pub_key> pub_encoded;
          hexenc<id> hash_code;

          app.db.get_key(keyid, pub_encoded);
          key_hash_code(keyid, pub_encoded, hash_code);
          cout << hash_code << " " << keyid << endl;
        }
      cout << endl;
    }

  if (privkeys.size() > 0)
    {
      cout << endl << "[private keys]" << endl;
      for (size_t i = 0; i < privkeys.size(); ++i)
        {
          rsa_keypair_id keyid = idx(privkeys, i)();
          base64< arc4<rsa_priv_key> > priv_encoded;
          hexenc<id> hash_code;
          app.db.get_key(keyid, priv_encoded);
          key_hash_code(keyid, priv_encoded, hash_code);
          cout << hash_code << " " << keyid << endl;
        }
      cout << endl;
    }

  if (pubkeys.size() == 0 &&
      privkeys.size() == 0)
    {
      if (args.size() == 0)
        P(F("no keys found\n"));
      else
        W(F("no keys found matching '%s'\n") % idx(args, 0)());
    }
}

// Deletes a revision from the local database.  This can be used to 'undo' a
// changed revision from a local database without leaving (much of) a trace.
static void
kill_rev_locally(app_state& app, std::string const& id)
{
  revision_id ident;
  complete(app, id, ident);
  N(app.db.revision_exists(ident),
    F("no such revision '%s'") % ident);

  //check that the revision does not have any children
  set<revision_id> children;
  app.db.get_revision_children(ident, children);
  N(!children.size(),
    F("revision %s already has children. We cannot kill it.") % ident);

  app.db.delete_existing_rev_and_certs(ident);
}

// The changes_summary structure holds a list all of files and directories
// affected in a revision, and is useful in the 'log' command to print this
// information easily.  It has to be constructed from all change_set objects
// that belong to a revision.
struct
changes_summary
{
  bool empty;
  change_set::path_rearrangement rearrangement;
  std::set<file_path> modified_files;

  changes_summary(void);
  void add_change_set(change_set const & cs);
  void print(std::ostream & os, size_t max_cols) const;
};

changes_summary::changes_summary(void) : empty(true)
{
}

void
changes_summary::add_change_set(change_set const & cs)
{
  if (cs.empty())
    return;
  empty = false;

  change_set::path_rearrangement const & pr = cs.rearrangement;

  for (std::set<file_path>::const_iterator i = pr.deleted_files.begin();
       i != pr.deleted_files.end(); i++)
    rearrangement.deleted_files.insert(*i);

  for (std::set<file_path>::const_iterator i = pr.deleted_dirs.begin();
       i != pr.deleted_dirs.end(); i++)
    rearrangement.deleted_dirs.insert(*i);

  for (std::map<file_path, file_path>::const_iterator
       i = pr.renamed_files.begin(); i != pr.renamed_files.end(); i++)
    rearrangement.renamed_files.insert(*i);

  for (std::map<file_path, file_path>::const_iterator
       i = pr.renamed_dirs.begin(); i != pr.renamed_dirs.end(); i++)
    rearrangement.renamed_dirs.insert(*i);

  for (std::set<file_path>::const_iterator i = pr.added_files.begin();
       i != pr.added_files.end(); i++)
    rearrangement.added_files.insert(*i);

  for (change_set::delta_map::const_iterator i = cs.deltas.begin();
       i != cs.deltas.end(); i++)
    {
      if (pr.added_files.find(i->first) == pr.added_files.end())
        modified_files.insert(i->first);
    }
}

static void
print_indented_set(std::ostream & os,
                   set<file_path> const & s,
                   size_t max_cols)
{
  size_t cols = 8;
  os << "       ";
  for (std::set<file_path>::const_iterator i = s.begin();
       i != s.end(); i++)
    {
      const std::string str = boost::lexical_cast<std::string>(*i);
      if (cols > 8 && cols + str.size() + 1 >= max_cols)
        {
          cols = 8;
          os << endl << "       ";
        }
      os << " " << str;
      cols += str.size() + 1;
    }
  os << endl;
}

void
changes_summary::print(std::ostream & os, size_t max_cols) const
{
  if (! rearrangement.deleted_files.empty())
    {
      os << "Deleted files:" << endl;
      print_indented_set(os, rearrangement.deleted_files, max_cols);
    }

  if (! rearrangement.deleted_dirs.empty())
    {
      os << "Deleted directories:" << endl;
      print_indented_set(os, rearrangement.deleted_dirs, max_cols);
    }

  if (! rearrangement.renamed_files.empty())
    {
      os << "Renamed files:" << endl;
      for (std::map<file_path, file_path>::const_iterator
           i = rearrangement.renamed_files.begin();
           i != rearrangement.renamed_files.end(); i++)
        os << "        " << i->first << " to " << i->second << endl;
    }

  if (! rearrangement.renamed_dirs.empty())
    {
      os << "Renamed directories:" << endl;
      for (std::map<file_path, file_path>::const_iterator
           i = rearrangement.renamed_dirs.begin();
           i != rearrangement.renamed_dirs.end(); i++)
        os << "        " << i->first << " to " << i->second << endl;
    }

  if (! rearrangement.added_files.empty())
    {
      os << "Added files:" << endl;
      print_indented_set(os, rearrangement.added_files, max_cols);
    }

  if (! modified_files.empty())
    {
      os << "Modified files:" << endl;
      print_indented_set(os, modified_files, max_cols);
    }
}

CMD(genkey, N_("key and cert"), N_("KEYID"), N_("generate an RSA key-pair"), OPT_NONE)
{
  if (args.size() != 1)
    throw usage(name);

  transaction_guard guard(app.db);
  rsa_keypair_id ident;
  internalize_rsa_keypair_id(idx(args, 0), ident);

  N(! app.db.key_exists(ident),
    F("key '%s' already exists in database") % ident);

  base64<rsa_pub_key> pub;
  base64< arc4<rsa_priv_key> > priv;
  P(F("generating key-pair '%s'\n") % ident);
  generate_key_pair(app.lua, ident, pub, priv);
  P(F("storing key-pair '%s' in database\n") % ident);
  app.db.put_key_pair(ident, pub, priv);

  guard.commit();
}

CMD(dropkey, N_("key and cert"), N_("KEYID"), N_("drop a public and private key"), OPT_NONE)
{
  bool key_deleted = false;

  if (args.size() != 1)
    throw usage(name);

  transaction_guard guard(app.db);
  rsa_keypair_id ident(idx(args, 0)());
  if (app.db.public_key_exists(ident))
    {
      P(F("dropping public key '%s' from database\n") % ident);
      app.db.delete_public_key(ident);
      key_deleted = true;
    }

  if (app.db.private_key_exists(ident))
    {
      P(F("dropping private key '%s' from database\n\n") % ident);
      W(F("the private key data may not have been erased from the\n"
          "database. it is recommended that you use 'db dump' and\n"
          "'db load' to be sure."));
      app.db.delete_private_key(ident);
      key_deleted = true;
    }

  N(key_deleted,
    F("public or private key '%s' does not exist in database") % idx(args, 0)());

  guard.commit();
}

CMD(chkeypass, N_("key and cert"), N_("KEYID"),
    N_("change passphrase of a private RSA key"),
    OPT_NONE)
{
  if (args.size() != 1)
    throw usage(name);

  transaction_guard guard(app.db);
  rsa_keypair_id ident;
  internalize_rsa_keypair_id(idx(args, 0), ident);

  N(app.db.key_exists(ident),
    F("key '%s' does not exist in database") % ident);

  base64< arc4<rsa_priv_key> > key;
  app.db.get_key(ident, key);
  change_key_passphrase(app.lua, ident, key);
  app.db.delete_private_key(ident);
  app.db.put_key(ident, key);
  P(F("passphrase changed\n"));

  guard.commit();
}

CMD(cert, N_("key and cert"), N_("REVISION CERTNAME [CERTVAL]"),
    N_("create a cert for a revision"), OPT_NONE)
{
  if ((args.size() != 3) && (args.size() != 2))
    throw usage(name);

  transaction_guard guard(app.db);

  hexenc<id> ident;
  revision_id rid;
  complete(app, idx(args, 0)(), rid);
  ident = rid.inner();

  cert_name name;
  internalize_cert_name(idx(args, 1), name);

  rsa_keypair_id key;
  if (app.signing_key() != "")
    key = app.signing_key;
  else
    N(guess_default_key(key, app),
      F("no unique private key found, and no key specified"));

  cert_value val;
  if (args.size() == 3)
    val = cert_value(idx(args, 2)());
  else
    val = cert_value(get_stdin());

  base64<cert_value> val_encoded;
  encode_base64(val, val_encoded);

  cert t(ident, name, val_encoded, key);

  packet_db_writer dbw(app);
  calculate_cert(app, t);
  dbw.consume_revision_cert(revision<cert>(t));
  guard.commit();
}

CMD(trusted, N_("key and cert"), N_("REVISION NAME VALUE SIGNER1 [SIGNER2 [...]]"),
    N_("test whether a hypothetical cert would be trusted\n"
      "by current settings"),
    OPT_NONE)
{
  if (args.size() < 4)
    throw usage(name);

  revision_id rid;
  complete(app, idx(args, 0)(), rid, false);
  hexenc<id> ident(rid.inner());

  cert_name name;
  internalize_cert_name(idx(args, 1), name);

  cert_value value(idx(args, 2)());

  set<rsa_keypair_id> signers;
  for (unsigned int i = 3; i != args.size(); ++i)
    {
      rsa_keypair_id keyid;
      internalize_rsa_keypair_id(idx(args, i), keyid);
      signers.insert(keyid);
    }


  bool trusted = app.lua.hook_get_revision_cert_trust(signers, ident,
                                                      name, value);


  ostringstream all_signers;
  copy(signers.begin(), signers.end(),
       ostream_iterator<rsa_keypair_id>(all_signers, " "));

  cout << F("if a cert on: %s\n"
            "with key: %s\n"
            "and value: %s\n"
            "was signed by: %s\n"
            "it would be: %s\n")
    % ident
    % name
    % value
    % all_signers.str()
    % (trusted ? _("trusted") : _("UNtrusted"));
}

CMD(tag, N_("review"), N_("REVISION TAGNAME"),
    N_("put a symbolic tag cert on a revision version"), OPT_NONE)
{
  if (args.size() != 2)
    throw usage(name);

  revision_id r;
  complete(app, idx(args, 0)(), r);
  packet_db_writer dbw(app);
  cert_revision_tag(r, idx(args, 1)(), app, dbw);
}


CMD(testresult, N_("review"), N_("ID (pass|fail|true|false|yes|no|1|0)"),
    N_("note the results of running a test on a revision"), OPT_NONE)
{
  if (args.size() != 2)
    throw usage(name);

  revision_id r;
  complete(app, idx(args, 0)(), r);
  packet_db_writer dbw(app);
  cert_revision_testresult(r, idx(args, 1)(), app, dbw);
}

CMD(approve, N_("review"), N_("REVISION"),
    N_("approve of a particular revision"),
    OPT_BRANCH_NAME)
{
  if (args.size() != 1)
    throw usage(name);

  revision_id r;
  complete(app, idx(args, 0)(), r);
  packet_db_writer dbw(app);
  cert_value branchname;
  guess_branch(r, app, branchname);
  N(app.branch_name() != "", F("need --branch argument for approval"));
  cert_revision_in_branch(r, app.branch_name(), app, dbw);
}


CMD(disapprove, N_("review"), N_("REVISION"),
    N_("disapprove of a particular revision"),
    OPT_BRANCH_NAME)
{
  if (args.size() != 1)
    throw usage(name);

  revision_id r;
  revision_set rev, rev_inverse;
  boost::shared_ptr<change_set> cs_inverse(new change_set());
  complete(app, idx(args, 0)(), r);
  app.db.get_revision(r, rev);

  N(rev.edges.size() == 1,
    F("revision '%s' has %d changesets, cannot invert\n") % r % rev.edges.size());

  cert_value branchname;
  guess_branch(r, app, branchname);
  N(app.branch_name() != "", F("need --branch argument for disapproval"));

  edge_entry const & old_edge (*rev.edges.begin());
  rev_inverse.new_manifest = edge_old_manifest(old_edge);
  manifest_map m_old;
  app.db.get_manifest(edge_old_manifest(old_edge), m_old);
  invert_change_set(edge_changes(old_edge), m_old, *cs_inverse);
  rev_inverse.edges.insert(make_pair(r, make_pair(rev.new_manifest, cs_inverse)));

  {
    transaction_guard guard(app.db);
    packet_db_writer dbw(app);

    revision_id inv_id;
    revision_data rdat;

    write_revision_set(rev_inverse, rdat);
    calculate_ident(rdat, inv_id);
    dbw.consume_revision_data(inv_id, rdat);

    cert_revision_in_branch(inv_id, branchname, app, dbw);
    cert_revision_date_now(inv_id, app, dbw);
    cert_revision_author_default(inv_id, app, dbw);
    cert_revision_changelog(inv_id, (boost::format("disapproval of revision '%s'") % r).str(), app, dbw);
    guard.commit();
  }
}

CMD(comment, N_("review"), N_("REVISION [COMMENT]"),
    N_("comment on a particular revision"), OPT_NONE)
{
  if (args.size() != 1 && args.size() != 2)
    throw usage(name);

  string comment;
  if (args.size() == 2)
    comment = idx(args, 1)();
  else
    N(app.lua.hook_edit_comment("", "", comment),
      F("edit comment failed"));

  N(comment.find_first_not_of(" \r\t\n") != string::npos,
    F("empty comment"));

  revision_id r;
  complete(app, idx(args, 0)(), r);
  packet_db_writer dbw(app);
  cert_revision_comment(r, comment, app, dbw);
}



CMD(add, N_("working copy"), N_("PATH..."),
    N_("add files to working copy"), OPT_NONE)
{
  if (args.size() < 1)
    throw usage(name);

  app.require_working_copy();

  manife