The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
/*
 * commit_util.c:  Driver for the WC commit process.
 *
 * ====================================================================
 *    Licensed to the Apache Software Foundation (ASF) under one
 *    or more contributor license agreements.  See the NOTICE file
 *    distributed with this work for additional information
 *    regarding copyright ownership.  The ASF licenses this file
 *    to you under the Apache License, Version 2.0 (the
 *    "License"); you may not use this file except in compliance
 *    with the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing,
 *    software distributed under the License is distributed on an
 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *    KIND, either express or implied.  See the License for the
 *    specific language governing permissions and limitations
 *    under the License.
 * ====================================================================
 */

/* ==================================================================== */


#include <string.h>

#include <apr_pools.h>
#include <apr_hash.h>
#include <apr_md5.h>

#include "client.h"
#include "svn_dirent_uri.h"
#include "svn_path.h"
#include "svn_types.h"
#include "svn_pools.h"
#include "svn_props.h"
#include "svn_iter.h"
#include "svn_hash.h"

#include <assert.h>
#include <stdlib.h>  /* for qsort() */

#include "svn_private_config.h"
#include "private/svn_wc_private.h"

/*** Uncomment this to turn on commit driver debugging. ***/
/*
#define SVN_CLIENT_COMMIT_DEBUG
*/

/* Wrap an RA error in a nicer error if one is available. */
static svn_error_t *
fixup_commit_error(const char *local_abspath,
                   const char *base_url,
                   const char *path,
                   svn_node_kind_t kind,
                   svn_error_t *err,
                   svn_client_ctx_t *ctx,
                   apr_pool_t *scratch_pool)
{
  if (err->apr_err == SVN_ERR_FS_NOT_FOUND
      || err->apr_err == SVN_ERR_FS_ALREADY_EXISTS
      || err->apr_err == SVN_ERR_FS_TXN_OUT_OF_DATE
      || err->apr_err == SVN_ERR_RA_DAV_PATH_NOT_FOUND
      || err->apr_err == SVN_ERR_RA_DAV_ALREADY_EXISTS
      || svn_error_find_cause(err, SVN_ERR_RA_OUT_OF_DATE))
    {
      if (ctx->notify_func2)
        {
          svn_wc_notify_t *notify;

          if (local_abspath)
            notify = svn_wc_create_notify(local_abspath,
                                          svn_wc_notify_failed_out_of_date,
                                          scratch_pool);
          else
            notify = svn_wc_create_notify_url(
                                svn_path_url_add_component2(base_url, path,
                                                            scratch_pool),
                                svn_wc_notify_failed_out_of_date,
                                scratch_pool);

          notify->kind = kind;
          notify->err = err;

          ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
        }

      return svn_error_createf(SVN_ERR_WC_NOT_UP_TO_DATE, err,
                               (kind == svn_node_dir
                                 ? _("Directory '%s' is out of date")
                                 : _("File '%s' is out of date")),
                               local_abspath
                                  ? svn_dirent_local_style(local_abspath,
                                                           scratch_pool)
                                  : svn_path_url_add_component2(base_url,
                                                                path,
                                                                scratch_pool));
    }
  else if (svn_error_find_cause(err, SVN_ERR_FS_NO_LOCK_TOKEN)
           || err->apr_err == SVN_ERR_FS_LOCK_OWNER_MISMATCH
           || err->apr_err == SVN_ERR_RA_NOT_LOCKED)
    {
      if (ctx->notify_func2)
        {
          svn_wc_notify_t *notify;

          if (local_abspath)
            notify = svn_wc_create_notify(local_abspath,
                                          svn_wc_notify_failed_locked,
                                          scratch_pool);
          else
            notify = svn_wc_create_notify_url(
                                svn_path_url_add_component2(base_url, path,
                                                            scratch_pool),
                                svn_wc_notify_failed_locked,
                                scratch_pool);

          notify->kind = kind;
          notify->err = err;

          ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
        }

      return svn_error_createf(SVN_ERR_CLIENT_NO_LOCK_TOKEN, err,
                   (kind == svn_node_dir
                     ? _("Directory '%s' is locked in another working copy")
                     : _("File '%s' is locked in another working copy")),
                   local_abspath
                      ? svn_dirent_local_style(local_abspath,
                                               scratch_pool)
                      : svn_path_url_add_component2(base_url,
                                                    path,
                                                    scratch_pool));
    }
  else if (svn_error_find_cause(err, SVN_ERR_RA_DAV_FORBIDDEN)
           || err->apr_err == SVN_ERR_AUTHZ_UNWRITABLE)
    {
      if (ctx->notify_func2)
        {
          svn_wc_notify_t *notify;

          if (local_abspath)
            notify = svn_wc_create_notify(
                                    local_abspath,
                                    svn_wc_notify_failed_forbidden_by_server,
                                    scratch_pool);
          else
            notify = svn_wc_create_notify_url(
                                svn_path_url_add_component2(base_url, path,
                                                            scratch_pool),
                                svn_wc_notify_failed_forbidden_by_server,
                                scratch_pool);

          notify->kind = kind;
          notify->err = err;

          ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
        }

      return svn_error_createf(SVN_ERR_CLIENT_FORBIDDEN_BY_SERVER, err,
                   (kind == svn_node_dir
                     ? _("Changing directory '%s' is forbidden by the server")
                     : _("Changing file '%s' is forbidden by the server")),
                   local_abspath
                      ? svn_dirent_local_style(local_abspath,
                                               scratch_pool)
                      : svn_path_url_add_component2(base_url,
                                                    path,
                                                    scratch_pool));
    }
  else
    return err;
}


/*** Harvesting Commit Candidates ***/


/* Add a new commit candidate (described by all parameters except
   `COMMITTABLES') to the COMMITTABLES hash.  All of the commit item's
   members are allocated out of RESULT_POOL. */
static svn_error_t *
add_committable(svn_client__committables_t *committables,
                const char *local_abspath,
                svn_node_kind_t kind,
                const char *repos_root_url,
                const char *repos_relpath,
                svn_revnum_t revision,
                const char *copyfrom_relpath,
                svn_revnum_t copyfrom_rev,
                apr_byte_t state_flags,
                apr_pool_t *result_pool,
                apr_pool_t *scratch_pool)
{
  apr_array_header_t *array;
  svn_client_commit_item3_t *new_item;

  /* Sanity checks. */
  SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
  SVN_ERR_ASSERT(repos_root_url && repos_relpath);

  /* ### todo: Get the canonical repository for this item, which will
     be the real key for the COMMITTABLES hash, instead of the above
     bogosity. */
  array = apr_hash_get(committables->by_repository,
                       repos_root_url,
                       APR_HASH_KEY_STRING);

  /* E-gads!  There is no array for this repository yet!  Oh, no
     problem, we'll just create (and add to the hash) one. */
  if (array == NULL)
    {
      array = apr_array_make(result_pool, 1, sizeof(new_item));
      apr_hash_set(committables->by_repository,
                   apr_pstrdup(result_pool, repos_root_url),
                   APR_HASH_KEY_STRING, array);
    }

  /* Now update pointer values, ensuring that their allocations live
     in POOL. */
  new_item = svn_client_commit_item3_create(result_pool);
  new_item->path           = apr_pstrdup(result_pool, local_abspath);
  new_item->kind           = kind;
  new_item->url            = svn_path_url_add_component2(repos_root_url,
                                                         repos_relpath,
                                                         result_pool);
  new_item->revision       = revision;
  new_item->copyfrom_url   = copyfrom_relpath
                                ? svn_path_url_add_component2(repos_root_url,
                                                              copyfrom_relpath,
                                                              result_pool)
                                : NULL;
  new_item->copyfrom_rev   = copyfrom_rev;
  new_item->state_flags    = state_flags;
  new_item->incoming_prop_changes = apr_array_make(result_pool, 1,
                                                   sizeof(svn_prop_t *));

  /* Now, add the commit item to the array. */
  APR_ARRAY_PUSH(array, svn_client_commit_item3_t *) = new_item;

  /* ... and to the hash. */
  apr_hash_set(committables->by_path,
               new_item->path,
               APR_HASH_KEY_STRING,
               new_item);

  return SVN_NO_ERROR;
}

/* If there is a commit item for PATH in COMMITTABLES, return it, else
   return NULL.  Use POOL for temporary allocation only. */
static svn_client_commit_item3_t *
look_up_committable(svn_client__committables_t *committables,
                    const char *path,
                    apr_pool_t *pool)
{
  return (svn_client_commit_item3_t *)
      apr_hash_get(committables->by_path, path, APR_HASH_KEY_STRING);
}

/* Helper for harvest_committables().
 * If ENTRY is a dir, return an SVN_ERR_WC_FOUND_CONFLICT error when
 * encountering a tree-conflicted immediate child node. However, do
 * not consider immediate children that are outside the bounds of DEPTH.
 *
 * TODO ### WC_CTX and LOCAL_ABSPATH ...
 * ENTRY, DEPTH, CHANGELISTS and POOL are the same ones
 * originally received by harvest_committables().
 *
 * Tree-conflicts information is stored in the victim's immediate parent.
 * In some cases of an absent tree-conflicted victim, the tree-conflict
 * information in its parent dir is the only indication that the node
 * is under version control. This function is necessary for this
 * particular case. In all other cases, this simply bails out a little
 * bit earlier. */
