The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
/*
 * commit.c :  routines for committing changes to the server
 *
 * ====================================================================
 *    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 <apr_pools.h>
#include <apr_hash.h>
#include <apr_uuid.h>

#define APR_WANT_STDIO
#define APR_WANT_STRFUNC
#include <apr_want.h>

#include "svn_pools.h"
#include "svn_error.h"
#include "svn_delta.h"
#include "svn_io.h"
#include "svn_ra.h"
#include "../libsvn_ra/ra_loader.h"
#include "svn_dirent_uri.h"
#include "svn_path.h"
#include "svn_xml.h"
#include "svn_dav.h"
#include "svn_props.h"

#include "svn_private_config.h"

#include "ra_neon.h"


#define APPLY_TO_VERSION "<D:apply-to-version/>"

/*
 * version_rsrc_t: identify the relevant pieces of a resource on the server
 *
 * NOTE:  If you tweak this structure, please update dup_resource() to
 * ensure that it continues to create complete deep copies!
 */
typedef struct version_rsrc_t
{
  svn_revnum_t revision;  /* resource's revision, or SVN_INVALID_REVNUM
                             if it's new or is the HEAD */
  const char *url;        /* public/viewable/original resource URL */
  const char *vsn_url;    /* version resource URL that we stored
                             locally; NULL if this is a just-added resource */
  const char *wr_url;     /* working resource URL for this resource;
                             NULL for resources not (yet) checked out */
  const char *name;       /* basename of the resource */
  apr_pool_t *pool;       /* pool in which this resource is allocated */

} version_rsrc_t;


typedef struct commit_ctx_t
{
  /* Pool for the whole of the commit context. */
  apr_pool_t *pool;

  /* Pointer to the RA session baton. */
  svn_ra_neon__session_t *ras;

  /* Commit anchor repository relpath. */
  const char *anchor_relpath;

  /* A hash of revision properties (log messages, etc.) we need to set
     on the commit transaction. */
  apr_hash_t *revprop_table;

  /* A hash mapping svn_string_t * paths (those which are valid as
     target in the MERGE response) to svn_node_kind_t kinds. */
  apr_hash_t *valid_targets;

  /* The (potential) author of this commit. */
  const char *user;

  /* The commit callback and baton */
  svn_commit_callback2_t callback;
  void *callback_baton;

  /* The hash of lock tokens owned by the working copy, and whether
     or not to keep them after this commit is complete. */
  apr_hash_t *lock_tokens;
  svn_boolean_t keep_locks;

  /* HTTP v2 stuff */
  const char *txn_url;           /* txn URL (!svn/txn/TXN_NAME) */
  const char *txn_root_url;      /* commit anchor txn root URL */

  /* HTTP v1 stuff (only valid when 'txn_url' is NULL) */
  const char *activity_url;      /* activity base URL... */

} commit_ctx_t;


/* Are we using HTTPv2 semantics for this commit? */
#define USING_HTTPV2_COMMIT_SUPPORT(commit_ctx) ((commit_ctx)->txn_url != NULL)


typedef struct put_baton_t
{
  apr_file_t *tmpfile;        /* may be NULL for content-less file */
  svn_stringbuf_t *fname;     /* may be NULL for content-less file */
  const char *base_checksum;  /* hex md5 of base text; may be null */
  apr_off_t progress;
  svn_ra_neon__session_t *ras;
  apr_pool_t *pool;
} put_baton_t;

typedef struct resource_baton_t
{
  commit_ctx_t *cc;
  version_rsrc_t *rsrc;
  svn_revnum_t base_revision; /* base revision */
  apr_hash_t *prop_changes; /* name/values pairs of new/changed properties. */
  apr_array_header_t *prop_deletes; /* names of properties to delete. */
  svn_boolean_t created; /* set if this is an add rather than an update */
  svn_boolean_t copied; /* set if this object was copied */
  apr_pool_t *pool; /* the pool from open_foo() / add_foo() */
  put_baton_t *put_baton;  /* baton for this file's PUT request */
  const char *token;       /* file's lock token, if available */
  const char *txn_root_url; /* URL under !svn/txr/ (HTTPv2 only) */
  const char *local_relpath; /* path relative to the root of the commit
                                (used for the get_wc_prop() and push_wc_prop()
                                callbacks). */
} resource_baton_t;

/* this property will be fetched from the server when we don't find it
   cached in the WC property store. */
static const ne_propname fetch_props[] =
{
  { "DAV:", "checked-in" },
  { NULL }
};

static const ne_propname log_message_prop = { SVN_DAV_PROP_NS_SVN, "log" };

/* Return a deep copy of BASE allocated from POOL. */
static version_rsrc_t * dup_resource(version_rsrc_t *base, apr_pool_t *pool)
{
  version_rsrc_t *rsrc = apr_pcalloc(pool, sizeof(*rsrc));
  rsrc->revision = base->revision;
  rsrc->url = base->url ?
    apr_pstrdup(pool, base->url) : NULL;
  rsrc->vsn_url = base->vsn_url ?
    apr_pstrdup(pool, base->vsn_url) : NULL;
  rsrc->wr_url = base->wr_url ?
    apr_pstrdup(pool, base->wr_url) : NULL;
  rsrc->name = base->name ?
    apr_pstrdup(pool, base->name) : NULL;
  rsrc->pool = pool;
  return rsrc;
}

/* Get the version resource URL for RSRC, storing it in
   RSRC->vsn_url.  Use POOL for all temporary allocations. */
