status.c   [plain text]


/*
 * status.c: construct a status structure from an entry structure
 *
 * ====================================================================
 * Copyright (c) 2000-2006 CollabNet.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://subversion.tigris.org/license-1.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 *
 * This software consists of voluntary contributions made by many
 * individuals.  For exact contribution history, see the revision
 * history and logs, available at http://subversion.tigris.org/.
 * ====================================================================
 */



#include <assert.h>
#include <string.h>
#include <apr_pools.h>
#include <apr_file_io.h>
#include <apr_hash.h>
#include "svn_pools.h"
#include "svn_types.h"
#include "svn_delta.h"
#include "svn_string.h"
#include "svn_error.h"
#include "svn_path.h"
#include "svn_io.h"
#include "svn_wc.h"
#include "svn_config.h"
#include "svn_time.h"
#include "svn_private_config.h"

#include "wc.h"
#include "lock.h"
#include "props.h"
#include "translate.h"



/*** Editor batons ***/

struct edit_baton
{
  /* For status, the "destination" of the edit and whether to honor
     any paths that are 'below'.  */
  const char *anchor;
  const char *target;
  svn_wc_adm_access_t *adm_access;
  svn_boolean_t descend;

  /* Do we want all statuses (instead of just the interesting ones) ? */
  svn_boolean_t get_all;
 
  /* Ignore the svn:ignores. */
  svn_boolean_t no_ignore;

  /* The comparison revision in the repository.  This is a reference
     because this editor returns this rev to the driver directly, as
     well as in each statushash entry. */
  svn_revnum_t *target_revision;

  /* Subversion configuration hash. */
  apr_hash_t *config;

  /* Status function/baton. */
  svn_wc_status_func2_t status_func;
  void *status_baton;

  /* Cancellation function/baton. */
  svn_cancel_func_t cancel_func;
  void *cancel_baton;

  /* The configured set of default ignores. */
  apr_array_header_t *ignores;

  /* Externals info harvested during the status run. */
  svn_wc_traversal_info_t *traversal_info;
  apr_hash_t *externals;

  /* Status item for the path represented by the anchor of the edit. */
  svn_wc_status2_t *anchor_status;

  /* Was open_root() called for this edit drive? */
  svn_boolean_t root_opened;

  /* The repository root URL, if set. */
  const char *repos_root;

  /* Repository locks, if set. */
  apr_hash_t *repos_locks;
};


struct dir_baton
{
  /* The path to this directory. */
  const char *path;

  /* Basename of this directory. */
  const char *name;

  /* The global edit baton. */
  struct edit_baton *edit_baton;

  /* Baton for this directory's parent, or NULL if this is the root
     directory. */
  struct dir_baton *parent_baton;

  /* 'svn status' shouldn't print status lines for things that are
     added;  we're only interest in asking if objects that the user
     *already* has are up-to-date or not.  Thus if this flag is set,
     the next two will be ignored.  :-)  */
  svn_boolean_t added;

  /* Gets set iff there's a change to this directory's properties, to
     guide us when syncing adm files later. */
  svn_boolean_t prop_changed;

  /* This means (in terms of 'svn status') that some child was deleted
     or added to the directory */
  svn_boolean_t text_changed;

  /* Working copy status structures for children of this directory.
     This hash maps const char * paths (relative to the root of the
     edit) to svn_wc_status2_t * status items. */
  apr_hash_t *statii;

  /* The pool in which this baton itself is allocated. */
  apr_pool_t *pool;

  /* The URI to this item in the repository. */
  const char *url;

  /* Out of date info corresponding to ood_* fields in svn_wc_status2_t. */
  svn_revnum_t ood_last_cmt_rev;
  apr_time_t ood_last_cmt_date;
  svn_node_kind_t ood_kind;
  const char *ood_last_cmt_author;
};


struct file_baton
{
  /* The global edit baton. */
  struct edit_baton *edit_baton;

  /* Baton for this file's parent directory. */
  struct dir_baton *dir_baton;

  /* Pool specific to this file_baton. */
  apr_pool_t *pool;

  /* Name of this file (its entry in the directory). */
  const char *name;

  /* Path to this file, either abs or relative to the change-root. */
  const char *path;

  /* 'svn status' shouldn't print status lines for things that are
     added;  we're only interest in asking if objects that the user
     *already* has are up-to-date or not.  Thus if this flag is set,
     the next two will be ignored.  :-)  */
  svn_boolean_t added;

  /* This gets set if the file underwent a text change, which guides
     the code that syncs up the adm dir and working copy. */
  svn_boolean_t text_changed;

  /* This gets set if the file underwent a prop change, which guides
     the code that syncs up the adm dir and working copy. */
  svn_boolean_t prop_changed;

  /* The URI to this item in the repository. */
  const char *url;

  /* Out of date info corresponding to ood_* fields in svn_wc_status2_t. */
  svn_revnum_t ood_last_cmt_rev;
  apr_time_t ood_last_cmt_date;
  svn_node_kind_t ood_kind;
  const char *ood_last_cmt_author;
};


/** Code **/

