Below is the file 'automate.cc' from this revision. You can also download the file.
// Copyright (C) 2004, 2007 Nathaniel Smith <njs@pobox.com> // Copyright (C) 2007 - 2008 Stephen Leake <stephen_leake@stephe-leake.org> // // This program is made available under the GNU GPL version 2.0 or // greater. See the accompanying file COPYING for details. // // This program is distributed WITHOUT ANY WARRANTY; without even the // implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR // PURPOSE. #include "base.hh" #include <algorithm> #include <iterator> #include <sstream> #include <unistd.h> #include "vector.hh" #include <boost/bind.hpp> #include <boost/function.hpp> #include <boost/lexical_cast.hpp> #include <boost/tuple/tuple.hpp> #include "app_state.hh" #include "project.hh" #include "basic_io.hh" #include "cert.hh" #include "cmd.hh" #include "commands.hh" #include "constants.hh" #include "inodeprint.hh" #include "keys.hh" #include "key_store.hh" #include "file_io.hh" #include "packet.hh" #include "restrictions.hh" #include "revision.hh" #include "roster.hh" #include "transforms.hh" #include "simplestring_xform.hh" #include "vocab.hh" #include "globish.hh" #include "charset.hh" #include "safe_map.hh" #include "work.hh" #include "xdelta.hh" #include "database.hh" using std::allocator; using std::basic_ios; using std::basic_stringbuf; using std::char_traits; using std::inserter; using std::make_pair; using std::map; using std::multimap; using std::ostream; using std::ostringstream; using std::pair; using std::set; using std::sort; using std::streamsize; using std::string; using std::vector; // Name: heads // Arguments: // 1: branch name (optional, default branch is used if non-existant) // Added in: 0.0 // Purpose: Prints the heads of the given branch. // Output format: A list of revision ids, in hexadecimal, each followed by a // newline. Revision ids are printed in alphabetically sorted order. // Error conditions: If the branch does not exist, prints nothing. (There are // no heads.) CMD_AUTOMATE(heads, N_("[BRANCH]"), N_("Prints the heads of the given branch"), "", options::opts::none) { N(args.size() < 2, F("wrong argument count")); database db(app); project_t project(db); branch_name branch; if (args.size() == 1) // branchname was explicitly given, use that branch = branch_name(idx(args, 0)()); else { workspace::require_workspace(F("with no argument, this command prints the heads of the workspace's branch")); branch = app.opts.branchname; } set<revision_id> heads; project.get_branch_heads(branch, heads, app.opts.ignore_suspend_certs); for (set<revision_id>::const_iterator i = heads.begin(); i != heads.end(); ++i) output << *i << '\n'; } // Name: ancestors // Arguments: // 1 or more: revision ids // Added in: 0.2 // Purpose: Prints the ancestors (exclusive) of the given revisions // Output format: A list of revision ids, in hexadecimal, each followed by a // newline. Revision ids are printed in alphabetically sorted order. // Error conditions: If any of the revisions do not exist, prints nothing to // stdout, prints an error message to stderr, and exits with status 1. CMD_AUTOMATE(ancestors, N_("REV1 [REV2 [REV3 [...]]]"), N_("Prints the ancestors of the given revisions"), "", options::opts::none) { N(args.size() > 0, F("wrong argument count")); database db(app); set<revision_id> ancestors; vector<revision_id> frontier; for (args_vector::const_iterator i = args.begin(); i != args.end(); ++i) { revision_id rid(decode_hexenc((*i)())); N(db.revision_exists(rid), F("no such revision '%s'") % rid); frontier.push_back(rid); } while (!frontier.empty()) { revision_id rid = frontier.back(); frontier.pop_back(); if(!null_id(rid)) { set<revision_id> parents; db.get_revision_parents(rid, parents); for (set<revision_id>::const_iterator i = parents.begin(); i != parents.end(); ++i) { if (ancestors.find(*i) == ancestors.end()) { frontier.push_back(*i); ancestors.insert(*i); } } } } for (set<revision_id>::const_iterator i = ancestors.begin(); i != ancestors.end(); ++i) if (!null_id(*i)) output << *i << '\n'; } // Name: descendents // Arguments: // 1 or more: revision ids // Added in: 0.1 // Purpose: Prints the descendents (exclusive) of the given revisions // Output format: A list of revision ids, in hexadecimal, each followed by a // newline. Revision ids are printed in alphabetically sorted order. // Error conditions: If any of the revisions do not exist, prints nothing to // stdout, prints an error message to stderr, and exits with status 1. CMD_AUTOMATE(descendents, N_("REV1 [REV2 [REV3 [...]]]"), N_("Prints the descendents of the given revisions"), "", options::opts::none) { N(args.size() > 0, F("wrong argument count")); database db(app); set<revision_id> descendents; vector<revision_id> frontier; for (args_vector::const_iterator i = args.begin(); i != args.end(); ++i) { revision_id rid(decode_hexenc((*i)())); N(db.revision_exists(rid), F("no such revision '%s'") % rid); frontier.push_back(rid); } while (!frontier.empty()) { revision_id rid = frontier.back(); frontier.pop_back(); set<revision_id> children; db.get_revision_children(rid, children); for (set<revision_id>::const_iterator i = children.begin(); i != children.end(); ++i) { if (descendents.find(*i) == descendents.end()) { frontier.push_back(*i); descendents.insert(*i); } } } for (set<revision_id>::const_iterator i = descendents.begin(); i != descendents.end(); ++i) output << *i << '\n'; } // Name: erase_ancestors // Arguments: // 0 or more: revision ids // Added in: 0.1 // Purpose: Prints all arguments, except those that are an ancestor of some // other argument. One way to think about this is that it prints the // minimal elements of the given set, under the ordering imposed by the // "child of" relation. Another way to think of it is if the arguments were // a branch, then we print the heads of that branch. // Output format: A list of revision ids, in hexadecimal, each followed by a // newline. Revision ids are printed in alphabetically sorted order. // Error conditions: If any of the revisions do not exist, prints nothing to // stdout, prints an error message to stderr, and exits with status 1. CMD_AUTOMATE(erase_ancestors, N_("[REV1 [REV2 [REV3 [...]]]]"), N_("Erases the ancestors in a list of revisions"), "", options::opts::none) { database db(app); set<revision_id> revs; for (args_vector::const_iterator i = args.begin(); i != args.end(); ++i) { revision_id rid(decode_hexenc((*i)())); N(db.revision_exists(rid), F("no such revision '%s'") % rid); revs.insert(rid); } erase_ancestors(db, revs); for (set<revision_id>::const_iterator i = revs.begin(); i != revs.end(); ++i) output << *i << '\n'; } // Name: toposort // Arguments: // 0 or more: revision ids // Added in: 0.1 // Purpose: Prints all arguments, topologically sorted. I.e., if A is an // ancestor of B, then A will appear before B in the output list. // Output format: A list of revision ids, in hexadecimal, each followed by a // newline. Revisions are printed in topologically sorted order. // Error conditions: If any of the revisions do not exist, prints nothing to // stdout, prints an error message to stderr, and exits with status 1. CMD_AUTOMATE(toposort, N_("[REV1 [REV2 [REV3 [...]]]]"), N_("Topologically sorts a list of revisions"), "", options::opts::none) { database db(app); set<revision_id> revs; for (args_vector::const_iterator i = args.begin(); i != args.end(); ++i) { revision_id rid(decode_hexenc((*i)())); N(db.revision_exists(rid), F("no such revision '%s'") % rid); revs.insert(rid); } vector<revision_id> sorted; toposort(db, revs, sorted); for (vector<revision_id>::const_iterator i = sorted.begin(); i != sorted.end(); ++i) output << *i << '\n'; } // Name: ancestry_difference // Arguments: // 1: a revision id // 0 or more further arguments: also revision ids // Added in: 0.1 // Purpose: Prints all ancestors of the first revision A, that are not also // ancestors of the other revision ids, the "Bs". For purposes of this // command, "ancestor" is an inclusive term; that is, A is an ancestor of // one of the Bs, it will not be printed, but otherwise, it will be; and // none of the Bs will ever be printed. If A is a new revision, and Bs are // revisions that you have processed before, then this command tells you // which revisions are new since then. // Output format: A list of revision ids, in hexadecimal, each followed by a // newline. Revisions are printed in topologically sorted order. // Error conditions: If any of the revisions do not exist, prints nothing to // stdout, prints an error message to stderr, and exits with status 1. CMD_AUTOMATE(ancestry_difference, N_("NEW_REV [OLD_REV1 [OLD_REV2 [...]]]"), N_("Lists the ancestors of the first revision given, not in " "the others"), "", options::opts::none) { N(args.size() > 0, F("wrong argument count")); database db(app); revision_id a; set<revision_id> bs; args_vector::const_iterator i = args.begin(); a = revision_id(decode_hexenc((*i)())); N(db.revision_exists(a), F("no such revision '%s'") % a); for (++i; i != args.end(); ++i) { revision_id b(decode_hexenc((*i)())); N(db.revision_exists(b), F("no such revision '%s'") % b); bs.insert(b); } set<revision_id> ancestors; ancestry_difference(db, a, bs, ancestors); vector<revision_id> sorted; toposort(db, ancestors, sorted); for (vector<revision_id>::const_iterator i = sorted.begin(); i != sorted.end(); ++i) output << *i << '\n'; } // Name: leaves // Arguments: // None // Added in: 0.1 // Purpose: Prints the leaves of the revision graph, i.e., all revisions that // have no children. This is similar, but not identical to the // functionality of 'heads', which prints every revision in a branch, that // has no descendents in that branch. If every revision in the database was // in the same branch, then they would be identical. Generally, every leaf // is the head of some branch, but not every branch head is a leaf. // Output format: A list of revision ids, in hexadecimal, each followed by a // newline. Revision ids are printed in alphabetically sorted order. // Error conditions: None. CMD_AUTOMATE(leaves, "", N_("Lists the leaves of the revision graph"), "", options::opts::none) { N(args.size() == 0, F("no arguments needed")); database db(app); set<revision_id> leaves; db.get_leaves(leaves); for (set<revision_id>::const_iterator i = leaves.begin(); i != leaves.end(); ++i) output << *i << '\n'; } // Name: roots // Arguments: // None // Added in: 4.3 // Purpose: Prints the roots of the revision graph, i.e. all revisions that // have no parents. // Output format: A list of revision ids, in hexadecimal, each followed by a // newline. Revision ids are printed in alphabetically sorted order. // Error conditions: None. CMD_AUTOMATE(roots, "", N_("Lists the roots of the revision graph"), "", options::opts::none) { N(args.size() == 0, F("no arguments needed")); database db(app); // the real root revisions are the children of one single imaginary root // with an empty revision id set<revision_id> roots; revision_id nullid; db.get_revision_children(nullid, roots); for (set<revision_id>::const_iterator i = roots.begin(); i != roots.end(); ++i) output << *i << '\n'; } // Name: parents // Arguments: // 1: a revision id // Added in: 0.2 // Purpose: Prints the immediate ancestors of the given revision, i.e., the // parents. // Output format: A list of revision ids, in hexadecimal, each followed by a // newline. Revision ids are printed in alphabetically sorted order. // Error conditions: If the revision does not exist, prints nothing to stdout, // prints an error message to stderr, and exits with status 1. CMD_AUTOMATE(parents, N_("REV"), N_("Prints the parents of a revision"), "", options::opts::none) { N(args.size() == 1, F("wrong argument count")); database db(app); revision_id rid(decode_hexenc(idx(args, 0)())); N(db.revision_exists(rid), F("no such revision '%s'") % rid); set<revision_id> parents; db.get_revision_parents(rid, parents); for (set<revision_id>::const_iterator i = parents.begin(); i != parents.end(); ++i) if (!null_id(*i)) output << *i << '\n'; } // Name: children // Arguments: // 1: a revision id // Added in: 0.2 // Purpose: Prints the immediate descendents of the given revision, i.e., the // children. // Output format: A list of revision ids, in hexadecimal, each followed by a // newline. Revision ids are printed in alphabetically sorted order. // Error conditions: If the revision does not exist, prints nothing to stdout, // prints an error message to stderr, and exits with status 1. CMD_AUTOMATE(children, N_("REV"), N_("Prints the children of a revision"), "", options::opts::none) { N(args.size() == 1, F("wrong argument count")); database db(app); revision_id rid(decode_hexenc(idx(args, 0)())); N(db.revision_exists(rid), F("no such revision '%s'") % rid); set<revision_id> children; db.get_revision_children(rid, children); for (set<revision_id>::const_iterator i = children.begin(); i != children.end(); ++i) if (!null_id(*i)) output << *i << '\n'; } // Name: graph // Arguments: // None // Added in: 0.2 // Purpose: Prints out the complete ancestry graph of this database. // Output format: // Each line begins with a revision id. Following this are zero or more // space-prefixed revision ids. Each revision id after the first is a // parent (in the sense of 'automate parents') of the first. For instance, // the following are valid lines: // 07804171823d963f78d6a0ff1763d694dd74ff40 // 07804171823d963f78d6a0ff1763d694dd74ff40 79d755c197e54dd3db65751d3803833d4cbf0d01 // 07804171823d963f78d6a0ff1763d694dd74ff40 79d755c197e54dd3db65751d3803833d4cbf0d01 a02e7a1390e3e4745c31be922f03f56450c13dce // The first would indicate that 07804171823d963f78d6a0ff1763d694dd74ff40 // was a root node; the second would indicate that it had one parent, and // the third would indicate that it had two parents, i.e., was a merge. // // The output as a whole is alphabetically sorted; additionally, the parents // within each line are alphabetically sorted. // Error conditions: None. CMD_AUTOMATE(graph, "", N_("Prints the complete ancestry graph"), "", options::opts::none) { N(args.size() == 0, F("no arguments needed")); database db(app); multimap<revision_id, revision_id> edges_mmap; map<revision_id, set<revision_id> > child_to_parents; db.get_revision_ancestry(edges_mmap); for (multimap<revision_id, revision_id>::const_iterator i = edges_mmap.begin(); i != edges_mmap.end(); ++i) { if (child_to_parents.find(i->second) == child_to_parents.end()) child_to_parents.insert(make_pair(i->second, set<revision_id>())); if (null_id(i->first)) continue; map<revision_id, set<revision_id> >::iterator j = child_to_parents.find(i->second); I(j->first == i->second); j->second.insert(i->first); } for (map<revision_id, set<revision_id> >::const_iterator i = child_to_parents.begin(); i != child_to_parents.end(); ++i) { output << i->first; for (set<revision_id>::const_iterator j = i->second.begin(); j != i->second.end(); ++j) output << ' ' << *j; output << '\n'; } } // Name: select // Arguments: // 1: selector // Added in: 0.2 // Purpose: Prints all the revisions that match the given selector. // Output format: A list of revision ids, in hexadecimal, each followed by a // newline. Revision ids are printed in alphabetically sorted order. // Error conditions: None. CMD_AUTOMATE(select, N_("SELECTOR"), N_("Lists the revisions that match a selector"), "", options::opts::none) { N(args.size() == 1, F("wrong argument count")); database db(app); project_t project(db); set<revision_id> completions; expand_selector(app.opts, app.lua, project, idx(args, 0)(), completions); for (set<revision_id>::const_iterator i = completions.begin(); i != completions.end(); ++i) output << *i << '\n'; } struct node_info { bool exists; // true if node_id is present in corresponding roster with the inventory map file_path // false if not present, or present with a different file_path // rest of data in this struct is invalid if false. node_id id; path::status type; file_id ident; full_attr_map_t attrs; node_info() : exists(false), type(path::nonexistent) {} }; static void get_node_info(node_t const & node, node_info & info) { info.exists = true; info.id = node->self; info.attrs = node->attrs; if (is_file_t(node)) { info.type = path::file; info.ident = downcast_to_file_t(node)->content; } else if (is_dir_t(node)) info.type = path::directory; else I(false); } struct inventory_item { // Records information about a pair of nodes with the same node_id in the // old roster and new roster, and the corresponding path in the // filesystem. node_info old_node; node_info new_node; file_path old_path; file_path new_path; path::status fs_type; file_id fs_ident; inventory_item() : fs_type(path::nonexistent) {} }; typedef std::map<file_path, inventory_item> inventory_map; // file_path will typically be an existing filesystem file, but in the case // of a dropped or rename_source file it is only in the old roster, and in // the case of a file added --bookkeep_only or rename_target // --bookkeep_only, it is only in the new roster. static void inventory_rosters(roster_t const & old_roster, roster_t const & new_roster, node_restriction const & nmask, path_restriction const & pmask, inventory_map & inventory) { std::map<int, file_path> old_paths; std::map<int, file_path> new_paths; node_map const & old_nodes = old_roster.all_nodes(); for (node_map::const_iterator i = old_nodes.begin(); i != old_nodes.end(); ++i) { if (nmask.includes(old_roster, i->first)) { file_path fp; old_roster.get_name(i->first, fp); if (pmask.includes(fp)) { get_node_info(old_roster.get_node(i->first), inventory[fp].old_node); old_paths[inventory[fp].old_node.id] = fp; } } } node_map const & new_nodes = new_roster.all_nodes(); for (node_map::const_iterator i = new_nodes.begin(); i != new_nodes.end(); ++i) { if (nmask.includes(new_roster, i->first)) { file_path fp; new_roster.get_name(i->first, fp); if (pmask.includes(fp)) { get_node_info(new_roster.get_node(i->first), inventory[fp].new_node); new_paths[inventory[fp].new_node.id] = fp; } } } std::map<int, file_path>::iterator i; for (i = old_paths.begin(); i != old_paths.end(); ++i) { if (new_paths.find(i->first) == new_paths.end()) { // There is no new node available; this is either a drop or a // rename to outside the current path restriction. if (new_roster.has_node(i->first)) { // record rename to outside restriction new_roster.get_name(i->first, inventory[i->second].new_path); continue; } else // drop; no new path continue; } file_path old_path(i->second); file_path new_path(new_paths[i->first]); // both paths are identical, no rename if (old_path == new_path) continue; // record rename inventory[new_path].old_path = old_path; inventory[old_path].new_path = new_path; } // Now look for new_paths that are renames from outside the current // restriction, and thus are not in old_paths. // FIXME: only need this if restriction is not null for (i = new_paths.begin(); i != new_paths.end(); ++i) { if (old_paths.find(i->first) == old_paths.end()) { // There is no old node available; this is either added or a // rename from outside the current path restriction. if (old_roster.has_node(i->first)) { // record rename from outside restriction old_roster.get_name(i->first, inventory[i->second].old_path); } else // added; no old path continue; } } } // check if the include/exclude paths contains paths to renamed nodes // if yes, add the corresponding old/new name of these nodes to the // paths as well, so the tree walker code will correctly identify them later // on or skips them if they should be excluded static void inventory_determine_corresponding_paths(roster_t const & old_roster, roster_t const & new_roster, vector<file_path> const & includes, vector<file_path> const & excludes, vector<file_path> & additional_includes, vector<file_path> & additional_excludes) { // at first check the includes vector for (int i=0, s=includes.size(); i<s; i++) { file_path fp = includes.at(i); if (old_roster.has_node(fp)) { node_t node = old_roster.get_node(fp); if (new_roster.has_node(node->self)) { file_path new_path; new_roster.get_name(node->self, new_path); if (fp != new_path && find(includes.begin(), includes.end(), new_path) == includes.end()) { additional_includes.push_back(new_path); } } } if (new_roster.has_node(fp)) { node_t node = new_roster.get_node(fp); if (old_roster.has_node(node->self)) { file_path old_path; old_roster.get_name(node->self, old_path); if (fp != old_path && find(includes.begin(), includes.end(), old_path) == includes.end()) { additional_includes.push_back(old_path); } } } } // and now the excludes vector vector<file_path> new_excludes; for (int i=0, s=excludes.size(); i<s; i++) { file_path fp = excludes.at(i); if (old_roster.has_node(fp)) { node_t node = old_roster.get_node(fp); if (new_roster.has_node(node->self)) { file_path new_path; new_roster.get_name(node->self, new_path); if (fp != new_path && find(excludes.begin(), excludes.end(), new_path) == excludes.end()) { additional_excludes.push_back(new_path); } } } if (new_roster.has_node(fp)) { node_t node = new_roster.get_node(fp); if (old_roster.has_node(node->self)) { file_path old_path; old_roster.get_name(node->self, old_path); if (fp != old_path && find(excludes.begin(), excludes.end(), old_path) == excludes.end()) { additional_excludes.push_back(old_path); } } } } } struct inventory_itemizer : public tree_walker { path_restriction const & mask; inventory_map & inventory; inodeprint_map ipm; workspace & work; inventory_itemizer(workspace & work, path_restriction const & m, inventory_map & i) : mask(m), inventory(i), work(work) { if (work.in_inodeprints_mode()) { data dat; work.read_inodeprints(dat); read_inodeprint_map(dat, ipm); } } virtual bool visit_dir(file_path const & path); virtual void visit_file(file_path const & path); }; bool inventory_itemizer::visit_dir(file_path const & path) { if(mask.includes(path)) { inventory[path].fs_type = path::directory; } // don't recurse into ignored subdirectories return !work.ignore_file(path); } void inventory_itemizer::visit_file(file_path const & path) { if (mask.includes(path)) { inventory_item & item = inventory[path]; item.fs_type = path::file; if (item.new_node.exists) { if (inodeprint_unchanged(ipm, path)) item.fs_ident = item.old_node.ident; else ident_existing_file(path, item.fs_ident); } } } static void inventory_filesystem(workspace & work, path_restriction const & mask, inventory_map & inventory) { inventory_itemizer itemizer(work, mask, inventory); file_path const root; // The constructor file_path() returns ""; the root directory. walk_tree // does not visit that node, so set fs_type now, if it meets the // restriction. if (mask.includes(root)) { inventory[root].fs_type = path::directory; } walk_tree(root, itemizer); } namespace { namespace syms { symbol const path("path"); symbol const old_type("old_type"); symbol const new_type("new_type"); symbol const fs_type("fs_type"); symbol const old_path("old_path"); symbol const new_path("new_path"); symbol const status("status"); symbol const changes("changes"); } } static void inventory_determine_states(workspace & work, file_path const & fs_path, inventory_item const & item, roster_t const & old_roster, roster_t const & new_roster, vector<string> & states) { // if both nodes exist, the only interesting case is // when the node ids aren't equal (so we have different nodes // with one and the same path in the old and the new roster) if (item.old_node.exists && item.new_node.exists && item.old_node.id != item.new_node.id) { if (new_roster.has_node(item.old_node.id)) states.push_back("rename_source"); else states.push_back("dropped"); if (old_roster.has_node(item.new_node.id)) states.push_back("rename_target"); else states.push_back("added"); } // this can be either a drop or a renamed item else if (item.old_node.exists && !item.new_node.exists) { if (new_roster.has_node(item.old_node.id)) states.push_back("rename_source"); else states.push_back("dropped"); } // this can be either an add or a renamed item else if (!item.old_node.exists && item.new_node.exists) { if (old_roster.has_node(item.new_node.id)) states.push_back("rename_target"); else states.push_back("added"); } // check the state of the file system item if (item.fs_type == path::nonexistent) { if (item.new_node.exists) states.push_back("missing"); } else // exists on filesystem { if (!item.new_node.exists) { if (work.ignore_file(fs_path)) { states.push_back("ignored"); } else { states.push_back("unknown"); } } else if (item.new_node.type != item.fs_type) { states.push_back("invalid"); } else { states.push_back("known"); } } } static void inventory_determine_changes(inventory_item const & item, roster_t const & old_roster, vector<string> & changes) { // old nodes do not have any recorded content changes and attributes, // so we can't print anything for them here if (!item.new_node.exists) return; // this is an existing item if (old_roster.has_node(item.new_node.id)) { // check if the content has changed - this makes only sense for files // for which we can get the content id of both new and old nodes. if (item.new_node.type == path::file && item.fs_type != path::nonexistent) { file_t old_file = downcast_to_file_t(old_roster.get_node(item.new_node.id)); switch (item.old_node.type) { case path::file: case path::nonexistent: // A file can be nonexistent due to mtn drop, user delete, mtn // rename, or user rename. If it was drop or delete, it would // not be in the new roster, and we would not get here. So // it's a rename, and we can get the content. This lets us // check if a user has edited a file after renaming it. if (item.fs_ident != old_file->content) changes.push_back("content"); break; case path::directory: break; } } // now look for changed attributes node_t old_node = old_roster.get_node(item.new_node.id); if (old_node->attrs != item.new_node.attrs) changes.push_back("attrs"); } else { // FIXME: paranoia: shall we I(new_roster.has_node(item.new_node.id)) here? // this is apparently a new item, if it is a file it gets at least // the "content" marker and we also check for recorded attributes if (item.new_node.type == path::file) changes.push_back("content"); if (item.new_node.attrs.size() > 0) changes.push_back("attrs"); } } // Name: inventory // Arguments: [PATH]... // Added in: 1.0 // Modified to basic_io in: 4.1 // Purpose: Prints a summary of every file or directory found in the // workspace or its associated base manifest. // See monotone.texi for output format description. // // Error conditions: If no workspace book keeping _MTN directory is found, // prints an error message to stderr, and exits with status 1. CMD_AUTOMATE(inventory, N_("[PATH]..."), N_("Prints a summary of files found in the workspace"), "", options::opts::depth | options::opts::exclude | options::opts::no_ignored | options::opts::no_unknown | options::opts::no_unchanged | options::opts::no_corresponding_renames) { database db(app); workspace work(app); parent_map parents; work.get_parent_rosters(db, parents); // for now, until we've figured out what the format could look like // and what conceptional model we can implement // see: http://www.venge.net/mtn-wiki/MultiParentWorkspaceFallout N(parents.size() == 1, F("this command can only be used in a single-parent workspace")); roster_t new_roster, old_roster = parent_roster(parents.begin()); temp_node_id_source nis; work.get_current_roster_shape(db, nis, new_roster); inventory_map inventory; vector<file_path> includes = args_to_paths(args); vector<file_path> excludes = args_to_paths(app.opts.exclude_patterns); if (!app.opts.no_corresponding_renames) { vector<file_path> add_includes, add_excludes; inventory_determine_corresponding_paths(old_roster, new_roster, includes, excludes, add_includes, add_excludes); copy(add_includes.begin(), add_includes.end(), inserter(includes, includes.end())); copy(add_excludes.begin(), add_excludes.end(), inserter(excludes, excludes.end())); } node_restriction nmask(work, includes, excludes, app.opts.depth, old_roster, new_roster); // skip the check of the workspace paths because some of them might // be missing and the user might want to query the recorded structure // of them anyways path_restriction pmask(work, includes, excludes, app.opts.depth, path_restriction::skip_check); inventory_rosters(old_roster, new_roster, nmask, pmask, inventory); inventory_filesystem(work, pmask, inventory); basic_io::printer pr; for (inventory_map::const_iterator i = inventory.begin(); i != inventory.end(); ++i) { file_path const & fp = i->first; inventory_item const & item = i->second; // // check if we should output this element at all // vector<string> states; inventory_determine_states(work, fp, item, old_roster, new_roster, states); if (find(states.begin(), states.end(), "ignored") != states.end() && app.opts.no_ignored) continue; if (find(states.begin(), states.end(), "unknown") != states.end() && app.opts.no_unknown) continue; vector<string> changes; inventory_determine_changes(item, old_roster, changes); bool is_tracked = find(states.begin(), states.end(), "unknown") == states.end() && find(states.begin(), states.end(), "ignored") == states.end(); bool has_changed = find(states.begin(), states.end(), "rename_source") != states.end() || find(states.begin(), states.end(), "rename_target") != states.end() || find(states.begin(), states.end(), "added") != states.end() || find(states.begin(), states.end(), "dropped") != states.end() || find(states.begin(), states.end(), "missing") != states.end() || !changes.empty(); if (is_tracked && !has_changed && app.opts.no_unchanged) continue; // // begin building the output stanza // basic_io::stanza st; st.push_file_pair(syms::path, fp); if (item.old_node.exists) { switch (item.old_node.type) { case path::file: st.push_str_pair(syms::old_type, "file"); break; case path::directory: st.push_str_pair(syms::old_type, "directory"); break; case path::nonexistent: I(false); } if (item.new_path.as_internal().length() > 0) { st.push_file_pair(syms::new_path, item.new_path); } } if (item.new_node.exists) { switch (item.new_node.type) { case path::file: st.push_str_pair(syms::new_type, "file"); break; case path::directory: st.push_str_pair(syms::new_type, "directory"); break; case path::nonexistent: I(false); } if (item.old_path.as_internal().length() > 0) { st.push_file_pair(syms::old_path, item.old_path); } } switch (item.fs_type) { case path::file: st.push_str_pair(syms::fs_type, "file"); break; case path::directory: st.push_str_pair(syms::fs_type, "directory"); break; case path::nonexistent: st.push_str_pair(syms::fs_type, "none"); break; } // // finally output the previously recorded states and changes // I(!states.empty()); st.push_str_multi(syms::status, states); if (!changes.empty()) st.push_str_multi(syms::changes, changes); pr.print_stanza(st); } output.write(pr.buf.data(), pr.buf.size()); } // Name: get_revision // Arguments: // 1: a revision id // Added in: 1.0 // Changed in: 7.0 (REVID argument is now mandatory) // Purpose: Prints change information for the specified revision id. // There are several changes that are described; each of these is // described by a different basic_io stanza. The first string pair // of each stanza indicates the type of change represented. // // All stanzas are formatted by basic_io. Stanzas are separated // by a blank line. Values will be escaped, '\' to '\\' and // '"' to '\"'. // // Possible values of this first value are along with an ordered list of // basic_io formatted stanzas that will be provided are: // // 'format_version' // used in case this format ever needs to change. // format: ('format_version', the string "1") // occurs: exactly once // 'new_manifest' // represents the new manifest associated with the revision. // format: ('new_manifest', manifest id) // occurs: exactly one // 'old_revision' // represents a parent revision. // format: ('old_revision', revision id) // occurs: either one or two times // 'delete // represents a file or directory that was deleted. // format: ('delete', path) // occurs: zero or more times // 'rename' // represents a file or directory that was renamed. // format: ('rename, old filename), ('to', new filename) // occurs: zero or more times // 'add_dir' // represents a directory that was added. // format: ('add_dir, path) // occurs: zero or more times // 'add_file' // represents a file that was added. // format: ('add_file', path), ('content', file id) // occurs: zero or more times // 'patch' // represents a file that was modified. // format: ('patch', filename), ('from', file id), ('to', file id) // occurs: zero or more times // 'clear' // represents an attr that was removed. // format: ('clear', filename), ('attr', attr name) // occurs: zero or more times // 'set' // represents an attr whose value was changed. // format: ('set', filename), ('attr', attr name), ('value', attr value) // occurs: zero or more times // // These stanzas will always occur in the order listed here; stanzas of // the same type will be sorted by the filename they refer to. // Error conditions: If the revision specified is unknown or invalid // prints an error message to stderr and exits with status 1. CMD_AUTOMATE(get_revision, N_("REVID"), N_("Shows change information for a revision"), "", options::opts::none) { N(args.size() == 1, F("wrong argument count")); database db(app); revision_data dat; revision_id rid(decode_hexenc(idx(args, 0)())); N(db.revision_exists(rid), F("no revision %s found in database") % rid); db.get_revision(rid, dat); L(FL("dumping revision %s") % rid); output << dat; } // Name: get_current_revision // Arguments: // 1: zero or more path names // Added in: 7.0 // Purpose: Outputs (an opti