static svn_error_t * get_version_url(commit_ctx_t *cc,
                                     const char *local_relpath,
                                     const version_rsrc_t *parent,
                                     version_rsrc_t *rsrc,
                                     svn_boolean_t force,
                                     apr_pool_t *pool)
{
  svn_ra_neon__resource_t *propres;
  const char *url;
  const svn_string_t *url_str;

  if (!force)
    {
      if (cc->ras->callbacks->get_wc_prop != NULL)
        {
          const svn_string_t *vsn_url_value;

          SVN_ERR(cc->ras->callbacks->get_wc_prop(cc->ras->callback_baton,
                                                  local_relpath,
                                                  SVN_RA_NEON__LP_VSN_URL,
                                                  &vsn_url_value,
                                                  pool));
          if (vsn_url_value != NULL)
            {
              rsrc->vsn_url = apr_pstrdup(rsrc->pool, vsn_url_value->data);
              return SVN_NO_ERROR;
            }
        }

      /* If we know the version resource URL of the parent and it is
         the same revision as RSRC, use that as a base to calculate
         the version resource URL of RSRC. */
      if (parent && parent->vsn_url && parent->revision == rsrc->revision)
        {
          rsrc->vsn_url = svn_path_url_add_component2(parent->vsn_url,
                                                      rsrc->name,
                                                      rsrc->pool);
          return SVN_NO_ERROR;
        }

      /* whoops. it wasn't there. go grab it from the server. */
    }

  if (rsrc->revision == SVN_INVALID_REVNUM)
    {
      /* We aren't trying to get a specific version -- use the HEAD. We
         fetch the version URL from the public URL. */
      url = rsrc->url;
    }
  else
    {
      const char *bc_url;
      const char *bc_relative;

      /* The version URL comes from a resource in the Baseline Collection. */
      SVN_ERR(svn_ra_neon__get_baseline_info(&bc_url, &bc_relative, NULL,
                                             cc->ras, rsrc->url,
                                             rsrc->revision, pool));
      url = svn_path_url_add_component2(bc_url, bc_relative, pool);
    }

  /* Get the DAV:checked-in property, which contains the URL of the
     Version Resource */
  SVN_ERR(svn_ra_neon__get_props_resource(&propres, cc->ras, url,
                                          NULL, fetch_props, pool));
  url_str = apr_hash_get(propres->propset,
                         SVN_RA_NEON__PROP_CHECKED_IN,
                         APR_HASH_KEY_STRING);
  if (url_str == NULL)
    {
      /* ### need a proper SVN_ERR here */
      return svn_error_create(APR_EGENERAL, NULL,
                              _("Could not fetch the Version Resource URL "
                                "(needed during an import or when it is "
                                "missing from the local, cached props)"));
    }

  /* ensure we get the proper lifetime for this URL since it is going into
     a resource object. */
  rsrc->vsn_url = apr_pstrdup(rsrc->pool, url_str->data);

  if (cc->ras->callbacks->push_wc_prop != NULL)
    {
      /* Now we can store the new version-url. */
      SVN_ERR(cc->ras->callbacks->push_wc_prop(cc->ras->callback_baton,
                                               local_relpath,
                                               SVN_RA_NEON__LP_VSN_URL,
                                               url_str,
                                               pool));
    }

  return SVN_NO_ERROR;
}

/* When FORCE is true, then we force a query to the server, ignoring any
   cached property. */
static svn_error_t * get_activity_collection(commit_ctx_t *cc,
                                             const svn_string_t **collection,
                                             svn_boolean_t force,
                                             apr_pool_t *pool)
{
  if (!force && cc->ras->callbacks->get_wc_prop != NULL)
    {
      /* with a get_wc_prop, we can just ask for the activity URL from the
         property store. */

      /* get the URL where we should create activities */
      SVN_ERR(cc->ras->callbacks->get_wc_prop(cc->ras->callback_baton,
                                              "",
                                              SVN_RA_NEON__LP_ACTIVITY_COLL,
                                              collection,
                                              pool));

      if (*collection != NULL)
        {
          /* the property was there. return it. */
          return SVN_NO_ERROR;
        }

      /* property not found for some reason. get it from the server. */
    }

  /* use our utility function to fetch the activity URL */
  SVN_ERR(svn_ra_neon__get_activity_collection(collection,
                                               cc->ras,
                                               pool));

  if (cc->ras->callbacks->push_wc_prop != NULL)
    {
      /* save the (new) activity collection URL into the directory */
      SVN_ERR(cc->ras->callbacks->push_wc_prop(cc->ras->callback_baton,
                                               "",
                                               SVN_RA_NEON__LP_ACTIVITY_COLL,
                                               *collection,
                                               pool));
    }

  return SVN_NO_ERROR;
}

static svn_error_t * create_activity(commit_ctx_t *cc,
                                     apr_pool_t *pool)
{
  const svn_string_t * activity_collection;
  const char *uuid_buf = svn_uuid_generate(pool);
  int code;
  const char *url;

  /* get the URL where we'll create activities, construct the URL for
     the activity, and create the activity.  The URL for our activity
     will be ACTIVITY_COLL/UUID */
  SVN_ERR(get_activity_collection(cc, &activity_collection, FALSE, pool));
  url = svn_path_url_add_component2(activity_collection->data,
                                    uuid_buf, pool);
  SVN_ERR(svn_ra_neon__simple_request(&code, cc->ras,
                                      "MKACTIVITY", url, NULL, NULL,
                                      201 /* Created */,
                                      404 /* Not Found */, pool));

  /* if we get a 404, then it generally means that the cached activity
     collection no longer exists. Retry the sequence, but force a query
     to the server for the activity collection. */
  if (code == 404)
    {
      SVN_ERR(get_activity_collection(cc, &activity_collection, TRUE, pool));
      url = svn_path_url_add_component2(activity_collection->data,
                                        uuid_buf, pool);
      SVN_ERR(svn_ra_neon__simple_request(&code, cc->ras,
                                          "MKACTIVITY", url, NULL, NULL,
                                          201, 0, pool));
    }

  cc->activity_url = apr_pstrdup(cc->pool, url);

  return SVN_NO_ERROR;
}

/* Add a child resource.  POOL should be as "temporary" as possible,
   but probably not as far as requiring a new temp pool. */
static svn_error_t * add_child(version_rsrc_t **child,
                               commit_ctx_t *cc,
                               const version_rsrc_t *parent,
                               const char *parent_local_relpath,
                               const char *name,
                               int created,
                               svn_revnum_t revision,
                               apr_pool_t *pool)
{
  version_rsrc_t *rsrc;

  /* ### todo:  This from Yoshiki Hayashi <yoshiki@xemacs.org>:

     Probably created flag in add_child can be removed because
        revision is valid => created is false
        revision is invalid => created is true
  */

  rsrc = apr_pcalloc(pool, sizeof(*rsrc));
  rsrc->pool = pool;
  rsrc->revision = revision;
  rsrc->name = name;
  rsrc->url = svn_path_url_add_component2(parent->url, name, pool);

  /* Case 1:  the resource is truly "new".  Either it was added as a
     completely new object, or implicitly created via a COPY.  Either
     way, it has no VR URL anywhere.  However, we *can* derive its WR
     URL by the rules of deltaV:  "copy structure is preserved below
     the WR you COPY to."  */
  if (created || (parent->vsn_url == NULL))
    {
      rsrc->wr_url = svn_path_url_add_component2(parent->wr_url, name, pool);
    }
  /* Case 2: the resource is already under version-control somewhere.
     This means it has a VR URL already, and the WR URL won't exist
     until it's "checked out". */
  else
    {
      SVN_ERR(get_version_url(cc, svn_relpath_join(parent_local_relpath,
                                                   name, pool),
                              parent, rsrc, FALSE, pool));
    }

  *child = rsrc;
  return SVN_NO_ERROR;
}