/* Fill in *STATUS for PATH, whose entry data is in ENTRY.  Allocate
   *STATUS in POOL. 

   ENTRY may be null, for non-versioned entities.  In this case, we
   will assemble a special status structure item which implies a
   non-versioned thing.

   PARENT_ENTRY is the entry for the parent directory of PATH, it may be
   NULL if ENTRY is NULL or if PATH is a working copy root.  The lifetime
   of PARENT_ENTRY's pool is not important.

   PATH_KIND is the node kind of PATH as determined by the caller.
   NOTE: this may be svn_node_unknown if the caller has made no such
   determination.

   If PATH_KIND is not svn_node_unknown, PATH_SPECIAL indicates whether
   the entry is a special file.

   If GET_ALL is zero, and ENTRY is not locally modified, then *STATUS
   will be set to NULL.  If GET_ALL is non-zero, then *STATUS will be
   allocated and returned no matter what.

   If IS_IGNORED is non-zero and this is a non-versioned entity, set
   the text_status to svn_wc_status_none.  Otherwise set the
   text_status to svn_wc_status_unversioned.

   If non-NULL, look up a repository lock in REPOS_LOCKS and set the repos_lock
   field of the status struct to that lock if it exists.  If REPOS_LOCKS is
   non-NULL, REPOS_ROOT must contain the repository root URL of the entry.
*/
static svn_error_t *
assemble_status(svn_wc_status2_t **status,
                const char *path,
                svn_wc_adm_access_t *adm_access,
                const svn_wc_entry_t *entry,
                const svn_wc_entry_t *parent_entry,
                svn_node_kind_t path_kind, svn_boolean_t path_special,
                svn_boolean_t get_all,
                svn_boolean_t is_ignored,
                apr_hash_t *repos_locks,
                const char *repos_root,
                apr_pool_t *pool)
{
  svn_wc_status2_t *stat;
  svn_boolean_t has_props;
  svn_boolean_t text_modified_p = FALSE;
  svn_boolean_t prop_modified_p = FALSE;
  svn_boolean_t locked_p = FALSE;
  svn_boolean_t switched_p = FALSE;
#ifdef HAVE_SYMLINK          
  svn_boolean_t wc_special;
#endif /* HAVE_SYMLINK */

  /* Defaults for two main variables. */
  enum svn_wc_status_kind final_text_status = svn_wc_status_normal;
  enum svn_wc_status_kind final_prop_status = svn_wc_status_none;

  svn_lock_t *repos_lock = NULL;

  /* Check for a repository lock. */
  if (repos_locks)
    {
      const char *abs_path;

      if (entry && entry->url)
        abs_path = entry->url + strlen(repos_root);
      else if (parent_entry && parent_entry->url)
        abs_path = svn_path_join(parent_entry->url + strlen(repos_root),
                                 svn_path_basename(path, pool), pool);
      else
        abs_path = NULL;

      if (abs_path)
        repos_lock = apr_hash_get(repos_locks,
                                  svn_path_uri_decode(abs_path, pool),
                                  APR_HASH_KEY_STRING);
    }

  /* Check the path kind for PATH. */
  if (path_kind == svn_node_unknown)
    SVN_ERR(svn_io_check_special_path(path, &path_kind, &path_special,
                                      pool));
  
  if (! entry)
    {
      /* return a blank structure. */
      stat = apr_pcalloc(pool, sizeof(*stat));
      stat->entry = NULL;
      stat->text_status = svn_wc_status_none;
      stat->prop_status = svn_wc_status_none;
      stat->repos_text_status = svn_wc_status_none;
      stat->repos_prop_status = svn_wc_status_none;
      stat->locked = FALSE;
      stat->copied = FALSE;
      stat->switched = FALSE;

      /* If this path has no entry, but IS present on disk, it's
         unversioned.  If this file is being explicitly ignored (due
         to matching an ignore-pattern), the text_status is set to
         svn_wc_status_ignored.  Otherwise the text_status is set to
         svn_wc_status_unversioned. */
      if (path_kind != svn_node_none)
        {
          if (is_ignored)
            stat->text_status = svn_wc_status_ignored;
          else
            stat->text_status = svn_wc_status_unversioned;
        }

      stat->repos_lock = repos_lock;
      stat->url = NULL;
      stat->ood_last_cmt_rev = SVN_INVALID_REVNUM;
      stat->ood_last_cmt_date = 0;
      stat->ood_kind = svn_node_none;
      stat->ood_last_cmt_author = NULL;

      *status = stat;
      return SVN_NO_ERROR;
    }

  /* Someone either deleted the administrative directory in the versioned
     subdir, or deleted the directory altogether and created a new one.
     In any case, what is currently there is in the way.
   */
  if (entry->kind == svn_node_dir)
    {
      if (path_kind == svn_node_dir)
        {
          if (svn_wc__adm_missing(adm_access, path))
            final_text_status = svn_wc_status_obstructed;
        }
      else if (path_kind != svn_node_none)
        final_text_status = svn_wc_status_obstructed;
    }

  /* Is this item switched?  Well, to be switched it must have both an URL
     and a parent with an URL, at the very least. */
  if (entry->url && parent_entry && parent_entry->url)
    {
      /* An item is switched if its working copy basename differs from the
         basename of its URL. */
      if (strcmp(svn_path_uri_encode(svn_path_basename(path, pool), pool),
                 svn_path_basename(entry->url, pool)))
        switched_p = TRUE;

      /* An item is switched if its URL, without the basename, does not
         equal its parent's URL. */
      if (! switched_p
          && strcmp(svn_path_dirname(entry->url, pool),
                    parent_entry->url))
        switched_p = TRUE;
    }

  if (final_text_status != svn_wc_status_obstructed)
    {
      /* Implement predecence rules: */

      /* 1. Set the two main variables to "discovered" values first (M, C).
            Together, these two stati are of lowest precedence, and C has
            precedence over M. */

      /* Does the entry have props? */
      SVN_ERR(svn_wc__has_props(&has_props, path, adm_access, pool));
      if (has_props)
        final_prop_status = svn_wc_status_normal;

      /* If the entry has a property file, see if it has local changes. */
      SVN_ERR(svn_wc_props_modified_p(&prop_modified_p, path, adm_access,
                                      pool));

#ifdef HAVE_SYMLINK
      if (has_props)
        SVN_ERR(svn_wc__get_special(&wc_special, path, adm_access, pool));
      else
        wc_special = FALSE;
#endif /* HAVE_SYMLINK */

      /* If the entry is a file, check for textual modifications */
      if ((entry->kind == svn_node_file)
#ifdef HAVE_SYMLINK          
          && (wc_special == path_special)
#endif /* HAVE_SYMLINK */          
          )
        SVN_ERR(svn_wc_text_modified_p(&text_modified_p, path, FALSE,
                                       adm_access, pool));

      if (text_modified_p)
        final_text_status = svn_wc_status_modified;

      if (prop_modified_p)
        final_prop_status = svn_wc_status_modified;

      if (entry->prejfile || entry->conflict_old ||
          entry->conflict_new || entry->conflict_wrk)
        {
          svn_boolean_t text_conflict_p, prop_conflict_p;
          const char *parent_dir;

          if (entry->kind == svn_node_dir)
            parent_dir = path;
          else  /* non-directory, that's all we need to know */
            parent_dir = svn_path_dirname(path, pool);

          SVN_ERR(svn_wc_conflicted_p(&text_conflict_p, &prop_conflict_p,
                                      parent_dir, entry, pool));

          if (text_conflict_p)
            final_text_status = svn_wc_status_conflicted;
          if (prop_conflict_p)
            final_prop_status = svn_wc_status_conflicted;
        }

      /* 2. Possibly overwrite the text_status variable with "scheduled"
            states from the entry (A, D, R).  As a group, these states are
            of medium precedence.  They also override any C or M that may
            be in the prop_status field at this point, although they do not
            override a C text status.*/

      if (entry->schedule == svn_wc_schedule_add
          && final_text_status != svn_wc_status_conflicted)
        {
          final_text_status = svn_wc_status_added;
          final_prop_status = svn_wc_status_none;
        }

      else if (entry->schedule == svn_wc_schedule_replace
               && final_text_status != svn_wc_status_conflicted)
        {
          final_text_status = svn_wc_status_replaced;
          final_prop_status = svn_wc_status_none;
        }

      else if (entry->schedule == svn_wc_schedule_delete
               && final_text_status != svn_wc_status_conflicted)
        {
          final_text_status = svn_wc_status_deleted;
          final_prop_status = svn_wc_status_none;
        }


      /* 3. Highest precedence:

            a. check to see if file or dir is just missing, or
               incomplete.  This overrides every possible state
               *except* deletion.  (If something is deleted or
               scheduled for it, we don't care if the working file
               exists.)

            b. check to see if the file or dir is present in the
               file system as the same kind it was versioned as.

         4. Check for locked directory (only for directories). */

      if (entry->incomplete
          && (final_text_status != svn_wc_status_deleted)
          && (final_text_status != svn_wc_status_added))
        {
          final_text_status = svn_wc_status_incomplete;
        }
      else if (path_kind == svn_node_none)
        {
          if (final_text_status != svn_wc_status_deleted)
            final_text_status = svn_wc_status_missing;
        }
      else if (path_kind != entry->kind)
        final_text_status = svn_wc_status_obstructed;
#ifdef HAVE_SYMLINK      
      else if (((! wc_special) && (path_special))
               || (wc_special && (! path_special))
               )
        final_text_status = svn_wc_status_obstructed;
#endif /* HAVE_SYMLINK */

      if (path_kind == svn_node_dir && entry->kind == svn_node_dir)
        SVN_ERR(svn_wc_locked(&locked_p, path, pool));
    }

  /* 5. Easy out:  unless we're fetching -every- entry, don't bother
     to allocate a struct for an uninteresting entry. */

  if (! get_all)
    if (((final_text_status == svn_wc_status_none)
         || (final_text_status == svn_wc_status_normal))
        && ((final_prop_status == svn_wc_status_none)
            || (final_prop_status == svn_wc_status_normal))
        && (! locked_p) && (! switched_p) && (! entry->lock_token)
        && (! repos_lock))
      {
        *status = NULL;
        return SVN_NO_ERROR;
      }


  /* 6. Build and return a status structure. */

  stat = apr_pcalloc(pool, sizeof(**status));
  stat->entry = svn_wc_entry_dup(entry, pool);
  stat->text_status = final_text_status;       
  stat->prop_status = final_prop_status;    
  stat->repos_text_status = svn_wc_status_none;   /* default */
  stat->repos_prop_status = svn_wc_status_none;   /* default */
  stat->locked = locked_p;
  stat->switched = switched_p;
  stat->copied = entry->copied;
  stat->repos_lock = repos_lock;
  stat->url = (entry->url ? entry->url : NULL);
  stat->ood_last_cmt_rev = SVN_INVALID_REVNUM;
  stat->ood_last_cmt_date = 0;
  stat->ood_kind = svn_node_none;
  stat->ood_last_cmt_author = NULL;

  *status = stat;

  return SVN_NO_ERROR;
}




/* Given an ENTRY object representing PATH, build a status structure
   and pass it off to the STATUS_FUNC/STATUS_BATON.  All other
   arguments are the same as those passed to assemble_status().  */
static svn_error_t *
send_status_structure(const char *path,
                      svn_wc_adm_access_t *adm_access,
                      const svn_wc_entry_t *entry,
                      const svn_wc_entry_t *parent_entry,
                      svn_node_kind_t path_kind,
                      svn_boolean_t path_special,
                      svn_boolean_t get_all,
                      svn_boolean_t is_ignored,
                      apr_hash_t *repos_locks,
                      const char *repos_root,
                      svn_wc_status_func2_t status_func,
                      void *status_baton,
                      apr_pool_t *pool)
{
  svn_wc_status2_t *statstruct;
  
  SVN_ERR(assemble_status(&statstruct, path, adm_access, entry, parent_entry,
                          path_kind, path_special, get_all, is_ignored,
                          repos_locks, repos_root, pool));
  if (statstruct && (status_func))
    (*status_func)(status_baton, path, statstruct);
  
  return SVN_NO_ERROR;
}


/* Store in PATTERNS a list of all svn:ignore properties from 
   the working copy directory, including the default ignores
   passed in as IGNORES.

   Upon return, *PATTERNS will contain zero or more (const char *) 
   patterns from the value of the SVN_PROP_IGNORE property set on 
   the working directory path.

   IGNORES is a list of patterns to include; typically this will 
   be the default ignores as, for example, specified in a config file.

   ADM_ACCESS is an access baton for the working copy path. 
   
   Allocate everything in POOL.

   None of the arguments may be NULL.
*/
static svn_error_t *
collect_ignore_patterns(apr_array_header_t **patterns,
                        apr_array_header_t *ignores,
                        svn_wc_adm_access_t *adm_access,
                        apr_pool_t *pool)
{
  int i;
  const svn_string_t *value;

  *patterns = apr_array_make(pool, 1, sizeof(const char *));

  /* Copy default ignores into the local PATTERNS array. */
  for (i = 0; i < ignores->nelts; i++)
    {
      const char *ignore = APR_ARRAY_IDX(ignores, i, const char *);
      APR_ARRAY_PUSH(*patterns, const char *) = ignore;
    }

  /* Then add any svn:ignore globs to the PATTERNS array. */
  SVN_ERR(svn_wc_prop_get(&value, SVN_PROP_IGNORE,
                          svn_wc_adm_access_path(adm_access), adm_access,
                          pool));
  if (value != NULL)
    svn_cstring_split_append(*patterns, value->data, "\n\r", FALSE, pool);

  return SVN_NO_ERROR;   
} 