static svn_error_t *
bail_on_tree_conflicted_children(svn_wc_context_t *wc_ctx,
                                 const char *local_abspath,
                                 svn_node_kind_t kind,
                                 svn_depth_t depth,
                                 apr_hash_t *changelists,
                                 svn_wc_notify_func2_t notify_func,
                                 void *notify_baton,
                                 apr_pool_t *pool)
{
  apr_hash_t *conflicts;
  apr_hash_index_t *hi;

  if ((depth == svn_depth_empty)
      || (kind != svn_node_dir))
    /* There can't possibly be tree-conflicts information here. */
    return SVN_NO_ERROR;

  SVN_ERR(svn_wc__get_all_tree_conflicts(&conflicts, wc_ctx, local_abspath,
                                         pool, pool));
  if (!conflicts)
    return SVN_NO_ERROR;

  for (hi = apr_hash_first(pool, conflicts); hi; hi = apr_hash_next(hi))
    {
      const svn_wc_conflict_description2_t *conflict =
          svn__apr_hash_index_val(hi);

      if ((conflict->node_kind == svn_node_dir) &&
          (depth == svn_depth_files))
        continue;

      /* So we've encountered a conflict that is included in DEPTH.
         Bail out. But if there are CHANGELISTS, avoid bailing out
         on an item that doesn't match the CHANGELISTS. */
      if (!svn_wc__changelist_match(wc_ctx, local_abspath, changelists, pool))
        continue;

      /* At this point, a conflict was found, and either there were no
         changelists, or the changelists matched. Bail out already! */

      if (notify_func != NULL)
        {
          notify_func(notify_baton,
                      svn_wc_create_notify(local_abspath,
                                           svn_wc_notify_failed_conflict,
                                           pool),
                      pool);
        }

      return svn_error_createf(
               SVN_ERR_WC_FOUND_CONFLICT, NULL,
               _("Aborting commit: '%s' remains in conflict"),
               svn_dirent_local_style(conflict->local_abspath, pool));
    }

  return SVN_NO_ERROR;
}

/* Helper function for svn_client__harvest_committables().
 * Determine whether we are within a tree-conflicted subtree of the
 * working copy and return an SVN_ERR_WC_FOUND_CONFLICT error if so. */
static svn_error_t *
bail_on_tree_conflicted_ancestor(svn_wc_context_t *wc_ctx,
                                 const char *local_abspath,
                                 svn_wc_notify_func2_t notify_func,
                                 void *notify_baton,
                                 apr_pool_t *scratch_pool)
{
  const char *wcroot_abspath;

  SVN_ERR(svn_wc__get_wc_root(&wcroot_abspath, wc_ctx, local_abspath,
                              scratch_pool, scratch_pool));

  local_abspath = svn_dirent_dirname(local_abspath, scratch_pool);

  while(svn_dirent_is_ancestor(wcroot_abspath, local_abspath))
    {
      svn_boolean_t tree_conflicted;

      /* Check if the parent has tree conflicts */
      SVN_ERR(svn_wc_conflicted_p3(NULL, NULL, &tree_conflicted,
                                   wc_ctx, local_abspath, scratch_pool));
      if (tree_conflicted)
        {
          if (notify_func != NULL)
            {
              notify_func(notify_baton,
                          svn_wc_create_notify(local_abspath,
                                               svn_wc_notify_failed_conflict,
                                               scratch_pool),
                          scratch_pool);
            }

          return svn_error_createf(
                   SVN_ERR_WC_FOUND_CONFLICT, NULL,
                   _("Aborting commit: '%s' remains in tree-conflict"),
                   svn_dirent_local_style(local_abspath, scratch_pool));
        }

      /* Step outwards */
      if (svn_dirent_is_root(local_abspath, strlen(local_abspath)))
        break;
      else
        local_abspath = svn_dirent_dirname(local_abspath, scratch_pool);
    }

  return SVN_NO_ERROR;
}


/* Recursively search for commit candidates in (and under) LOCAL_ABSPATH using
   WC_CTX and add those candidates to COMMITTABLES.  If in ADDS_ONLY modes,
   only new additions are recognized.

   DEPTH indicates how to treat files and subdirectories of LOCAL_ABSPATH
   when LOCAL_ABSPATH is itself a directory; see
   svn_client__harvest_committables() for its behavior.

   Lock tokens of candidates will be added to LOCK_TOKENS, if
   non-NULL.  JUST_LOCKED indicates whether to treat non-modified items with
   lock tokens as commit candidates.

   If COMMIT_RELPATH is not NULL, treat not-added nodes as if it is destined to
   be added as COMMIT_RELPATH, and add 'deleted' entries to COMMITTABLES as
   items to delete in the copy destination.  COPY_MODE_ROOT should be set TRUE
   for the first call for which COPY_MODE is TRUE, i.e. not for for the
   recursive calls, and FALSE otherwise.

   If CHANGELISTS is non-NULL, it is a hash whose keys are const char *
   changelist names used as a restrictive filter
   when harvesting committables; that is, don't add a path to
   COMMITTABLES unless it's a member of one of those changelists.

   DANGLERS is a hash table mapping const char* absolute paths of a parent
   to a const char * absolute path of a child. See the comment about
   danglers at the top of svn_client__harvest_committables().

   If CANCEL_FUNC is non-null, call it with CANCEL_BATON to see
   if the user has cancelled the operation.

   Any items added to COMMITTABLES are allocated from the COMITTABLES
   hash pool, not POOL.  SCRATCH_POOL is used for temporary allocations. */