static svn_error_t * do_checkout(commit_ctx_t *cc,
                                 const char *vsn_url,
                                 svn_boolean_t allow_404,
                                 const char *token,
                                 svn_boolean_t is_vcc,
                                 int *code,
                                 const char **locn,
                                 apr_pool_t *pool)
{
  svn_ra_neon__request_t *request;
  const char *body;
  apr_hash_t *extra_headers = NULL;
  svn_error_t *err = SVN_NO_ERROR;

  /* assert: vsn_url != NULL */

  /* ### send a CHECKOUT resource on vsn_url; include cc->activity_url;
     ### place result into res->wr_url and return it */

  /* create/prep the request */
  SVN_ERR(svn_ra_neon__request_create(&request, cc->ras, "CHECKOUT", vsn_url,
                                      pool));

  /* ### store this into cc to avoid pool growth */
  body = apr_psprintf(request->pool,
                      "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
                      "<D:checkout xmlns:D=\"DAV:\">"
                      "<D:activity-set>"
                      "<D:href>%s</D:href>"
                      "</D:activity-set>%s</D:checkout>",
                      cc->activity_url,
                      is_vcc ? APPLY_TO_VERSION: "");

  if (token)
    {
      extra_headers = apr_hash_make(request->pool);
      svn_ra_neon__set_header(extra_headers, "If",
                              apr_psprintf(request->pool, "(<%s>)", token));
    }

  /* run the request and get the resulting status code (and svn_error_t) */
  err = svn_ra_neon__request_dispatch(code, request, extra_headers, body,
                                      201 /* Created */,
                                      allow_404 ? 404 /* Not Found */ : 0,
                                      pool);
  if (err)
    goto cleanup;

  if (allow_404 && *code == 404 && request->err)
    {
      svn_error_clear(request->err);
      request->err = SVN_NO_ERROR;
    }

  *locn = svn_ra_neon__request_get_location(request, pool);

 cleanup:
  svn_ra_neon__request_destroy(request);

  return err;
}


static svn_error_t * checkout_resource(commit_ctx_t *cc,
                                       const char *local_relpath,
                                       version_rsrc_t *rsrc,
                                       svn_boolean_t allow_404,
                                       const char *token,
                                       svn_boolean_t is_vcc,
                                       apr_pool_t *pool)
{
  int code;
  const char *locn = NULL;
  ne_uri parse;
  svn_error_t *err;

  if (rsrc->wr_url != NULL)
    {
      /* already checked out! */
      return NULL;
    }

  /* check out the Version Resource */
  err = do_checkout(cc, rsrc->vsn_url, allow_404, token,
                    is_vcc, &code, &locn, pool);

  /* possibly run the request again, with a re-fetched Version Resource */
  if (err == NULL && allow_404 && code == 404)
    {
      locn = NULL;

      /* re-fetch, forcing a query to the server */
      SVN_ERR(get_version_url(cc, local_relpath, NULL, rsrc, TRUE, pool));

      /* do it again, but don't allow a 404 this time */
      err = do_checkout(cc, rsrc->vsn_url, FALSE, token,
                        is_vcc, &code, &locn, pool);
    }

  /* special-case when conflicts occur */
  if (err)
    {
      /* ### TODO: it's a shame we don't have the full path from the
         ### root of the drive here, nor the type of the resource.
         ### Because we lack this information, the error message is
         ### overly generic.  See issue #2740. */
      if (err->apr_err == SVN_ERR_FS_CONFLICT)
        return svn_error_createf
          (SVN_ERR_FS_OUT_OF_DATE, err,
           _("File or directory '%s' is out of date; try updating"),
           local_relpath);
      return err;
    }

  /* we got the header, right? */
  if (locn == NULL)
    return svn_error_create(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL,
                            _("The CHECKOUT response did not contain a "
                              "'Location:' header"));

  /* The location is an absolute URI. We want just the path portion. */
  /* ### what to do with the rest? what if it points somewhere other
     ### than the current session? */
  if (ne_uri_parse(locn, &parse) != 0)
    {
      ne_uri_free(&parse);
      return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL,
                               _("Unable to parse URL '%s'"), locn);
    }

  rsrc->wr_url = apr_pstrdup(rsrc->pool, parse.path);
  ne_uri_free(&parse);

  return SVN_NO_ERROR;
}

static void record_prop_change(apr_pool_t *pool,
                               resource_baton_t *r,
                               const char *name,
                               const svn_string_t *value)
{
  /* copy the name into the pool so we get the right lifetime (who knows
     what the caller will do with it) */
  name = apr_pstrdup(pool, name);

  if (value)
    {
      /* changed/new property */
      if (r->prop_changes == NULL)
        r->prop_changes = apr_hash_make(pool);
      apr_hash_set(r->prop_changes, name, APR_HASH_KEY_STRING,
                   svn_string_dup(value, pool));
    }
  else
    {
      /* deleted property. */
      if (r->prop_deletes == NULL)
        r->prop_deletes = apr_array_make(pool, 5, sizeof(char *));
      APR_ARRAY_PUSH(r->prop_deletes, const char *) = name;
    }
}

/* Send a Neon COPY request to the location identified by
   COPYFROM_PATH and COPYFROM_REVISION, using COPY_DST_URL as the
   "Destination" of that copy. */
static svn_error_t * copy_resource(svn_ra_neon__session_t *ras,
                                   const char *copyfrom_path,
                                   svn_revnum_t copyfrom_revision,
                                   const char *copy_dst_url,
                                   apr_pool_t *scratch_pool)
{
  const char *bc_url;
  const char *bc_relative;
  const char *copy_src_url;

  SVN_ERR(svn_ra_neon__get_baseline_info(&bc_url, &bc_relative, NULL,
                                         ras, copyfrom_path,
                                         copyfrom_revision, scratch_pool));
  copy_src_url = svn_path_url_add_component2(bc_url, bc_relative,
                                             scratch_pool);
  SVN_ERR(svn_ra_neon__copy(ras, 1 /* overwrite */, SVN_RA_NEON__DEPTH_INFINITE,
                            copy_src_url, copy_dst_url, scratch_pool));

  return SVN_NO_ERROR;
}


static svn_error_t * do_proppatch(resource_baton_t *rb,
                                  apr_pool_t *pool)
{
  apr_hash_t *extra_headers = apr_hash_make(pool);
  const char *proppatch_target =
    USING_HTTPV2_COMMIT_SUPPORT(rb->cc) ? rb->txn_root_url : rb->rsrc->wr_url;

  if (SVN_IS_VALID_REVNUM(rb->base_revision))
    svn_ra_neon__set_header(extra_headers, SVN_DAV_VERSION_NAME_HEADER,
                            apr_psprintf(pool, "%ld", rb->base_revision));

  if (rb->token)
    apr_hash_set(extra_headers, "If", APR_HASH_KEY_STRING,
                 apr_psprintf(pool, "(<%s>)", rb->token));

  return svn_error_trace(svn_ra_neon__do_proppatch(rb->cc->ras,
                                                   proppatch_target,
                                                   rb->prop_changes,
                                                   rb->prop_deletes,
                                                   NULL, extra_headers,
                                                   pool));
}


static void
add_valid_target(commit_ctx_t *cc,
                 const char *path,
                 enum svn_recurse_kind kind)
{
  apr_hash_t *hash = cc->valid_targets;
  svn_string_t *path_str = svn_string_create(path, apr_hash_pool_get(hash));
  apr_hash_set(hash, path_str->data, path_str->len, (void*)kind);
}


/* Helper func for commit_delete_entry.  Find all keys in LOCK_TOKENS
   which are children of DIR.  Returns the keys (and their vals) in
   CHILD_TOKENS.   No keys or values are reallocated or dup'd.  If no
   keys are children, then return an empty hash.  Use POOL to allocate
   new hash. */
