/*
 * mtcc.c -- Multi Command Context implementation. This allows
 *           performing many operations without a working copy.
 *
 * ====================================================================
 *    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 "svn_dirent_uri.h"
#include "svn_hash.h"
#include "svn_path.h"
#include "svn_props.h"
#include "svn_pools.h"
#include "svn_subst.h"

#include "private/svn_client_mtcc.h"


#include "svn_private_config.h"

#include "client.h"

#include <assert.h>

#define SVN_PATH_IS_EMPTY(s) ((s)[0] == '\0')

/* The kind of operation to perform in an mtcc_op_t */
typedef enum mtcc_kind_t
{
  OP_OPEN_DIR,
  OP_OPEN_FILE,
  OP_ADD_DIR,
  OP_ADD_FILE,
  OP_DELETE
} mtcc_kind_t;

typedef struct mtcc_op_t
{
  const char *name;                 /* basename of operation */
  mtcc_kind_t kind;                 /* editor operation */

  apr_array_header_t *children;     /* List of mtcc_op_t * */

  const char *src_relpath;              /* For ADD_DIR, ADD_FILE */
  svn_revnum_t src_rev;                 /* For ADD_DIR, ADD_FILE */
  svn_stream_t *src_stream;             /* For ADD_FILE, OPEN_FILE */
  svn_checksum_t *src_checksum;         /* For ADD_FILE, OPEN_FILE */
  svn_stream_t *base_stream;            /* For ADD_FILE, OPEN_FILE */
  const svn_checksum_t *base_checksum;  /* For ADD_FILE, OPEN_FILE */

  apr_array_header_t *prop_mods;        /* For all except DELETE
                                           List of svn_prop_t */

  svn_boolean_t performed_stat;         /* Verified kind with repository */
} mtcc_op_t;

/* Check if the mtcc doesn't contain any modifications yet */
#define MTCC_UNMODIFIED(mtcc)                                               \
    ((mtcc->root_op->kind == OP_OPEN_DIR                                    \
                            || mtcc->root_op->kind == OP_OPEN_FILE)         \
     && (mtcc->root_op->prop_mods == NULL                                   \
                            || !mtcc->root_op->prop_mods->nelts)            \
     && (mtcc->root_op->children == NULL                                    \
                            || !mtcc->root_op->children->nelts))

struct svn_client__mtcc_t
{
  apr_pool_t *pool;
  svn_revnum_t head_revision;
  svn_revnum_t base_revision;

  svn_ra_session_t *ra_session;
  svn_client_ctx_t *ctx;

  mtcc_op_t *root_op;
};

static mtcc_op_t *
mtcc_op_create(const char *name,
               svn_boolean_t add,
               svn_boolean_t directory,
               apr_pool_t *result_pool)
{
  mtcc_op_t *op;

  op = apr_pcalloc(result_pool, sizeof(*op));
  op->name = name ? apr_pstrdup(result_pool, name) : "";

  if (add)
    op->kind = directory ? OP_ADD_DIR : OP_ADD_FILE;
  else
    op->kind = directory ? OP_OPEN_DIR : OP_OPEN_FILE;

  if (directory)
    op->children = apr_array_make(result_pool, 4, sizeof(mtcc_op_t *));

  op->src_rev = SVN_INVALID_REVNUM;

  return op;
}

static svn_error_t *
mtcc_op_find(mtcc_op_t **op,
             svn_boolean_t *created,
             const char *relpath,
             mtcc_op_t *base_op,
             svn_boolean_t find_existing,
             svn_boolean_t find_deletes,
             svn_boolean_t create_file,
             apr_pool_t *result_pool,
             apr_pool_t *scratch_pool)
{
  const char *name;
  const char *child;
  int i;

  assert(svn_relpath_is_canonical(relpath));
  if (created)
    *created = FALSE;

  if (SVN_PATH_IS_EMPTY(relpath))
    {
      if (find_existing)
        *op = base_op;
      else
        *op = NULL;

      return SVN_NO_ERROR;
    }

  child = strchr(relpath, '/');

  if (child)
    {
      name = apr_pstrmemdup(scratch_pool, relpath, (child-relpath));
      child++; /* Skip '/' */
    }
  else
    name = relpath;

  if (!base_op->children)
    {
      if (!created)
        {
          *op = NULL;
           return SVN_NO_ERROR;
        }
      else
        return svn_error_createf(SVN_ERR_FS_NOT_DIRECTORY, NULL,
                                 _("Can't operate on '%s' because '%s' is not a "
                                   "directory"),
                                 name, base_op->name);
    }

  for (i = base_op->children->nelts-1; i >= 0 ; i--)
    {
      mtcc_op_t *cop;

      cop = APR_ARRAY_IDX(base_op->children, i, mtcc_op_t *);

      if (! strcmp(cop->name, name)
          && (find_deletes || cop->kind != OP_DELETE))
        {
          return svn_error_trace(
                        mtcc_op_find(op, created, child ? child : "", cop,
                                     find_existing, find_deletes, create_file,
                                     result_pool, scratch_pool));
        }
    }

  if (!created)
    {
      *op = NULL;
      return SVN_NO_ERROR;
    }

  {
    mtcc_op_t *cop;

    cop = mtcc_op_create(name, FALSE, child || !create_file, result_pool);

    APR_ARRAY_PUSH(base_op->children, mtcc_op_t *) = cop;

    if (!child)
      {
        *op = cop;
        *created = TRUE;
        return SVN_NO_ERROR;
      }

    return svn_error_trace(
                mtcc_op_find(op, created, child, cop, find_existing,
                             find_deletes, create_file,
                             result_pool, scratch_pool));
  }
}

/* Gets the original repository location of RELPATH, checking things
   like copies, moves, etc.  */