static svn_error_t *
harvest_committables(svn_wc_context_t *wc_ctx,
                     const char *local_abspath,
                     svn_client__committables_t *committables,
                     apr_hash_t *lock_tokens,
                     const char *repos_root_url,
                     const char *commit_relpath,
                     svn_boolean_t copy_mode_root,
                     svn_depth_t depth,
                     svn_boolean_t just_locked,
                     apr_hash_t *changelists,
                     svn_boolean_t skip_files,
                     svn_boolean_t skip_dirs,
                     apr_hash_t *danglers,
                     svn_client__check_url_kind_t check_url_func,
                     void *check_url_baton,
                     svn_cancel_func_t cancel_func,
                     void *cancel_baton,
                     svn_wc_notify_func2_t notify_func,
                     void *notify_baton,
                     apr_pool_t *result_pool,
                     apr_pool_t *scratch_pool)
{
  svn_boolean_t text_mod = FALSE;
  svn_boolean_t prop_mod = FALSE;
  apr_byte_t state_flags = 0;
  svn_node_kind_t working_kind;
  svn_node_kind_t db_kind;
  const char *node_relpath;
  const char *node_lock_token;
  svn_revnum_t node_rev;
  const char *cf_relpath = NULL;
  svn_revnum_t cf_rev = SVN_INVALID_REVNUM;
  svn_boolean_t matches_changelists;
  svn_boolean_t is_special;
  svn_boolean_t is_added;
  svn_boolean_t is_deleted;
  svn_boolean_t is_replaced;
  svn_boolean_t is_not_present;
  svn_boolean_t is_excluded;
  svn_boolean_t is_op_root;
  svn_boolean_t is_symlink;
  svn_boolean_t conflicted;
  const char *node_changelist;
  svn_boolean_t is_update_root;
  svn_revnum_t original_rev;
  const char *original_relpath;
  svn_boolean_t copy_mode = (commit_relpath != NULL);

  SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));

  /* Early out if the item is already marked as committable. */
  if (look_up_committable(committables, local_abspath, scratch_pool))
    return SVN_NO_ERROR;

  SVN_ERR_ASSERT((copy_mode && commit_relpath)
                 || (! copy_mode && ! commit_relpath));
  SVN_ERR_ASSERT((copy_mode_root && copy_mode) || ! copy_mode_root);
  SVN_ERR_ASSERT((just_locked && lock_tokens) || !just_locked);

  if (cancel_func)
    SVN_ERR(cancel_func(cancel_baton));

  /* Return error on unknown path kinds.  We check both the entry and
     the node itself, since a path might have changed kind since its
     entry was written. */
  SVN_ERR(svn_wc__node_get_commit_status(&db_kind, &is_added, &is_deleted,
                                         &is_replaced,
                                         &is_not_present, &is_excluded,
                                         &is_op_root, &is_symlink,
                                         &node_rev, &node_relpath,
                                         &original_rev, &original_relpath,
                                         &conflicted,
                                         &node_changelist,
                                         &prop_mod, &is_update_root,
                                         &node_lock_token,
                                         wc_ctx, local_abspath,
                                         scratch_pool, scratch_pool));

  if ((skip_files && db_kind == svn_node_file) || is_excluded)
    return SVN_NO_ERROR;

  if (!node_relpath && commit_relpath)
    node_relpath = commit_relpath;

  SVN_ERR(svn_io_check_special_path(local_abspath, &working_kind, &is_special,
                                    scratch_pool));

  /* ### In 1.6 an obstructed dir would fail when locking before we
         got here.  Locking now doesn't fail so perhaps we should do
         some sort of checking here. */

  if ((working_kind != svn_node_file)
      && (working_kind != svn_node_dir)
      && (working_kind != svn_node_none))
    {
      return svn_error_createf
        (SVN_ERR_NODE_UNKNOWN_KIND, NULL,
         _("Unknown entry kind for '%s'"),
         svn_dirent_local_style(local_abspath, scratch_pool));
    }

  /* Save the result for reuse. */
  matches_changelists = ((changelists == NULL)
                         || (node_changelist != NULL
                             && apr_hash_get(changelists, node_changelist,
                                             APR_HASH_KEY_STRING) != NULL));

  /* Early exit. */
  if (working_kind != svn_node_dir && working_kind != svn_node_none
      && ! matches_changelists)
    {
      return SVN_NO_ERROR;
    }

  /* Verify that the node's type has not changed before attempting to
     commit. */
  if ((((!is_symlink) && (is_special))
#ifdef HAVE_SYMLINK
       || (is_symlink && (! is_special))
#endif /* HAVE_SYMLINK */
       ) && (working_kind != svn_node_none))
    {
      return svn_error_createf
        (SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
         _("Entry '%s' has unexpectedly changed special status"),
         svn_dirent_local_style(local_abspath, scratch_pool));
    }

  if (copy_mode
      && is_update_root
      && db_kind == svn_node_file)
    {
      if (copy_mode)
        return SVN_NO_ERROR;
    }

  /* If NODE is in our changelist, then examine it for conflicts. We
     need to bail out if any conflicts exist.  */
  if (conflicted && matches_changelists)
    {
      svn_boolean_t tc, pc, treec;

      SVN_ERR(svn_wc_conflicted_p3(&tc, &pc, &treec, wc_ctx,
                                   local_abspath, scratch_pool));
      if (tc || pc || treec)
        {
          if (notify_func != NULL)
            {
              notify_func(notify_baton,
                          svn_wc_create_notify(local_abspath,
                                               svn_wc_notify_failed_conflict,
                                               scratch_pool),
                          scratch_pool);
            }

          return svn_error_createf(
            SVN_ERR_WC_FOUND_CONFLICT, NULL,
            _("Aborting commit: '%s' remains in conflict"),
            svn_dirent_local_style(local_abspath, scratch_pool));
        }
    }

  if (is_deleted && !is_op_root /* && !is_added */)
    return SVN_NO_ERROR; /* Not an operational delete and not an add. */

  if (node_relpath == NULL)
    SVN_ERR(svn_wc__node_get_repos_relpath(&node_relpath,
                                           wc_ctx, local_abspath,
                                           scratch_pool, scratch_pool));
  /* Check for the deletion case.
     * We delete explicitly deleted nodes (duh!)
     * We delete not-present children of copies
     * We delete nodes that directly replace a node in its ancestor
   */

  if (is_deleted || is_replaced)
    state_flags |= SVN_CLIENT_COMMIT_ITEM_DELETE;
  else if (is_not_present)
    {
      if (! copy_mode)
        return SVN_NO_ERROR;

      /* We should check if we should really add a delete operation */
      if (check_url_func)
        {
          svn_revnum_t revision;
          const char *repos_relpath;
          svn_node_kind_t kind;

          /* Determine from what parent we would be the deleted child */
          SVN_ERR(svn_wc__node_get_origin(NULL, &revision, &repos_relpath,
                                          NULL, NULL, NULL, wc_ctx,
                                          svn_dirent_dirname(local_abspath,
                                                             scratch_pool),
                                          FALSE, scratch_pool, scratch_pool));

          repos_relpath = svn_relpath_join(repos_relpath,
                                           svn_dirent_basename(local_abspath,
                                                               NULL),
                                           scratch_pool);

          SVN_ERR(check_url_func(check_url_baton, &kind,
                                 svn_path_url_add_component2(repos_root_url,
                                                             repos_relpath,
                                                             scratch_pool),
                                 revision, scratch_pool));

          if (kind == svn_node_none)
            return SVN_NO_ERROR; /* This node can't be deleted */
        }

      state_flags |= SVN_CLIENT_COMMIT_ITEM_DELETE;
    }

  /* Check for adds and copies */
  if (is_added && is_op_root)
    {
      /* Root of local add or copy */
      state_flags |= SVN_CLIENT_COMMIT_ITEM_ADD;

      if (original_relpath)
        {
          /* Root of copy */
          state_flags |= SVN_CLIENT_COMMIT_ITEM_IS_COPY;
          cf_relpath = original_relpath;
          cf_rev = original_rev;
        }
    }

  /* Further additions occur in copy mode. */
  if (copy_mode
      && (!is_added || copy_mode_root)
      && !(state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE))
    {
      svn_revnum_t dir_rev;

      if (!copy_mode_root)
        SVN_ERR(svn_wc__node_get_base_rev(&dir_rev, wc_ctx,
                                          svn_dirent_dirname(local_abspath,
                                                             scratch_pool),
                                          scratch_pool));

      if (copy_mode_root || node_rev != dir_rev)
        {
          state_flags |= SVN_CLIENT_COMMIT_ITEM_ADD;

          SVN_ERR(svn_wc__node_get_origin(NULL, &cf_rev,
                                      &cf_relpath, NULL,
                                      NULL, NULL,
                                      wc_ctx, local_abspath, FALSE,
                                      scratch_pool, scratch_pool));

          if (cf_relpath)
            state_flags |= SVN_CLIENT_COMMIT_ITEM_IS_COPY;
        }
    }

  /* If an add is scheduled to occur, dig around for some more
     information about it. */
  if (state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
    {
      /* First of all, the working file or directory must exist.
         See issue #3198. */
      if (working_kind == svn_node_none)
        {
          if (notify_func != NULL)
            {
              notify_func(notify_baton,
                          svn_wc_create_notify(local_abspath,
                                               svn_wc_notify_failed_missing,
                                               scratch_pool),
                          scratch_pool);
            }
          return svn_error_createf(
             SVN_ERR_WC_PATH_NOT_FOUND, NULL,
             _("'%s' is scheduled for addition, but is missing"),
             svn_dirent_local_style(local_abspath, scratch_pool));
        }

      /* Regular adds of files have text mods, but for copies we have
         to test for textual mods.  Directories simply don't have text! */
      if (db_kind == svn_node_file)
        {
          /* Check for text mods.  */
          if (state_flags & SVN_CLIENT_COMMIT_ITEM_IS_COPY)
            SVN_ERR(svn_wc_text_modified_p2(&text_mod, wc_ctx, local_abspath,
                                            FALSE, scratch_pool));
          else
            text_mod = TRUE;
        }
    }

  /* Else, if we aren't deleting this item, we'll have to look for
     local text or property mods to determine if the path might be
     committable. */
  else if (! (state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE))
    {
      /* Check for text mods on files.  If EOL_PROP_CHANGED is TRUE,
         then we need to force a translated byte-for-byte comparison
         against the text-base so that a timestamp comparison won't
         bail out early.  Depending on how the svn:eol-style prop was
         changed, we might have to send new text to the server to
         match the new newline style.  */
      if (db_kind == svn_node_file)
        SVN_ERR(svn_wc_text_modified_p2(&text_mod, wc_ctx, local_abspath,
                                        FALSE, scratch_pool));
    }

  /* Set text/prop modification flags accordingly. */
  if (text_mod)
    state_flags |= SVN_CLIENT_COMMIT_ITEM_TEXT_MODS;
  if (prop_mod)
    state_flags |= SVN_CLIENT_COMMIT_ITEM_PROP_MODS;

  /* If the entry has a lock token and it is already a commit candidate,
     or the caller wants unmodified locked items to be treated as
     such, note this fact. */
  if (node_lock_token && lock_tokens && (state_flags || just_locked))
    {
      state_flags |= SVN_CLIENT_COMMIT_ITEM_LOCK_TOKEN;
    }

  /* Now, if this is something to commit, add it to our list. */
  if (state_flags)
    {
      if (matches_changelists)
        {
          /* Finally, add the committable item. */
          SVN_ERR(add_committable(committables, local_abspath, db_kind,
                                  repos_root_url,
                                  copy_mode
                                      ? commit_relpath
                                      : node_relpath,
                                  copy_mode
                                      ? SVN_INVALID_REVNUM
                                      : node_rev,
                                  cf_relpath,
                                  cf_rev,
                                  state_flags,
                                  result_pool, scratch_pool));
          if (state_flags & SVN_CLIENT_COMMIT_ITEM_LOCK_TOKEN)
            apr_hash_set(lock_tokens,
                         svn_path_url_add_component2(
                             repos_root_url, node_relpath,
                             apr_hash_pool_get(lock_tokens)),
                         APR_HASH_KEY_STRING,
                         apr_pstrdup(apr_hash_pool_get(lock_tokens),
                                     node_lock_token));
        }
    }

    /* Fetch lock tokens for descendants of deleted nodes. */
  if (lock_tokens
      && (state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE))
    {
      apr_hash_t *local_relpath_tokens;
      apr_hash_index_t *hi;
      apr_pool_t *token_pool = apr_hash_pool_get(lock_tokens);

      SVN_ERR(svn_wc__node_get_lock_tokens_recursive(
                  &local_relpath_tokens, wc_ctx, local_abspath,
                  token_pool, scratch_pool));

      /* Add tokens to existing hash. */
      for (hi = apr_hash_first(scratch_pool, local_relpath_tokens);
           hi;
           hi = apr_hash_next(hi))
        {
          const void *key;
          apr_ssize_t klen;
          void * val;

          apr_hash_this(hi, &key, &klen, &val);

          apr_hash_set(lock_tokens, key, klen, val);
        }
    }

  /* Make sure we check for dangling children on additions */
  if (state_flags && is_added && danglers)
    {
      /* If a node is added, it's parent must exist in the repository at the
         time of committing */

      svn_boolean_t parent_added;
      const char *parent_abspath = svn_dirent_dirname(local_abspath,
                                                      scratch_pool);

      SVN_ERR(svn_wc__node_is_added(&parent_added, wc_ctx, parent_abspath,
                                    scratch_pool));

      if (parent_added)
        {
          const char *copy_root_abspath;
          svn_boolean_t parent_is_copy;

          /* The parent is added, so either it is a copy, or a locally added
           * directory. In either case, we require the op-root of the parent
           * to be part of the commit. See issue #4059. */
          SVN_ERR(svn_wc__node_get_origin(&parent_is_copy, NULL, NULL, NULL,
                                          NULL, &copy_root_abspath,
                                          wc_ctx, parent_abspath,
                                          FALSE, scratch_pool, scratch_pool));

          if (parent_is_copy)
            parent_abspath = copy_root_abspath;

          if (!apr_hash_get(danglers, parent_abspath, APR_HASH_KEY_STRING))
            {
              apr_hash_set(danglers,
                           apr_pstrdup(result_pool, parent_abspath),
                           APR_HASH_KEY_STRING,
                           apr_pstrdup(result_pool, local_abspath));
            }
        }
    }

  if (db_kind != svn_node_dir || depth <= svn_depth_empty)
    return SVN_NO_ERROR;

  SVN_ERR(bail_on_tree_conflicted_children(wc_ctx, local_abspath,
                                           db_kind, depth, changelists,
                                           notify_func, notify_baton,
                                           scratch_pool));

  /* Recursively handle each node according to depth, except when the
     node is only being deleted. */
  if ((! (state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE))
      || (state_flags & SVN_CLIENT_COMMIT_ITEM_ADD))
    {
      const apr_array_header_t *children;
      apr_pool_t *iterpool = svn_pool_create(scratch_pool);
      int i;
      svn_depth_t depth_below_here = depth;

      if (depth < svn_depth_infinity)
        depth_below_here = svn_depth_empty; /* Stop recursing */

      SVN_ERR(svn_wc__node_get_children_of_working_node(
                &children, wc_ctx, local_abspath, copy_mode,
                scratch_pool, iterpool));
      for (i = 0; i < children->nelts; i++)
        {
          const char *this_abspath = APR_ARRAY_IDX(children, i, const char *);
          const char *name = svn_dirent_basename(this_abspath, NULL);
          const char *this_commit_relpath;

          svn_pool_clear(iterpool);

          if (commit_relpath == NULL)
            this_commit_relpath = NULL;
          else
            this_commit_relpath = svn_relpath_join(commit_relpath, name,
                                                   iterpool);

          SVN_ERR(harvest_committables(wc_ctx, this_abspath,
                                       committables, lock_tokens,
                                       repos_root_url,
                                       this_commit_relpath,
                                       FALSE, /* COPY_MODE_ROOT */
                                       depth_below_here,
                                       just_locked,
                                       changelists,
                                       (depth < svn_depth_files),
                                       (depth < svn_depth_immediates),
                                       NULL, /* danglers */
                                       check_url_func, check_url_baton,
                                       cancel_func, cancel_baton,
                                       notify_func, notify_baton,
                                       result_pool,
                                       iterpool));
        }

      svn_pool_destroy(iterpool);
    }

  return SVN_NO_ERROR;
}

