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 -*- // vim: et:sw=2:sts=2:ts=2:cino=>2s,{s,\:s,+s,t0,g0,^-2,e-2,n-2,p2s,(0,=s: // 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 "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 "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" #include "merge.hh" #include "roster_merge.hh" #include "roster.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(FL("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(FL("expanded command to '%s'") % 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(i18n_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) { size_t cmp = display_width(utf8(safe_gettext(idx(sorted, i)->cmdgroup.c_str()))); col2 = col2 > cmp ? col2 : cmp; } 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 = display_width(utf8(safe_gettext(idx(sorted, i)->cmdgroup.c_str()))) + 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(FL("executing command '%s'\n") % cmd); cmds[cmd]->exec(app, args); return 0; } else { P(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; }; CMD(help, N_("informative"), N_("command [ARGS...]"), N_("display command help"), OPT_NONE) { if (args.size() < 1) throw usage(""); string full_cmd = complete_command(idx(args, 0)()); if (cmds.find(full_cmd) == cmds.end()) throw usage(""); throw usage(full_cmd); } static void maybe_update_inodeprints(app_state & app) { if (!in_inodeprints_mode()) return; inodeprint_map ipm_new; revision_set rev; roster_t old_roster, new_roster; get_unrestricted_working_revision_and_rosters(app, rev, old_roster, new_roster); node_map const & new_nodes = new_roster.all_nodes(); for (node_map::const_iterator i = new_nodes.begin(); i != new_nodes.end(); ++i) { node_id nid = i->first; if (old_roster.has_node(nid)) { node_t old_node = old_roster.get_node(nid); if (is_file_t(old_node)) { node_t new_node = i->second; I(is_file_t(new_node)); file_t old_file = downcast_to_file_t(old_node); file_t new_file = downcast_to_file_t(new_node); if (new_file->content == old_file->content) { split_path sp; new_roster.get_name(nid, sp); file_path fp(sp); hexenc<inodeprint> ip; if (inodeprint_file(fp, ip)) ipm_new.insert(inodeprint_entry(fp, 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_interactively(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(i18n_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, i18n_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, i18n_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, false); 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 = display_width(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' << (i18n_format(str) % idx(certs, i).key() % stat % idx(certs, i).name() % idx(lines, 0)); for (size_t i = 1; i < lines.size(); ++i) cout << (i18n_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> pubs; vector<rsa_keypair_id> privkeys; std::string pattern; if (args.size() == 1) pattern = idx(args, 0)(); else if (args.size() > 1) throw usage(name); if (app.db.database_specified()) { transaction_guard guard(app.db, false); app.db.get_key_ids(pattern, pubs); guard.commit(); } app.keys.get_key_ids(pattern, privkeys); // true if it is in the database, false otherwise map<rsa_keypair_id, bool> pubkeys; for (vector<rsa_keypair_id>::const_iterator i = pubs.begin(); i != pubs.end(); i++) pubkeys[*i] = true; bool all_in_db = true; for (vector<rsa_keypair_id>::const_iterator i = privkeys.begin(); i != privkeys.end(); i++) { if (pubkeys.find(*i) == pubkeys.end()) { pubkeys[*i] = false; all_in_db = false; } } if (pubkeys.size() > 0) { cout << endl << "[public keys]" << endl; for (map<rsa_keypair_id, bool>::iterator i = pubkeys.begin(); i != pubkeys.end(); i++) { base64<rsa_pub_key> pub_encoded; hexenc<id> hash_code; rsa_keypair_id keyid = i->first; bool indb = i->second; if (indb) app.db.get_key(keyid, pub_encoded); else { keypair kp; app.keys.get_key_pair(keyid, kp); pub_encoded = kp.pub; } key_hash_code(keyid, pub_encoded, hash_code); if (indb) cout << hash_code << " " << keyid << endl; else cout << hash_code << " " << keyid << " (*)" << endl; } if (!all_in_db) cout << F("(*) - only in %s/") % app.keys.get_key_dir() << endl; cout << endl; } if (privkeys.size() > 0) { cout << endl << "[private keys]" << endl; for (vector<rsa_keypair_id>::iterator i = privkeys.begin(); i != privkeys.end(); i++) { keypair kp; hexenc<id> hash_code; app.keys.get_key_pair(*i, kp); key_hash_code(*i, kp.priv, hash_code); cout << hash_code << " " << *i << 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 cset objects // that belong to a revision. struct changes_summary { cset cs; changes_summary(void); void add_change_set(cset const & cs); void print(std::ostream & os, size_t max_cols) const; }; changes_summary::changes_summary(void) { } void changes_summary::add_change_set(cset const & c) { if (c.empty()) return; // FIXME: not sure whether it matters for an informal summary // object like this, but the pre-state names in deletes and renames // are not really sensible to union; they refer to different trees // so mixing them up in a single set is potentially ambiguous. copy(c.nodes_deleted.begin(), c.nodes_deleted.end(), inserter(cs.nodes_deleted, cs.nodes_deleted.begin())); copy(c.files_added.begin(), c.files_added.end(), inserter(cs.files_added, cs.files_added.begin())); copy(c.dirs_added.begin(), c.dirs_added.end(), inserter(cs.dirs_added, cs.dirs_added.begin())); copy(c.nodes_renamed.begin(), c.nodes_renamed.end(), inserter(cs.nodes_renamed, cs.nodes_renamed.begin())); copy(c.deltas_applied.begin(), c.deltas_applied.end(), inserter(cs.deltas_applied, cs.deltas_applied.begin())); copy(c.attrs_cleared.begin(), c.attrs_cleared.end(), inserter(cs.attrs_cleared, cs.attrs_cleared.begin())); copy(c.attrs_set.begin(), c.attrs_set.end(), inserter(cs.attrs_set, cs.attrs_set.begin())); } static void print_indented_set(std::ostream & os, path_set const & s, size_t max_cols) { size_t cols = 8; os << " "; for (path_set::const_iterator i = s.begin(); i != s.end(); i++) { const std::string str = boost::lexical_cast<std::string>(file_path(*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 (! cs.nodes_deleted.empty()) { os << "Deleted entries:" << endl; print_indented_set(os, cs.nodes_deleted, max_cols); } if (! cs.nodes_renamed.empty()) { os << "Renamed entries:" << endl; for (std::map<split_path, split_path>::const_iterator i = cs.nodes_renamed.begin(); i != cs.nodes_renamed.end(); i++) os << " " << file_path(i->first) << " to " << file_path(i->second) << endl; } if (! cs.files_added.empty()) { path_set tmp; for (std::map<split_path, file_id>::const_iterator i = cs.files_added.begin(); i != cs.files_added.end(); ++i) tmp.insert(i->first); os << "Added files:" << endl; print_indented_set(os, tmp, max_cols); } if (! cs.dirs_added.empty()) { os << "Added directories:" << endl; print_indented_set(os, cs.dirs_added, max_cols); } if (! cs.deltas_applied.empty()) { path_set tmp; for (std::map<split_path, std::pair<file_id, file_id> >::const_iterator i = cs.deltas_applied.begin(); i != cs.deltas_applied.end(); ++i) tmp.insert(i->first); os << "Modified files:" << endl; print_indented_set(os, tmp, max_cols); } if (! cs.attrs_set.empty() || ! cs.attrs_cleared.empty()) { path_set tmp; for (std::set<std::pair<split_path, attr_key> >::const_iterator i = cs.attrs_cleared.begin(); i != cs.attrs_cleared.end(); ++i) tmp.insert(i->first); for (std::map<std::pair<split_path, attr_key>, attr_value>::const_iterator i = cs.attrs_set.begin(); i != cs.attrs_set.end(); ++i) tmp.insert(i->first.first); os << "Modified attrs:" << endl; print_indented_set(os, tmp, 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); rsa_keypair_id ident; internalize_rsa_keypair_id(idx(args, 0), ident); bool exists = app.keys.key_pair_exists(ident); if (app.db.database_specified()) { transaction_guard guard(app.db); exists = exists || app.db.public_key_exists(ident); guard.commit(); } N(!exists, F("key '%s' already exists") % ident); keypair kp; P(F("generating key-pair '%s'") % ident); generate_key_pair(app.lua, ident, kp); P(F("storing key-pair '%s' in %s/") % ident % app.keys.get_key_dir()); app.keys.put_key_pair(ident, kp); } 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); rsa_keypair_id ident(idx(args, 0)()); bool checked_db = false; if (app.db.database_specified()) { transaction_guard guard(app.db); 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; } guard.commit(); checked_db = true; } if (app.keys.key_pair_exists(ident)) { P(F("dropping key pair '%s' from keystore\n") % ident); app.keys.delete_key(ident); key_deleted = true; } i18n_format fmt; if (checked_db) fmt = F("public or private key '%s' does not exist in keystore or database"); else fmt = F("public or private key '%s' does not exist in keystore, and no database was specified"); N(key_deleted, fmt % idx(args, 0)()); } 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); rsa_keypair_id ident; internalize_rsa_keypair_id(idx(args, 0), ident); N(app.keys.key_pair_exists(ident), F("key '%s' does not exist in the keystore") % ident); keypair key; app.keys.get_key_pair(ident, key); change_key_passphrase(app.lua, ident, key.priv); app.keys.delete_key(ident); app.keys.put_key_pair(ident, key); P(F("passphrase changed\n")); } 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; get_user_key(key, app); 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