static svn_error_t *
get_origin(svn_boolean_t *done,
           const char **origin_relpath,
           svn_revnum_t *rev,
           mtcc_op_t *op,
           const char *relpath,
           apr_pool_t *result_pool,
           apr_pool_t *scratch_pool)
{
  const char *child;
  const char *name;
  if (SVN_PATH_IS_EMPTY(relpath))
    {
      if (op->kind == OP_ADD_DIR || op->kind == OP_ADD_FILE)
        *done = TRUE;
      *origin_relpath = op->src_relpath
                                ? apr_pstrdup(result_pool, op->src_relpath)
                                : NULL;
      *rev = op->src_rev;
      return SVN_NO_ERROR;
    }

  child = strchr(relpath, '/');
  if (child)
    {
      name = apr_pstrmemdup(scratch_pool, relpath, child-relpath);
      child++; /* Skip '/' */
    }
  else
    name = relpath;

  if (op->children && op->children->nelts)
    {
      int i;

      for (i = op->children->nelts-1; i >= 0; i--)
        {
           mtcc_op_t *cop;

           cop = APR_ARRAY_IDX(op->children, i, mtcc_op_t *);

           if (! strcmp(cop->name, name))
            {
              if (cop->kind == OP_DELETE)
                {
                  *done = TRUE;
                  return SVN_NO_ERROR;
                }

              SVN_ERR(get_origin(done, origin_relpath, rev,
                                 cop, child ? child : "",
                                 result_pool, scratch_pool));

              if (*origin_relpath || *done)
                return SVN_NO_ERROR;

              break;
            }
        }
    }

  if (op->kind == OP_ADD_DIR || op->kind == OP_ADD_FILE)
    {
      *done = TRUE;
      if (op->src_relpath)
        {
          *origin_relpath = svn_relpath_join(op->src_relpath, relpath,
                                             result_pool);
          *rev = op->src_rev;
        }
    }

  return SVN_NO_ERROR;
}

/* Obtains the original repository location for an mtcc relpath as
   *ORIGIN_RELPATH @ *REV, if it has one. If it has not and IGNORE_ENOENT
   is TRUE report *ORIGIN_RELPATH as NULL, otherwise return an error */
static svn_error_t *
mtcc_get_origin(const char **origin_relpath,
                svn_revnum_t *rev,
                const char *relpath,
                svn_boolean_t ignore_enoent,
                svn_client__mtcc_t *mtcc,
                apr_pool_t *result_pool,
                apr_pool_t *scratch_pool)
{
  svn_boolean_t done = FALSE;

  *origin_relpath = NULL;
  *rev = SVN_INVALID_REVNUM;

  SVN_ERR(get_origin(&done, origin_relpath, rev, mtcc->root_op, relpath,
                     result_pool, scratch_pool));

  if (!*origin_relpath && !done)
    {
      *origin_relpath = apr_pstrdup(result_pool, relpath);
      *rev = mtcc->base_revision;
    }
  else if (!ignore_enoent)
    {
      return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
                               _("No origin found for node at '%s'"),
                               relpath);
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__mtcc_create(svn_client__mtcc_t **mtcc,
                        const char *anchor_url,
                        svn_revnum_t base_revision,
                        svn_client_ctx_t *ctx,
                        apr_pool_t *result_pool,
                        apr_pool_t *scratch_pool)
{
  apr_pool_t *mtcc_pool;

  mtcc_pool = svn_pool_create(result_pool);

  *mtcc = apr_pcalloc(mtcc_pool, sizeof(**mtcc));
  (*mtcc)->pool = mtcc_pool;

  (*mtcc)->root_op = mtcc_op_create(NULL, FALSE, TRUE, mtcc_pool);

  (*mtcc)->ctx = ctx;

  SVN_ERR(svn_client_open_ra_session2(&(*mtcc)->ra_session, anchor_url,
                                      NULL /* wri_abspath */, ctx,
                                      mtcc_pool, scratch_pool));

  SVN_ERR(svn_ra_get_latest_revnum((*mtcc)->ra_session, &(*mtcc)->head_revision,
                                   scratch_pool));

  if (SVN_IS_VALID_REVNUM(base_revision))
    (*mtcc)->base_revision = base_revision;
  else
    (*mtcc)->base_revision = (*mtcc)->head_revision;

  if ((*mtcc)->base_revision > (*mtcc)->head_revision)
    return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL,
                             _("No such revision %ld (HEAD is %ld)"),
                             base_revision, (*mtcc)->head_revision);

  return SVN_NO_ERROR;
}

static svn_error_t *
update_copy_src(mtcc_op_t *op,
                const char *add_relpath,
                apr_pool_t *result_pool)
{
  int i;

  if (op->src_relpath)
    op->src_relpath = svn_relpath_join(add_relpath, op->src_relpath,
                                       result_pool);

  if (!op->children)
    return SVN_NO_ERROR;

  for (i = 0; i < op->children->nelts; i++)
    {
      mtcc_op_t *cop;

      cop = APR_ARRAY_IDX(op->children, i, mtcc_op_t *);

      SVN_ERR(update_copy_src(cop, add_relpath, result_pool));
    }

  return SVN_NO_ERROR;
}