static apr_hash_t *get_child_tokens(apr_hash_t *lock_tokens,
                                    const char *dir,
                                    apr_pool_t *pool)
{
  apr_hash_index_t *hi;
  apr_hash_t *tokens = apr_hash_make(pool);
  apr_pool_t *subpool = svn_pool_create(pool);

  for (hi = apr_hash_first(pool, lock_tokens); hi; hi = apr_hash_next(hi))
    {
      const void *key;
      apr_ssize_t klen;
      void *val;

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

      if (svn_relpath__is_child(dir, key, subpool))
        apr_hash_set(tokens, key, klen, val);
    }

  svn_pool_destroy(subpool);
  return tokens;
}


/* PROPPATCH the appropriate resource in order to set the revision
   properties in CC->REVPROP_TABLE on the commit transaction.  Use
   POOL for temporary allocations.  */
static svn_error_t *
apply_revprops(commit_ctx_t *cc,
               apr_pool_t *pool)
{
  const char *proppatch_url;

  if (USING_HTTPV2_COMMIT_SUPPORT(cc))
    {
      proppatch_url = cc->txn_url;
    }
  else
    {
      const char *vcc;
      version_rsrc_t vcc_rsrc = { SVN_INVALID_REVNUM };
      svn_error_t *err = NULL;
      int retry_count = 5;

      /* Fetch the DAV:version-controlled-configuration from the
         session's URL.  */
      SVN_ERR(svn_ra_neon__get_vcc(&vcc, cc->ras, cc->ras->root.path, pool));

      do {
        svn_error_clear(err);

        vcc_rsrc.pool = pool;
        vcc_rsrc.vsn_url = vcc;

        /* To set the revision properties, we must checkout the latest
           baseline and get back a mutable "working" baseline.  */
        err = checkout_resource(cc, "", &vcc_rsrc, FALSE, NULL, TRUE, pool);

        /* There's a small chance of a race condition here, if apache
           is experiencing heavy commit concurrency or if the network
           has long latency.  It's possible that the value of HEAD
           changed between the time we fetched the latest baseline and
           the time we checkout that baseline.  If that happens,
           apache will throw us a BAD_BASELINE error (deltaV says you
           can only checkout the latest baseline).  We just ignore
           that specific error and retry a few times, asking for the
           latest baseline again. */
        if (err && err->apr_err != SVN_ERR_APMOD_BAD_BASELINE)
          return err;

      } while (err && (--retry_count > 0));

      /* Yikes, if we couldn't hold onto HEAD after a few retries, throw a
         real error.*/
      if (err)
        return err;

      proppatch_url = vcc_rsrc.wr_url;
    }

  return svn_error_trace(svn_ra_neon__do_proppatch(cc->ras, proppatch_url,
                                                   cc->revprop_table,
                                                   NULL, NULL, NULL, pool));
}


/*** Commit Editor Functions ***/

static svn_error_t * commit_open_root(void *edit_baton,
                                      svn_revnum_t base_revision,
                                      apr_pool_t *dir_pool,
                                      void **root_baton)
{
  commit_ctx_t *cc = edit_baton;
  resource_baton_t *root;
  version_rsrc_t *rsrc = NULL;

  root = apr_pcalloc(dir_pool, sizeof(*root));
  root->pool = dir_pool;
  root->cc = cc;
  root->base_revision = base_revision;
  root->created = FALSE;
  root->local_relpath = "";

  if (SVN_RA_NEON__HAVE_HTTPV2_SUPPORT(cc->ras))
    {
      /* POST to the 'me' resource to create our server-side
         transaction (and, optionally, a corresponding activity). */
      svn_ra_neon__request_t *req;
      const char *header_val;
      svn_error_t *err;

      SVN_ERR(svn_ra_neon__request_create(&req, cc->ras, "POST",
                                          cc->ras->me_resource, dir_pool));
      ne_add_request_header(req->ne_req, "Content-Type", SVN_SKEL_MIME_TYPE);

#ifdef SVN_DAV_SEND_VTXN_NAME
      /* Enable this to exercise the VTXN-NAME code based on a client
         supplied transaction name. */
      ne_add_request_header(req->ne_req, SVN_DAV_VTXN_NAME_HEADER,
                            svn_uuid_generate(dir_pool));
#endif

      err = svn_ra_neon__request_dispatch(NULL, req, NULL, "( create-txn )",
                                          201, 0, dir_pool);
      if (!err)
        {
          /* Check the response headers for either the virtual transaction
             details, or the real transaction details.  We need to have
             one or the other of those!  */
          if ((header_val = ne_get_response_header(req->ne_req,
                                                   SVN_DAV_VTXN_NAME_HEADER)))
            {
              cc->txn_url = svn_path_url_add_component2(cc->ras->vtxn_stub,
                                                        header_val, cc->pool);
              cc->txn_root_url
                = svn_path_url_add_component2(cc->ras->vtxn_root_stub,
                                              header_val, cc->pool);
            }
          else if ((header_val
                    = ne_get_response_header(req->ne_req,
                                             SVN_DAV_TXN_NAME_HEADER)))
            {
              cc->txn_url = svn_path_url_add_component2(cc->ras->txn_stub,
                                                        header_val, cc->pool);
              cc->txn_root_url
                = svn_path_url_add_component2(cc->ras->txn_root_stub,
                                              header_val, cc->pool);
            }
          else
            err = svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL,
                                    _("POST request did not return transaction "
                                      "information"));
        }
      svn_ra_neon__request_destroy(req);
      SVN_ERR(err);

      root->rsrc = NULL;
      root->txn_root_url = svn_path_url_add_component2(cc->txn_root_url,
                                                       cc->anchor_relpath,
                                                       dir_pool);
    }
  else
    {
      /* Use MKACTIVITY against a unique child of activity collection
         to create a server-side activity which corresponds directly
         to an FS transaction.  We will check out all further
         resources within the context of this activity. */
      SVN_ERR(create_activity(cc, dir_pool));

      /* Create the root resource.  (We don't yet know the wr_url.) */
      rsrc = apr_pcalloc(dir_pool, sizeof(*rsrc));
      rsrc->pool = dir_pool;
      rsrc->revision = base_revision;
      rsrc->url = cc->ras->root.path;

      SVN_ERR(get_version_url(cc, root->local_relpath, NULL, rsrc,
                              FALSE, dir_pool));

      root->rsrc = rsrc;
      root->txn_root_url = NULL;
    }

  /* Find the latest baseline resource, check it out, and then apply
     the log message onto the thing. */
  SVN_ERR(apply_revprops(cc, dir_pool));


  *root_baton = root;

  return SVN_NO_ERROR;
}


