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