static svn_error_t *
mtcc_reparent(const char *new_anchor_url,
              svn_client__mtcc_t *mtcc,
              apr_pool_t *scratch_pool)
{
  const char *session_url;
  const char *up;

  SVN_ERR(svn_ra_get_session_url(mtcc->ra_session, &session_url,
                                 scratch_pool));

  up = svn_uri_skip_ancestor(new_anchor_url, session_url, scratch_pool);

  if (! up)
    {
      return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
                               _("'%s' is not an ancestor of  '%s'"),
                               new_anchor_url, session_url);
    }
  else if (!*up)
    {
      return SVN_NO_ERROR; /* Same url */
    }

  /* Update copy origins recursively...:( */
  SVN_ERR(update_copy_src(mtcc->root_op, up, mtcc->pool));

  SVN_ERR(svn_ra_reparent(mtcc->ra_session, new_anchor_url, scratch_pool));

  /* Create directory open operations for new ancestors */
  while (*up)
    {
      mtcc_op_t *root_op;

      mtcc->root_op->name = svn_relpath_basename(up, mtcc->pool);
      up = svn_relpath_dirname(up, scratch_pool);

      root_op = mtcc_op_create(NULL, FALSE, TRUE, mtcc->pool);

      APR_ARRAY_PUSH(root_op->children, mtcc_op_t *) = mtcc->root_op;

      mtcc->root_op = root_op;
    }

  return SVN_NO_ERROR;
}

/* Check if it is safe to create a new node at NEW_RELPATH. Return a proper
   error if it is not */
static svn_error_t *
mtcc_verify_create(svn_client__mtcc_t *mtcc,
                   const char *new_relpath,
                   apr_pool_t *scratch_pool)
{
  svn_node_kind_t kind;

  if (*new_relpath || !MTCC_UNMODIFIED(mtcc))
    {
      mtcc_op_t *op;

      SVN_ERR(mtcc_op_find(&op, NULL, new_relpath, mtcc->root_op, TRUE, FALSE,
                           FALSE, mtcc->pool, scratch_pool));

      if (op)
        return svn_error_createf(SVN_ERR_FS_ALREADY_EXISTS, NULL,
                                 _("Path '%s' already exists"),
                                 new_relpath);

      SVN_ERR(mtcc_op_find(&op, NULL, new_relpath, mtcc->root_op, TRUE, TRUE,
                           FALSE, mtcc->pool, scratch_pool));

      if (op)
        return SVN_NO_ERROR; /* Node is explicitly deleted. We can replace */
    }

  /* mod_dav_svn used to allow overwriting existing directories. Let's hide
     that for users of this api */
  SVN_ERR(svn_client__mtcc_check_path(&kind, new_relpath, FALSE,
                                      mtcc, scratch_pool));

  if (kind != svn_node_none)
    return svn_error_createf(SVN_ERR_FS_ALREADY_EXISTS, NULL,
                             _("Path '%s' already exists"),
                             new_relpath);

  return SVN_NO_ERROR;
}


svn_error_t *
svn_client__mtcc_add_add_file(const char *relpath,
                              svn_stream_t *src_stream,
                              const svn_checksum_t *src_checksum,
                              svn_client__mtcc_t *mtcc,
                              apr_pool_t *scratch_pool)
{
  mtcc_op_t *op;
  svn_boolean_t created;
  SVN_ERR_ASSERT(svn_relpath_is_canonical(relpath) && src_stream);

  SVN_ERR(mtcc_verify_create(mtcc, relpath, scratch_pool));

  if (SVN_PATH_IS_EMPTY(relpath) && MTCC_UNMODIFIED(mtcc))
    {
      /* Turn the root operation into a file addition */
      op = mtcc->root_op;
    }
  else
    {
      SVN_ERR(mtcc_op_find(&op, &created, relpath, mtcc->root_op, FALSE, FALSE,
                           TRUE, mtcc->pool, scratch_pool));

      if (!op || !created)
        {
          return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
                                   _("Can't add file at '%s'"),
                                   relpath);
        }
    }

  op->kind = OP_ADD_FILE;
  op->src_stream = src_stream;
  op->src_checksum = src_checksum ? svn_checksum_dup(src_checksum, mtcc->pool)
                                  : NULL;

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__mtcc_add_copy(const char *src_relpath,
                          svn_revnum_t revision,
                          const char *dst_relpath,
                          svn_client__mtcc_t *mtcc,
                          apr_pool_t *scratch_pool)
{
  mtcc_op_t *op;
  svn_boolean_t created;
  svn_node_kind_t kind;

  SVN_ERR_ASSERT(svn_relpath_is_canonical(src_relpath)
                 && svn_relpath_is_canonical(dst_relpath));

  if (! SVN_IS_VALID_REVNUM(revision))
    revision = mtcc->head_revision;
  else if (revision > mtcc->head_revision)
    {
      return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL,
                               _("No such revision %ld"), revision);
    }

  SVN_ERR(mtcc_verify_create(mtcc, dst_relpath, scratch_pool));

  /* Subversion requires the kind of a copy */
  SVN_ERR(svn_ra_check_path(mtcc->ra_session, src_relpath, revision, &kind,
                            scratch_pool));

  if (kind != svn_node_dir && kind != svn_node_file)
    {
      return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
                               _("Path '%s' not found in revision %ld"),
                               src_relpath, revision);
    }

  SVN_ERR(mtcc_op_find(&op, &created, dst_relpath, mtcc->root_op, FALSE, FALSE,
                       (kind == svn_node_file), mtcc->pool, scratch_pool));

  if (!op || !created)
    {
      return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
                               _("Can't add node at '%s'"),
                               dst_relpath);
    }

  op->kind = (kind == svn_node_file) ? OP_ADD_FILE : OP_ADD_DIR;
  op->src_relpath = apr_pstrdup(mtcc->pool, src_relpath);
  op->src_rev = revision;

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__mtcc_add_delete(const char *relpath,
                            svn_client__mtcc_t *mtcc,
                            apr_pool_t *scratch_pool)
{
  mtcc_op_t *op;
  svn_boolean_t created;
  svn_node_kind_t kind;

  SVN_ERR_ASSERT(svn_relpath_is_canonical(relpath));

  SVN_ERR(svn_client__mtcc_check_path(&kind, relpath, FALSE,
                                      mtcc, scratch_pool));

  if (kind == svn_node_none)
    return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
                             _("Can't delete node at '%s' as it "
                                "does not exist"),
                             relpath);

  if (SVN_PATH_IS_EMPTY(relpath) && MTCC_UNMODIFIED(mtcc))
    {
      /* Turn root operation into delete */
      op = mtcc->root_op;
    }
  else
    {
      SVN_ERR(mtcc_op_find(&op, &created, relpath, mtcc->root_op, FALSE, TRUE,
                           TRUE, mtcc->pool, scratch_pool));

      if (!op || !created)
        {
          return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
                                   _("Can't delete node at '%s'"),
                                   relpath);
        }
    }

  op->kind = OP_DELETE;
  op->children = NULL;
  op->prop_mods = NULL;

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__mtcc_add_mkdir(const char *relpath,
                           svn_client__mtcc_t *mtcc,
                           apr_pool_t *scratch_pool)
{
  mtcc_op_t *op;
  svn_boolean_t created;
  SVN_ERR_ASSERT(svn_relpath_is_canonical(relpath));

  SVN_ERR(mtcc_verify_create(mtcc, relpath, scratch_pool));

  if (SVN_PATH_IS_EMPTY(relpath) && MTCC_UNMODIFIED(mtcc))
    {
      /* Turn the root of the operation in an MKDIR */
      mtcc->root_op->kind = OP_ADD_DIR;

      return SVN_NO_ERROR;
    }

  SVN_ERR(mtcc_op_find(&op, &created, relpath, mtcc->root_op, FALSE, FALSE,
                       FALSE, mtcc->pool, scratch_pool));

  if (!op || !created)
    {
      return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
                               _("Can't create directory at '%s'"),
                               relpath);
    }

  op->kind = OP_ADD_DIR;

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__mtcc_add_move(const char *src_relpath,
                          const char *dst_relpath,
                          svn_client__mtcc_t *mtcc,
                          apr_pool_t *scratch_pool)
{
  const char *origin_relpath;
  svn_revnum_t origin_rev;

  SVN_ERR(mtcc_get_origin(&origin_relpath, &origin_rev,
                          src_relpath, FALSE, mtcc,
                          scratch_pool, scratch_pool));

  SVN_ERR(svn_client__mtcc_add_copy(src_relpath, mtcc->base_revision,
                                    dst_relpath, mtcc, scratch_pool));
  SVN_ERR(svn_client__mtcc_add_delete(src_relpath, mtcc, scratch_pool));

  return SVN_NO_ERROR;
}