/* Compare PATH with items in the EXTERNALS hash to see if PATH is the
   drop location for, or an intermediate directory of the drop
   location for, an externals definition.  Use POOL for
   scratchwork. */
static svn_boolean_t
is_external_path(apr_hash_t *externals,
                 const char *path,
                 apr_pool_t *pool)
{
  apr_hash_index_t *hi;

  /* First try: does the path exist as a key in the hash? */
  if (apr_hash_get(externals, path, APR_HASH_KEY_STRING))
    return TRUE;

  /* Failing that, we need to check if any external is a child of
     PATH. */
  for (hi = apr_hash_first(pool, externals); hi; hi = apr_hash_next(hi))
    {
      const void *key;
      apr_hash_this(hi, &key, NULL, NULL);
      if (svn_path_is_child(path, key, pool))
        return TRUE;
    }

  return FALSE;
}


/* Assuming that NAME is unversioned, send a status structure
   for it through STATUS_FUNC/STATUS_BATON unless this path is being
   ignored.  This function should never be called on a versioned entry.

   NAME is the basename of the unversioned file whose status is being
   requested.  PATH_KIND is the node kind of NAME as determined by the
   caller.  PATH_SPECIAL is the special status of the path, also determined
   by the caller.  ADM_ACCESS is an access baton for the working copy path.
   PATTERNS points to a list of filename patterns which are marked as
   ignored.  None of these parameter may be NULL.  EXTERNALS is a hash
   of known externals definitions for this status run.

   If NO_IGNORE is non-zero, the item will be added regardless of
   whether it is ignored; otherwise we will only add the item if it
   does not match any of the patterns in PATTERNS.

   Allocate everything in POOL.
*/
static svn_error_t *
send_unversioned_item(const char *name, 
                      svn_node_kind_t path_kind, svn_boolean_t path_special,
                      svn_wc_adm_access_t *adm_access, 
                      apr_array_header_t *patterns,
                      apr_hash_t *externals,
                      svn_boolean_t no_ignore,
                      apr_hash_t *repos_locks,
                      const char *repos_root,
                      svn_wc_status_func2_t status_func,
                      void *status_baton,
                      apr_pool_t *pool)
{
  int ignore_me = svn_cstring_match_glob_list(name, patterns);
  const char *path = svn_path_join(svn_wc_adm_access_path(adm_access), 
                                   name, pool);
  int is_external = is_external_path(externals, path, pool);
  svn_wc_status2_t *status;

  SVN_ERR(assemble_status(&status, path, adm_access, NULL, NULL, 
                          path_kind, path_special, FALSE, ignore_me,
                          repos_locks, repos_root, pool));

  if (is_external)
    status->text_status = svn_wc_status_external;

  /* If we aren't ignoring it, or if it's an externals path, or it has a lock
     in the repository, pass this entry to the status func. */
  if (no_ignore || (! ignore_me) || is_external || status->repos_lock)
    (status_func)(status_baton, path, status);

  return SVN_NO_ERROR;
}


/* Prototype for untangling a tango-ing two-some. */
static svn_error_t *get_dir_status(struct edit_baton *eb,
                                   const svn_wc_entry_t *parent_entry,
                                   svn_wc_adm_access_t *adm_access,
                                   const char *entry,
                                   apr_array_header_t *ignores,
                                   svn_boolean_t descend,
                                   svn_boolean_t get_all,
                                   svn_boolean_t no_ignore,
                                   svn_boolean_t skip_this_dir,
                                   svn_wc_status_func2_t status_func,
                                   void *status_baton,
                                   svn_cancel_func_t cancel_func,
                                   void *cancel_baton,
                                   apr_pool_t *pool);

/* Handle NAME (whose entry is ENTRY) as a directory entry of the
   directory represented by ADM_ACCESS (and whose entry is
   DIR_ENTRY).  All other arguments are the same as those passed to
   get_dir_status(), the function for which this one is a helper.  */
static svn_error_t *
handle_dir_entry(struct edit_baton *eb,
                 svn_wc_adm_access_t *adm_access,
                 const char *name,
                 const svn_wc_entry_t *dir_entry,
                 const svn_wc_entry_t *entry,
                 svn_node_kind_t kind,
                 svn_boolean_t special,
                 apr_array_header_t *ignores,
                 svn_boolean_t descend,
                 svn_boolean_t get_all,
                 svn_boolean_t no_ignore,
                 svn_wc_status_func2_t status_func,
                 void *status_baton,
                 svn_cancel_func_t cancel_func,
                 void *cancel_baton,
                 apr_pool_t *pool)
{
  const char *dirname = svn_wc_adm_access_path(adm_access);
  const char *path = svn_path_join(dirname, name, pool);

  if (kind == svn_node_dir)
    {
      /* Directory entries are incomplete.  We must get their full
         entry from their own THIS_DIR entry.  svn_wc_entry does this
         for us if it can.

         Of course, if there has been a kind-changing replacement (for
         example, there is an entry for a file 'foo', but 'foo' exists
         as a *directory* on disk), we don't want to reach down into
         that subdir to try to flesh out a "complete entry".  */
      const svn_wc_entry_t *full_entry = entry;
          
      if (entry->kind == kind)
        {
          SVN_ERR(svn_wc_entry(&full_entry, path, adm_access, FALSE, pool));
          if (! full_entry)
            return svn_error_createf(SVN_ERR_UNVERSIONED_RESOURCE, NULL,
                                     _("'%s' is not under version control"),
                                     svn_path_local_style(path, pool));
        }

      /* Descend only if the subdirectory is a working copy directory
         (and DESCEND is non-zero ofcourse)  */
      if (descend && (full_entry != entry))
        {
          svn_wc_adm_access_t *dir_access;
          SVN_ERR(svn_wc_adm_retrieve(&dir_access, adm_access, path, pool));
          SVN_ERR(get_dir_status(eb, dir_entry, dir_access, NULL, ignores, 
                                 descend, get_all, no_ignore, FALSE, 
                                 status_func, status_baton, cancel_func,
                                 cancel_baton, pool));
        }
      else
        {
          SVN_ERR(send_status_structure(path, adm_access, full_entry, 
                                        dir_entry, kind, special, get_all,
                                        FALSE, eb->repos_locks,
                                        eb->repos_root,
                                        status_func, status_baton, pool));
        }
    }
  else
    {
      /* File entries are ... just fine! */
      SVN_ERR(send_status_structure(path, adm_access, entry, dir_entry, 
                                    kind, special, get_all, FALSE,
                                    eb->repos_locks, eb->repos_root,
                                    status_func, status_baton, pool));
    }
  return SVN_NO_ERROR;
}


/* Send svn_wc_status2_t * structures for the directory ADM_ACCESS and
   for all its entries through STATUS_FUNC/STATUS_BATON, or, if ENTRY
   is non-NULL, only for that directory entry.

   PARENT_ENTRY is the entry for the parent of the directory or NULL
   if that directory is a working copy root.

   If SKIP_THIS_DIR is TRUE (and ENTRY is NULL), the directory's own
   status will not be reported.  However, upon recursing, all subdirs
   *will* be reported, regardless of this parameter's value.

   Other arguments are the same as those passed to
   svn_wc_get_status_editor2().  */