/* Baton for handle_descendants */
struct handle_descendants_baton
{
  svn_wc_context_t *wc_ctx;
  svn_cancel_func_t cancel_func;
  void *cancel_baton;
  svn_client__check_url_kind_t check_url_func;
  void *check_url_baton;
};

/* Helper for the commit harvesters */
static svn_error_t *
handle_descendants(void *baton,
                       const void *key, apr_ssize_t klen, void *val,
                       apr_pool_t *pool)
{
  struct handle_descendants_baton *hdb = baton;
  apr_array_header_t *commit_items = val;
  apr_pool_t *iterpool = svn_pool_create(pool);
  int i;

  for (i = 0; i < commit_items->nelts; i++)
    {
      svn_client_commit_item3_t *item =
        APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *);
      const apr_array_header_t *absent_descendants;
      int j;

      /* Is this a copy operation? */
      if (!(item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
          || ! item->copyfrom_url)
        continue;

      if (hdb->cancel_func)
        SVN_ERR(hdb->cancel_func(hdb->cancel_baton));

      svn_pool_clear(iterpool);

      SVN_ERR(svn_wc__get_not_present_descendants(&absent_descendants,
                                                  hdb->wc_ctx, item->path,
                                                  iterpool, iterpool));

      for (j = 0; j < absent_descendants->nelts; j++)
        {
          int k;
          svn_boolean_t found_item = FALSE;
          svn_node_kind_t kind;
          const char *relpath = APR_ARRAY_IDX(absent_descendants, j,
                                              const char *);
          const char *local_abspath = svn_dirent_join(item->path, relpath,
                                                      iterpool);

          /* If the path has a commit operation, we do nothing.
             (It will be deleted by the operation) */
          for (k = 0; k < commit_items->nelts; k++)
            {
              svn_client_commit_item3_t *cmt_item =
                 APR_ARRAY_IDX(commit_items, k, svn_client_commit_item3_t *);

              if (! strcmp(cmt_item->path, local_abspath))
                {
                  found_item = TRUE;
                  break;
                }
            }

          if (found_item)
            continue; /* We have an explicit delete or replace for this path */

          /* ### Need a sub-iterpool? */

          if (hdb->check_url_func)
            {
              const char *from_url = svn_path_url_add_component2(
                                                item->copyfrom_url, relpath,
                                                iterpool);

              SVN_ERR(hdb->check_url_func(hdb->check_url_baton,
                                          &kind, from_url, item->copyfrom_rev,
                                          iterpool));

              if (kind == svn_node_none)
                continue; /* This node is already deleted */
            }
          else
            kind = svn_node_unknown; /* 'Ok' for a delete of something */

          {
            /* Add a new commit item that describes the delete */
            apr_pool_t *result_pool = commit_items->pool;
            svn_client_commit_item3_t *new_item
                  = svn_client_commit_item3_create(result_pool);

            new_item->path = svn_dirent_join(item->path, relpath,
                                             result_pool);
            new_item->kind = kind;
            new_item->url = svn_path_url_add_component2(item->url, relpath,
                                                        result_pool);
            new_item->revision = SVN_INVALID_REVNUM;
            new_item->state_flags = SVN_CLIENT_COMMIT_ITEM_DELETE;
            new_item->incoming_prop_changes = apr_array_make(result_pool, 1,
                                                 sizeof(svn_prop_t *));

            APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *)
                  = new_item;
          }
        }
      }

  svn_pool_destroy(iterpool);
  return SVN_NO_ERROR;
}

/* Allocate and initialize the COMMITTABLES structure from POOL.
 */
static void
create_committables(svn_client__committables_t **committables,
                    apr_pool_t *pool)
{
  *committables = apr_palloc(pool, sizeof(**committables));

  (*committables)->by_repository = apr_hash_make(pool);
  (*committables)->by_path = apr_hash_make(pool);
}

svn_error_t *
svn_client__harvest_committables(svn_client__committables_t **committables,
                                 apr_hash_t **lock_tokens,
                                 const char *base_dir_abspath,
                                 const apr_array_header_t *targets,
                                 svn_depth_t depth,
                                 svn_boolean_t just_locked,
                                 const apr_array_header_t *changelists,
                                 svn_client__check_url_kind_t check_url_func,
                                 void *check_url_baton,
                                 svn_client_ctx_t *ctx,
                                 apr_pool_t *result_pool,
                                 apr_pool_t *scratch_pool)
{
  int i;
  apr_pool_t *iterpool = svn_pool_create(scratch_pool);
  apr_hash_t *changelist_hash = NULL;
  svn_wc_context_t *wc_ctx = ctx->wc_ctx;
  struct handle_descendants_baton hdb;
  apr_hash_index_t *hi;

  /* It's possible that one of the named targets has a parent that is
   * itself scheduled for addition or replacement -- that is, the
   * parent is not yet versioned in the repository.  This is okay, as
   * long as the parent itself is part of this same commit, either
   * directly, or by virtue of a grandparent, great-grandparent, etc,
   * being part of the commit.
   *
   * Since we don't know what's included in the commit until we've
   * harvested all the targets, we can't reliably check this as we
   * go.  So in `danglers', we record named targets whose parents
   * do not yet exist in the repository. Then after harvesting the total
   * commit group, we check to make sure those parents are included.
   *
   * Each key of danglers is a parent which does not exist in the
   * repository.  The (const char *) value is one of that parent's
   * children which is named as part of the commit; the child is
   * included only to make a better error message.
   *
   * (The reason we don't bother to check unnamed -- i.e, implicit --
   * targets is that they can only join the commit if their parents
   * did too, so this situation can't arise for them.)
   */
  apr_hash_t *danglers = apr_hash_make(scratch_pool);

  SVN_ERR_ASSERT(svn_dirent_is_absolute(base_dir_abspath));

  /* Create the COMMITTABLES structure. */
  create_committables(committables, result_pool);

  /* And the LOCK_TOKENS dito. */
  *lock_tokens = apr_hash_make(result_pool);

  /* If we have a list of changelists, convert that into a hash with
     changelist keys. */
  if (changelists && changelists->nelts)
    SVN_ERR(svn_hash_from_cstring_keys(&changelist_hash, changelists,
                                       scratch_pool));

  for (i = 0; i < targets->nelts; ++i)
    {
      const char *target_abspath;
      svn_node_kind_t kind;
      const char *repos_root_url;

      svn_pool_clear(iterpool);

      /* Add the relative portion to the base abspath.  */
      target_abspath = svn_dirent_join(base_dir_abspath,
                                       APR_ARRAY_IDX(targets, i, const char *),
                                       iterpool);

      SVN_ERR(svn_wc_read_kind(&kind, wc_ctx, target_abspath,
                               FALSE, /* show_hidden */
                               iterpool));
      if (kind == svn_node_none)
        {
          /* If a target of the commit is a tree-conflicted node that
           * has no entry (e.g. locally deleted), issue a proper tree-
           * conflicts error instead of a "not under version control". */
          const svn_wc_conflict_description2_t *conflict;
          SVN_ERR(svn_wc__get_tree_conflict(&conflict, wc_ctx, target_abspath,
                                            iterpool, iterpool));
          if (conflict != NULL)
            return svn_error_createf(
                       SVN_ERR_WC_FOUND_CONFLICT, NULL,
                       _("Aborting commit: '%s' remains in conflict"),
                       svn_dirent_local_style(conflict->local_abspath,
                                              iterpool));
          else
            return svn_error_createf(
                       SVN_ERR_ILLEGAL_TARGET, NULL,
                       _("'%s' is not under version control"),
                       svn_dirent_local_style(target_abspath, iterpool));
        }

      SVN_ERR(svn_wc__node_get_repos_info(&repos_root_url, NULL, wc_ctx,
                                          target_abspath,
                                          result_pool, iterpool));

      /* Handle our TARGET. */
      /* Make sure this isn't inside a working copy subtree that is
       * marked as tree-conflicted. */
      SVN_ERR(bail_on_tree_conflicted_ancestor(ctx->wc_ctx, target_abspath,
                                               ctx->notify_func2,
                                               ctx->notify_baton2,
                                               iterpool));

      SVN_ERR(harvest_committables(ctx->wc_ctx, target_abspath,
                                   *committables, *lock_tokens,
                                   repos_root_url,
                                   NULL /* COMMIT_RELPATH */,
                                   FALSE /* COPY_MODE_ROOT */,
                                   depth, just_locked, changelist_hash,
                                   FALSE, FALSE,
                                   danglers,
                                   check_url_func, check_url_baton,
                                   ctx->cancel_func, ctx->cancel_baton,
                                   ctx->notify_func2, ctx->notify_baton2,
                                   result_pool, iterpool));
    }

  hdb.wc_ctx = ctx->wc_ctx;
  hdb.cancel_func = ctx->cancel_func;
  hdb.cancel_baton = ctx->cancel_baton;
  hdb.check_url_func = check_url_func;
  hdb.check_url_baton = check_url_baton;

  SVN_ERR(svn_iter_apr_hash(NULL, (*committables)->by_repository,
                            handle_descendants, &hdb, iterpool));

  /* Make sure that every path in danglers is part of the commit. */
  for (hi = apr_hash_first(scratch_pool, danglers); hi; hi = apr_hash_next(hi))
    {
      const char *dangling_parent = svn__apr_hash_index_key(hi);

      svn_pool_clear(iterpool);

      if (! look_up_committable(*committables, dangling_parent, iterpool))
        {
          const char *dangling_child = svn__apr_hash_index_val(hi);

          if (ctx->notify_func2 != NULL)
            {
              svn_wc_notify_t *notify;

              notify = svn_wc_create_notify(dangling_child,
                                            svn_wc_notify_failed_no_parent,
                                            scratch_pool);

              ctx->notify_func2(ctx->notify_baton2, notify, iterpool);
            }

          return svn_error_createf(
                           SVN_ERR_ILLEGAL_TARGET, NULL,
                           _("'%s' is not known to exist in the repository "
                             "and is not part of the commit, "
                             "yet its child '%s' is part of the commit"),
                           /* Probably one or both of these is an entry, but
                              safest to local_stylize just in case. */
                           svn_dirent_local_style(dangling_parent, iterpool),
                           svn_dirent_local_style(dangling_child, iterpool));
        }
    }

  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}