static svn_error_t * commit_delete_entry(const char *path,
                                         svn_revnum_t revision,
                                         void *parent_baton,
                                         apr_pool_t *pool)
{
  resource_baton_t *parent = parent_baton;
  const char *name = svn_relpath_basename(path, NULL);
  apr_hash_t *extra_headers = apr_hash_make(pool);
  int code;
  svn_error_t *serr;
  const char *delete_target;

  if (USING_HTTPV2_COMMIT_SUPPORT(parent->cc))
    {
      delete_target = svn_path_url_add_component2(parent->txn_root_url,
                                                  name, pool);
    }
  else
    {
      /* Get the URL to the working collection.  */
      SVN_ERR(checkout_resource(parent->cc, parent->local_relpath,
                                parent->rsrc, TRUE, NULL, FALSE, pool));

      /* Create the URL for the child resource. */
      delete_target = svn_path_url_add_component2(parent->rsrc->wr_url,
                                                  name, pool);
    }

  /* If we have a base revision for the server to compare against,
     pass that along in a custom header. */
  if (SVN_IS_VALID_REVNUM(revision))
    svn_ra_neon__set_header(extra_headers, SVN_DAV_VERSION_NAME_HEADER,
                            apr_psprintf(pool, "%ld", revision));

  /* We start out assuming that we're deleting a file; try to lookup
     the path itself in the token-hash, and if found, attach it to the
     If: header. */
  if (parent->cc->lock_tokens)
    {
      const char *token =
        apr_hash_get(parent->cc->lock_tokens, path, APR_HASH_KEY_STRING);

      if (token)
        {
          const char *token_header_val;
          const char *token_uri;

          token_uri = svn_path_url_add_component2(parent->cc->ras->url->data,
                                                  path, pool);
          token_header_val = apr_psprintf(pool, "<%s> (<%s>)",
                                          token_uri, token);
          extra_headers = apr_hash_make(pool);
          apr_hash_set(extra_headers, "If", APR_HASH_KEY_STRING,
                       token_header_val);
        }
    }

  /* dav_method_delete() always calls dav_unlock(), but if the svn
     client passed --no-unlock to 'svn commit', then we need to send a
     header which prevents mod_dav_svn from actually doing the unlock. */
  if (parent->cc->keep_locks)
    {
      apr_hash_set(extra_headers, SVN_DAV_OPTIONS_HEADER,
                   APR_HASH_KEY_STRING, SVN_DAV_OPTION_KEEP_LOCKS);
    }

  serr = svn_ra_neon__simple_request(&code, parent->cc->ras,
                                     "DELETE", delete_target,
                                     extra_headers, NULL,
                                     204 /* No Content */,
                                     0, pool);

  /* A locking-related error most likely means we were deleting a
     directory rather than a file, and didn't send all of the
     necessary lock-tokens within the directory. */
  if (serr && ((serr->apr_err == SVN_ERR_FS_BAD_LOCK_TOKEN)
               || (serr->apr_err == SVN_ERR_FS_NO_LOCK_TOKEN)
               || (serr->apr_err == SVN_ERR_FS_LOCK_OWNER_MISMATCH)
               || (serr->apr_err == SVN_ERR_FS_PATH_ALREADY_LOCKED)))
    {
      /* Re-attempt the DELETE request as if the path were a
         directory.  Discover all lock-tokens within the directory,
         and send them in the body of the request (which is normally
         empty).  Of course, if we don't *find* any additional
         lock-tokens, don't bother to retry (it ain't gonna do any
         good).

         Note that we're not sending the locks in the If: header, for
         the same reason we're not sending in MERGE's headers: httpd
         has limits on the amount of data it's willing to receive in
         headers. */

      apr_hash_t *child_tokens = NULL;
      svn_ra_neon__request_t *request;
      const char *body;
      const char *token;
      svn_stringbuf_t *locks_list;
      svn_error_t *err = SVN_NO_ERROR;

      if (parent->cc->lock_tokens)
        child_tokens = get_child_tokens(parent->cc->lock_tokens, path, pool);

      /* No kiddos?  Return the original error.  Else, clear it so it
         doesn't get leaked.  */
      if ((! child_tokens) || (apr_hash_count(child_tokens) == 0))
        return serr;
      else
        svn_error_clear(serr);

      /* In preparation of directory locks, go ahead and add the actual
         target's lock token to those of its children. */
      if ((token = apr_hash_get(parent->cc->lock_tokens, path,
                                APR_HASH_KEY_STRING)))
        {
          /* ### copy PATH into the right pool. which?  */
          apr_hash_set(child_tokens, path, APR_HASH_KEY_STRING, token);
        }

      SVN_ERR(svn_ra_neon__request_create(&request, parent->cc->ras, "DELETE",
                                          delete_target, pool));

      err = svn_ra_neon__assemble_locktoken_body(&locks_list,
                                                 child_tokens, request->pool);
      if (err)
        goto cleanup;

      body = apr_psprintf(request->pool,
                          "<?xml version=\"1.0\" encoding=\"utf-8\"?> %s",
                          locks_list->data);

      err = svn_ra_neon__request_dispatch(&code, request, NULL, body,
                                          204 /* Created */,
                                          404 /* Not Found */,
                                          pool);
    cleanup:
      svn_ra_neon__request_destroy(request);
      SVN_ERR(err);
    }
  else if (serr)
    return serr;

  /* Add this path to the valid targets hash. */
  add_valid_target(parent->cc, path, svn_nonrecursive);

  return SVN_NO_ERROR;
}


static svn_error_t * commit_add_dir(const char *path,
                                    void *parent_baton,
                                    const char *copyfrom_path,
                                    svn_revnum_t copyfrom_revision,
                                    apr_pool_t *dir_pool,
                                    void **child_baton)
{
  resource_baton_t *parent = parent_baton;
  resource_baton_t *child;
  int code;
  const char *name = svn_relpath_basename(path, dir_pool);
  apr_pool_t *workpool = svn_pool_create(dir_pool);
  version_rsrc_t *rsrc = NULL;
  const char *mkcol_target = NULL;

  /* create a child object that contains all the resource urls */
  child = apr_pcalloc(dir_pool, sizeof(*child));
  child->pool = dir_pool;
  child->base_revision = SVN_INVALID_REVNUM;
  child->cc = parent->cc;
  child->created = TRUE;
  child->local_relpath = svn_relpath_join(parent->local_relpath,
                                          name, dir_pool);

  if (USING_HTTPV2_COMMIT_SUPPORT(parent->cc))
    {
      child->rsrc = NULL;
      child->txn_root_url = svn_path_url_add_component2(parent->txn_root_url,
                                                        name, dir_pool);
      mkcol_target = child->txn_root_url;
    }
  else
    {
      /* check out the parent resource so that we can create the new collection
         as one of its children. */
      SVN_ERR(checkout_resource(parent->cc, parent->local_relpath,
                                parent->rsrc, TRUE, NULL, FALSE, dir_pool));
      SVN_ERR(add_child(&rsrc, parent->cc, parent->rsrc, parent->local_relpath,
                        name, 1, SVN_INVALID_REVNUM, workpool));

      child->rsrc = dup_resource(rsrc, dir_pool);
      child->txn_root_url = NULL;
      mkcol_target = child->rsrc->wr_url;
    }

  if (! copyfrom_path)
    {
      /* This a new directory with no history, so just create a new,
         empty collection */
      SVN_ERR(svn_ra_neon__simple_request(&code, parent->cc->ras, "MKCOL",
                                          mkcol_target, NULL, NULL,
                                          201 /* Created */, 0, workpool));
    }
  else
    {
      /* This add has history, so we need to do a COPY. */
      SVN_ERR(copy_resource(parent->cc->ras, copyfrom_path, copyfrom_revision,
                            mkcol_target, workpool));

      /* Remember that this object was copied. */
      child->copied = TRUE;
    }

  /* Add this path to the valid targets hash. */
  add_valid_target(parent->cc, path,
                   copyfrom_path ? svn_recursive : svn_nonrecursive);

  svn_pool_destroy(workpool);
  *child_baton = child;
  return SVN_NO_ERROR;
}