static svn_error_t *
get_dir_status(struct edit_baton *eb,
               const svn_wc_entry_t *parent_entry,
               svn_wc_adm_access_t *adm_access,
               const char *entry,
               apr_array_header_t *ignores,
               svn_boolean_t descend,
               svn_boolean_t get_all,
               svn_boolean_t no_ignore,
               svn_boolean_t skip_this_dir,
               svn_wc_status_func2_t status_func,
               void *status_baton,
               svn_cancel_func_t cancel_func,
               void *cancel_baton,
               apr_pool_t *pool)
{
  apr_hash_t *entries;
  apr_hash_index_t *hi;
  const svn_wc_entry_t *dir_entry;
  const char *path = svn_wc_adm_access_path(adm_access);
  apr_hash_t *dirents;
  apr_array_header_t *patterns = NULL;
  apr_pool_t *iterpool, *subpool = svn_pool_create(pool);

  /* See if someone wants to cancel this operation. */
  if (cancel_func)
    SVN_ERR(cancel_func(cancel_baton));

  /* Load entries file for the directory into the requested pool. */
  SVN_ERR(svn_wc_entries_read(&entries, adm_access, FALSE, subpool));

  /* Read PATH's dirents. */
  SVN_ERR(svn_io_get_dirents2(&dirents, path, subpool));

  /* Get this directory's entry. */
  SVN_ERR(svn_wc_entry(&dir_entry, path, adm_access, FALSE, subpool));

  /* If "this dir" has "svn:externals" property set on it, store its
     name and value in traversal_info.  Also, we want to track the
     externals internally so we can report status more accurately. */
    {
      const svn_string_t *prop_val;
      SVN_ERR(svn_wc_prop_get(&prop_val, SVN_PROP_EXTERNALS, path, 
                              adm_access, subpool));
      if (prop_val)
        {
          apr_array_header_t *ext_items;
          int i;

          if (eb->traversal_info)
            {
              apr_pool_t *dup_pool = eb->traversal_info->pool;
              const char *dup_path = apr_pstrdup(dup_pool, path);
              const char *dup_val = apr_pstrmemdup(dup_pool, prop_val->data, 
                                                   prop_val->len);

              /* First things first -- we put the externals information
                 into the "global" traversal info structure. */
              apr_hash_set(eb->traversal_info->externals_old,
                           dup_path, APR_HASH_KEY_STRING, dup_val);
              apr_hash_set(eb->traversal_info->externals_new,
                           dup_path, APR_HASH_KEY_STRING, dup_val);
            }

          /* Now, parse the thing, and copy the parsed results into
             our "global" externals hash. */
          SVN_ERR(svn_wc_parse_externals_description2(&ext_items, path,
                                                      prop_val->data, pool));
          for (i = 0; ext_items && i < ext_items->nelts; i++)
            {
              svn_wc_external_item_t *item;

              item = APR_ARRAY_IDX(ext_items, i, svn_wc_external_item_t *);
              apr_hash_set(eb->externals, svn_path_join(path,
                                                        item->target_dir,
                                                        pool),
                           APR_HASH_KEY_STRING, item);
            }
        }
    }

  /* Early out -- our caller only cares about a single ENTRY in this
     directory.  */
  if (entry)
    {
      const svn_wc_entry_t *entry_entry;
      svn_io_dirent_t* dirent_p = apr_hash_get(dirents, entry,
                                               APR_HASH_KEY_STRING);
      entry_entry = apr_hash_get(entries, entry, APR_HASH_KEY_STRING);

      /* If ENTRY is versioned, send its versioned status. */
      if (entry_entry)
        {
          SVN_ERR(handle_dir_entry(eb, adm_access, entry, dir_entry, 
                                   entry_entry,
                                   dirent_p ? dirent_p->kind : svn_node_none,
                                   dirent_p ? dirent_p->special : FALSE,
                                   ignores, descend, get_all, 
                                   no_ignore, status_func, status_baton, 
                                   cancel_func, cancel_baton, subpool));
        }
      /* Otherwise, if it exists, send its unversioned status. */
      else if (dirent_p)
        {
          if (ignores && ! patterns)
            SVN_ERR(collect_ignore_patterns(&patterns, ignores, 
                                            adm_access, subpool));
          SVN_ERR(send_unversioned_item(entry, dirent_p->kind,
                                        dirent_p->special, adm_access, 
                                        patterns, eb->externals, no_ignore,
                                        eb->repos_locks, eb->repos_root,
                                        status_func, status_baton, subpool));
        }

      /* Regardless, we're done here.  Let's go home. */
      return SVN_NO_ERROR;
    }

  /** If we get here, ENTRY is NULL and we are handling all the
      directory entries. */

  /* Make our iteration pool. */
  iterpool = svn_pool_create(subpool);

  /* Add empty status structures for each of the unversioned things. */
  for (hi = apr_hash_first(subpool, dirents); hi; hi = apr_hash_next(hi))
    {
      const void *key;
      apr_ssize_t klen;
      void *val;
      svn_io_dirent_t *dirent_p;

      apr_hash_this(hi, &key, &klen, &val);
        
      /* Skip versioned things, and skip the administrative
         directory. */
      if (apr_hash_get(entries, key, klen)
          || svn_wc_is_adm_dir(key, subpool))
        continue;

      svn_pool_clear(iterpool);

      if (ignores && ! patterns)
        SVN_ERR(collect_ignore_patterns(&patterns, ignores, 
                                        adm_access, subpool));

      /* Make an unversioned status item for KEY, and put it into our
         return hash. */
      dirent_p = val;
      SVN_ERR(send_unversioned_item(key, dirent_p->kind, dirent_p->special,
                                    adm_access, 
                                    patterns, eb->externals, no_ignore,
                                    eb->repos_locks, eb->repos_root,
                                    status_func, status_baton, iterpool));
    }

  /* Handle "this-dir" first. */
  if (! skip_this_dir)
    SVN_ERR(send_status_structure(path, adm_access, dir_entry, 
                                  parent_entry, svn_node_dir, FALSE,
                                  get_all, FALSE, eb->repos_locks,
                                  eb->repos_root, status_func, status_baton,
                                  subpool));

  /* Loop over entries hash */
  for (hi = apr_hash_first(pool, entries); hi; hi = apr_hash_next(hi))
    {
      const void *key;
      void *val;
      svn_io_dirent_t *dirent_p;

      /* Get the next entry */
      apr_hash_this(hi, &key, NULL, &val);

      dirent_p = apr_hash_get(dirents, key, APR_HASH_KEY_STRING);

      /* ### todo: What if the subdir is from another repository? */
          
      /* Skip "this-dir". */
      if (strcmp(key, SVN_WC_ENTRY_THIS_DIR) == 0)
        continue;

      /* Clear the iteration subpool. */
      svn_pool_clear(iterpool);

      /* Handle this directory entry (possibly recursing). */
      SVN_ERR(handle_dir_entry(eb, adm_access, key, dir_entry, val,
                               dirent_p ? dirent_p->kind : svn_node_none,
                               dirent_p ? dirent_p->special : FALSE,
                               ignores, 
                               descend, get_all, no_ignore, 
                               status_func, status_baton, cancel_func, 
                               cancel_baton, iterpool));
    }
  
  /* Destroy our subpools. */
  svn_pool_destroy(subpool);

  return SVN_NO_ERROR;
}



/*** Helpers ***/

/* A faux status callback function for stashing STATUS item in an hash
   (which is the BATON), keyed on PATH.  This implements the
   svn_wc_status_func2_t interface. */
static void
hash_stash(void *baton,
           const char *path,
           svn_wc_status2_t *status)
{
  apr_hash_t *stat_hash = baton;
  apr_pool_t *hash_pool = apr_hash_pool_get(stat_hash);
  assert(! apr_hash_get(stat_hash, path, APR_HASH_KEY_STRING));
  apr_hash_set(stat_hash, apr_pstrdup(hash_pool, path), 
               APR_HASH_KEY_STRING, svn_wc_dup_status2(status, hash_pool));
}


/* Look up the key PATH in BATON->STATII.  IS_DIR_BATON indicates whether
   baton is a struct *dir_baton or struct *file_baton.  If the value doesn't
   yet exist, and the REPOS_TEXT_STATUS indicates that this is an
   addition, create a new status struct using the hash's pool.  Merge
   REPOS_TEXT_STATUS and REPOS_PROP_STATUS into the status structure's
   "network" fields.
   If a new struct was added, set the repos_lock to REPOS_LOCK. */
static svn_error_t *
tweak_statushash(void *baton,
                 svn_boolean_t is_dir_baton,
                 svn_wc_adm_access_t *adm_access,
                 const char *path,
                 svn_boolean_t is_dir,
                 enum svn_wc_status_kind repos_text_status,
                 enum svn_wc_status_kind repos_prop_status,
                 svn_lock_t *repos_lock)
{
  svn_wc_status2_t *statstruct;
  apr_pool_t *pool;
  apr_hash_t *statushash;

  if (is_dir_baton)
    statushash = ((struct dir_baton *) baton)->statii;
  else
    statushash = ((struct file_baton *) baton)->dir_baton->statii;
  pool = apr_hash_pool_get(statushash);

  /* Is PATH already a hash-key? */
  statstruct = apr_hash_get(statushash, path, APR_HASH_KEY_STRING);

  /* If not, make it so. */
  if (! statstruct)
    {
      /* If this item isn't being added, then we're most likely
         dealing with a non-recursive (or at least partially
         non-recursive) working copy.  Due to bugs in how the client
         reports the state of non-recursive working copies, the
         repository can send back responses about paths that don't
         even exist locally.  Our best course here is just to ignore
         those responses.  After all, if the client had reported
         correctly in the first, that path would either be mentioned
         as an 'add' or not mentioned at all, depending on how we
         eventually fix the bugs in non-recursivity.  See issue
         #2122 for details. */
      if (repos_text_status != svn_wc_status_added)
        return SVN_NO_ERROR;

      /* Use the public API to get a statstruct, and put it into the hash. */
      SVN_ERR(svn_wc_status2(&statstruct, path, adm_access, pool));
      statstruct->repos_lock = repos_lock;
      apr_hash_set(statushash, apr_pstrdup(pool, path), 
                   APR_HASH_KEY_STRING, statstruct);
    }

  /* Merge a repos "delete" + "add" into a single "replace". */
  if ((repos_text_status == svn_wc_status_added)
      && (statstruct->repos_text_status == svn_wc_status_deleted))
    repos_text_status = svn_wc_status_replaced;

  /* Tweak the structure's repos fields. */
  if (repos_text_status)
    statstruct->repos_text_status = repos_text_status;
  if (repos_prop_status)
    statstruct->repos_prop_status = repos_prop_status;
  
  /* Copy out of date info. */
  if (is_dir_baton)
    {
      struct dir_baton *b = baton;
      if (b->url)
        statstruct->url = apr_pstrdup(pool, b->url);
      statstruct->ood_kind = b->ood_kind;
      /* The last committed rev, date, and author for deleted items
         isn't available. */
      if (statstruct->repos_text_status != svn_wc_status_deleted)
        {
          statstruct->ood_last_cmt_rev = b->ood_last_cmt_rev;
          statstruct->ood_last_cmt_date = b->ood_last_cmt_date;
          if (b->ood_last_cmt_author)
            statstruct->ood_last_cmt_author =
              apr_pstrdup(pool, b->ood_last_cmt_author);
        }
    }
  else
    {
      struct file_baton *b = baton;
      if (b->url)
        statstruct->url = apr_pstrdup(pool, b->url);
      statstruct->ood_last_cmt_rev = b->ood_last_cmt_rev;
      statstruct->ood_last_cmt_date = b->ood_last_cmt_date;
      statstruct->ood_kind = b->ood_kind;
      if (b->ood_last_cmt_author)
        statstruct->ood_last_cmt_author =
          apr_pstrdup(pool, b->ood_last_cmt_author);
    }
  return SVN_NO_ERROR;
}

