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; }; 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; 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