static svn_error_t * commit_open_dir(const char *path,
                                     void *parent_baton,
                                     svn_revnum_t base_revision,
                                     apr_pool_t *dir_pool,
                                     void **child_baton)
{
  resource_baton_t *parent = parent_baton;
  resource_baton_t *child = apr_pcalloc(dir_pool, sizeof(*child));
  const char *name = svn_relpath_basename(path, dir_pool);
  version_rsrc_t *rsrc = NULL;

  child->pool = dir_pool;
  child->base_revision = base_revision;
  child->cc = parent->cc;
  child->created = FALSE;
  child->local_relpath = svn_relpath_join(parent->local_relpath,
                                          name, dir_pool);

  if (USING_HTTPV2_COMMIT_SUPPORT(parent->cc))
    {
      child->rsrc = NULL;
      child->txn_root_url = svn_path_url_add_component2(parent->txn_root_url,
                                                        name, dir_pool);
    }
  else
    {
      apr_pool_t *workpool = svn_pool_create(dir_pool);

      SVN_ERR(add_child(&rsrc, parent->cc, parent->rsrc, parent->local_relpath,
                        name, 0, base_revision, workpool));
      child->rsrc = dup_resource(rsrc, dir_pool);
      child->txn_root_url = NULL;

      svn_pool_destroy(workpool);
    }

  /* We don't do any real work here -- open_dir() just sets up the
     baton for this directory for use later when we operate on one of
     its children. */
  *child_baton = child;
  return SVN_NO_ERROR;
}

static svn_error_t * commit_change_dir_prop(void *dir_baton,
                                            const char *name,
                                            const svn_string_t *value,
                                            apr_pool_t *pool)
{
  resource_baton_t *dir = dir_baton;

  /* record the change. it will be applied at close_dir time. */
  record_prop_change(dir->pool, dir, name, value);

  if (! USING_HTTPV2_COMMIT_SUPPORT(dir->cc))
    {
      /* We might as well CHECKOUT now (if we haven't already).  Why
         wait?  */
      SVN_ERR(checkout_resource(dir->cc, dir->local_relpath, dir->rsrc,
                                TRUE, NULL, FALSE, pool));
    }

    /* Add the path to the valid targets hash. */
  add_valid_target(dir->cc, dir->local_relpath, svn_nonrecursive);

  return SVN_NO_ERROR;
}

static svn_error_t * commit_close_dir(void *dir_baton,
                                      apr_pool_t *pool)
{
  resource_baton_t *dir = dir_baton;

  /* Perform all of the property changes on the directory. Note that we
     checked out the directory when the first prop change was noted. */
  return do_proppatch(dir, pool);
}

static svn_error_t * commit_add_file(const char *path,
                                     void *parent_baton,
                                     const char *copyfrom_path,
                                     svn_revnum_t copyfrom_revision,
                                     apr_pool_t *file_pool,
                                     void **file_baton)
{
  resource_baton_t *parent = parent_baton;
  resource_baton_t *file;
  const char *name = svn_relpath_basename(path, file_pool);
  apr_pool_t *workpool = svn_pool_create(file_pool);
  const char *put_target = NULL;

  /*
  ** To add a new file into the repository, we CHECKOUT the parent
  ** collection, then PUT the file as a member of the resulting working
  ** collection.
  **
  ** If the file was copied from elsewhere, then we will use the COPY
  ** method to copy into the working collection.
  */

  /* Construct a file_baton that contains all the resource urls. */
  file = apr_pcalloc(file_pool, sizeof(*file));
  file->base_revision = SVN_INVALID_REVNUM;
  file->pool = file_pool;
  file->cc = parent->cc;
  file->created = TRUE;
  file->local_relpath = svn_relpath_join(parent->local_relpath,
                                         name, file_pool);

  if (USING_HTTPV2_COMMIT_SUPPORT(parent->cc))
    {
      file->rsrc = NULL;
      file->txn_root_url = svn_path_url_add_component2(parent->txn_root_url,
                                                       name, file_pool);
      put_target = file->txn_root_url;
    }
  else
    {
      version_rsrc_t *rsrc = NULL;

      /* Do the parent CHECKOUT first */
      SVN_ERR(checkout_resource(parent->cc, parent->local_relpath, parent->rsrc,
                                TRUE, NULL, FALSE, workpool));
      SVN_ERR(add_child(&rsrc, parent->cc, parent->rsrc, parent->local_relpath,
                        name, 1, SVN_INVALID_REVNUM, workpool));

      file->rsrc = dup_resource(rsrc, file_pool);
      file->txn_root_url = NULL;
      put_target = file->rsrc->wr_url;
    }

  if (parent->cc->lock_tokens)
    file->token = apr_hash_get(parent->cc->lock_tokens, path,
                               APR_HASH_KEY_STRING);

  /* If the parent directory existed before this commit then there may
     be a file with this URL already. We need to ensure such a file
     does not exist, which we do by attempting a PROPFIND in both
     public URL (the path in HEAD) and the working URL (the path
     within the transaction), since we cannot differentiate between
     deleted items.

     ### For now, we'll assume that if this path has already been
     ### added to the valid targets hash, that addition occurred
     ### during the "delete" phase (if that's not the case, this
     ### editor is being driven incorrectly, as we should never visit
     ### the same path twice except in a delete+add situation). */
  if ((! parent->created)
      && (! apr_hash_get(file->cc->valid_targets, path, APR_HASH_KEY_STRING)))
    {
      static const ne_propname restype_props[] =
      {
        { "DAV:", "resourcetype" },
        { NULL }
      };
      svn_ra_neon__resource_t *res;
      const char *public_url;
      svn_error_t *err1, *err2;

      public_url = svn_path_url_add_component2(file->cc->ras->url->data,
                                               path, workpool);
      err1 = svn_ra_neon__get_props_resource(&res, parent->cc->ras, put_target,
                                             NULL, restype_props, workpool);
      err2 = svn_ra_neon__get_props_resource(&res, parent->cc->ras, public_url,
                                             NULL, restype_props, workpool);
      if (! err1 && ! err2)
        {
          /* If the PROPFINDs succeed the file already exists */
          return svn_error_createf(SVN_ERR_RA_DAV_ALREADY_EXISTS, NULL,
                                   _("File '%s' already exists"), path);
        }
      else if ((err1 && (err1->apr_err == SVN_ERR_FS_NOT_FOUND))
               || (err2 && (err2->apr_err == SVN_ERR_FS_NOT_FOUND)))
        {
          svn_error_clear(err1);
          svn_error_clear(err2);
        }
      else
        {
          /* A real error */
          return svn_error_compose_create(err1, err2);
        }
    }

  if (! copyfrom_path)
    {
      /* This a truly new file. */

      /* Wait for apply_txdelta() before doing a PUT.  It might arrive
         a "long time" from now -- certainly after many other
         operations -- we don't want to start a PUT just yet. */
    }
  else
    {
      /* This add has history, so we need to do a COPY. */
      SVN_ERR(copy_resource(parent->cc->ras, copyfrom_path, copyfrom_revision,
                            put_target, workpool));

      /* Remember that this object was copied. */
      file->copied = TRUE;
    }

  /* Add this path to the valid targets hash. */
  add_valid_target(parent->cc, path, svn_nonrecursive);

  svn_pool_destroy(workpool);

  /* return the file_baton */
  *file_baton = file;
  return SVN_NO_ERROR;
}