/* Returns the URL for DB, or NULL: */
static const char *
find_dir_url(const struct dir_baton *db, apr_pool_t *pool)
{
  /* If we have no name, we're the root, return the anchor URL. */
  if (! db->name)
    return db->edit_baton->anchor_status->entry->url;
  else
    {
      const char *url;
      struct dir_baton *pb = db->parent_baton;
      svn_wc_status2_t *status = apr_hash_get(pb->statii, db->name,
                                              APR_HASH_KEY_STRING);
      /* Note that status->entry->url is NULL in the case of a missing
       * directory, which means we need to recurse up another level to
       * get a useful URL. */
      if (status && status->entry && status->entry->url)
        return status->entry->url;

      url = find_dir_url(pb, pool);
      if (url)
        return svn_path_url_add_component(url, db->name, pool);
      else
        return NULL;
    }
}



/* Create a new dir_baton for subdir PATH. */
static svn_error_t *
make_dir_baton(void **dir_baton,
               const char *path,
               struct edit_baton *edit_baton,
               struct dir_baton *parent_baton,
               apr_pool_t *pool)
{
  struct dir_baton *pb = parent_baton;
  struct edit_baton *eb = edit_baton;
  struct dir_baton *d = apr_pcalloc(pool, sizeof(*d));
  const char *full_path; 
  svn_wc_status2_t *parent_status;

  /* Don't do this.  Just do NOT do this to me. */
  if (pb && (! path))
    abort();

  /* Construct the full path of this directory. */
  if (pb)
    full_path = svn_path_join(eb->anchor, path, pool);
  else
    full_path = apr_pstrdup(pool, eb->anchor);

  /* Finish populating the baton members. */
  d->path = full_path;
  d->name = path ? (svn_path_basename(path, pool)) : NULL;
  d->edit_baton = edit_baton;
  d->parent_baton = parent_baton;
  d->pool = pool;
  d->statii = apr_hash_make(pool);
  d->url = apr_pstrdup(pool, find_dir_url(d, pool));
  d->ood_last_cmt_rev = SVN_INVALID_REVNUM;
  d->ood_last_cmt_date = 0;
  d->ood_kind = svn_node_dir;
  d->ood_last_cmt_author = NULL;

  /* Get the status for this path's children.  Of course, we only want
     to do this if the path is versioned as a directory. */
  if (pb)
    parent_status = apr_hash_get(pb->statii, d->path, APR_HASH_KEY_STRING);
  else
    parent_status = eb->anchor_status;

  /* Order is important here.  We can't depend on parent_status->entry
     being non-NULL until after we've checked all the conditions that
     might indicate that the parent is unversioned ("unversioned" for
     our purposes includes being an external or ignored item). */
  if (parent_status
      && (parent_status->text_status != svn_wc_status_unversioned)
      && (parent_status->text_status != svn_wc_status_deleted)
      && (parent_status->text_status != svn_wc_status_missing)
      && (parent_status->text_status != svn_wc_status_obstructed)
      && (parent_status->text_status != svn_wc_status_external)
      && (parent_status->text_status != svn_wc_status_ignored)
      && (parent_status->entry->kind == svn_node_dir)
      && (eb->descend || (! pb)))
    {
      svn_wc_adm_access_t *dir_access;
      apr_array_header_t *ignores = eb->ignores;
      SVN_ERR(svn_wc_adm_retrieve(&dir_access, eb->adm_access, 
                                  d->path, pool));
      SVN_ERR(get_dir_status(eb, parent_status->entry, dir_access, NULL, 
                             ignores, FALSE, TRUE, TRUE, TRUE, hash_stash, 
                             d->statii, NULL, NULL, pool));
    }

  *dir_baton = d;
  return SVN_NO_ERROR;
}


/* Make a file baton, using a new subpool of PARENT_DIR_BATON's pool.
   NAME is just one component, not a path. */
static struct file_baton *
make_file_baton(struct dir_baton *parent_dir_baton, 
                const char *path,
                apr_pool_t *pool)
{
  struct dir_baton *pb = parent_dir_baton;
  struct edit_baton *eb = pb->edit_baton;
  struct file_baton *f = apr_pcalloc(pool, sizeof(*f));
  const char *full_path;
 
  /* Construct the full path of this file. */
  full_path = svn_path_join(eb->anchor, path, pool);

  /* Finish populating the baton members. */
  f->path = full_path;
  f->name = svn_path_basename(path, pool);
  f->pool = pool;
  f->dir_baton = pb;
  f->edit_baton = eb;
  f->url = svn_path_url_add_component(find_dir_url(pb, pool),
                                      svn_path_basename(full_path, pool),
                                      pool);
  f->ood_last_cmt_rev = SVN_INVALID_REVNUM;
  f->ood_last_cmt_date = 0;
  f->ood_kind = svn_node_file;
  f->ood_last_cmt_author = NULL;
  return f;
}

/* Return a boolean answer to the question "Is STATUS something that
   should be reported?".  EB is the edit baton. */
static svn_boolean_t 
is_sendable_status(svn_wc_status2_t *status,
                   struct edit_baton *eb)
{
  /* If the repository status was touched at all, it's interesting. */
  if (status->repos_text_status != svn_wc_status_none)
    return TRUE;
  if (status->repos_prop_status != svn_wc_status_none)
    return TRUE;

  /* If there is a lock in the repository, send it. */
  if (status->repos_lock)
    return TRUE;

  /* If the item is ignored, and we don't want ignores, skip it. */
  if ((status->text_status == svn_wc_status_ignored) && (! eb->no_ignore))
    return FALSE;

  /* If we want everything, we obviously want this single-item subset
     of everything. */
  if (eb->get_all)
    return TRUE;

  /* If the item is unversioned, display it. */
  if (status->text_status == svn_wc_status_unversioned)
    return TRUE;

  /* If the text or property states are interesting, send it. */
  if ((status->text_status != svn_wc_status_none)
      && (status->text_status != svn_wc_status_normal))
    return TRUE;
  if ((status->prop_status != svn_wc_status_none)
      && (status->prop_status != svn_wc_status_normal))
    return TRUE;

  /* If it's locked or switched, send it. */
  if (status->locked)
    return TRUE;
  if (status->switched)
    return TRUE;

  /* If there is a lock token, send it. */
  if (status->entry && status->entry->lock_token)
    return TRUE;

  /* Otherwise, don't send it. */
  return FALSE;
}


/* Baton for mark_status. */
struct status_baton
{
  svn_wc_status_func2_t real_status_func;   /* real status function */
  void *real_status_baton;                 /* real status baton */
};

/* A status callback function which wraps the *real* status
   function/baton.   It simply sets the "repos_text_status" field of the
   STATUS to svn_wc_status_deleted and passes it off to the real
   status func/baton. */
static void
mark_deleted(void *baton,
             const char *path,
             svn_wc_status2_t *status)
{
  struct status_baton *sb = baton;
  status->repos_text_status = svn_wc_status_deleted;
  sb->real_status_func(sb->real_status_baton, path, status);
}


/* Handle a directory's STATII hash.  EB is the edit baton.  DIR_PATH
   and DIR_ENTRY are the on-disk path and entry, respectively, for the
   directory itself.  If DESCEND is set, this function will recurse
   into subdirectories.  Also, if DIR_WAS_DELETED is set, each status
   that is reported through this function will have its
   repos_text_status field showing a deletion.  Use POOL for all
   allocations. */
