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 <boost/lexical_cast.hpp> #include <boost/shared_ptr.hpp> #include <boost/tokenizer.hpp> #include <boost/filesystem/fstream.hpp> #include <boost/filesystem/path.hpp> #include <boost/filesystem/convenience.hpp> #include <boost/filesystem/exception.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" // // 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 { 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) { return ((self.cmdgroup < other.cmdgroup) || ((self.cmdgroup == other.cmdgroup) && (self.name < other.name))); } 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; } 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 = i->second->params; 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(i->second->desc, 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 << " " << idx(sorted, i)->cmdgroup; 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 %s command\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(fs::path const & p) : path(p) { if (path.empty()) return; N(!fs::exists(path), F("pid file '%s' already exists") % path.string()); file.open(path); file << get_process_id(); file.flush(); } ~pid_file() { if (path.empty()) return; pid_t pid; fs::ifstream(path) >> pid; if (pid == get_process_id()) { file.close(); fs::remove(path); } } private: fs::ofstream file; fs::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 Log. 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) { P(F("note: branch '%s' has multiple heads\nnote: perhaps consider 'monotone merge'") % 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); } static void complete(app_state & app, string const & str, manifest_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 = manifest_id(str); return; } set<manifest_id> completions; app.db.complete(str, completions); N(completions.size() != 0, F("partial id '%s' does not have a unique expansion") % str); if (completions.size() > 1) { string err = (F("partial id '%s' has multiple ambiguous expansions: \n") % str).str(); for (set<manifest_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("expanding partial id '%s'\n") % str); P(F("expanded to '%s'\n") % completion); } static void complete(app_state & app, string const & str, file_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 = file_id(str); return; } set<file_id> completions; app.db.complete(str, completions); N(completions.size() != 0, F("partial id '%s' does not have a unique expansion") % str); if (completions.size() > 1) { string err = (F("partial id '%s' has multiple ambiguous expansions: \n") % str).str(); for (set<file_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("expanding partial id '%s'\n") % str); P(F("expanded to '%s'\n") % 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("warning: no public key '%s' found in database\n") % 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()); 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 << "-----------------------------------------------------------------" << endl << "Key : " << idx(certs, i).key() << endl << "Sig : " << stat << endl << "Name : " << idx(certs, i).name() << endl << "Value : " << idx(lines, 0) << endl; for (size_t i = 1; i < lines.size(); ++i) cout << " : " << idx(lines, i) << endl; } 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 revision %s found in database") % 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 = (*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, "key and cert", "KEYID", "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, "key and cert", "KEYID", "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")); W(F("database. it is recommended that you use 'db dump' and")); W(F("'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, "key and cert", "KEYID", "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, "key and cert", "REVISION CERTNAME [CERTVAL]", "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, "key and cert", "REVISION NAME VALUE SIGNER1 [SIGNER2 [...]]", "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); cout << "if a cert on: " << ident << endl << "with key: " << name << endl << "and value: " << value << endl << "was signed by: "; for (set<rsa_keypair_id>::const_iterator i = signers.begin(); i != signers.end(); ++i) cout << *i << " "; cout << endl << "it would be: " << (trusted ? "trusted" : "UNtrusted") << endl; } CMD(tag, "review", "REVISION TAGNAME", "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, "review", "ID (pass|fail|true|false|yes|no|1|0)", "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, "review", "REVISION", "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, "review", "REVISION", "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, (F("disapproval of revision %s") % r).str(), app, dbw); guard.commit(); } } CMD(comment, "review", "REVISION [COMMENT]", "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, "working copy", "PATH...", "add files to working copy", OPT_NONE) { if (args.size() < 1) throw usage(name); app.require_working_copy(); manifest_map m_old; get_base_manifest(app, m_old); change_set::path_rearrangement work; get_path_rearrangement(work); vector<file_path> paths; for (vector<utf8>::const_iterator i = args.begin(); i != args.end(); ++i) paths.push_back(app.prefix(*i)); build_additions(paths, m_old, app, work); put_path_rearrangement(work); update_any_attrs(app); } CMD(drop, "working copy", "PATH...", "drop files from working copy", OPT_NONE) { if (args.size() < 1) throw usage