/* Baton for mtcc_prop_getter */
struct mtcc_prop_get_baton
{
  svn_client__mtcc_t *mtcc;
  const char *relpath;
  svn_cancel_func_t cancel_func;
  void *cancel_baton;
};

/* Implements svn_wc_canonicalize_svn_prop_get_file_t */
static svn_error_t *
mtcc_prop_getter(const svn_string_t **mime_type,
                 svn_stream_t *stream,
                 void *baton,
                 apr_pool_t *pool)
{
  struct mtcc_prop_get_baton *mpgb = baton;
  const char *origin_relpath;
  svn_revnum_t origin_rev;
  apr_hash_t *props = NULL;

  mtcc_op_t *op;

  if (mime_type)
    *mime_type = NULL;

  /* Check if we have the information locally */
  SVN_ERR(mtcc_op_find(&op, NULL, mpgb->relpath, mpgb->mtcc->root_op, TRUE,
                       FALSE, FALSE, pool, pool));

  if (op)
    {
      if (mime_type)
        {
          int i;

          for (i = 0; op->prop_mods && i < op->prop_mods->nelts; i++)
            {
              const svn_prop_t *mod = &APR_ARRAY_IDX(op->prop_mods, i,
                                                     svn_prop_t);

              if (! strcmp(mod->name, SVN_PROP_MIME_TYPE))
                {
                  *mime_type = svn_string_dup(mod->value, pool);
                  mime_type = NULL;
                }
            }
        }

      if (stream && op->src_stream)
        {
          svn_stream_mark_t *mark;
          svn_error_t *err;

          /* Is the source stream capable of being read multiple times? */
          err = svn_stream_mark(op->src_stream, &mark, pool);

          if (err && err->apr_err != SVN_ERR_STREAM_SEEK_NOT_SUPPORTED)
            return svn_error_trace(err);
          svn_error_clear(err);

          if (!err)
            {
              err = svn_stream_copy3(svn_stream_disown(op->src_stream, pool),
                                     svn_stream_disown(stream, pool),
                                     mpgb->cancel_func, mpgb->cancel_baton,
                                     pool);

              SVN_ERR(svn_error_compose_create(
                            err,
                            svn_stream_seek(op->src_stream, mark)));
            }
          /* else: ### Create tempfile? */

          stream = NULL; /* Stream is handled */
        }
    }

  if (!stream && !mime_type)
    return SVN_NO_ERROR;

  SVN_ERR(mtcc_get_origin(&origin_relpath, &origin_rev, mpgb->relpath, TRUE,
                          mpgb->mtcc, pool, pool));

  if (!origin_relpath)
    return SVN_NO_ERROR; /* Nothing to fetch at repository */

  SVN_ERR(svn_ra_get_file(mpgb->mtcc->ra_session, origin_relpath, origin_rev,
                          stream, NULL, mime_type ? &props : NULL, pool));

  if (mime_type && props)
    *mime_type = svn_hash_gets(props, SVN_PROP_MIME_TYPE);

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__mtcc_add_propset(const char *relpath,
                             const char *propname,
                             const svn_string_t *propval,
                             svn_boolean_t skip_checks,
                             svn_client__mtcc_t *mtcc,
                             apr_pool_t *scratch_pool)
{
  mtcc_op_t *op;
  SVN_ERR_ASSERT(svn_relpath_is_canonical(relpath));

  if (! svn_prop_name_is_valid(propname))
    return svn_error_createf(SVN_ERR_CLIENT_PROPERTY_NAME, NULL,
                             _("Bad property name: '%s'"), propname);

  if (svn_prop_is_known_svn_rev_prop(propname))
    return svn_error_createf(SVN_ERR_CLIENT_PROPERTY_NAME, NULL,
                             _("Revision property '%s' not allowed "
                               "in this context"), propname);

  if (svn_property_kind2(propname) == svn_prop_wc_kind)
    return svn_error_createf(SVN_ERR_CLIENT_PROPERTY_NAME, NULL,
                             _("'%s' is a wcprop, thus not accessible "
                               "to clients"), propname);

  if (!skip_checks && svn_prop_needs_translation(propname))
    {
      svn_string_t *translated_value;
      SVN_ERR_W(svn_subst_translate_string2(&translated_value, NULL,
                                            NULL, propval,
                                            NULL, FALSE,
                                            scratch_pool, scratch_pool),
                _("Error normalizing property value"));

      propval = translated_value;
    }

  if (propval && svn_prop_is_svn_prop(propname))
    {
      struct mtcc_prop_get_baton mpbg;
      svn_node_kind_t kind;
      SVN_ERR(svn_client__mtcc_check_path(&kind, relpath, FALSE, mtcc,
                                          scratch_pool));

      mpbg.mtcc = mtcc;
      mpbg.relpath = relpath;
      mpbg.cancel_func = mtcc->ctx->cancel_func;
      mpbg.cancel_baton = mtcc->ctx->cancel_baton;

      SVN_ERR(svn_wc_canonicalize_svn_prop(&propval, propname, propval,
                                           relpath, kind, skip_checks,
                                           mtcc_prop_getter, &mpbg,
                                           scratch_pool));
    }

  if (SVN_PATH_IS_EMPTY(relpath) && MTCC_UNMODIFIED(mtcc))
    {
      svn_node_kind_t kind;

      /* Probing the node for an unmodified root will fix the node type to
         a file if necessary */

      SVN_ERR(svn_client__mtcc_check_path(&kind, relpath, FALSE,
                                          mtcc, scratch_pool));

      if (kind == svn_node_none)
        return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
                                 _("Can't set properties at not existing '%s'"),
                                   relpath);

      op = mtcc->root_op;
    }
  else
    {
      SVN_ERR(mtcc_op_find(&op, NULL, relpath, mtcc->root_op, TRUE, FALSE,
                           FALSE, mtcc->pool, scratch_pool));

      if (!op)
        {
          svn_node_kind_t kind;
          svn_boolean_t created;

          /* ### TODO: Check if this node is within a newly copied directory,
                       and update origin values accordingly */

          SVN_ERR(svn_client__mtcc_check_path(&kind, relpath, FALSE,
                                              mtcc, scratch_pool));

          if (kind == svn_node_none)
            return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
                                     _("Can't set properties at not existing '%s'"),
                                     relpath);

          SVN_ERR(mtcc_op_find(&op, &created, relpath, mtcc->root_op, TRUE, FALSE,
                               (kind != svn_node_dir),
                               mtcc->pool, scratch_pool));

          SVN_ERR_ASSERT(op != NULL);
        }
    }

  if (!op->prop_mods)
      op->prop_mods = apr_array_make(mtcc->pool, 4, sizeof(svn_prop_t));

  {
    svn_prop_t propchange;
    propchange.name = apr_pstrdup(mtcc->pool, propname);

    if (propval)
      propchange.value = svn_string_dup(propval, mtcc->pool);
    else
      propchange.value = NULL;

    APR_ARRAY_PUSH(op->prop_mods, svn_prop_t) = propchange;
  }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__mtcc_add_update_file(const char *relpath,
                                 svn_stream_t *src_stream,
                                 const svn_checksum_t *src_checksum,
                                 svn_stream_t *base_stream,
                                 const svn_checksum_t *base_checksum,
                                 svn_client__mtcc_t *mtcc,
                                 apr_pool_t *scratch_pool)
{
  mtcc_op_t *op;
  svn_boolean_t created;
  svn_node_kind_t kind;
  SVN_ERR_ASSERT(svn_relpath_is_canonical(relpath) && src_stream);

  SVN_ERR(svn_client__mtcc_check_path(&kind, relpath, FALSE,
                                      mtcc, scratch_pool));

  if (kind != svn_node_file)
    return svn_error_createf(SVN_ERR_FS_NOT_FILE, NULL,
                             _("Can't update '%s' because it is not a file"),
                             relpath);

  SVN_ERR(mtcc_op_find(&op, &created, relpath, mtcc->root_op, TRUE, FALSE,
                       TRUE, mtcc->pool, scratch_pool));

  if (!op
      || (op->kind != OP_OPEN_FILE && op->kind != OP_ADD_FILE)
      || (op->src_stream != NULL))
    {
      return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
                               _("Can't update file at '%s'"), relpath);
    }

  op->src_stream = src_stream;
  op->src_checksum = src_checksum ? svn_checksum_dup(src_checksum, mtcc->pool)
                                  : NULL;

  op->base_stream = base_stream;
  op->base_checksum = base_checksum ? svn_checksum_dup(base_checksum,
                                                       mtcc->pool)
                                    : NULL;

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__mtcc_check_path(svn_node_kind_t *kind,
                            const char *relpath,
                            svn_boolean_t check_repository,
                            svn_client__mtcc_t *mtcc,
                            apr_pool_t *scratch_pool)
{
  const char *origin_relpath;
  svn_revnum_t origin_rev;
  mtcc_op_t *op;

  SVN_ERR_ASSERT(svn_relpath_is_canonical(relpath));

  if (SVN_PATH_IS_EMPTY(relpath) && MTCC_UNMODIFIED(mtcc)
      && !mtcc->root_op->performed_stat)
    {
      /* We know nothing about the root. Perhaps it is a file? */
      SVN_ERR(svn_ra_check_path(mtcc->ra_session, "", mtcc->base_revision,
                                kind, scratch_pool));

      mtcc->root_op->performed_stat = TRUE;
      if (*kind == svn_node_file)
        {
          mtcc->root_op->kind = OP_OPEN_FILE;
          mtcc->root_op->children = NULL;
        }
      return SVN_NO_ERROR;
    }

  SVN_ERR(mtcc_op_find(&op, NULL, relpath, mtcc->root_op, TRUE, FALSE,
                       FALSE, mtcc->pool, scratch_pool));

  if (!op || (check_repository && !op->performed_stat))
    {
      SVN_ERR(mtcc_get_origin(&origin_relpath, &origin_rev,
                              relpath, TRUE, mtcc,
                              scratch_pool, scratch_pool));

      if (!origin_relpath)
        *kind = svn_node_none;
      else
        SVN_ERR(svn_ra_check_path(mtcc->ra_session, origin_relpath,
                                  origin_rev, kind, scratch_pool));

      if (op && *kind == svn_node_dir)
        {
          if (op->kind == OP_OPEN_DIR || op->kind == OP_ADD_DIR)
            op->performed_stat = TRUE;
          else if (op->kind == OP_OPEN_FILE || op->kind == OP_ADD_FILE)
            return svn_error_createf(SVN_ERR_FS_NOT_FILE, NULL,
                                     _("Can't perform file operation "
                                       "on '%s' as it is not a file"),
                                     relpath);
        }
      else if (op && *kind == svn_node_file)
        {
          if (op->kind == OP_OPEN_FILE || op->kind == OP_ADD_FILE)
            op->performed_stat = TRUE;
          else if (op->kind == OP_OPEN_DIR || op->kind == OP_ADD_DIR)
            return svn_error_createf(SVN_ERR_FS_NOT_DIRECTORY, NULL,
                                     _("Can't perform directory operation "
                                       "on '%s' as it is not a directory"),
                                     relpath);
        }
      else if (op && (op->kind == OP_OPEN_DIR || op->kind == OP_OPEN_FILE))
        {
          return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
                                   _("Can't open '%s' as it does not exist"),
                                   relpath);
        }

      return SVN_NO_ERROR;
    }

  /* op != NULL */
  if (op->kind == OP_OPEN_DIR || op->kind == OP_ADD_DIR)
    {
      *kind = svn_node_dir;
      return SVN_NO_ERROR;
    }
  else if (op->kind == OP_OPEN_FILE || op->kind == OP_ADD_FILE)
    {
      *kind = svn_node_file;
      return SVN_NO_ERROR;
    }
  SVN_ERR_MALFUNCTION(); /* No other kinds defined as delete is filtered */
}