static svn_error_t *
handle_statii(struct edit_baton *eb,
              svn_wc_entry_t *dir_entry,
              const char *dir_path,
              apr_hash_t *statii,
              svn_boolean_t dir_was_deleted,
              svn_boolean_t descend,
              apr_pool_t *pool)
{
  apr_array_header_t *ignores = eb->ignores;
  apr_hash_index_t *hi; 
  apr_pool_t *subpool = svn_pool_create(pool);
  svn_wc_status_func2_t status_func = eb->status_func;
  void *status_baton = eb->status_baton;
  struct status_baton sb;

  if (dir_was_deleted)
    {
      sb.real_status_func = eb->status_func;
      sb.real_status_baton = eb->status_baton;
      status_func = mark_deleted;
      status_baton = &sb;
    }

  /* Loop over all the statuses still in our hash, handling each one. */
  for (hi = apr_hash_first(pool, statii); hi; hi = apr_hash_next(hi))
    {
      const void *key;
      void *val;
      svn_wc_status2_t *status;

      apr_hash_this(hi, &key, NULL, &val);
      status = val;

      /* Clear the subpool. */
      svn_pool_clear(subpool);

      /* Now, handle the status. */
      if (svn_wc__adm_missing(eb->adm_access, key))
        status->text_status = svn_wc_status_missing;
      else if (descend && status->entry && status->entry->kind == svn_node_dir)
        {
          svn_wc_adm_access_t *dir_access;
          SVN_ERR(svn_wc_adm_retrieve(&dir_access, eb->adm_access,
                                      key, subpool));
          SVN_ERR(get_dir_status(eb, dir_entry, dir_access, NULL,
                                 ignores, TRUE, eb->get_all, 
                                 eb->no_ignore, TRUE, status_func, 
                                 status_baton, eb->cancel_func, 
                                 eb->cancel_baton, subpool));
        }
      if (dir_was_deleted)
        status->repos_text_status = svn_wc_status_deleted;
      if (is_sendable_status(status, eb))
        (eb->status_func)(eb->status_baton, key, status);
    }
    
  /* Destroy the subpool. */
  svn_pool_destroy(subpool);

  return SVN_NO_ERROR;
}


/*----------------------------------------------------------------------*/

/*** The callbacks we'll plug into an svn_delta_editor_t structure. ***/

static svn_error_t *
set_target_revision(void *edit_baton, 
                    svn_revnum_t target_revision,
                    apr_pool_t *pool)
{
  struct edit_baton *eb = edit_baton;
  *(eb->target_revision) = target_revision;
  return SVN_NO_ERROR;
}


static svn_error_t *
open_root(void *edit_baton,
          svn_revnum_t base_revision,
          apr_pool_t *pool,
          void **dir_baton)
{
  struct edit_baton *eb = edit_baton;
  eb->root_opened = TRUE;
  return make_dir_baton(dir_baton, NULL, eb, NULL, pool);
}


static svn_error_t *
delete_entry(const char *path,
             svn_revnum_t revision,
             void *parent_baton,
             apr_pool_t *pool)
{
  struct dir_baton *db = parent_baton;
  struct edit_baton *eb = db->edit_baton;
  apr_hash_t *entries;
  const char *name = svn_path_basename(path, pool);
  const char *full_path = svn_path_join(eb->anchor, path, pool);
  const char *dir_path;
  svn_node_kind_t kind;
  svn_wc_adm_access_t *adm_access;
  const char *hash_key;
  svn_error_t *err;

  /* Note:  when something is deleted, it's okay to tweak the
     statushash immediately.  No need to wait until close_file or
     close_dir, because there's no risk of having to honor the 'added'
     flag.  We already know this item exists in the working copy. */

  /* Read the parent's entries file.  If the deleted thing is not
     versioned in this working copy, it was probably deleted via this
     working copy.  No need to report such a thing. */
  /* ### use svn_wc_entry() instead? */
  SVN_ERR(svn_io_check_path(full_path, &kind, pool));
  if (kind == svn_node_dir)
    {
      dir_path = full_path;
      hash_key = SVN_WC_ENTRY_THIS_DIR;
    }
  else
    {
      dir_path = svn_path_dirname(full_path, pool);
      hash_key = name;
    }

  err = svn_wc_adm_retrieve(&adm_access, eb->adm_access, dir_path, pool);
  if (err)
    {
      if ((kind == svn_node_none) && (err->apr_err == SVN_ERR_WC_NOT_LOCKED))
        {
          /* We're probably dealing with a non-recursive, (or
             partially non-recursive, working copy.  Due to deep bugs
             in how the client reports the state of non-recursive
             working copies, the repository can report that a path is
             deleted in an area where we not only don't have the path
             in question, we don't even have its parent(s).  A
             complete fix would require a serious revamp of how
             non-recursive working copies store and report themselves,
             plus some thinking about the UI behavior we want when
             someone runs 'svn st -u' in a [partially] non-recursive
             working copy.

             For now, we just do our best to detect the condition and
             not report an error if it holds.  See issue #2122. */
          svn_error_clear(err);
          return SVN_NO_ERROR;
        }
      else
        return err;
    }

  SVN_ERR(svn_wc_entries_read(&entries, adm_access, FALSE, pool));
  if (apr_hash_get(entries, hash_key, APR_HASH_KEY_STRING))
    SVN_ERR(tweak_statushash(db, TRUE, eb->adm_access,
                             full_path, kind == svn_node_dir,
                             svn_wc_status_deleted, 0, NULL));

  /* Mark the parent dir -- it lost an entry (unless that parent dir
     is the root node and we're not supposed to report on the root
     node).  */
  if (db->parent_baton && (! *eb->target))
    SVN_ERR(tweak_statushash(db->parent_baton, TRUE, eb->adm_access,
                             db->path, kind == svn_node_dir,
                             svn_wc_status_modified, 0, NULL));

  return SVN_NO_ERROR;
}


static svn_error_t *
add_directory(const char *path,
              void *parent_baton,
              const char *copyfrom_path,
              svn_revnum_t copyfrom_revision,
              apr_pool_t *pool,
              void **child_baton)
{
  struct dir_baton *pb = parent_baton;
  struct edit_baton *eb = pb->edit_baton;
  struct dir_baton *new_db;

  SVN_ERR(make_dir_baton(child_baton, path, eb, pb, pool));

  /* Make this dir as added. */
  new_db = *child_baton;
  new_db->added = TRUE;

  /* Mark the parent as changed;  it gained an entry. */
  pb->text_changed = TRUE;

  return SVN_NO_ERROR;
}


static svn_error_t *
open_directory(const char *path,
               void *parent_baton,
               svn_revnum_t base_revision,
               apr_pool_t *pool,
               void **child_baton)
{
  struct dir_baton *pb = parent_baton;
  return make_dir_baton(child_baton, path, pb->edit_baton, pb, pool);
}


static svn_error_t *
change_dir_prop(void *dir_baton,
                const char *name,
                const svn_string_t *value,
                apr_pool_t *pool)
{
  struct dir_baton *db = dir_baton;
  if (svn_wc_is_normal_prop(name))    
    db->prop_changed = TRUE;

  /* Note any changes to the repository. */
  if (value != NULL)
    {
      if (strcmp(name, SVN_PROP_ENTRY_COMMITTED_REV) == 0)
        db->ood_last_cmt_rev = SVN_STR_TO_REV(value->data);
      else if (strcmp(name, SVN_PROP_ENTRY_LAST_AUTHOR) == 0)
        db->ood_last_cmt_author = apr_pstrdup(db->pool, value->data);
      else if (strcmp(name, SVN_PROP_ENTRY_COMMITTED_DATE) == 0)
        {
          apr_time_t tm;
          SVN_ERR(svn_time_from_cstring(&tm, value->data, db->pool));
          db->ood_last_cmt_date = tm;
        }
    }

  return SVN_NO_ERROR;
}



static svn_error_t *
close_directory(void *dir_baton,
                apr_pool_t *pool)
{
  struct dir_baton *db = dir_baton;
  struct dir_baton *pb = db->parent_baton;
  struct edit_baton *eb = db->edit_baton;
  svn_wc_status2_t *dir_status = NULL;
  
  /* If nothing has changed, return. */
  if (db->added || db->prop_changed || db->text_changed)
    {
      enum svn_wc_status_kind repos_text_status;
      enum svn_wc_status_kind repos_prop_status;
  
      /* If this is a new directory, add it to the statushash. */
      if (db->added)
        {
          repos_text_status = svn_wc_status_added;
          repos_prop_status = db->prop_changed ? svn_wc_status_added
                              : svn_wc_status_none;
        }
      else
        {
          repos_text_status = db->text_changed ? svn_wc_status_modified
                              : svn_wc_status_none;
          repos_prop_status = db->prop_changed ? svn_wc_status_modified
                              : svn_wc_status_none;
        }

      /* Maybe add this directory to its parent's status hash.  Note
         that tweak_statushash won't do anything if repos_text_status
         is not svn_wc_status_added. */
      if (pb)
        {
          /* ### When we add directory locking, we need to find a
             ### directory lock here. */
          SVN_ERR(tweak_statushash(pb, TRUE,
                                   eb->adm_access,
                                   db->path, TRUE,
                                   repos_text_status,
                                   repos_prop_status, NULL));
        }
      else
        {
          /* We're editing the root dir of the WC.  As its repos
             status info isn't otherwise set, set it directly to
             trigger invocation of the status callback below. */
          eb->anchor_status->repos_prop_status = repos_prop_status;
          eb->anchor_status->repos_text_status = repos_text_status;
        }
    }

  /* Handle this directory's statuses, and then note in the parent
     that this has been done. */
  if (pb && eb->descend)
    {
      svn_boolean_t was_deleted = FALSE;

      /* See if the directory was deleted or replaced. */
      dir_status = apr_hash_get(pb->statii, db->path, APR_HASH_KEY_STRING);
      if (dir_status &&
          ((dir_status->repos_text_status == svn_wc_status_deleted)
           || (dir_status->repos_text_status == svn_wc_status_replaced)))
        was_deleted = TRUE;

      /* Now do the status reporting. */
      SVN_ERR(handle_statii(eb, dir_status ? dir_status->entry : NULL, 
                            db->path, db->statii, was_deleted, TRUE, pool));
      if (dir_status && is_sendable_status(dir_status, eb))
        (eb->status_func)(eb->status_baton, db->path, dir_status);
      apr_hash_set(pb->statii, db->path, APR_HASH_KEY_STRING, NULL);
    }
  else if (! pb)
    {
      /* If this is the top-most directory, and the operation had a
         target, we should only report the target. */
      if (*eb->target)
        {
          svn_wc_status2_t *tgt_status;
          const char *path = svn_path_join(eb->anchor, eb->target, pool);
          dir_status = eb->anchor_status;
          tgt_status = apr_hash_get(db->statii, path, APR_HASH_KEY_STRING);
          if (tgt_status)
            {
              if ((eb->descend)
                  && (tgt_status->entry)
                  && (tgt_status->entry->kind == svn_node_dir))
                {
                  svn_wc_adm_access_t *dir_access;
                  SVN_ERR(svn_wc_adm_retrieve(&dir_access, eb->adm_access, 
                                              path, pool));
                  SVN_ERR(get_dir_status 
                          (eb, tgt_status->entry, dir_access, NULL,
                           eb->ignores, TRUE, eb->get_all, eb->no_ignore, 
                           TRUE, eb->status_func, eb->status_baton, 
                           eb->cancel_func, eb->cancel_baton, pool));
                }
              if (is_sendable_status(tgt_status, eb))
                (eb->status_func)(eb->status_baton, path, tgt_status);
            }
        }
      else
        {
          /* Otherwise, we report on all our children and ourself.
             Note that our directory couldn't have been deleted,
             because it is the root of the edit drive. */
          SVN_ERR(handle_statii(eb, eb->anchor_status->entry, db->path, 
                                db->statii, FALSE, eb->descend, pool));
          if (is_sendable_status(eb->anchor_status, eb))
            (eb->status_func)(eb->status_baton, db->path, eb->anchor_status);
          eb->anchor_status = NULL;
        }
    }
  return SVN_NO_ERROR;
}



