<commit_before>/*
 * 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 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.

   IS_EXPLICIT_TARGET should always be passed as TRUE, except when
   harvest_committables() calls itself in recursion. This provides a way to
   tell whether LOCAL_ABSPATH was an original target or whether it was reached
   by recursing deeper into a dir target. (This is used to skip all file
   externals that aren't explicit commit targets.)

   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,
                     svn_boolean_t is_explicit_target,
                     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));
    }

  /* Handle file externals.
   * (IS_UPDATE_ROOT is more generally defined, but at the moment this
   * condition matches only file externals.)
   *
   * Don't copy files that svn:externals brought into the WC. So in copy_mode,
   * even explicit targets are skipped.
   *
   * Exclude file externals from recursion. Hande file externals only when
   * passed as explicit target. Note that svn_client_commit6() passes all
   * committable externals in as explicit targets iff they count.
   *
   * Also note that dir externals will never be reached recursively by this
   * function, since svn_wc__node_get_children_of_working_node() (used below
   * to recurse) does not return switched subdirs. */
  if (is_update_root
      && db_kind == svn_node_file
      && (copy_mode
          || ! is_explicit_target))
    {
      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 && is_explicit_target && 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)
        {
          /* The op-root of the parent must be part of the commit */
          const char *copy_root_abspath;
          svn_boolean_t parent_is_copy;

          /* Copies are always committed recursively as long as the
           * copy root is in the commit target list.
           * So for nodes copied along with a parent, the copy root path
           * is the dangling parent. 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;

          /* Copy the parent and target into pool; iterpool
             lasts only for this loop iteration, and we check
             danglers after the loop is over. */
          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,<commit_msg>* subversion/libsvn_client/commit_util.c   (harvest_commitables): Tweak a comment for clarity.<commit_after>/*
 * 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 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.

   IS_EXPLICIT_TARGET should always be passed as TRUE, except when
   harvest_committables() calls itself in recursion. This provides a way to
   tell whether LOCAL_ABSPATH was an original target or whether it was reached
   by recursing deeper into a dir target. (This is used to skip all file
   externals that aren't explicit commit targets.)

   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,
                     svn_boolean_t is_explicit_target,
                     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));
    }

  /* Handle file externals.
   * (IS_UPDATE_ROOT is more generally defined, but at the moment this
   * condition matches only file externals.)
   *
   * Don't copy files that svn:externals brought into the WC. So in copy_mode,
   * even explicit targets are skipped.
   *
   * Exclude file externals from recursion. Hande file externals only when
   * passed as explicit target. Note that svn_client_commit6() passes all
   * committable externals in as explicit targets iff they count.
   *
   * Also note that dir externals will never be reached recursively by this
   * function, since svn_wc__node_get_children_of_working_node() (used below
   * to recurse) does not return switched subdirs. */
  if (is_update_root
      && db_kind == svn_node_file
      && (copy_mode
          || ! is_explicit_target))
    {
      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 && is_explicit_target && 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;

          /* Copy the parent and target into pool; iterpool
             lasts only for this loop iteration, and we check
             danglers after the loop is over. */
          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,
{'fraction_non_alphanumeric': 0.07143900444028979, 'fraction_numerical': 0.0014898340733816313, 'mean_word_length': 1.7002563596923683, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 91, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 334, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '960936', 'n_tokens_mistral': 19995, 'n_tokens_neox': 19529, 'n_words': 4745}