static svn_error_t *
commit_properties(const svn_delta_editor_t *editor,
                  const mtcc_op_t *op,
                  void *node_baton,
                  apr_pool_t *scratch_pool)
{
  int i;
  apr_pool_t *iterpool;

  if (!op->prop_mods || op->prop_mods->nelts == 0)
    return SVN_NO_ERROR;

  iterpool = svn_pool_create(scratch_pool);
  for (i = 0; i < op->prop_mods->nelts; i++)
    {
      const svn_prop_t *mod = &APR_ARRAY_IDX(op->prop_mods, i, svn_prop_t);

      svn_pool_clear(iterpool);

      if (op->kind == OP_ADD_DIR || op->kind == OP_OPEN_DIR)
        SVN_ERR(editor->change_dir_prop(node_baton, mod->name, mod->value,
                                        iterpool));
      else if (op->kind == OP_ADD_FILE || op->kind == OP_OPEN_FILE)
        SVN_ERR(editor->change_file_prop(node_baton, mod->name, mod->value,
                                         iterpool));
    }

  svn_pool_destroy(iterpool);
  return SVN_NO_ERROR;
}

/* Handles updating a file to a delta editor and then closes it */
static svn_error_t *
commit_file(const svn_delta_editor_t *editor,
            mtcc_op_t *op,
            void *file_baton,
            const char *session_url,
            const char *relpath,
            svn_client_ctx_t *ctx,
            apr_pool_t *scratch_pool)
{
  const char *text_checksum = NULL;
  svn_checksum_t *src_checksum = op->src_checksum;
  SVN_ERR(commit_properties(editor, op, file_baton, scratch_pool));

  if (op->src_stream)
    {
      const char *base_checksum = NULL;
      apr_pool_t *txdelta_pool = scratch_pool;
      svn_txdelta_window_handler_t window_handler;
      void *handler_baton;
      svn_stream_t *src_stream = op->src_stream;

      if (op->base_checksum && op->base_checksum->kind == svn_checksum_md5)
        base_checksum = svn_checksum_to_cstring(op->base_checksum, scratch_pool);

      /* ### TODO: Future enhancement: Allocate in special pool and send
                   files after the true edit operation, like a wc commit */
      SVN_ERR(editor->apply_textdelta(file_baton, base_checksum, txdelta_pool,
                                      &window_handler, &handler_baton));

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

          notify = svn_wc_create_notify_url(
                            svn_path_url_add_component2(session_url, relpath,
                                                        scratch_pool),
                            svn_wc_notify_commit_postfix_txdelta,
                            scratch_pool);

          notify->path = relpath;
          notify->kind = svn_node_file;

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

      if (window_handler != svn_delta_noop_window_handler)
        {
          if (!src_checksum || src_checksum->kind != svn_checksum_md5)
            src_stream = svn_stream_checksummed2(src_stream, &src_checksum, NULL,
                                                 svn_checksum_md5,
                                                 TRUE, scratch_pool);

          if (!op->base_stream)
            SVN_ERR(svn_txdelta_send_stream(src_stream,
                                            window_handler, handler_baton, NULL,
                                            scratch_pool));
          else
            SVN_ERR(svn_txdelta_run(op->base_stream, src_stream,
                                    window_handler, handler_baton,
                                    svn_checksum_md5, NULL,
                                    ctx->cancel_func, ctx->cancel_baton,
                                    scratch_pool, scratch_pool));
        }

      SVN_ERR(svn_stream_close(src_stream));
      if (op->base_stream)
        SVN_ERR(svn_stream_close(op->base_stream));
    }

  if (src_checksum && src_checksum->kind == svn_checksum_md5)
    text_checksum = svn_checksum_to_cstring(src_checksum, scratch_pool);

  return svn_error_trace(editor->close_file(file_baton, text_checksum,
                                            scratch_pool));
}