static svn_error_t * commit_open_file(const char *path,
                                      void *parent_baton,
                                      svn_revnum_t base_revision,
                                      apr_pool_t *file_pool,
                                      void **file_baton)
{
  resource_baton_t *parent = parent_baton;
  resource_baton_t *file;
  const char *name = svn_relpath_basename(path, file_pool);

  file = apr_pcalloc(file_pool, sizeof(*file));
  file->pool = file_pool;
  file->base_revision = base_revision;
  file->cc = parent->cc;
  file->created = FALSE;
  file->local_relpath = svn_relpath_join(parent->local_relpath,
                                         name, file_pool);

  if (parent->cc->lock_tokens)
    file->token = apr_hash_get(parent->cc->lock_tokens, path,
                               APR_HASH_KEY_STRING);

  if (USING_HTTPV2_COMMIT_SUPPORT(parent->cc))
    {
      file->rsrc = NULL;
      file->txn_root_url = svn_path_url_add_component2(parent->txn_root_url,
                                                       name, file_pool);
    }
  else
    {
      version_rsrc_t *rsrc = NULL;
      apr_pool_t *workpool = svn_pool_create(file_pool);

      SVN_ERR(add_child(&rsrc, parent->cc, parent->rsrc, parent->local_relpath,
                        name, 0, base_revision, workpool));
      file->rsrc = dup_resource(rsrc, file_pool);
      file->txn_root_url = NULL;

      /* Do the CHECKOUT now.  */
      SVN_ERR(checkout_resource(parent->cc, file->local_relpath, file->rsrc,
                                TRUE, file->token, FALSE, workpool));

      svn_pool_destroy(workpool);
    }

  /* Wait for apply_txdelta() before doing a PUT.  It might arrive
     a "long time" from now -- certainly after many other
     operations -- we don't want to start a PUT just yet. */

  *file_baton = file;
  return SVN_NO_ERROR;
}

static svn_error_t * commit_stream_write(void *baton,
                                         const char *data,
                                         apr_size_t *len)
{
  put_baton_t *pb = baton;
  svn_ra_neon__session_t *ras = pb->ras;
  apr_status_t status;

  if (ras->callbacks && ras->callbacks->cancel_func)
    SVN_ERR(ras->callbacks->cancel_func(ras->callback_baton));

  /* drop the data into our temp file */
  status = apr_file_write_full(pb->tmpfile, data, *len, NULL);
  if (status)
    return svn_error_wrap_apr(status,
                              _("Could not write svndiff to temp file"));

  if (ras->progress_func)
    {
      pb->progress += *len;
      ras->progress_func(pb->progress, -1, ras->progress_baton, pb->pool);
    }

  return SVN_NO_ERROR;
}

static svn_error_t *
commit_apply_txdelta(void *file_baton,
                     const char *base_checksum,
                     apr_pool_t *pool,
                     svn_txdelta_window_handler_t *handler,
                     void **handler_baton)
{
  resource_baton_t *file = file_baton;
  put_baton_t *baton;
  svn_stream_t *stream;

  baton = apr_pcalloc(file->pool, sizeof(*baton));
  baton->ras = file->cc->ras;
  baton->pool = file->pool;
  file->put_baton = baton;

  if (base_checksum)
    baton->base_checksum = apr_pstrdup(file->pool, base_checksum);
  else
    baton->base_checksum = NULL;

  /* ### oh, hell. Neon's request body support is either text (a C string),
     ### or a FILE*. since we are getting binary data, we must use a FILE*
     ### for now. isn't that special? */
  SVN_ERR(svn_io_open_unique_file3(&baton->tmpfile, NULL, NULL,
                                   svn_io_file_del_on_pool_cleanup,
                                   file->pool, pool));

  stream = svn_stream_create(baton, pool);
  svn_stream_set_write(stream, commit_stream_write);

  svn_txdelta_to_svndiff2(handler, handler_baton, stream, 0, pool);

  /* Add this path to the valid targets hash. */
  add_valid_target(file->cc, file->local_relpath, svn_nonrecursive);

  return SVN_NO_ERROR;
}

static svn_error_t * commit_change_file_prop(void *file_baton,
                                             const char *name,
                                             const svn_string_t *value,
                                             apr_pool_t *pool)
{
  resource_baton_t *file = file_baton;

  /* Record the change.  It will be applied at close_file() time. */
  record_prop_change(file->pool, file, name, value);

  if (! USING_HTTPV2_COMMIT_SUPPORT(file->cc))
    {
      /* We might as well CHECKOUT now (if we haven't already).  Why
         wait?  */
      SVN_ERR(checkout_resource(file->cc, file->local_relpath, file->rsrc,
                                TRUE, file->token, FALSE, pool));
    }

    /* Add the path to the valid targets hash. */
  add_valid_target(file->cc, file->local_relpath, svn_nonrecursive);

  return SVN_NO_ERROR;
}