struct copy_committables_baton
{
  svn_client_ctx_t *ctx;
  svn_client__committables_t *committables;
  apr_pool_t *result_pool;
  svn_client__check_url_kind_t check_url_func;
  void *check_url_baton;
};

static svn_error_t *
harvest_copy_committables(void *baton, void *item, apr_pool_t *pool)
{
  struct copy_committables_baton *btn = baton;
  svn_client__copy_pair_t *pair = *(svn_client__copy_pair_t **)item;
  const char *repos_root_url;
  const char *commit_relpath;
  struct handle_descendants_baton hdb;

  /* Read the entry for this SRC. */
  SVN_ERR_ASSERT(svn_dirent_is_absolute(pair->src_abspath_or_url));

  SVN_ERR(svn_wc__node_get_repos_info(&repos_root_url, NULL, btn->ctx->wc_ctx,
                                      pair->src_abspath_or_url,
                                      pool, pool));

  commit_relpath = svn_uri_skip_ancestor(repos_root_url,
                                         pair->dst_abspath_or_url, pool);

  /* Handle this SRC. */
  SVN_ERR(harvest_committables(btn->ctx->wc_ctx,
                               pair->src_abspath_or_url,
                               btn->committables, NULL,
                               repos_root_url,
                               commit_relpath,
                               TRUE,  /* COPY_MODE_ROOT */
                               svn_depth_infinity,
                               FALSE,  /* JUST_LOCKED */
                               NULL,
                               FALSE, FALSE, /* skip files, dirs */
                               NULL,
                               btn->check_url_func,
                               btn->check_url_baton,
                               btn->ctx->cancel_func,
                               btn->ctx->cancel_baton,
                               btn->ctx->notify_func2,
                               btn->ctx->notify_baton2,
                               btn->result_pool, pool));

  hdb.wc_ctx = btn->ctx->wc_ctx;
  hdb.cancel_func = btn->ctx->cancel_func;
  hdb.cancel_baton = btn->ctx->cancel_baton;
  hdb.check_url_func = btn->check_url_func;
  hdb.check_url_baton = btn->check_url_baton;

  SVN_ERR(svn_iter_apr_hash(NULL, btn->committables->by_repository,
                            handle_descendants, &hdb, pool));

  return SVN_NO_ERROR;
}



svn_error_t *
svn_client__get_copy_committables(svn_client__committables_t **committables,
                                  const apr_array_header_t *copy_pairs,
                                  svn_client__check_url_kind_t check_url_func,
                                  void *check_url_baton,
                                  svn_client_ctx_t *ctx,
                                  apr_pool_t *result_pool,
                                  apr_pool_t *scratch_pool)
{
  struct copy_committables_baton btn;

  /* Create the COMMITTABLES structure. */
  create_committables(committables, result_pool);

  btn.ctx = ctx;
  btn.committables = *committables;
  btn.result_pool = result_pool;

  btn.check_url_func = check_url_func;
  btn.check_url_baton = check_url_baton;

  /* For each copy pair, harvest the committables for that pair into the
     committables hash. */
  return svn_iter_apr_array(NULL, copy_pairs,
                            harvest_copy_committables, &btn, scratch_pool);
}


int svn_client__sort_commit_item_urls(const void *a, const void *b)
{
  const svn_client_commit_item3_t *item1
    = *((const svn_client_commit_item3_t * const *) a);
  const svn_client_commit_item3_t *item2
    = *((const svn_client_commit_item3_t * const *) b);
  return svn_path_compare_paths(item1->url, item2->url);
}