/* Handles updating a directory to a delta editor and then closes it */
static svn_error_t *
commit_directory(const svn_delta_editor_t *editor,
                 mtcc_op_t *op,
                 const char *relpath,
                 svn_revnum_t base_rev,
                 void *dir_baton,
                 const char *session_url,
                 svn_client_ctx_t *ctx,
                 apr_pool_t *scratch_pool)
{
  SVN_ERR(commit_properties(editor, op, dir_baton, scratch_pool));

  if (op->children && op->children->nelts > 0)
    {
      apr_pool_t *iterpool = svn_pool_create(scratch_pool);
      int i;

      for (i = 0; i < op->children->nelts; i++)
        {
          mtcc_op_t *cop;
          const char * child_relpath;
          void *child_baton;

          cop = APR_ARRAY_IDX(op->children, i, mtcc_op_t *);

          svn_pool_clear(iterpool);

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

          child_relpath = svn_relpath_join(relpath, cop->name, iterpool);

          switch (cop->kind)
            {
              case OP_DELETE:
                SVN_ERR(editor->delete_entry(child_relpath, base_rev,
                                             dir_baton, iterpool));
                break;

              case OP_ADD_DIR:
                SVN_ERR(editor->add_directory(child_relpath, dir_baton,
                                              cop->src_relpath
                                                ? svn_path_url_add_component2(
                                                              session_url,
                                                              cop->src_relpath,
                                                              iterpool)
                                                : NULL,
                                              cop->src_rev,
                                              iterpool, &child_baton));
                SVN_ERR(commit_directory(editor, cop, child_relpath,
                                         SVN_INVALID_REVNUM, child_baton,
                                         session_url, ctx, iterpool));
                break;
              case OP_OPEN_DIR:
                SVN_ERR(editor->open_directory(child_relpath, dir_baton,
                                               base_rev, iterpool, &child_baton));
                SVN_ERR(commit_directory(editor, cop, child_relpath,
                                         base_rev, child_baton,
                                         session_url, ctx, iterpool));
                break;

              case OP_ADD_FILE:
                SVN_ERR(editor->add_file(child_relpath, dir_baton,
                                         cop->src_relpath
                                            ? svn_path_url_add_component2(
                                                            session_url,
                                                            cop->src_relpath,
                                                            iterpool)
                                            : NULL,
                                         cop->src_rev,
                                         iterpool, &child_baton));
                SVN_ERR(commit_file(editor, cop, child_baton,
                                    session_url, child_relpath, ctx, iterpool));
                break;
              case OP_OPEN_FILE:
                SVN_ERR(editor->open_file(child_relpath, dir_baton, base_rev,
                                          iterpool, &child_baton));
                SVN_ERR(commit_file(editor, cop, child_baton,
                                    session_url, child_relpath, ctx, iterpool));
                break;

              default:
                SVN_ERR_MALFUNCTION();
            }
        }
    }

  return svn_error_trace(editor->close_directory(dir_baton, scratch_pool));
}