static svn_error_t * commit_close_file(void *file_baton,
                                       const char *text_checksum,
                                       apr_pool_t *pool)
{
  resource_baton_t *file = file_baton;
  commit_ctx_t *cc = file->cc;

  /* If this is a newly added file, not copied, and the editor driver
     didn't call apply_textdelta(), then we'll pretend they *did* call
     apply_textdelta() and described a zero-byte empty file. */
  if ((! file->put_baton) && file->created && (! file->copied))
    {
      /* Make a dummy put_baton, with NULL fields to indicate that
         we're dealing with a content-less (zero-byte) file. */
      file->put_baton = apr_pcalloc(file->pool, sizeof(*(file->put_baton)));
    }

  if (file->put_baton)
    {
      svn_error_t *err = SVN_NO_ERROR;
      put_baton_t *pb = file->put_baton;
      apr_hash_t *extra_headers;
      svn_ra_neon__request_t *request;
      int code;
      const char *public_url =
        svn_path_url_add_component2(file->cc->ras->url->data,
                                    file->local_relpath, pool);
      const char *put_target =
        USING_HTTPV2_COMMIT_SUPPORT(cc) ? file->txn_root_url
                                        : file->rsrc->wr_url;

      /* create/prep the request */
      SVN_ERR(svn_ra_neon__request_create(&request, cc->ras, "PUT",
                                          put_target, pool));

      extra_headers = apr_hash_make(request->pool);

      if (file->token)
        svn_ra_neon__set_header(extra_headers, "If",
                                apr_psprintf(pool, "<%s> (<%s>)",
                                             public_url, file->token));

      if (pb->base_checksum)
        svn_ra_neon__set_header(extra_headers,
                                SVN_DAV_BASE_FULLTEXT_MD5_HEADER,
                                pb->base_checksum);

      if (text_checksum)
        svn_ra_neon__set_header(extra_headers,
                                SVN_DAV_RESULT_FULLTEXT_MD5_HEADER,
                                text_checksum);

      if (SVN_IS_VALID_REVNUM(file->base_revision))
        svn_ra_neon__set_header(extra_headers,
                                SVN_DAV_VERSION_NAME_HEADER,
                                apr_psprintf(pool, "%ld", file->base_revision));

      if (pb->tmpfile)
        {
          svn_ra_neon__set_header(extra_headers, "Content-Type",
                                  SVN_SVNDIFF_MIME_TYPE);

          /* Give the file to neon. The provider will rewind the file. */
          err = svn_ra_neon__set_neon_body_provider(request, pb->tmpfile);
          if (err)
            goto cleanup;
        }
      else
        {
          ne_set_request_body_buffer(request->ne_req, "", 0);
        }

      /* run the request and get the resulting status code (and svn_error_t) */
      err = svn_ra_neon__request_dispatch(&code, request, extra_headers, NULL,
                                          201 /* Created */,
                                          204 /* No Content */,
                                          pool);

      if (err && (err->apr_err == SVN_ERR_RA_DAV_REQUEST_FAILED))
        {
          switch (code)
          {
            case 423:
               err = svn_error_createf(SVN_ERR_RA_NOT_LOCKED, err,
                                       _("No lock on path '%s'"
                                         " (Status %d on PUT Request)"),
                                       put_target, code);
            default:
              break;
          }
      }
    cleanup:
      svn_ra_neon__request_destroy(request);
      SVN_ERR(err);

      if (pb->tmpfile)
        {
          /* We're done with the file.  this should delete it. Note: it
             isn't a big deal if this line is never executed -- the pool
             will eventually get it. We're just being proactive here. */
          (void) apr_file_close(pb->tmpfile);
        }
    }

  /* Perform all of the property changes on the file. Note that we
     checked out the file when the first prop change was noted. */
  return do_proppatch(file, pool);
}


static svn_error_t * commit_close_edit(void *edit_baton,
                                       apr_pool_t *pool)
{
  commit_ctx_t *cc = edit_baton;
  svn_commit_info_t *commit_info = svn_create_commit_info(pool);
  const char *merge_resource_url =
    USING_HTTPV2_COMMIT_SUPPORT(cc) ? cc->txn_url : cc->activity_url;

  SVN_ERR(svn_ra_neon__merge_activity(&(commit_info->revision),
                                      &(commit_info->date),
                                      &(commit_info->author),
                                      &(commit_info->post_commit_err),
                                      cc->ras,
                                      cc->ras->root.path,
                                      merge_resource_url,
                                      cc->valid_targets,
                                      cc->lock_tokens,
                                      cc->keep_locks,
                                      cc->ras->callbacks->push_wc_prop == NULL,
                                      pool));

  /* DELETE any activity that might be left on the server. */
  if (cc->activity_url)
    {
      SVN_ERR(svn_ra_neon__simple_request(NULL, cc->ras, "DELETE",
                                          cc->activity_url, NULL, NULL,
                                          204 /* No Content */,
                                          404 /* Not Found */, pool));
    }

  if (cc->callback && commit_info->revision != SVN_INVALID_REVNUM)
    SVN_ERR(cc->callback(commit_info, cc->callback_baton, pool));

  return SVN_NO_ERROR;
}


static svn_error_t * commit_abort_edit(void *edit_baton,
                                       apr_pool_t *pool)
{
  commit_ctx_t *cc = edit_baton;
  const char *delete_target = NULL;

  /* If we started an activity and/or transaction, we need to (try to)
     delete it.  (If it doesn't exist, that's okay, too.) */
  if (USING_HTTPV2_COMMIT_SUPPORT(cc))
    delete_target = cc->txn_url;
  else
    delete_target = cc->activity_url;

  if (delete_target)
    SVN_ERR(svn_ra_neon__simple_request(NULL, cc->ras, "DELETE",
                                        delete_target, NULL, NULL,
                                        204 /* No Content */,
                                        404 /* Not Found */, pool));
  return SVN_NO_ERROR;
}


svn_error_t * svn_ra_neon__get_commit_editor(svn_ra_session_t *session,
                                             const svn_delta_editor_t **editor,
                                             void **edit_baton,
                                             apr_hash_t *revprop_table,
                                             svn_commit_callback2_t callback,
                                             void *callback_baton,
                                             apr_hash_t *lock_tokens,
                                             svn_boolean_t keep_locks,
                                             apr_pool_t *pool)
{
  svn_ra_neon__session_t *ras = session->priv;
  svn_delta_editor_t *commit_editor;
  commit_ctx_t *cc;
  apr_hash_index_t *hi;

  /* Build the main commit editor's baton. */
  cc = apr_pcalloc(pool, sizeof(*cc));
  cc->pool = pool;
  cc->ras = ras;
  cc->valid_targets = apr_hash_make(pool);
  cc->callback = callback;
  cc->callback_baton = callback_baton;
  cc->lock_tokens = lock_tokens;
  cc->keep_locks = keep_locks;

  /* Dup the revprops into POOL, in case the caller clears the pool
     they're in before driving the editor that this function returns. */
  cc->revprop_table = apr_hash_make(pool);
  for (hi = apr_hash_first(pool, revprop_table); 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(cc->revprop_table, apr_pstrdup(pool, key), klen,
                   svn_string_dup(val, pool));
    }

  /* Calculate the commit anchor's repository relpath. */
  SVN_ERR(svn_ra_neon__get_path_relative_to_root(session,
                                                 &(cc->anchor_relpath),
                                                 ras->url->data, pool));

  /* Set up the editor. */
  commit_editor = svn_delta_default_editor(pool);
  commit_editor->open_root = commit_open_root;
  commit_editor->delete_entry = commit_delete_entry;
  commit_editor->add_directory = commit_add_dir;
  commit_editor->open_directory = commit_open_dir;
  commit_editor->change_dir_prop = commit_change_dir_prop;
  commit_editor->close_directory = commit_close_dir;
  commit_editor->add_file = commit_add_file;
  commit_editor->open_file = commit_open_file;
  commit_editor->apply_textdelta = commit_apply_txdelta;
  commit_editor->change_file_prop = commit_change_file_prop;
  commit_editor->close_file = commit_close_file;
  commit_editor->close_edit = commit_close_edit;
  commit_editor->abort_edit = commit_abort_edit;

  *editor = commit_editor;
  *edit_baton = cc;
  return SVN_NO_ERROR;
}