static svn_error_t *
add_file(const char *path,
         void *parent_baton,
         const char *copyfrom_path,
         svn_revnum_t copyfrom_revision,
         apr_pool_t *pool,
         void **file_baton)
{
  struct dir_baton *pb = parent_baton;
  struct file_baton *new_fb = make_file_baton(pb, path, pool);

  /* Mark parent dir as changed */  
  pb->text_changed = TRUE;

  /* Make this file as added. */
  new_fb->added = TRUE;

  *file_baton = new_fb;
  return SVN_NO_ERROR;
}


static svn_error_t *
open_file(const char *path,
          void *parent_baton,
          svn_revnum_t base_revision,
          apr_pool_t *pool,
          void **file_baton)
{
  struct dir_baton *pb = parent_baton;
  struct file_baton *new_fb = make_file_baton(pb, path, pool);

  *file_baton = new_fb;
  return SVN_NO_ERROR;
}


static svn_error_t *
apply_textdelta(void *file_baton, 
                const char *base_checksum,
                apr_pool_t *pool,
                svn_txdelta_window_handler_t *handler,
                void **handler_baton)
{
  struct file_baton *fb = file_baton;
  
  /* Mark file as having textual mods. */
  fb->text_changed = TRUE;

  /* Send back a NULL window handler -- we don't need the actual diffs. */
  *handler_baton = NULL;
  *handler = svn_delta_noop_window_handler;

  return SVN_NO_ERROR;
}


static svn_error_t *
change_file_prop(void *file_baton,
                 const char *name,
                 const svn_string_t *value,
                 apr_pool_t *pool)
{
  struct file_baton *fb = file_baton;
  if (svn_wc_is_normal_prop(name))
    fb->prop_changed = TRUE;

  /* Note any changes to the repository. */
  if (value != NULL)
    {
      if (strcmp(name, SVN_PROP_ENTRY_COMMITTED_REV) == 0)
        fb->ood_last_cmt_rev = SVN_STR_TO_REV(value->data);
      else if (strcmp(name, SVN_PROP_ENTRY_LAST_AUTHOR) == 0)
        fb->ood_last_cmt_author = apr_pstrdup(fb->dir_baton->pool,
                                              value->data);
      else if (strcmp(name, SVN_PROP_ENTRY_COMMITTED_DATE) == 0)
        {
          apr_time_t tm;
          SVN_ERR(svn_time_from_cstring(&tm, value->data,
                                        fb->dir_baton->pool));
          fb->ood_last_cmt_date = tm;
        }
    }

  return SVN_NO_ERROR;
}


static svn_error_t *
close_file(void *file_baton,
           const char *text_checksum,  /* ignored, as we receive no data */
           apr_pool_t *pool)
{
  struct file_baton *fb = file_baton;
  enum svn_wc_status_kind repos_text_status;
  enum svn_wc_status_kind repos_prop_status;
  svn_lock_t *repos_lock = NULL;
  
  /* If nothing has changed, return. */
  if (! (fb->added || fb->prop_changed || fb->text_changed))
    return SVN_NO_ERROR;

  /* If this is a new file, add it to the statushash. */
  if (fb->added)
    {
      const char *url;
      repos_text_status = svn_wc_status_added;
      repos_prop_status = fb->prop_changed ? svn_wc_status_added : 0;

      if (fb->edit_baton->repos_locks)
        {
          url = find_dir_url(fb->dir_baton, pool);
          if (url)
            {
              url = svn_path_url_add_component(url, fb->name, pool);
              repos_lock = apr_hash_get
                (fb->edit_baton->repos_locks,
                 svn_path_uri_decode(url +
                                     strlen(fb->edit_baton->repos_root),
                                     pool), APR_HASH_KEY_STRING);
            }
        }
    }
  else
    {
      repos_text_status = fb->text_changed ? svn_wc_status_modified : 0;
      repos_prop_status = fb->prop_changed ? svn_wc_status_modified : 0;
    }

  SVN_ERR(tweak_statushash(fb, FALSE,
                           fb->edit_baton->adm_access,
                           fb->path, FALSE,
                           repos_text_status,
                           repos_prop_status,
                           repos_lock));

  return SVN_NO_ERROR;
}


static svn_error_t *
close_edit(void *edit_baton,
           apr_pool_t *pool)
{
  struct edit_baton *eb = edit_baton;
  apr_array_header_t *ignores = eb->ignores;
  svn_error_t *err = NULL;

  /* If we get here and the root was not opened as part of the edit,
     we need to transmit statuses for everything.  Otherwise, we
     should be done. */
  if (eb->root_opened)
    goto cleanup;

  /* If we have a target, that's the thing we're sending, otherwise
     we're sending the anchor. */

  if (*eb->target)
    {
      svn_node_kind_t kind;
      const char *full_path = svn_path_join(eb->anchor, eb->target, pool);

      err = svn_io_check_path(full_path, &kind, pool);
      if (err) goto cleanup;

      if (kind == svn_node_dir)
        {
          svn_wc_adm_access_t *tgt_access;
          const svn_wc_entry_t *tgt_entry;

          err = svn_wc_entry(&tgt_entry, full_path, eb->adm_access,
                             FALSE, pool);
          if (err) goto cleanup;

          if (! tgt_entry)
            {
              err = get_dir_status(eb, NULL, eb->adm_access, eb->target, 
                                   ignores, FALSE, eb->get_all, TRUE,
                                   TRUE, eb->status_func, eb->status_baton,
                                   eb->cancel_func, eb->cancel_baton,
                                   pool);
              if (err) goto cleanup;
            }
          else
            {
              err = svn_wc_adm_retrieve(&tgt_access, eb->adm_access,
                                        full_path, pool);
              if (err) goto cleanup;

              err = get_dir_status(eb, NULL, tgt_access, NULL, ignores, 
                                   eb->descend, eb->get_all, 
                                   eb->no_ignore, FALSE, 
                                   eb->status_func, eb->status_baton, 
                                   eb->cancel_func, eb->cancel_baton,
                                   pool);
              if (err) goto cleanup;
            }
        }
      else
        {
          err = get_dir_status(eb, NULL, eb->adm_access, eb->target, 
                               ignores, FALSE, eb->get_all, TRUE,
                               TRUE, eb->status_func, eb->status_baton,
                               eb->cancel_func, eb->cancel_baton, pool);
          if (err) goto cleanup;
        }
    }
  else
    {
      err = get_dir_status(eb, NULL, eb->adm_access, NULL, ignores, 
                           eb->descend, eb->get_all, eb->no_ignore, 
                           FALSE, eb->status_func, eb->status_baton, 
                           eb->cancel_func, eb->cancel_baton, pool);
      if (err) goto cleanup;
    }

 cleanup:
  /* Let's make sure that we didn't harvest any traversal info for the
     anchor if we had a target. */
  if (eb->traversal_info && *eb->target)
    {
      apr_hash_set(eb->traversal_info->externals_old,
                   eb->anchor, APR_HASH_KEY_STRING, NULL);
      apr_hash_set(eb->traversal_info->externals_new,
                   eb->anchor, APR_HASH_KEY_STRING, NULL);
    }
  
  return err;
}



/*** Public API ***/