svn_error_t *
svn_client__condense_commit_items(const char **base_url,
                                  apr_array_header_t *commit_items,
                                  apr_pool_t *pool)
{
  apr_array_header_t *ci = commit_items; /* convenience */
  const char *url;
  svn_client_commit_item3_t *item, *last_item = NULL;
  int i;

  SVN_ERR_ASSERT(ci && ci->nelts);

  /* Sort our commit items by their URLs. */
  qsort(ci->elts, ci->nelts,
        ci->elt_size, svn_client__sort_commit_item_urls);

  /* Loop through the URLs, finding the longest usable ancestor common
     to all of them, and making sure there are no duplicate URLs.  */
  for (i = 0; i < ci->nelts; i++)
    {
      item = APR_ARRAY_IDX(ci, i, svn_client_commit_item3_t *);
      url = item->url;

      if ((last_item) && (strcmp(last_item->url, url) == 0))
        return svn_error_createf
          (SVN_ERR_CLIENT_DUPLICATE_COMMIT_URL, NULL,
           _("Cannot commit both '%s' and '%s' as they refer to the same URL"),
           svn_dirent_local_style(item->path, pool),
           svn_dirent_local_style(last_item->path, pool));

      /* In the first iteration, our BASE_URL is just our only
         encountered commit URL to date.  After that, we find the
         longest ancestor between the current BASE_URL and the current
         commit URL.  */
      if (i == 0)
        *base_url = apr_pstrdup(pool, url);
      else
        *base_url = svn_uri_get_longest_ancestor(*base_url, url, pool);

      /* If our BASE_URL is itself a to-be-committed item, and it is
         anything other than an already-versioned directory with
         property mods, we'll call its parent directory URL the
         BASE_URL.  Why?  Because we can't have a file URL as our base
         -- period -- and all other directory operations (removal,
         addition, etc.) require that we open that directory's parent
         dir first.  */
      /* ### I don't understand the strlen()s here, hmmm.  -kff */
      if ((strlen(*base_url) == strlen(url))
          && (! ((item->kind == svn_node_dir)
                 && item->state_flags == SVN_CLIENT_COMMIT_ITEM_PROP_MODS)))
        *base_url = svn_uri_dirname(*base_url, pool);

      /* Stash our item here for the next iteration. */
      last_item = item;
    }

  /* Now that we've settled on a *BASE_URL, go hack that base off
     of all of our URLs and store it as session_relpath. */
  for (i = 0; i < ci->nelts; i++)
    {
      svn_client_commit_item3_t *this_item
        = APR_ARRAY_IDX(ci, i, svn_client_commit_item3_t *);
      size_t url_len = strlen(this_item->url);
      size_t base_url_len = strlen(*base_url);

      if (url_len > base_url_len)
        this_item->session_relpath = svn_uri__is_child(*base_url,
                                                       this_item->url, pool);
      else
        this_item->session_relpath = "";
    }
#ifdef SVN_CLIENT_COMMIT_DEBUG
  /* ### TEMPORARY CODE ### */
  SVN_DBG(("COMMITTABLES: (base URL=%s)\n", *base_url));
  SVN_DBG(("   FLAGS     REV  REL-URL (COPY-URL)\n"));
  for (i = 0; i < ci->nelts; i++)
    {
      svn_client_commit_item3_t *this_item
        = APR_ARRAY_IDX(ci, i, svn_client_commit_item3_t *);
      char flags[6];
      flags[0] = (this_item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
                   ? 'a' : '-';
      flags[1] = (this_item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
                   ? 'd' : '-';
      flags[2] = (this_item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS)
                   ? 't' : '-';
      flags[3] = (this_item->state_flags & SVN_CLIENT_COMMIT_ITEM_PROP_MODS)
                   ? 'p' : '-';
      flags[4] = (this_item->state_flags & SVN_CLIENT_COMMIT_ITEM_IS_COPY)
                   ? 'c' : '-';
      flags[5] = '\0';
      SVN_DBG(("   %s  %6ld  '%s' (%s)\n",
               flags,
               this_item->revision,
               this_item->url ? this_item->url : "",
               this_item->copyfrom_url ? this_item->copyfrom_url : "none"));
    }
#endif /* SVN_CLIENT_COMMIT_DEBUG */

  return SVN_NO_ERROR;
}


struct file_mod_t
{
  const svn_client_commit_item3_t *item;
  void *file_baton;
};


/* A baton for use while driving a path-based editor driver for commit */
struct item_commit_baton
{
  const svn_delta_editor_t *editor;    /* commit editor */
  void *edit_baton;                    /* commit editor's baton */
  apr_hash_t *file_mods;               /* hash: path->file_mod_t */
  const char *notify_path_prefix;      /* notification path prefix
                                          (NULL is okay, else abs path) */
  svn_client_ctx_t *ctx;               /* client context baton */
  apr_hash_t *commit_items;            /* the committables */
  const char *base_url;                /* The session url for the commit */
};


/* Drive CALLBACK_BATON->editor with the change described by the item in
 * CALLBACK_BATON->commit_items that is keyed by PATH.  If the change
 * includes a text mod, however, call the editor's file_open() function
 * but do not send the text mod to the editor; instead, add a mapping of
 * "item-url => (commit-item, file-baton)" into CALLBACK_BATON->file_mods.
 *
 * Before driving the editor, call the cancellation and notification
 * callbacks in CALLBACK_BATON->ctx, if present.
 *
 * This implements svn_delta_path_driver_cb_func_t. */
static svn_error_t *
do_item_commit(void **dir_baton,
               void *parent_baton,
               void *callback_baton,
               const char *path,
               apr_pool_t *pool)
{
  struct item_commit_baton *icb = callback_baton;
  const svn_client_commit_item3_t *item = apr_hash_get(icb->commit_items,
                                                       path,
                                                       APR_HASH_KEY_STRING);
  svn_node_kind_t kind = item->kind;
  void *file_baton = NULL;
  apr_pool_t *file_pool = NULL;
  const svn_delta_editor_t *editor = icb->editor;
  apr_hash_t *file_mods = icb->file_mods;
  svn_client_ctx_t *ctx = icb->ctx;
  svn_error_t *err;
  const char *local_abspath = NULL;

  /* Do some initializations. */
  *dir_baton = NULL;
  if (item->kind != svn_node_none && item->path)
    {
      /* We always get an absolute path, see svn_client_commit_item3_t. */
      SVN_ERR_ASSERT(svn_dirent_is_absolute(item->path));
      local_abspath = item->path;
    }

  /* If this is a file with textual mods, we'll be keeping its baton
     around until the end of the commit.  So just lump its memory into
     a single, big, all-the-file-batons-in-here pool.  Otherwise, we
     can just use POOL, and trust our caller to clean that mess up. */
  if ((kind == svn_node_file)
      && (item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS))
    file_pool = apr_hash_pool_get(file_mods);
  else
    file_pool = pool;

  /* Call the cancellation function. */
  if (ctx->cancel_func)
    SVN_ERR(ctx->cancel_func(ctx->cancel_baton));

  /* Validation. */
  if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_IS_COPY)
    {
      if (! item->copyfrom_url)
        return svn_error_createf
          (SVN_ERR_BAD_URL, NULL,
           _("Commit item '%s' has copy flag but no copyfrom URL"),
           svn_dirent_local_style(path, pool));
      if (! SVN_IS_VALID_REVNUM(item->copyfrom_rev))
        return svn_error_createf
          (SVN_ERR_CLIENT_BAD_REVISION, NULL,
           _("Commit item '%s' has copy flag but an invalid revision"),
           svn_dirent_local_style(path, pool));
    }

  /* If a feedback table was supplied by the application layer,
     describe what we're about to do to this item. */
  if (ctx->notify_func2 && item->path)
    {
      const char *npath = item->path;
      svn_wc_notify_t *notify;

      if ((item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
          && (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD))
        {
          /* We don't print the "(bin)" notice for binary files when
             replacing, only when adding.  So we don't bother to get
             the mime-type here. */
          if (item->copyfrom_url)
            notify = svn_wc_create_notify(npath,
                                          svn_wc_notify_commit_copied_replaced,
                                          pool);
          else
            notify = svn_wc_create_notify(npath, svn_wc_notify_commit_replaced,
                                          pool);

        }
      else if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
        {
          notify = svn_wc_create_notify(npath, svn_wc_notify_commit_deleted,
                                        pool);
        }
      else if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
        {
          if (item->copyfrom_url)
            notify = svn_wc_create_notify(npath, svn_wc_notify_commit_copied,
                                          pool);
          else
            notify = svn_wc_create_notify(npath, svn_wc_notify_commit_added,
                                          pool);

          if (item->kind == svn_node_file)
            {
              const svn_string_t *propval;

              SVN_ERR(svn_wc_prop_get2(&propval, ctx->wc_ctx, local_abspath,
                                       SVN_PROP_MIME_TYPE, pool, pool));

              if (propval)
                notify->mime_type = propval->data;
            }
        }
      else if ((item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS)
               || (item->state_flags & SVN_CLIENT_COMMIT_ITEM_PROP_MODS))
        {
          notify = svn_wc_create_notify(npath, svn_wc_notify_commit_modified,
                                        pool);
          if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS)
            notify->content_state = svn_wc_notify_state_changed;
          else
            notify->content_state = svn_wc_notify_state_unchanged;
          if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_PROP_MODS)
            notify->prop_state = svn_wc_notify_state_changed;
          else
            notify->prop_state = svn_wc_notify_state_unchanged;
        }
      else
        notify = NULL;

      if (notify)
        {
          notify->kind = item->kind;
          notify->path_prefix = icb->notify_path_prefix;
          (*ctx->notify_func2)(ctx->notify_baton2, notify, pool);
        }
    }

  /* If this item is supposed to be deleted, do so. */
  if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
    {
      SVN_ERR_ASSERT(parent_baton);
      err = editor->delete_entry(path, item->revision,
                                 parent_baton, pool);

      if (err)
        return svn_error_trace(fixup_commit_error(local_abspath,
                                                  icb->base_url,
                                                  path, item->kind,
                                                  err, ctx, pool));
    }

  /* If this item is supposed to be added, do so. */
  if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
    {
      if (kind == svn_node_file)
        {
          SVN_ERR_ASSERT(parent_baton);
          err = editor->add_file(
                   path, parent_baton, item->copyfrom_url,
                   item->copyfrom_url ? item->copyfrom_rev : SVN_INVALID_REVNUM,
                   file_pool, &file_baton);
        }
      else /* May be svn_node_none when adding parent dirs for a copy. */
        {
          SVN_ERR_ASSERT(parent_baton);
          err = editor->add_directory(
                   path, parent_baton, item->copyfrom_url,
                   item->copyfrom_url ? item->copyfrom_rev : SVN_INVALID_REVNUM,
                   pool, dir_baton);
        }

      if (err)
        return svn_error_trace(fixup_commit_error(local_abspath,
                                                  icb->base_url,
                                                  path, kind, err,
                                                  ctx, pool));

      /* Set other prop-changes, if available in the baton */
      if (item->outgoing_prop_changes)
        {
          svn_prop_t *prop;
          apr_array_header_t *prop_changes = item->outgoing_prop_changes;
          int ctr;
          for (ctr = 0; ctr < prop_changes->nelts; ctr++)
            {
              prop = APR_ARRAY_IDX(prop_changes, ctr, svn_prop_t *);
              if (kind == svn_node_file)
                {
                  editor->change_file_prop(file_baton, prop->name,
                                           prop->value, pool);
                }
              else
                {
                  editor->change_dir_prop(*dir_baton, prop->name,
                                          prop->value, pool);
                }
            }
        }
    }

  /* Now handle property mods. */
  if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_PROP_MODS)
    {
      if (kind == svn_node_file)
        {
          if (! file_baton)
            {
              SVN_ERR_ASSERT(parent_baton);
              err = editor->open_file(path, parent_baton,
                                      item->revision,
                                      file_pool, &file_baton);

              if (err)
                return svn_error_trace(fixup_commit_error(local_abspath,
                                                          icb->base_url,
                                                          path, kind,
                                                          err, ctx,
                                                          pool));
            }
        }
      else
        {
          if (! *dir_baton)
            {
              if (! parent_baton)
                {
                  err = editor->open_root(icb->edit_baton, item->revision,
                                          pool, dir_baton);
                }
              else
                {
                  err = editor->open_directory(path, parent_baton,
                                               item->revision,
                                               pool, dir_baton);
                }

              if (err)
                return svn_error_trace(fixup_commit_error(local_abspath,
                                                          icb->base_url,
                                                          path, kind,
                                                          err, ctx,
                                                          pool));
            }
        }

      /* When committing a directory that no longer exists in the
         repository, a "not found" error does not occur immediately
         upon opening the directory.  It appears here during the delta
         transmisssion. */
      err = svn_wc_transmit_prop_deltas2(
              ctx->wc_ctx, local_abspath, editor,
              (kind == svn_node_dir) ? *dir_baton : file_baton, pool);

      if (err)
        return svn_error_trace(fixup_commit_error(local_abspath,
                                                  icb->base_url,
                                                  path, kind, err,
                                                  ctx, pool));

      /* Make any additional client -> repository prop changes. */
      if (item->outgoing_prop_changes)
        {
          svn_prop_t *prop;
          int i;

          for (i = 0; i < item->outgoing_prop_changes->nelts; i++)
            {
              prop = APR_ARRAY_IDX(item->outgoing_prop_changes, i,
                                   svn_prop_t *);
              if (kind == svn_node_file)
                {
                  editor->change_file_prop(file_baton, prop->name,
                                           prop->value, pool);
                }
              else
                {
                  editor->change_dir_prop(*dir_baton, prop->name,
                                          prop->value, pool);
                }
            }
        }
    }

  /* Finally, handle text mods (in that we need to open a file if it
     hasn't already been opened, and we need to put the file baton in
     our FILES hash). */
  if ((kind == svn_node_file)
      && (item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS))
    {
      struct file_mod_t *mod = apr_palloc(file_pool, sizeof(*mod));

      if (! file_baton)
        {
          SVN_ERR_ASSERT(parent_baton);
          err = editor->open_file(path, parent_baton,
                                    item->revision,
                                    file_pool, &file_baton);

          if (err)
            return svn_error_trace(fixup_commit_error(local_abspath,
                                                      icb->base_url,
                                                      path, kind,
                                                      err, ctx, pool));
        }

      /* Add this file mod to the FILE_MODS hash. */
      mod->item = item;
      mod->file_baton = file_baton;
      apr_hash_set(file_mods, item->session_relpath, APR_HASH_KEY_STRING, mod);
    }
  else if (file_baton)
    {
      /* Close any outstanding file batons that didn't get caught by
         the "has local mods" conditional above. */
      err = editor->close_file(file_baton, NULL, file_pool);

      if (err)
        return svn_error_trace(fixup_commit_error(local_abspath,
                                                  icb->base_url,
                                                  path, kind,
                                                  err, ctx, pool));
    }

  return SVN_NO_ERROR;
}