/* Helper function to recursively create svn_client_commit_item3_t items
   to provide to the log message callback */
static svn_error_t *
add_commit_items(mtcc_op_t *op,
                 const char *session_url,
                 const char *url,
                 apr_array_header_t *commit_items,
                 apr_pool_t *result_pool,
                 apr_pool_t *scratch_pool)
{
  if ((op->kind != OP_OPEN_DIR && op->kind != OP_OPEN_FILE)
      || (op->prop_mods && op->prop_mods->nelts)
      || (op->src_stream))
    {
      svn_client_commit_item3_t *item;

      item = svn_client_commit_item3_create(result_pool);

      item->path = NULL;
      if (op->kind == OP_OPEN_DIR || op->kind == OP_ADD_DIR)
        item->kind = svn_node_dir;
      else if (op->kind == OP_OPEN_FILE || op->kind == OP_ADD_FILE)
        item->kind = svn_node_file;
      else
        item->kind = svn_node_unknown;

      item->url = apr_pstrdup(result_pool, url);
      item->session_relpath = svn_uri_skip_ancestor(session_url, item->url,
                                                    result_pool);

      if (op->src_relpath)
        {
          item->copyfrom_url = svn_path_url_add_component2(session_url,
                                                           op->src_relpath,
                                                           result_pool);
          item->copyfrom_rev = op->src_rev;
          item->state_flags |= SVN_CLIENT_COMMIT_ITEM_IS_COPY;
        }
      else
        item->copyfrom_rev = SVN_INVALID_REVNUM;

      if (op->kind == OP_ADD_DIR || op->kind == OP_ADD_FILE)
        item->state_flags = SVN_CLIENT_COMMIT_ITEM_ADD;
      else if (op->kind == OP_DELETE)
        item->state_flags = SVN_CLIENT_COMMIT_ITEM_DELETE;
      /* else item->state_flags = 0; */

      if (op->prop_mods && op->prop_mods->nelts)
        item->state_flags |= SVN_CLIENT_COMMIT_ITEM_PROP_MODS;

      if (op->src_stream)
        item->state_flags |= SVN_CLIENT_COMMIT_ITEM_TEXT_MODS;

      APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item;
    }

  if (op->children && op->children->nelts)
    {
      int i;
      apr_pool_t *iterpool = svn_pool_create(scratch_pool);

      for (i = 0; i < op->children->nelts; i++)
        {
          mtcc_op_t *cop;
          const char * child_url;

          cop = APR_ARRAY_IDX(op->children, i, mtcc_op_t *);

          svn_pool_clear(iterpool);

          child_url = svn_path_url_add_component2(url, cop->name, iterpool);

          SVN_ERR(add_commit_items(cop, session_url, child_url, commit_items,
                                   result_pool, iterpool));
        }

      svn_pool_destroy(iterpool);
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__mtcc_commit(apr_hash_t *revprop_table,
                        svn_commit_callback2_t commit_callback,
                        void *commit_baton,
                        svn_client__mtcc_t *mtcc,
                        apr_pool_t *scratch_pool)
{
  const svn_delta_editor_t *editor;
  void *edit_baton;
  void *root_baton;
  apr_hash_t *commit_revprops;
  svn_node_kind_t kind;
  svn_error_t *err;
  const char *session_url;
  const char *log_msg;

  if (MTCC_UNMODIFIED(mtcc))
    {
      /* No changes -> no revision. Easy out */
      svn_pool_destroy(mtcc->pool);
      return SVN_NO_ERROR;
    }

  SVN_ERR(svn_ra_get_session_url(mtcc->ra_session, &session_url, scratch_pool));

  if (mtcc->root_op->kind != OP_OPEN_DIR)
    {
      const char *name;

      svn_uri_split(&session_url, &name, session_url, scratch_pool);

      if (*name)
        {
          SVN_ERR(mtcc_reparent(session_url, mtcc, scratch_pool));

          SVN_ERR(svn_ra_reparent(mtcc->ra_session, session_url, scratch_pool));
        }
    }

    /* Create new commit items and add them to the array. */
  if (SVN_CLIENT__HAS_LOG_MSG_FUNC(mtcc->ctx))
    {
      svn_client_commit_item3_t *item;
      const char *tmp_file;
      apr_array_header_t *commit_items
                = apr_array_make(scratch_pool, 32, sizeof(item));

      SVN_ERR(add_commit_items(mtcc->root_op, session_url, session_url,
                               commit_items, scratch_pool, scratch_pool));

      SVN_ERR(svn_client__get_log_msg(&log_msg, &tmp_file, commit_items,
                                      mtcc->ctx, scratch_pool));

      if (! log_msg)
        return SVN_NO_ERROR;
    }
  else
    log_msg = "";

  SVN_ERR(svn_client__ensure_revprop_table(&commit_revprops, revprop_table,
                                           log_msg, mtcc->ctx, scratch_pool));

  /* Ugly corner case: The ra session might have died while we were waiting
     for the callback */

  err = svn_ra_check_path(mtcc->ra_session, "", mtcc->base_revision, &kind,
                          scratch_pool);

  if (err)
    {
      svn_error_t *err2 = svn_client_open_ra_session2(&mtcc->ra_session,
                                                      session_url,
                                                      NULL, mtcc->ctx,
                                                      mtcc->pool,
                                                      scratch_pool);

      if (err2)
        {
          svn_pool_destroy(mtcc->pool);
          return svn_error_trace(svn_error_compose_create(err, err2));
        }
      svn_error_clear(err);

      SVN_ERR(svn_ra_check_path(mtcc->ra_session, "",
                                mtcc->base_revision, &kind, scratch_pool));
    }

  if (kind != svn_node_dir)
    return svn_error_createf(SVN_ERR_FS_NOT_DIRECTORY, NULL,
                             _("Can't commit to '%s' because it "
                               "is not a directory"),
                             session_url);

  /* Beware that the editor object must not live longer than the MTCC.
     Otherwise, txn objects etc. in EDITOR may live longer than their
     respective FS objects.  So, we can't use SCRATCH_POOL here. */
  SVN_ERR(svn_ra_get_commit_editor3(mtcc->ra_session, &editor, &edit_baton,
                                    commit_revprops,
                                    commit_callback, commit_baton,
                                    NULL /* lock_tokens */,
                                    FALSE /* keep_locks */,
                                    mtcc->pool));

  err = editor->open_root(edit_baton, mtcc->base_revision, scratch_pool, &root_baton);

  if (!err)
    err = commit_directory(editor, mtcc->root_op, "", mtcc->base_revision,
                           root_baton, session_url, mtcc->ctx, scratch_pool);

  if (!err)
    {
      if (mtcc->ctx->notify_func2)
        {
          svn_wc_notify_t *notify;
          notify = svn_wc_create_notify_url(session_url,
                                            svn_wc_notify_commit_finalizing,
                                            scratch_pool);
          mtcc->ctx->notify_func2(mtcc->ctx->notify_baton2, notify,
                                  scratch_pool);
        }
      SVN_ERR(editor->close_edit(edit_baton, scratch_pool));
    }
  else
    err = svn_error_compose_create(err,
                                   editor->abort_edit(edit_baton, scratch_pool));

  svn_pool_destroy(mtcc->pool);

  return svn_error_trace(err);
}