svn_error_t *
svn_wc_get_status_editor2(const svn_delta_editor_t **editor,
                          void **edit_baton,
                          void **set_locks_baton,
                          svn_revnum_t *edit_revision,
                          svn_wc_adm_access_t *anchor,
                          const char *target,
                          apr_hash_t *config,
                          svn_boolean_t recurse,
                          svn_boolean_t get_all,
                          svn_boolean_t no_ignore,
                          svn_wc_status_func2_t status_func,
                          void *status_baton,
                          svn_cancel_func_t cancel_func,
                          void *cancel_baton,
                          svn_wc_traversal_info_t *traversal_info,
                          apr_pool_t *pool)
{
  struct edit_baton *eb;
  svn_delta_editor_t *tree_editor = svn_delta_default_editor(pool);

  /* Construct an edit baton. */
  eb = apr_palloc(pool, sizeof(*eb));
  eb->descend           = recurse;
  eb->target_revision   = edit_revision;
  eb->adm_access        = anchor;
  eb->config            = config;
  eb->get_all           = get_all;
  eb->no_ignore         = no_ignore;
  eb->status_func       = status_func;
  eb->status_baton      = status_baton;
  eb->cancel_func       = cancel_func;
  eb->cancel_baton      = cancel_baton;
  eb->traversal_info    = traversal_info;
  eb->externals         = apr_hash_make(pool);
  eb->anchor            = svn_wc_adm_access_path(anchor);
  eb->target            = target;
  eb->root_opened       = FALSE;
  eb->repos_locks       = NULL;
  eb->repos_root        = NULL;

  /* The edit baton's status structure maps to PATH, and the editor
     have to be aware of whether that is the anchor or the target. */
  SVN_ERR(svn_wc_status2(&(eb->anchor_status), eb->anchor, anchor, pool));

  /* Get the set of default ignores. */
  SVN_ERR(svn_wc_get_default_ignores(&(eb->ignores), eb->config, pool));

  /* Construct an editor. */
  tree_editor->set_target_revision = set_target_revision;
  tree_editor->open_root = open_root;
  tree_editor->delete_entry = delete_entry;
  tree_editor->add_directory = add_directory;
  tree_editor->open_directory = open_directory;
  tree_editor->change_dir_prop = change_dir_prop;
  tree_editor->close_directory = close_directory;
  tree_editor->add_file = add_file;
  tree_editor->open_file = open_file;
  tree_editor->apply_textdelta = apply_textdelta;
  tree_editor->change_file_prop = change_file_prop;
  tree_editor->close_file = close_file;
  tree_editor->close_edit = close_edit;

  /* Conjoin a cancellation editor with our status editor. */
  SVN_ERR(svn_delta_get_cancellation_editor(cancel_func, cancel_baton,
                                            tree_editor, eb, editor,
                                            edit_baton, pool));

  if (set_locks_baton)
    *set_locks_baton = eb;

  return SVN_NO_ERROR;
}



/* Helpers for deprecated svn_wc_status_editor(), of type
   svn_wc_status_func2_t. */
struct old_status_func_cb_baton
{
  svn_wc_status_func_t original_func;
  void *original_baton;
};

static void old_status_func_cb(void *baton,
                               const char *path,
                               svn_wc_status2_t *status)
{
  struct old_status_func_cb_baton *b = baton;
  svn_wc_status_t *stat = (svn_wc_status_t *) status;
  
  b->original_func(b->original_baton, path, stat);
}

svn_error_t *
svn_wc_get_status_editor(const svn_delta_editor_t **editor,
                         void **edit_baton,
                         svn_revnum_t *edit_revision,
                         svn_wc_adm_access_t *anchor,
                         const char *target,
                         apr_hash_t *config,
                         svn_boolean_t recurse,
                         svn_boolean_t get_all,
                         svn_boolean_t no_ignore,
                         svn_wc_status_func_t status_func,
                         void *status_baton,
                         svn_cancel_func_t cancel_func,
                         void *cancel_baton,
                         svn_wc_traversal_info_t *traversal_info,
                         apr_pool_t *pool)
{
  struct old_status_func_cb_baton *b = apr_pcalloc(pool, sizeof(*b));
  b->original_func = status_func;
  b->original_baton = status_baton;

  return svn_wc_get_status_editor2(editor, edit_baton, NULL, edit_revision,
                                   anchor, target, config, recurse,
                                   get_all, no_ignore, old_status_func_cb,
                                   b, cancel_func, cancel_baton,
                                   traversal_info, pool);
}


svn_error_t *
svn_wc_status_set_repos_locks(void *edit_baton,
                              apr_hash_t *locks,
                              const char *repos_root,
                              apr_pool_t *pool)
{
  struct edit_baton *eb = edit_baton;

  eb->repos_locks = locks;
  eb->repos_root = apr_pstrdup(pool, repos_root);

  return SVN_NO_ERROR;
}

svn_error_t *
svn_wc_get_default_ignores(apr_array_header_t **patterns,
                           apr_hash_t *config,
                           apr_pool_t *pool)
{
  svn_config_t *cfg = config ? apr_hash_get(config, 
                                            SVN_CONFIG_CATEGORY_CONFIG, 
                                            APR_HASH_KEY_STRING) : NULL;
  const char *val;

  /* Check the Subversion run-time configuration for global ignores.
     If no configuration value exists, we fall back to our defaults. */
  svn_config_get(cfg, &val, SVN_CONFIG_SECTION_MISCELLANY, 
                 SVN_CONFIG_OPTION_GLOBAL_IGNORES,
                 SVN_CONFIG_DEFAULT_GLOBAL_IGNORES);
  *patterns = apr_array_make(pool, 16, sizeof(const char *));

  /* Split the patterns on whitespace, and stuff them into *PATTERNS. */
  svn_cstring_split_append(*patterns, val, "\n\r\t\v ", FALSE, pool);
  return SVN_NO_ERROR;
}

                        
svn_error_t *
svn_wc_status2(svn_wc_status2_t **status,
               const char *path,
               svn_wc_adm_access_t *adm_access,
               apr_pool_t *pool)
{
  const svn_wc_entry_t *entry = NULL;
  const svn_wc_entry_t *parent_entry = NULL;

  if (adm_access)
    SVN_ERR(svn_wc_entry(&entry, path, adm_access, FALSE, pool));

  if (entry && ! svn_path_is_empty(path))
    {
      const char *parent_path = svn_path_dirname(path, pool);
      svn_wc_adm_access_t *parent_access;
      SVN_ERR(svn_wc__adm_retrieve_internal(&parent_access, adm_access,
                                            parent_path, pool));
      if (parent_access)
        SVN_ERR(svn_wc_entry(&parent_entry, parent_path, parent_access,
                             FALSE, pool));
    }

  SVN_ERR(assemble_status(status, path, adm_access, entry, parent_entry,
                          svn_node_unknown, FALSE, /* bogus */
                          TRUE, FALSE, NULL, NULL, pool));
  return SVN_NO_ERROR;
}


svn_error_t *
svn_wc_status(svn_wc_status_t **status,
              const char *path,
              svn_wc_adm_access_t *adm_access,
              apr_pool_t *pool)
{
  svn_wc_status2_t *stat2;

  SVN_ERR(svn_wc_status2(&stat2, path, adm_access, pool));
  *status = (svn_wc_status_t *) stat2;
  return SVN_NO_ERROR;
}



svn_wc_status2_t *
svn_wc_dup_status2(svn_wc_status2_t *orig_stat,
                   apr_pool_t *pool)
{
  svn_wc_status2_t *new_stat = apr_palloc(pool, sizeof(*new_stat));
  
  /* Shallow copy all members. */
  *new_stat = *orig_stat;
  
  /* No go back and dup the deep item. */
  if (orig_stat->entry)
    new_stat->entry = svn_wc_entry_dup(orig_stat->entry, pool);

  if (orig_stat->repos_lock)
    new_stat->repos_lock = svn_lock_dup(orig_stat->repos_lock, pool);

  if (orig_stat->url)
    new_stat->url = apr_pstrdup(pool, orig_stat->url);

  if (orig_stat->ood_last_cmt_author)
    new_stat->ood_last_cmt_author
      = apr_pstrdup(pool, orig_stat->ood_last_cmt_author);

  /* Return the new hotness. */
  return new_stat;
}


svn_wc_status_t *
svn_wc_dup_status(svn_wc_status_t *orig_stat,
                  apr_pool_t *pool)
{
  svn_wc_status_t *new_stat = apr_palloc(pool, sizeof(*new_stat));
  
  /* Shallow copy all members. */
  *new_stat = *orig_stat;
  
  /* No go back and dup the deep item. */
  if (orig_stat->entry)
    new_stat->entry = svn_wc_entry_dup(orig_stat->entry, pool);

  /* Return the new hotness. */
  return new_stat;
}

svn_error_t *
svn_wc_get_ignores(apr_array_header_t **patterns,
                   apr_hash_t *config,
                   svn_wc_adm_access_t *adm_access,
                   apr_pool_t *pool)
{
  apr_array_header_t *default_ignores;

  SVN_ERR(svn_wc_get_default_ignores(&default_ignores, config, pool));
  SVN_ERR(collect_ignore_patterns(patterns, default_ignores, adm_access, 
                                  pool));

  return SVN_NO_ERROR;
}