#ifdef SVN_CLIENT_COMMIT_DEBUG
/* Prototype for function below */
static svn_error_t *get_test_editor(const svn_delta_editor_t **editor,
                                    void **edit_baton,
                                    const svn_delta_editor_t *real_editor,
                                    void *real_eb,
                                    const char *base_url,
                                    apr_pool_t *pool);
#endif /* SVN_CLIENT_COMMIT_DEBUG */

svn_error_t *
svn_client__do_commit(const char *base_url,
                      const apr_array_header_t *commit_items,
                      const svn_delta_editor_t *editor,
                      void *edit_baton,
                      const char *notify_path_prefix,
                      apr_hash_t **md5_checksums,
                      apr_hash_t **sha1_checksums,
                      svn_client_ctx_t *ctx,
                      apr_pool_t *result_pool,
                      apr_pool_t *scratch_pool)
{
  apr_hash_t *file_mods = apr_hash_make(scratch_pool);
  apr_hash_t *items_hash = apr_hash_make(scratch_pool);
  apr_pool_t *iterpool = svn_pool_create(scratch_pool);
  apr_hash_index_t *hi;
  int i;
  struct item_commit_baton cb_baton;
  apr_array_header_t *paths =
    apr_array_make(scratch_pool, commit_items->nelts, sizeof(const char *));

#ifdef SVN_CLIENT_COMMIT_DEBUG
  {
    SVN_ERR(get_test_editor(&editor, &edit_baton,
                            editor, edit_baton,
                            base_url, scratch_pool));
  }
#endif /* SVN_CLIENT_COMMIT_DEBUG */

  /* Ditto for the checksums. */
  if (md5_checksums)
    *md5_checksums = apr_hash_make(result_pool);
  if (sha1_checksums)
    *sha1_checksums = apr_hash_make(result_pool);

  /* Build a hash from our COMMIT_ITEMS array, keyed on the
     relative paths (which come from the item URLs).  And
     keep an array of those decoded paths, too.  */
  for (i = 0; i < commit_items->nelts; i++)
    {
      svn_client_commit_item3_t *item =
        APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *);
      const char *path = item->session_relpath;
      apr_hash_set(items_hash, path, APR_HASH_KEY_STRING, item);
      APR_ARRAY_PUSH(paths, const char *) = path;
    }

  /* Setup the callback baton. */
  cb_baton.editor = editor;
  cb_baton.edit_baton = edit_baton;
  cb_baton.file_mods = file_mods;
  cb_baton.notify_path_prefix = notify_path_prefix;
  cb_baton.ctx = ctx;
  cb_baton.commit_items = items_hash;
  cb_baton.base_url = base_url;

  /* Drive the commit editor! */
  SVN_ERR(svn_delta_path_driver(editor, edit_baton, SVN_INVALID_REVNUM,
                                paths, do_item_commit, &cb_baton,
                                scratch_pool));

  /* Transmit outstanding text deltas. */
  for (hi = apr_hash_first(scratch_pool, file_mods);
       hi;
       hi = apr_hash_next(hi))
    {
      struct file_mod_t *mod = svn__apr_hash_index_val(hi);
      const svn_client_commit_item3_t *item = mod->item;
      const svn_checksum_t *new_text_base_md5_checksum;
      const svn_checksum_t *new_text_base_sha1_checksum;
      svn_boolean_t fulltext = FALSE;
      svn_error_t *err;

      svn_pool_clear(iterpool);

      /* Transmit the entry. */
      if (ctx->cancel_func)
        SVN_ERR(ctx->cancel_func(ctx->cancel_baton));

      if (ctx->notify_func2)
        {
          svn_wc_notify_t *notify;
          notify = svn_wc_create_notify(item->path,
                                        svn_wc_notify_commit_postfix_txdelta,
                                        iterpool);
          notify->kind = svn_node_file;
          notify->path_prefix = notify_path_prefix;
          ctx->notify_func2(ctx->notify_baton2, notify, iterpool);
        }

      /* If the node has no history, transmit full text */
      if ((item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
          && ! (item->state_flags & SVN_CLIENT_COMMIT_ITEM_IS_COPY))
        fulltext = TRUE;

      err = svn_wc_transmit_text_deltas3(&new_text_base_md5_checksum,
                                         &new_text_base_sha1_checksum,
                                         ctx->wc_ctx, item->path,
                                         fulltext, editor, mod->file_baton,
                                         result_pool, iterpool);

      if (err)
        {
          svn_pool_destroy(iterpool); /* Close tempfiles */
          return svn_error_trace(fixup_commit_error(item->path,
                                                    base_url,
                                                    item->session_relpath,
                                                    svn_node_file,
                                                    err, ctx, scratch_pool));
        }

      if (md5_checksums)
        apr_hash_set(*md5_checksums, item->path, APR_HASH_KEY_STRING,
                     new_text_base_md5_checksum);
      if (sha1_checksums)
        apr_hash_set(*sha1_checksums, item->path, APR_HASH_KEY_STRING,
                     new_text_base_sha1_checksum);
    }

  svn_pool_destroy(iterpool);

  /* Close the edit. */
  return svn_error_trace(editor->close_edit(edit_baton, scratch_pool));
}


#ifdef SVN_CLIENT_COMMIT_DEBUG

/*** Temporary test editor ***/

struct edit_baton
{
  const char *path;

  const svn_delta_editor_t *real_editor;
  void *real_eb;
};

struct item_baton
{
  struct edit_baton *eb;
  void *real_baton;

  const char *path;
};

static struct item_baton *
make_baton(struct edit_baton *eb,
           void *real_baton,
           const char *path,
           apr_pool_t *pool)
{
  struct item_baton *new_baton = apr_pcalloc(pool, sizeof(*new_baton));
  new_baton->eb = eb;
  new_baton->real_baton = real_baton;
  new_baton->path = apr_pstrdup(pool, path);
  return new_baton;
}

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;
  return (*eb->real_editor->set_target_revision)(eb->real_eb,
                                                 target_revision,
                                                 pool);
}

static svn_error_t *
open_root(void *edit_baton,
          svn_revnum_t base_revision,
          apr_pool_t *dir_pool,
          void **root_baton)
{
  struct edit_baton *eb = edit_baton;
  struct item_baton *new_baton = make_baton(eb, NULL, eb->path, dir_pool);
  fprintf(stderr, "TEST EDIT STARTED (base URL=%s)\n", eb->path);
  *root_baton = new_baton;
  return (*eb->real_editor->open_root)(eb->real_eb,
                                       base_revision,
                                       dir_pool,
                                       &new_baton->real_baton);
}

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 **baton)
{
  struct item_baton *db = parent_baton;
  struct item_baton *new_baton = make_baton(db->eb, NULL, path, pool);
  const char *copystuffs = "";
  if (copyfrom_path && SVN_IS_VALID_REVNUM(copyfrom_revision))
    copystuffs = apr_psprintf(pool,
                              " (copied from %s:%ld)",
                              copyfrom_path,
                              copyfrom_revision);
  fprintf(stderr, "   Adding  : %s%s\n", path, copystuffs);
  *baton = new_baton;
  return (*db->eb->real_editor->add_file)(path, db->real_baton,
                                          copyfrom_path, copyfrom_revision,
                                          pool, &new_baton->real_baton);
}

static svn_error_t *
delete_entry(const char *path,
             svn_revnum_t revision,
             void *parent_baton,
             apr_pool_t *pool)
{
  struct item_baton *db = parent_baton;
  fprintf(stderr, "   Deleting: %s\n", path);
  return (*db->eb->real_editor->delete_entry)(path, revision,
                                              db->real_baton, pool);
}

static svn_error_t *
open_file(const char *path,
          void *parent_baton,
          svn_revnum_t base_revision,
          apr_pool_t *pool,
          void **baton)
{
  struct item_baton *db = parent_baton;
  struct item_baton *new_baton = make_baton(db->eb, NULL, path, pool);
  fprintf(stderr, "   Opening : %s\n", path);
  *baton = new_baton;
  return (*db->eb->real_editor->open_file)(path, db->real_baton,
                                           base_revision, pool,
                                           &new_baton->real_baton);
}

static svn_error_t *
close_file(void *baton, const char *text_checksum, apr_pool_t *pool)
{
  struct item_baton *fb = baton;
  fprintf(stderr, "   Closing : %s\n", fb->path);
  return (*fb->eb->real_editor->close_file)(fb->real_baton,
                                            text_checksum, pool);
}


static svn_error_t *
change_file_prop(void *file_baton,
                 const char *name,
                 const svn_string_t *value,
                 apr_pool_t *pool)
{
  struct item_baton *fb = file_baton;
  fprintf(stderr, "      PropSet (%s=%s)\n", name, value ? value->data : "");
  return (*fb->eb->real_editor->change_file_prop)(fb->real_baton,
                                                  name, value, pool);
}

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 item_baton *fb = file_baton;
  fprintf(stderr, "      Transmitting text...\n");
  return (*fb->eb->real_editor->apply_textdelta)(fb->real_baton,
                                                 base_checksum, pool,
                                                 handler, handler_baton);
}

static svn_error_t *
close_edit(void *edit_baton, apr_pool_t *pool)
{
  struct edit_baton *eb = edit_baton;
  fprintf(stderr, "TEST EDIT COMPLETED\n");
  return (*eb->real_editor->close_edit)(eb->real_eb, pool);
}

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 **baton)
{
  struct item_baton *db = parent_baton;
  struct item_baton *new_baton = make_baton(db->eb, NULL, path, pool);
  const char *copystuffs = "";
  if (copyfrom_path && SVN_IS_VALID_REVNUM(copyfrom_revision))
    copystuffs = apr_psprintf(pool,
                              " (copied from %s:%ld)",
                              copyfrom_path,
                              copyfrom_revision);
  fprintf(stderr, "   Adding  : %s%s\n", path, copystuffs);
  *baton = new_baton;
  return (*db->eb->real_editor->add_directory)(path,
                                               db->real_baton,
                                               copyfrom_path,
                                               copyfrom_revision,
                                               pool,
                                               &new_baton->real_baton);
}

static svn_error_t *
open_directory(const char *path,
               void *parent_baton,
               svn_revnum_t base_revision,
               apr_pool_t *pool,
               void **baton)
{
  struct item_baton *db = parent_baton;
  struct item_baton *new_baton = make_baton(db->eb, NULL, path, pool);
  fprintf(stderr, "   Opening : %s\n", path);
  *baton = new_baton;
  return (*db->eb->real_editor->open_directory)(path, db->real_baton,
                                                base_revision, pool,
                                                &new_baton->real_baton);
}

static svn_error_t *
change_dir_prop(void *dir_baton,
                const char *name,
                const svn_string_t *value,
                apr_pool_t *pool)
{
  struct item_baton *db = dir_baton;
  fprintf(stderr, "      PropSet (%s=%s)\n", name, value ? value->data : "");
  return (*db->eb->real_editor->change_dir_prop)(db->real_baton,
                                                 name, value, pool);
}

static svn_error_t *
close_directory(void *baton, apr_pool_t *pool)
{
  struct item_baton *db = baton;
  fprintf(stderr, "   Closing : %s\n", db->path);
  return (*db->eb->real_editor->close_directory)(db->real_baton, pool);
}

static svn_error_t *
abort_edit(void *edit_baton, apr_pool_t *pool)
{
  struct edit_baton *eb = edit_baton;
  fprintf(stderr, "TEST EDIT ABORTED\n");
  return (*eb->real_editor->abort_edit)(eb->real_eb, pool);
}

static svn_error_t *
get_test_editor(const svn_delta_editor_t **editor,
                void **edit_baton,
                const svn_delta_editor_t *real_editor,
                void *real_eb,
                const char *base_url,
                apr_pool_t *pool)
{
  svn_delta_editor_t *ed = svn_delta_default_editor(pool);
  struct edit_baton *eb = apr_pcalloc(pool, sizeof(*eb));

  eb->path = apr_pstrdup(pool, base_url);
  eb->real_editor = real_editor;
  eb->real_eb = real_eb;

  /* We don't implement absent_file() or absent_directory() in this
     editor, because presumably commit would never send that. */
  ed->set_target_revision = set_target_revision;
  ed->open_root = open_root;
  ed->add_directory = add_directory;
  ed->open_directory = open_directory;
  ed->close_directory = close_directory;
  ed->add_file = add_file;
  ed->open_file = open_file;
  ed->close_file = close_file;
  ed->delete_entry = delete_entry;
  ed->apply_textdelta = apply_textdelta;
  ed->change_dir_prop = change_dir_prop;
  ed->change_file_prop = change_file_prop;
  ed->close_edit = close_edit;
  ed->abort_edit = abort_edit;

  *editor = ed;
  *edit_baton = eb;
  return SVN_NO_ERROR;
}
#endif /* SVN_CLIENT_COMMIT_DEBUG */

svn_error_t *
svn_client__get_log_msg(const char **log_msg,
                        const char **tmp_file,
                        const apr_array_header_t *commit_items,
                        svn_client_ctx_t *ctx,
                        apr_pool_t *pool)
{
  if (ctx->log_msg_func3)
    {
      /* The client provided a callback function for the current API.
         Forward the call to it directly. */
      return (*ctx->log_msg_func3)(log_msg, tmp_file, commit_items,
                                   ctx->log_msg_baton3, pool);
    }
  else if (ctx->log_msg_func2 || ctx->log_msg_func)
    {
      /* The client provided a pre-1.5 (or pre-1.3) API callback
         function.  Convert the commit_items list to the appropriate
         type, and forward call to it. */
      svn_error_t *err;
      apr_pool_t *scratch_pool = svn_pool_create(pool);
      apr_array_header_t *old_commit_items =
        apr_array_make(scratch_pool, commit_items->nelts, sizeof(void*));

      int i;
      for (i = 0; i < commit_items->nelts; i++)
        {
          svn_client_commit_item3_t *item =
            APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *);

          if (ctx->log_msg_func2)
            {
              svn_client_commit_item2_t *old_item =
                apr_pcalloc(scratch_pool, sizeof(*old_item));

              old_item->path = item->path;
              old_item->kind = item->kind;
              old_item->url = item->url;
              old_item->revision = item->revision;
              old_item->copyfrom_url = item->copyfrom_url;
              old_item->copyfrom_rev = item->copyfrom_rev;
              old_item->state_flags = item->state_flags;
              old_item->wcprop_changes = item->incoming_prop_changes;

              APR_ARRAY_PUSH(old_commit_items, svn_client_commit_item2_t *) =
                old_item;
            }
          else /* ctx->log_msg_func */
            {
              svn_client_commit_item_t *old_item =
                apr_pcalloc(scratch_pool, sizeof(*old_item));

              old_item->path = item->path;
              old_item->kind = item->kind;
              old_item->url = item->url;
              /* The pre-1.3 API used the revision field for copyfrom_rev
                 and revision depeding of copyfrom_url. */
              old_item->revision = item->copyfrom_url ?
                item->copyfrom_rev : item->revision;
              old_item->copyfrom_url = item->copyfrom_url;
              old_item->state_flags = item->state_flags;
              old_item->wcprop_changes = item->incoming_prop_changes;

              APR_ARRAY_PUSH(old_commit_items, svn_client_commit_item_t *) =
                old_item;
            }
        }

      if (ctx->log_msg_func2)
        err = (*ctx->log_msg_func2)(log_msg, tmp_file, old_commit_items,
                                    ctx->log_msg_baton2, pool);
      else
        err = (*ctx->log_msg_func)(log_msg, tmp_file, old_commit_items,
                                   ctx->log_msg_baton, pool);
      svn_pool_destroy(scratch_pool);
      return err;
    }
  else
    {
      /* No log message callback was provided by the client. */
      *log_msg = "";
      *tmp_file = NULL;
      return SVN_NO_ERROR;
    }
}

svn_error_t *
svn_client__ensure_revprop_table(apr_hash_t **revprop_table_out,
                                 const apr_hash_t *revprop_table_in,
                                 const char *log_msg,
                                 svn_client_ctx_t *ctx,
                                 apr_pool_t *pool)
{
  apr_hash_t *new_revprop_table;
  if (revprop_table_in)
    {
      if (svn_prop_has_svn_prop(revprop_table_in, pool))
        return svn_error_create(SVN_ERR_CLIENT_PROPERTY_NAME, NULL,
                                _("Standard properties can't be set "
                                  "explicitly as revision properties"));
      new_revprop_table = apr_hash_copy(pool, revprop_table_in);
    }
  else
    {
      new_revprop_table = apr_hash_make(pool);
    }
  apr_hash_set(new_revprop_table, SVN_PROP_REVISION_LOG, APR_HASH_KEY_STRING,
               svn_string_create(log_msg, pool));
  *revprop_table_out = new_revprop_table;
  return SVN_NO_ERROR;
}