diff options
Diffstat (limited to 'subversion/libsvn_ra_serf/commit.c')
-rw-r--r-- | subversion/libsvn_ra_serf/commit.c | 2468 |
1 files changed, 2468 insertions, 0 deletions
diff --git a/subversion/libsvn_ra_serf/commit.c b/subversion/libsvn_ra_serf/commit.c new file mode 100644 index 0000000..aaf1717 --- /dev/null +++ b/subversion/libsvn_ra_serf/commit.c @@ -0,0 +1,2468 @@ +/* + * commit.c : entry point for commit RA functions for ra_serf + * + * ==================================================================== + * 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_uri.h> +#include <serf.h> + +#include "svn_hash.h" +#include "svn_pools.h" +#include "svn_ra.h" +#include "svn_dav.h" +#include "svn_xml.h" +#include "svn_config.h" +#include "svn_delta.h" +#include "svn_base64.h" +#include "svn_dirent_uri.h" +#include "svn_path.h" +#include "svn_props.h" + +#include "svn_private_config.h" +#include "private/svn_dep_compat.h" +#include "private/svn_fspath.h" +#include "private/svn_skel.h" + +#include "ra_serf.h" +#include "../libsvn_ra/ra_loader.h" + + +/* Baton passed back with the commit editor. */ +typedef struct commit_context_t { + /* Pool for our commit. */ + apr_pool_t *pool; + + svn_ra_serf__session_t *session; + svn_ra_serf__connection_t *conn; + + apr_hash_t *revprop_table; + + svn_commit_callback2_t callback; + void *callback_baton; + + apr_hash_t *lock_tokens; + svn_boolean_t keep_locks; + apr_hash_t *deleted_entries; /* deleted files (for delete+add detection) */ + + /* 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... */ + const char *baseline_url; /* the working-baseline resource */ + const char *checked_in_url; /* checked-in root to base CHECKOUTs from */ + const char *vcc_url; /* vcc url */ + +} commit_context_t; + +#define USING_HTTPV2_COMMIT_SUPPORT(commit_ctx) ((commit_ctx)->txn_url != NULL) + +/* Structure associated with a PROPPATCH request. */ +typedef struct proppatch_context_t { + apr_pool_t *pool; + + const char *relpath; + const char *path; + + commit_context_t *commit; + + /* Changed and removed properties. */ + apr_hash_t *changed_props; + apr_hash_t *removed_props; + + /* Same, for the old value (*old_value_p). */ + apr_hash_t *previous_changed_props; + apr_hash_t *previous_removed_props; + + /* In HTTP v2, this is the file/directory version we think we're changing. */ + svn_revnum_t base_revision; + +} proppatch_context_t; + +typedef struct delete_context_t { + const char *path; + + svn_revnum_t revision; + + const char *lock_token; + apr_hash_t *lock_token_hash; + svn_boolean_t keep_locks; + +} delete_context_t; + +/* Represents a directory. */ +typedef struct dir_context_t { + /* Pool for our directory. */ + apr_pool_t *pool; + + /* The root commit we're in progress for. */ + commit_context_t *commit; + + /* URL to operate against (used for CHECKOUT and PROPPATCH before + HTTP v2, for PROPPATCH in HTTP v2). */ + const char *url; + + /* How many pending changes we have left in this directory. */ + unsigned int ref_count; + + /* Is this directory being added? (Otherwise, just opened.) */ + svn_boolean_t added; + + /* Our parent */ + struct dir_context_t *parent_dir; + + /* The directory name; if "", we're the 'root' */ + const char *relpath; + + /* The basename of the directory. "" for the 'root' */ + const char *name; + + /* The base revision of the dir. */ + svn_revnum_t base_revision; + + const char *copy_path; + svn_revnum_t copy_revision; + + /* Changed and removed properties */ + apr_hash_t *changed_props; + apr_hash_t *removed_props; + + /* The checked-out working resource for this directory. May be NULL; if so + call checkout_dir() first. */ + const char *working_url; + +} dir_context_t; + +/* Represents a file to be committed. */ +typedef struct file_context_t { + /* Pool for our file. */ + apr_pool_t *pool; + + /* The root commit we're in progress for. */ + commit_context_t *commit; + + /* Is this file being added? (Otherwise, just opened.) */ + svn_boolean_t added; + + dir_context_t *parent_dir; + + const char *relpath; + const char *name; + + /* The checked-out working resource for this file. */ + const char *working_url; + + /* The base revision of the file. */ + svn_revnum_t base_revision; + + /* Copy path and revision */ + const char *copy_path; + svn_revnum_t copy_revision; + + /* stream */ + svn_stream_t *stream; + + /* Temporary file containing the svndiff. */ + apr_file_t *svndiff; + + /* Our base checksum as reported by the WC. */ + const char *base_checksum; + + /* Our resulting checksum as reported by the WC. */ + const char *result_checksum; + + /* Changed and removed properties. */ + apr_hash_t *changed_props; + apr_hash_t *removed_props; + + /* URL to PUT the file at. */ + const char *url; + +} file_context_t; + + +/* Setup routines and handlers for various requests we'll invoke. */ + +static svn_error_t * +return_response_err(svn_ra_serf__handler_t *handler) +{ + svn_error_t *err; + + /* We should have captured SLINE and LOCATION in the HANDLER. */ + SVN_ERR_ASSERT(handler->handler_pool != NULL); + + /* Ye Olde Fallback Error */ + err = svn_error_compose_create( + handler->server_error != NULL + ? handler->server_error->error + : SVN_NO_ERROR, + svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, + _("%s of '%s': %d %s"), + handler->method, handler->path, + handler->sline.code, handler->sline.reason)); + + /* Try to return one of the standard errors for 301, 404, etc., + then look for an error embedded in the response. */ + return svn_error_compose_create(svn_ra_serf__error_on_status( + handler->sline.code, + handler->path, + handler->location), + err); +} + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_checkout_body(serf_bucket_t **bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + const char *activity_url = baton; + serf_bucket_t *body_bkt; + + body_bkt = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_xml_header_buckets(body_bkt, alloc); + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:checkout", + "xmlns:D", "DAV:", + NULL); + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:activity-set", NULL); + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:href", NULL); + + SVN_ERR_ASSERT(activity_url != NULL); + svn_ra_serf__add_cdata_len_buckets(body_bkt, alloc, + activity_url, + strlen(activity_url)); + + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:href"); + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:activity-set"); + svn_ra_serf__add_tag_buckets(body_bkt, "D:apply-to-version", NULL, alloc); + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:checkout"); + + *bkt = body_bkt; + return SVN_NO_ERROR; +} + + +/* Using the HTTPv1 protocol, perform a CHECKOUT of NODE_URL within the + given COMMIT_CTX. The resulting working resource will be returned in + *WORKING_URL, allocated from RESULT_POOL. All temporary allocations + are performed in SCRATCH_POOL. + + ### are these URLs actually repos relpath values? or fspath? or maybe + ### the abspath portion of the full URL. + + This function operates synchronously. + + Strictly speaking, we could perform "all" of the CHECKOUT requests + when the commit starts, and only block when we need a specific + answer. Or, at a minimum, send off these individual requests async + and block when we need the answer (eg PUT or PROPPATCH). + + However: the investment to speed this up is not worthwhile, given + that CHECKOUT (and the related round trip) is completely obviated + in HTTPv2. +*/ +static svn_error_t * +checkout_node(const char **working_url, + const commit_context_t *commit_ctx, + const char *node_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__handler_t handler = { 0 }; + apr_status_t status; + apr_uri_t uri; + + /* HANDLER_POOL is the scratch pool since we don't need to remember + anything from the handler. We just want the working resource. */ + handler.handler_pool = scratch_pool; + handler.session = commit_ctx->session; + handler.conn = commit_ctx->conn; + + handler.body_delegate = create_checkout_body; + handler.body_delegate_baton = (/* const */ void *)commit_ctx->activity_url; + handler.body_type = "text/xml"; + + handler.response_handler = svn_ra_serf__expect_empty_body; + handler.response_baton = &handler; + + handler.method = "CHECKOUT"; + handler.path = node_url; + + SVN_ERR(svn_ra_serf__context_run_one(&handler, scratch_pool)); + + if (handler.sline.code != 201) + return svn_error_trace(return_response_err(&handler)); + + if (handler.location == NULL) + return svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("No Location header received")); + + /* We only want the path portion of the Location header. + (code.google.com sometimes returns an 'http:' scheme for an + 'https:' transaction ... we'll work around that by stripping the + scheme, host, and port here and re-adding the correct ones + later. */ + status = apr_uri_parse(scratch_pool, handler.location, &uri); + if (status) + return svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Error parsing Location header value")); + + *working_url = svn_urlpath__canonicalize(uri.path, result_pool); + + return SVN_NO_ERROR; +} + + +/* This is a wrapper around checkout_node() (which see for + documentation) which simply retries the CHECKOUT request when it + fails due to an SVN_ERR_APMOD_BAD_BASELINE error return from the + server. + + See http://subversion.tigris.org/issues/show_bug.cgi?id=4127 for + details. +*/ +static svn_error_t * +retry_checkout_node(const char **working_url, + const commit_context_t *commit_ctx, + const char *node_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_error_t *err = SVN_NO_ERROR; + int retry_count = 5; /* Magic, arbitrary number. */ + + do + { + svn_error_clear(err); + + err = checkout_node(working_url, commit_ctx, node_url, + result_pool, scratch_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 try to 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--); + + return err; +} + + +static svn_error_t * +checkout_dir(dir_context_t *dir, + apr_pool_t *scratch_pool) +{ + svn_error_t *err; + dir_context_t *p_dir = dir; + const char *checkout_url; + const char **working; + + if (dir->working_url) + { + return SVN_NO_ERROR; + } + + /* Is this directory or one of our parent dirs newly added? + * If so, we're already implicitly checked out. */ + while (p_dir) + { + if (p_dir->added) + { + /* Implicitly checkout this dir now. */ + dir->working_url = svn_path_url_add_component2( + dir->parent_dir->working_url, + dir->name, dir->pool); + return SVN_NO_ERROR; + } + p_dir = p_dir->parent_dir; + } + + /* We could be called twice for the root: once to checkout the baseline; + * once to checkout the directory itself if we need to do so. + * Note: CHECKOUT_URL should live longer than HANDLER. + */ + if (!dir->parent_dir && !dir->commit->baseline_url) + { + checkout_url = dir->commit->vcc_url; + working = &dir->commit->baseline_url; + } + else + { + checkout_url = dir->url; + working = &dir->working_url; + } + + /* Checkout our directory into the activity URL now. */ + err = retry_checkout_node(working, dir->commit, checkout_url, + dir->pool, scratch_pool); + if (err) + { + if (err->apr_err == SVN_ERR_FS_CONFLICT) + SVN_ERR_W(err, apr_psprintf(scratch_pool, + _("Directory '%s' is out of date; try updating"), + svn_dirent_local_style(dir->relpath, scratch_pool))); + return err; + } + + return SVN_NO_ERROR; +} + + +/* Set *CHECKED_IN_URL to the appropriate DAV version url for + * RELPATH (relative to the root of SESSION). + * + * Try to find this version url in three ways: + * First, if SESSION->callbacks->get_wc_prop() is defined, try to read the + * version url from the working copy properties. + * Second, if the version url of the parent directory PARENT_VSN_URL is + * defined, set *CHECKED_IN_URL to the concatenation of PARENT_VSN_URL with + * RELPATH. + * Else, fetch the version url for the root of SESSION using CONN and + * BASE_REVISION, and set *CHECKED_IN_URL to the concatenation of that + * with RELPATH. + * + * Allocate the result in RESULT_POOL, and use SCRATCH_POOL for + * temporary allocation. + */ +static svn_error_t * +get_version_url(const char **checked_in_url, + svn_ra_serf__session_t *session, + const char *relpath, + svn_revnum_t base_revision, + const char *parent_vsn_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *root_checkout; + + if (session->wc_callbacks->get_wc_prop) + { + const svn_string_t *current_version; + + SVN_ERR(session->wc_callbacks->get_wc_prop( + session->wc_callback_baton, + relpath, + SVN_RA_SERF__WC_CHECKED_IN_URL, + ¤t_version, scratch_pool)); + + if (current_version) + { + *checked_in_url = + svn_urlpath__canonicalize(current_version->data, result_pool); + return SVN_NO_ERROR; + } + } + + if (parent_vsn_url) + { + root_checkout = parent_vsn_url; + } + else + { + const char *propfind_url; + svn_ra_serf__connection_t *conn = session->conns[0]; + + if (SVN_IS_VALID_REVNUM(base_revision)) + { + /* mod_dav_svn can't handle the "Label:" header that + svn_ra_serf__deliver_props() is going to try to use for + this lookup, so we'll do things the hard(er) way, by + looking up the version URL from a resource in the + baseline collection. */ + /* ### conn==NULL for session->conns[0]. same as CONN. */ + SVN_ERR(svn_ra_serf__get_stable_url(&propfind_url, + NULL /* latest_revnum */, + session, NULL /* conn */, + NULL /* url */, base_revision, + scratch_pool, scratch_pool)); + } + else + { + propfind_url = session->session_url.path; + } + + SVN_ERR(svn_ra_serf__fetch_dav_prop(&root_checkout, + conn, propfind_url, base_revision, + "checked-in", + scratch_pool, scratch_pool)); + if (!root_checkout) + return svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, + _("Path '%s' not present"), + session->session_url.path); + + root_checkout = svn_urlpath__canonicalize(root_checkout, scratch_pool); + } + + *checked_in_url = svn_path_url_add_component2(root_checkout, relpath, + result_pool); + + return SVN_NO_ERROR; +} + +static svn_error_t * +checkout_file(file_context_t *file, + apr_pool_t *scratch_pool) +{ + svn_error_t *err; + dir_context_t *parent_dir = file->parent_dir; + const char *checkout_url; + + /* Is one of our parent dirs newly added? If so, we're already + * implicitly checked out. + */ + while (parent_dir) + { + if (parent_dir->added) + { + /* Implicitly checkout this file now. */ + file->working_url = svn_path_url_add_component2( + parent_dir->working_url, + svn_relpath_skip_ancestor( + parent_dir->relpath, file->relpath), + file->pool); + return SVN_NO_ERROR; + } + parent_dir = parent_dir->parent_dir; + } + + SVN_ERR(get_version_url(&checkout_url, + file->commit->session, + file->relpath, file->base_revision, + NULL, scratch_pool, scratch_pool)); + + /* Checkout our file into the activity URL now. */ + err = retry_checkout_node(&file->working_url, file->commit, checkout_url, + file->pool, scratch_pool); + if (err) + { + if (err->apr_err == SVN_ERR_FS_CONFLICT) + SVN_ERR_W(err, apr_psprintf(scratch_pool, + _("File '%s' is out of date; try updating"), + svn_dirent_local_style(file->relpath, scratch_pool))); + return err; + } + + return SVN_NO_ERROR; +} + +/* Helper function for proppatch_walker() below. */ +static svn_error_t * +get_encoding_and_cdata(const char **encoding_p, + const svn_string_t **encoded_value_p, + serf_bucket_alloc_t *alloc, + const svn_string_t *value, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (value == NULL) + { + *encoding_p = NULL; + *encoded_value_p = NULL; + return SVN_NO_ERROR; + } + + /* If a property is XML-safe, XML-encode it. Else, base64-encode + it. */ + if (svn_xml_is_xml_safe(value->data, value->len)) + { + svn_stringbuf_t *xml_esc = NULL; + svn_xml_escape_cdata_string(&xml_esc, value, scratch_pool); + *encoding_p = NULL; + *encoded_value_p = svn_string_create_from_buf(xml_esc, result_pool); + } + else + { + *encoding_p = "base64"; + *encoded_value_p = svn_base64_encode_string2(value, TRUE, result_pool); + } + + return SVN_NO_ERROR; +} + +typedef struct walker_baton_t { + serf_bucket_t *body_bkt; + apr_pool_t *body_pool; + + apr_hash_t *previous_changed_props; + apr_hash_t *previous_removed_props; + + const char *path; + + /* Hack, since change_rev_prop(old_value_p != NULL, value = NULL) uses D:set + rather than D:remove... (see notes/http-and-webdav/webdav-protocol) */ + enum { + filter_all_props, + filter_props_with_old_value, + filter_props_without_old_value + } filter; + + /* Is the property being deleted? */ + svn_boolean_t deleting; +} walker_baton_t; + +/* If we have (recorded in WB) the old value of the property named NS:NAME, + * then set *HAVE_OLD_VAL to TRUE and set *OLD_VAL_P to that old value + * (which may be NULL); else set *HAVE_OLD_VAL to FALSE. */ +static svn_error_t * +derive_old_val(svn_boolean_t *have_old_val, + const svn_string_t **old_val_p, + walker_baton_t *wb, + const char *ns, + const char *name) +{ + *have_old_val = FALSE; + + if (wb->previous_changed_props) + { + const svn_string_t *val; + val = svn_ra_serf__get_prop_string(wb->previous_changed_props, + wb->path, ns, name); + if (val) + { + *have_old_val = TRUE; + *old_val_p = val; + } + } + + if (wb->previous_removed_props) + { + const svn_string_t *val; + val = svn_ra_serf__get_prop_string(wb->previous_removed_props, + wb->path, ns, name); + if (val) + { + *have_old_val = TRUE; + *old_val_p = NULL; + } + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +proppatch_walker(void *baton, + const char *ns, + const char *name, + const svn_string_t *val, + apr_pool_t *scratch_pool) +{ + walker_baton_t *wb = baton; + serf_bucket_t *body_bkt = wb->body_bkt; + serf_bucket_t *cdata_bkt; + serf_bucket_alloc_t *alloc; + const char *encoding; + svn_boolean_t have_old_val; + const svn_string_t *old_val; + const svn_string_t *encoded_value; + const char *prop_name; + + SVN_ERR(derive_old_val(&have_old_val, &old_val, wb, ns, name)); + + /* Jump through hoops to work with D:remove and its val = (""-for-NULL) + * representation. */ + if (wb->filter != filter_all_props) + { + if (wb->filter == filter_props_with_old_value && ! have_old_val) + return SVN_NO_ERROR; + if (wb->filter == filter_props_without_old_value && have_old_val) + return SVN_NO_ERROR; + } + if (wb->deleting) + val = NULL; + + alloc = body_bkt->allocator; + + SVN_ERR(get_encoding_and_cdata(&encoding, &encoded_value, alloc, val, + wb->body_pool, scratch_pool)); + if (encoded_value) + { + cdata_bkt = SERF_BUCKET_SIMPLE_STRING_LEN(encoded_value->data, + encoded_value->len, + alloc); + } + else + { + cdata_bkt = NULL; + } + + /* Use the namespace prefix instead of adding the xmlns attribute to support + property names containing ':' */ + if (strcmp(ns, SVN_DAV_PROP_NS_SVN) == 0) + prop_name = apr_pstrcat(wb->body_pool, "S:", name, (char *)NULL); + else if (strcmp(ns, SVN_DAV_PROP_NS_CUSTOM) == 0) + prop_name = apr_pstrcat(wb->body_pool, "C:", name, (char *)NULL); + + if (cdata_bkt) + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, prop_name, + "V:encoding", encoding, + NULL); + else + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, prop_name, + "V:" SVN_DAV__OLD_VALUE__ABSENT, "1", + NULL); + + if (have_old_val) + { + const char *encoding2; + const svn_string_t *encoded_value2; + serf_bucket_t *cdata_bkt2; + + SVN_ERR(get_encoding_and_cdata(&encoding2, &encoded_value2, + alloc, old_val, + wb->body_pool, scratch_pool)); + + if (encoded_value2) + { + cdata_bkt2 = SERF_BUCKET_SIMPLE_STRING_LEN(encoded_value2->data, + encoded_value2->len, + alloc); + } + else + { + cdata_bkt2 = NULL; + } + + if (cdata_bkt2) + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, + "V:" SVN_DAV__OLD_VALUE, + "V:encoding", encoding2, + NULL); + else + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, + "V:" SVN_DAV__OLD_VALUE, + "V:" SVN_DAV__OLD_VALUE__ABSENT, "1", + NULL); + + if (cdata_bkt2) + serf_bucket_aggregate_append(body_bkt, cdata_bkt2); + + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, + "V:" SVN_DAV__OLD_VALUE); + } + if (cdata_bkt) + serf_bucket_aggregate_append(body_bkt, cdata_bkt); + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, prop_name); + + return SVN_NO_ERROR; +} + +/* Possible add the lock-token "If:" precondition header to HEADERS if + an examination of COMMIT_CTX and RELPATH indicates that this is the + right thing to do. + + Generally speaking, if the client provided a lock token for + RELPATH, it's the right thing to do. There is a notable instance + where this is not the case, however. If the file at RELPATH was + explicitly deleted in this commit already, then mod_dav removed its + lock token when it fielded the DELETE request, so we don't want to + set the lock precondition again. (See + http://subversion.tigris.org/issues/show_bug.cgi?id=3674 for details.) +*/ +static svn_error_t * +maybe_set_lock_token_header(serf_bucket_t *headers, + commit_context_t *commit_ctx, + const char *relpath, + apr_pool_t *pool) +{ + const char *token; + + if (! (relpath && commit_ctx->lock_tokens)) + return SVN_NO_ERROR; + + if (! svn_hash_gets(commit_ctx->deleted_entries, relpath)) + { + token = svn_hash_gets(commit_ctx->lock_tokens, relpath); + if (token) + { + const char *token_header; + const char *token_uri; + apr_uri_t uri = commit_ctx->session->session_url; + + /* Supplying the optional URI affects apache response when + the lock is broken, see issue 4369. When present any URI + must be absolute (RFC 2518 9.4). */ + uri.path = (char *)svn_path_url_add_component2(uri.path, relpath, + pool); + token_uri = apr_uri_unparse(pool, &uri, 0); + + token_header = apr_pstrcat(pool, "<", token_uri, "> (<", token, ">)", + (char *)NULL); + serf_bucket_headers_set(headers, "If", token_header); + } + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +setup_proppatch_headers(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ + proppatch_context_t *proppatch = baton; + + if (SVN_IS_VALID_REVNUM(proppatch->base_revision)) + { + serf_bucket_headers_set(headers, SVN_DAV_VERSION_NAME_HEADER, + apr_psprintf(pool, "%ld", + proppatch->base_revision)); + } + + SVN_ERR(maybe_set_lock_token_header(headers, proppatch->commit, + proppatch->relpath, pool)); + + return SVN_NO_ERROR; +} + + +struct proppatch_body_baton_t { + proppatch_context_t *proppatch; + + /* Content in the body should be allocated here, to live long enough. */ + apr_pool_t *body_pool; +}; + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_proppatch_body(serf_bucket_t **bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *scratch_pool) +{ + struct proppatch_body_baton_t *pbb = baton; + proppatch_context_t *ctx = pbb->proppatch; + serf_bucket_t *body_bkt; + walker_baton_t wb = { 0 }; + + body_bkt = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_xml_header_buckets(body_bkt, alloc); + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:propertyupdate", + "xmlns:D", "DAV:", + "xmlns:V", SVN_DAV_PROP_NS_DAV, + "xmlns:C", SVN_DAV_PROP_NS_CUSTOM, + "xmlns:S", SVN_DAV_PROP_NS_SVN, + NULL); + + wb.body_bkt = body_bkt; + wb.body_pool = pbb->body_pool; + wb.previous_changed_props = ctx->previous_changed_props; + wb.previous_removed_props = ctx->previous_removed_props; + wb.path = ctx->path; + + if (apr_hash_count(ctx->changed_props) > 0) + { + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:set", NULL); + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:prop", NULL); + + wb.filter = filter_all_props; + wb.deleting = FALSE; + SVN_ERR(svn_ra_serf__walk_all_props(ctx->changed_props, ctx->path, + SVN_INVALID_REVNUM, + proppatch_walker, &wb, + scratch_pool)); + + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:prop"); + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:set"); + } + + if (apr_hash_count(ctx->removed_props) > 0) + { + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:set", NULL); + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:prop", NULL); + + wb.filter = filter_props_with_old_value; + wb.deleting = TRUE; + SVN_ERR(svn_ra_serf__walk_all_props(ctx->removed_props, ctx->path, + SVN_INVALID_REVNUM, + proppatch_walker, &wb, + scratch_pool)); + + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:prop"); + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:set"); + } + + if (apr_hash_count(ctx->removed_props) > 0) + { + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:remove", NULL); + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:prop", NULL); + + wb.filter = filter_props_without_old_value; + wb.deleting = TRUE; + SVN_ERR(svn_ra_serf__walk_all_props(ctx->removed_props, ctx->path, + SVN_INVALID_REVNUM, + proppatch_walker, &wb, + scratch_pool)); + + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:prop"); + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:remove"); + } + + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:propertyupdate"); + + *bkt = body_bkt; + return SVN_NO_ERROR; +} + +static svn_error_t* +proppatch_resource(proppatch_context_t *proppatch, + commit_context_t *commit, + apr_pool_t *pool) +{ + svn_ra_serf__handler_t *handler; + struct proppatch_body_baton_t pbb; + + handler = apr_pcalloc(pool, sizeof(*handler)); + handler->handler_pool = pool; + handler->method = "PROPPATCH"; + handler->path = proppatch->path; + handler->conn = commit->conn; + handler->session = commit->session; + + handler->header_delegate = setup_proppatch_headers; + handler->header_delegate_baton = proppatch; + + pbb.proppatch = proppatch; + pbb.body_pool = pool; + handler->body_delegate = create_proppatch_body; + handler->body_delegate_baton = &pbb; + + handler->response_handler = svn_ra_serf__handle_multistatus_only; + handler->response_baton = handler; + + SVN_ERR(svn_ra_serf__context_run_one(handler, pool)); + + if (handler->sline.code != 207 + || (handler->server_error != NULL + && handler->server_error->error != NULL)) + { + return svn_error_create( + SVN_ERR_RA_DAV_PROPPATCH_FAILED, + return_response_err(handler), + _("At least one property change failed; repository" + " is unchanged")); + } + + return SVN_NO_ERROR; +} + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_put_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + file_context_t *ctx = baton; + apr_off_t offset; + + /* We need to flush the file, make it unbuffered (so that it can be + * zero-copied via mmap), and reset the position before attempting to + * deliver the file. + * + * N.B. If we have APR 1.3+, we can unbuffer the file to let us use mmap + * and zero-copy the PUT body. However, on older APR versions, we can't + * check the buffer status; but serf will fall through and create a file + * bucket for us on the buffered svndiff handle. + */ + apr_file_flush(ctx->svndiff); +#if APR_VERSION_AT_LEAST(1, 3, 0) + apr_file_buffer_set(ctx->svndiff, NULL, 0); +#endif + offset = 0; + apr_file_seek(ctx->svndiff, APR_SET, &offset); + + *body_bkt = serf_bucket_file_create(ctx->svndiff, alloc); + return SVN_NO_ERROR; +} + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_empty_put_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + *body_bkt = SERF_BUCKET_SIMPLE_STRING("", alloc); + return SVN_NO_ERROR; +} + +static svn_error_t * +setup_put_headers(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ + file_context_t *ctx = baton; + + if (SVN_IS_VALID_REVNUM(ctx->base_revision)) + { + serf_bucket_headers_set(headers, SVN_DAV_VERSION_NAME_HEADER, + apr_psprintf(pool, "%ld", ctx->base_revision)); + } + + if (ctx->base_checksum) + { + serf_bucket_headers_set(headers, SVN_DAV_BASE_FULLTEXT_MD5_HEADER, + ctx->base_checksum); + } + + if (ctx->result_checksum) + { + serf_bucket_headers_set(headers, SVN_DAV_RESULT_FULLTEXT_MD5_HEADER, + ctx->result_checksum); + } + + SVN_ERR(maybe_set_lock_token_header(headers, ctx->commit, + ctx->relpath, pool)); + + return APR_SUCCESS; +} + +static svn_error_t * +setup_copy_file_headers(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ + file_context_t *file = baton; + apr_uri_t uri; + const char *absolute_uri; + + /* The Dest URI must be absolute. Bummer. */ + uri = file->commit->session->session_url; + uri.path = (char*)file->url; + absolute_uri = apr_uri_unparse(pool, &uri, 0); + + serf_bucket_headers_set(headers, "Destination", absolute_uri); + + serf_bucket_headers_setn(headers, "Depth", "0"); + serf_bucket_headers_setn(headers, "Overwrite", "T"); + + return SVN_NO_ERROR; +} + +static svn_error_t * +setup_copy_dir_headers(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ + dir_context_t *dir = baton; + apr_uri_t uri; + const char *absolute_uri; + + /* The Dest URI must be absolute. Bummer. */ + uri = dir->commit->session->session_url; + + if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit)) + { + uri.path = (char *)dir->url; + } + else + { + uri.path = (char *)svn_path_url_add_component2( + dir->parent_dir->working_url, + dir->name, pool); + } + absolute_uri = apr_uri_unparse(pool, &uri, 0); + + serf_bucket_headers_set(headers, "Destination", absolute_uri); + + serf_bucket_headers_setn(headers, "Depth", "infinity"); + serf_bucket_headers_setn(headers, "Overwrite", "T"); + + /* Implicitly checkout this dir now. */ + dir->working_url = apr_pstrdup(dir->pool, uri.path); + + return SVN_NO_ERROR; +} + +static svn_error_t * +setup_delete_headers(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ + delete_context_t *ctx = baton; + + serf_bucket_headers_set(headers, SVN_DAV_VERSION_NAME_HEADER, + apr_ltoa(pool, ctx->revision)); + + if (ctx->lock_token_hash) + { + ctx->lock_token = svn_hash_gets(ctx->lock_token_hash, ctx->path); + + if (ctx->lock_token) + { + const char *token_header; + + token_header = apr_pstrcat(pool, "<", ctx->path, "> (<", + ctx->lock_token, ">)", (char *)NULL); + + serf_bucket_headers_set(headers, "If", token_header); + + if (ctx->keep_locks) + serf_bucket_headers_setn(headers, SVN_DAV_OPTIONS_HEADER, + SVN_DAV_OPTION_KEEP_LOCKS); + } + } + + return SVN_NO_ERROR; +} + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_delete_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + delete_context_t *ctx = baton; + serf_bucket_t *body; + + body = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_xml_header_buckets(body, alloc); + + svn_ra_serf__merge_lock_token_list(ctx->lock_token_hash, ctx->path, + body, alloc, pool); + + *body_bkt = body; + return SVN_NO_ERROR; +} + +/* Helper function to write the svndiff stream to temporary file. */ +static svn_error_t * +svndiff_stream_write(void *file_baton, + const char *data, + apr_size_t *len) +{ + file_context_t *ctx = file_baton; + apr_status_t status; + + status = apr_file_write_full(ctx->svndiff, data, *len, NULL); + if (status) + return svn_error_wrap_apr(status, _("Failed writing updated file")); + + return SVN_NO_ERROR; +} + + + +/* POST against 'me' resource handlers. */ + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_txn_post_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + apr_hash_t *revprops = baton; + svn_skel_t *request_skel; + svn_stringbuf_t *skel_str; + + request_skel = svn_skel__make_empty_list(pool); + if (revprops) + { + svn_skel_t *proplist_skel; + + SVN_ERR(svn_skel__unparse_proplist(&proplist_skel, revprops, pool)); + svn_skel__prepend(proplist_skel, request_skel); + svn_skel__prepend_str("create-txn-with-props", request_skel, pool); + skel_str = svn_skel__unparse(request_skel, pool); + *body_bkt = SERF_BUCKET_SIMPLE_STRING(skel_str->data, alloc); + } + else + { + *body_bkt = SERF_BUCKET_SIMPLE_STRING("( create-txn )", alloc); + } + + return SVN_NO_ERROR; +} + +/* Implements svn_ra_serf__request_header_delegate_t */ +static svn_error_t * +setup_post_headers(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ +#ifdef SVN_DAV_SEND_VTXN_NAME + /* Enable this to exercise the VTXN-NAME code based on a client + supplied transaction name. */ + serf_bucket_headers_set(headers, SVN_DAV_VTXN_NAME_HEADER, + svn_uuid_generate(pool)); +#endif + + return SVN_NO_ERROR; +} + + +/* Handler baton for POST request. */ +typedef struct post_response_ctx_t +{ + svn_ra_serf__handler_t *handler; + commit_context_t *commit_ctx; +} post_response_ctx_t; + + +/* This implements serf_bucket_headers_do_callback_fn_t. */ +static int +post_headers_iterator_callback(void *baton, + const char *key, + const char *val) +{ + post_response_ctx_t *prc = baton; + commit_context_t *prc_cc = prc->commit_ctx; + svn_ra_serf__session_t *sess = prc_cc->session; + + /* If we provided a UUID to the POST request, we should get back + from the server an SVN_DAV_VTXN_NAME_HEADER header; otherwise we + expect the SVN_DAV_TXN_NAME_HEADER. We certainly don't expect to + see both. */ + + if (svn_cstring_casecmp(key, SVN_DAV_TXN_NAME_HEADER) == 0) + { + /* Build out txn and txn-root URLs using the txn name we're + given, and store the whole lot of it in the commit context. */ + prc_cc->txn_url = + svn_path_url_add_component2(sess->txn_stub, val, prc_cc->pool); + prc_cc->txn_root_url = + svn_path_url_add_component2(sess->txn_root_stub, val, prc_cc->pool); + } + + if (svn_cstring_casecmp(key, SVN_DAV_VTXN_NAME_HEADER) == 0) + { + /* Build out vtxn and vtxn-root URLs using the vtxn name we're + given, and store the whole lot of it in the commit context. */ + prc_cc->txn_url = + svn_path_url_add_component2(sess->vtxn_stub, val, prc_cc->pool); + prc_cc->txn_root_url = + svn_path_url_add_component2(sess->vtxn_root_stub, val, prc_cc->pool); + } + + return 0; +} + + +/* A custom serf_response_handler_t which is mostly a wrapper around + svn_ra_serf__expect_empty_body -- it just notices POST response + headers, too. + + Implements svn_ra_serf__response_handler_t */ +static svn_error_t * +post_response_handler(serf_request_t *request, + serf_bucket_t *response, + void *baton, + apr_pool_t *scratch_pool) +{ + post_response_ctx_t *prc = baton; + serf_bucket_t *hdrs = serf_bucket_response_get_headers(response); + + /* Then see which ones we can discover. */ + serf_bucket_headers_do(hdrs, post_headers_iterator_callback, prc); + + /* Execute the 'real' response handler to XML-parse the repsonse body. */ + return svn_ra_serf__expect_empty_body(request, response, + prc->handler, scratch_pool); +} + + + +/* Commit baton callbacks */ + +static svn_error_t * +open_root(void *edit_baton, + svn_revnum_t base_revision, + apr_pool_t *dir_pool, + void **root_baton) +{ + commit_context_t *ctx = edit_baton; + svn_ra_serf__handler_t *handler; + proppatch_context_t *proppatch_ctx; + dir_context_t *dir; + apr_hash_index_t *hi; + const char *proppatch_target = NULL; + + if (SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(ctx->session)) + { + post_response_ctx_t *prc; + const char *rel_path; + svn_boolean_t post_with_revprops + = (NULL != svn_hash_gets(ctx->session->supported_posts, + "create-txn-with-props")); + + /* Create our activity URL now on the server. */ + handler = apr_pcalloc(ctx->pool, sizeof(*handler)); + handler->handler_pool = ctx->pool; + handler->method = "POST"; + handler->body_type = SVN_SKEL_MIME_TYPE; + handler->body_delegate = create_txn_post_body; + handler->body_delegate_baton = + post_with_revprops ? ctx->revprop_table : NULL; + handler->header_delegate = setup_post_headers; + handler->header_delegate_baton = NULL; + handler->path = ctx->session->me_resource; + handler->conn = ctx->session->conns[0]; + handler->session = ctx->session; + + prc = apr_pcalloc(ctx->pool, sizeof(*prc)); + prc->handler = handler; + prc->commit_ctx = ctx; + + handler->response_handler = post_response_handler; + handler->response_baton = prc; + + SVN_ERR(svn_ra_serf__context_run_one(handler, ctx->pool)); + + if (handler->sline.code != 201) + { + apr_status_t status = SVN_ERR_RA_DAV_REQUEST_FAILED; + + switch (handler->sline.code) + { + case 403: + status = SVN_ERR_RA_DAV_FORBIDDEN; + break; + case 404: + status = SVN_ERR_FS_NOT_FOUND; + break; + } + + return svn_error_createf(status, NULL, + _("%s of '%s': %d %s (%s://%s)"), + handler->method, handler->path, + handler->sline.code, handler->sline.reason, + ctx->session->session_url.scheme, + ctx->session->session_url.hostinfo); + } + if (! (ctx->txn_root_url && ctx->txn_url)) + { + return svn_error_createf( + SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, + _("POST request did not return transaction information")); + } + + /* Fixup the txn_root_url to point to the anchor of the commit. */ + SVN_ERR(svn_ra_serf__get_relative_path(&rel_path, + ctx->session->session_url.path, + ctx->session, NULL, dir_pool)); + ctx->txn_root_url = svn_path_url_add_component2(ctx->txn_root_url, + rel_path, ctx->pool); + + /* Build our directory baton. */ + dir = apr_pcalloc(dir_pool, sizeof(*dir)); + dir->pool = dir_pool; + dir->commit = ctx; + dir->base_revision = base_revision; + dir->relpath = ""; + dir->name = ""; + dir->changed_props = apr_hash_make(dir->pool); + dir->removed_props = apr_hash_make(dir->pool); + dir->url = apr_pstrdup(dir->pool, ctx->txn_root_url); + + /* If we included our revprops in the POST, we need not + PROPPATCH them. */ + proppatch_target = post_with_revprops ? NULL : ctx->txn_url; + } + else + { + const char *activity_str = ctx->session->activity_collection_url; + + if (!activity_str) + SVN_ERR(svn_ra_serf__v1_get_activity_collection(&activity_str, + ctx->session->conns[0], + ctx->pool, + ctx->pool)); + + /* Cache the result. */ + if (activity_str) + { + ctx->session->activity_collection_url = + apr_pstrdup(ctx->session->pool, activity_str); + } + else + { + return svn_error_create(SVN_ERR_RA_DAV_OPTIONS_REQ_FAILED, NULL, + _("The OPTIONS response did not include the " + "requested activity-collection-set value")); + } + + ctx->activity_url = + svn_path_url_add_component2(activity_str, svn_uuid_generate(ctx->pool), + ctx->pool); + + /* Create our activity URL now on the server. */ + handler = apr_pcalloc(ctx->pool, sizeof(*handler)); + handler->handler_pool = ctx->pool; + handler->method = "MKACTIVITY"; + handler->path = ctx->activity_url; + handler->conn = ctx->session->conns[0]; + handler->session = ctx->session; + + handler->response_handler = svn_ra_serf__expect_empty_body; + handler->response_baton = handler; + + SVN_ERR(svn_ra_serf__context_run_one(handler, ctx->pool)); + + if (handler->sline.code != 201) + { + apr_status_t status = SVN_ERR_RA_DAV_REQUEST_FAILED; + + switch (handler->sline.code) + { + case 403: + status = SVN_ERR_RA_DAV_FORBIDDEN; + break; + case 404: + status = SVN_ERR_FS_NOT_FOUND; + break; + } + + return svn_error_createf(status, NULL, + _("%s of '%s': %d %s (%s://%s)"), + handler->method, handler->path, + handler->sline.code, handler->sline.reason, + ctx->session->session_url.scheme, + ctx->session->session_url.hostinfo); + } + + /* Now go fetch our VCC and baseline so we can do a CHECKOUT. */ + SVN_ERR(svn_ra_serf__discover_vcc(&(ctx->vcc_url), ctx->session, + ctx->conn, ctx->pool)); + + + /* Build our directory baton. */ + dir = apr_pcalloc(dir_pool, sizeof(*dir)); + dir->pool = dir_pool; + dir->commit = ctx; + dir->base_revision = base_revision; + dir->relpath = ""; + dir->name = ""; + dir->changed_props = apr_hash_make(dir->pool); + dir->removed_props = apr_hash_make(dir->pool); + + SVN_ERR(get_version_url(&dir->url, dir->commit->session, + dir->relpath, + dir->base_revision, ctx->checked_in_url, + dir->pool, dir->pool /* scratch_pool */)); + ctx->checked_in_url = dir->url; + + /* Checkout our root dir */ + SVN_ERR(checkout_dir(dir, dir->pool /* scratch_pool */)); + + proppatch_target = ctx->baseline_url; + } + + /* Unless this is NULL -- which means we don't need to PROPPATCH the + transaction with our revprops -- then, you know, PROPPATCH the + transaction with our revprops. */ + if (proppatch_target) + { + proppatch_ctx = apr_pcalloc(ctx->pool, sizeof(*proppatch_ctx)); + proppatch_ctx->pool = dir_pool; + proppatch_ctx->commit = ctx; + proppatch_ctx->path = proppatch_target; + proppatch_ctx->changed_props = apr_hash_make(proppatch_ctx->pool); + proppatch_ctx->removed_props = apr_hash_make(proppatch_ctx->pool); + proppatch_ctx->base_revision = SVN_INVALID_REVNUM; + + for (hi = apr_hash_first(ctx->pool, ctx->revprop_table); hi; + hi = apr_hash_next(hi)) + { + const char *name = svn__apr_hash_index_key(hi); + svn_string_t *value = svn__apr_hash_index_val(hi); + const char *ns; + + if (strncmp(name, SVN_PROP_PREFIX, sizeof(SVN_PROP_PREFIX) - 1) == 0) + { + ns = SVN_DAV_PROP_NS_SVN; + name += sizeof(SVN_PROP_PREFIX) - 1; + } + else + { + ns = SVN_DAV_PROP_NS_CUSTOM; + } + + svn_ra_serf__set_prop(proppatch_ctx->changed_props, + proppatch_ctx->path, + ns, name, value, proppatch_ctx->pool); + } + + SVN_ERR(proppatch_resource(proppatch_ctx, dir->commit, ctx->pool)); + } + + *root_baton = dir; + + return SVN_NO_ERROR; +} + +static svn_error_t * +delete_entry(const char *path, + svn_revnum_t revision, + void *parent_baton, + apr_pool_t *pool) +{ + dir_context_t *dir = parent_baton; + delete_context_t *delete_ctx; + svn_ra_serf__handler_t *handler; + const char *delete_target; + svn_error_t *err; + + if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit)) + { + delete_target = svn_path_url_add_component2(dir->commit->txn_root_url, + path, dir->pool); + } + else + { + /* Ensure our directory has been checked out */ + SVN_ERR(checkout_dir(dir, pool /* scratch_pool */)); + delete_target = svn_path_url_add_component2(dir->working_url, + svn_relpath_basename(path, + NULL), + pool); + } + + /* DELETE our entry */ + delete_ctx = apr_pcalloc(pool, sizeof(*delete_ctx)); + delete_ctx->path = apr_pstrdup(pool, path); + delete_ctx->revision = revision; + delete_ctx->lock_token_hash = dir->commit->lock_tokens; + delete_ctx->keep_locks = dir->commit->keep_locks; + + handler = apr_pcalloc(pool, sizeof(*handler)); + handler->handler_pool = pool; + handler->session = dir->commit->session; + handler->conn = dir->commit->conn; + + handler->response_handler = svn_ra_serf__expect_empty_body; + handler->response_baton = handler; + + handler->header_delegate = setup_delete_headers; + handler->header_delegate_baton = delete_ctx; + + handler->method = "DELETE"; + handler->path = delete_target; + + err = svn_ra_serf__context_run_one(handler, pool); + + if (err && + (err->apr_err == SVN_ERR_FS_BAD_LOCK_TOKEN || + err->apr_err == SVN_ERR_FS_NO_LOCK_TOKEN || + err->apr_err == SVN_ERR_FS_LOCK_OWNER_MISMATCH || + err->apr_err == SVN_ERR_FS_PATH_ALREADY_LOCKED)) + { + svn_error_clear(err); + + /* An error has been registered on the connection. Reset the thing + so that we can use it again. */ + serf_connection_reset(handler->conn->conn); + + handler->body_delegate = create_delete_body; + handler->body_delegate_baton = delete_ctx; + handler->body_type = "text/xml"; + + SVN_ERR(svn_ra_serf__context_run_one(handler, pool)); + } + else if (err) + { + return err; + } + + /* 204 No Content: item successfully deleted */ + if (handler->sline.code != 204) + { + return svn_error_trace(return_response_err(handler)); + } + + svn_hash_sets(dir->commit->deleted_entries, + apr_pstrdup(dir->commit->pool, path), (void *)1); + + return SVN_NO_ERROR; +} + +static svn_error_t * +add_directory(const char *path, + void *parent_baton, + const char *copyfrom_path, + svn_revnum_t copyfrom_revision, + apr_pool_t *dir_pool, + void **child_baton) +{ + dir_context_t *parent = parent_baton; + dir_context_t *dir; + svn_ra_serf__handler_t *handler; + apr_status_t status; + const char *mkcol_target; + + dir = apr_pcalloc(dir_pool, sizeof(*dir)); + + dir->pool = dir_pool; + dir->parent_dir = parent; + dir->commit = parent->commit; + dir->added = TRUE; + dir->base_revision = SVN_INVALID_REVNUM; + dir->copy_revision = copyfrom_revision; + dir->copy_path = copyfrom_path; + dir->relpath = apr_pstrdup(dir->pool, path); + dir->name = svn_relpath_basename(dir->relpath, NULL); + dir->changed_props = apr_hash_make(dir->pool); + dir->removed_props = apr_hash_make(dir->pool); + + if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit)) + { + dir->url = svn_path_url_add_component2(parent->commit->txn_root_url, + path, dir->pool); + mkcol_target = dir->url; + } + else + { + /* Ensure our parent is checked out. */ + SVN_ERR(checkout_dir(parent, dir->pool /* scratch_pool */)); + + dir->url = svn_path_url_add_component2(parent->commit->checked_in_url, + dir->name, dir->pool); + mkcol_target = svn_path_url_add_component2( + parent->working_url, + dir->name, dir->pool); + } + + handler = apr_pcalloc(dir->pool, sizeof(*handler)); + handler->handler_pool = dir->pool; + handler->conn = dir->commit->conn; + handler->session = dir->commit->session; + + handler->response_handler = svn_ra_serf__expect_empty_body; + handler->response_baton = handler; + if (!dir->copy_path) + { + handler->method = "MKCOL"; + handler->path = mkcol_target; + } + else + { + apr_uri_t uri; + const char *req_url; + + status = apr_uri_parse(dir->pool, dir->copy_path, &uri); + if (status) + { + return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Unable to parse URL '%s'"), + dir->copy_path); + } + + /* ### conn==NULL for session->conns[0]. same as commit->conn. */ + SVN_ERR(svn_ra_serf__get_stable_url(&req_url, NULL /* latest_revnum */, + dir->commit->session, + NULL /* conn */, + uri.path, dir->copy_revision, + dir_pool, dir_pool)); + + handler->method = "COPY"; + handler->path = req_url; + + handler->header_delegate = setup_copy_dir_headers; + handler->header_delegate_baton = dir; + } + + SVN_ERR(svn_ra_serf__context_run_one(handler, dir->pool)); + + switch (handler->sline.code) + { + case 201: /* Created: item was successfully copied */ + case 204: /* No Content: item successfully replaced an existing target */ + break; + + case 403: + return svn_error_createf(SVN_ERR_RA_DAV_FORBIDDEN, NULL, + _("Access to '%s' forbidden"), + handler->path); + default: + return svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, + _("Adding directory failed: %s on %s " + "(%d %s)"), + handler->method, handler->path, + handler->sline.code, handler->sline.reason); + } + + *child_baton = dir; + + return SVN_NO_ERROR; +} + +static svn_error_t * +open_directory(const char *path, + void *parent_baton, + svn_revnum_t base_revision, + apr_pool_t *dir_pool, + void **child_baton) +{ + dir_context_t *parent = parent_baton; + dir_context_t *dir; + + dir = apr_pcalloc(dir_pool, sizeof(*dir)); + + dir->pool = dir_pool; + + dir->parent_dir = parent; + dir->commit = parent->commit; + + dir->added = FALSE; + dir->base_revision = base_revision; + dir->relpath = apr_pstrdup(dir->pool, path); + dir->name = svn_relpath_basename(dir->relpath, NULL); + dir->changed_props = apr_hash_make(dir->pool); + dir->removed_props = apr_hash_make(dir->pool); + + if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit)) + { + dir->url = svn_path_url_add_component2(parent->commit->txn_root_url, + path, dir->pool); + } + else + { + SVN_ERR(get_version_url(&dir->url, + dir->commit->session, + dir->relpath, dir->base_revision, + dir->commit->checked_in_url, + dir->pool, dir->pool /* scratch_pool */)); + } + *child_baton = dir; + + return SVN_NO_ERROR; +} + +static svn_error_t * +change_dir_prop(void *dir_baton, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + dir_context_t *dir = dir_baton; + const char *ns; + const char *proppatch_target; + + + if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit)) + { + proppatch_target = dir->url; + } + else + { + /* Ensure we have a checked out dir. */ + SVN_ERR(checkout_dir(dir, pool /* scratch_pool */)); + + proppatch_target = dir->working_url; + } + + name = apr_pstrdup(dir->pool, name); + if (strncmp(name, SVN_PROP_PREFIX, sizeof(SVN_PROP_PREFIX) - 1) == 0) + { + ns = SVN_DAV_PROP_NS_SVN; + name += sizeof(SVN_PROP_PREFIX) - 1; + } + else + { + ns = SVN_DAV_PROP_NS_CUSTOM; + } + + if (value) + { + value = svn_string_dup(value, dir->pool); + svn_ra_serf__set_prop(dir->changed_props, proppatch_target, + ns, name, value, dir->pool); + } + else + { + value = svn_string_create_empty(dir->pool); + svn_ra_serf__set_prop(dir->removed_props, proppatch_target, + ns, name, value, dir->pool); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +close_directory(void *dir_baton, + apr_pool_t *pool) +{ + dir_context_t *dir = dir_baton; + + /* Huh? We're going to be called before the texts are sent. Ugh. + * Therefore, just wave politely at our caller. + */ + + /* PROPPATCH our prop change and pass it along. */ + if (apr_hash_count(dir->changed_props) || + apr_hash_count(dir->removed_props)) + { + proppatch_context_t *proppatch_ctx; + + proppatch_ctx = apr_pcalloc(pool, sizeof(*proppatch_ctx)); + proppatch_ctx->pool = pool; + proppatch_ctx->commit = dir->commit; + proppatch_ctx->relpath = dir->relpath; + proppatch_ctx->changed_props = dir->changed_props; + proppatch_ctx->removed_props = dir->removed_props; + proppatch_ctx->base_revision = dir->base_revision; + + if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit)) + { + proppatch_ctx->path = dir->url; + } + else + { + proppatch_ctx->path = dir->working_url; + } + + SVN_ERR(proppatch_resource(proppatch_ctx, dir->commit, dir->pool)); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +add_file(const char *path, + void *parent_baton, + const char *copy_path, + svn_revnum_t copy_revision, + apr_pool_t *file_pool, + void **file_baton) +{ + dir_context_t *dir = parent_baton; + file_context_t *new_file; + const char *deleted_parent = path; + + new_file = apr_pcalloc(file_pool, sizeof(*new_file)); + new_file->pool = file_pool; + + dir->ref_count++; + + new_file->parent_dir = dir; + new_file->commit = dir->commit; + new_file->relpath = apr_pstrdup(new_file->pool, path); + new_file->name = svn_relpath_basename(new_file->relpath, NULL); + new_file->added = TRUE; + new_file->base_revision = SVN_INVALID_REVNUM; + new_file->copy_path = copy_path; + new_file->copy_revision = copy_revision; + new_file->changed_props = apr_hash_make(new_file->pool); + new_file->removed_props = apr_hash_make(new_file->pool); + + /* Ensure that the file doesn't exist by doing a HEAD on the + resource. If we're using HTTP v2, we'll just look into the + transaction root tree for this thing. */ + if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit)) + { + new_file->url = svn_path_url_add_component2(dir->commit->txn_root_url, + path, new_file->pool); + } + else + { + /* Ensure our parent directory has been checked out */ + SVN_ERR(checkout_dir(dir, new_file->pool /* scratch_pool */)); + + new_file->url = + svn_path_url_add_component2(dir->working_url, + new_file->name, new_file->pool); + } + + while (deleted_parent && deleted_parent[0] != '\0') + { + if (svn_hash_gets(dir->commit->deleted_entries, deleted_parent)) + { + break; + } + deleted_parent = svn_relpath_dirname(deleted_parent, file_pool); + } + + if (! ((dir->added && !dir->copy_path) || + (deleted_parent && deleted_parent[0] != '\0'))) + { + svn_ra_serf__handler_t *handler; + + handler = apr_pcalloc(new_file->pool, sizeof(*handler)); + handler->handler_pool = new_file->pool; + handler->session = new_file->commit->session; + handler->conn = new_file->commit->conn; + handler->method = "HEAD"; + handler->path = svn_path_url_add_component2( + dir->commit->session->session_url.path, + path, new_file->pool); + handler->response_handler = svn_ra_serf__expect_empty_body; + handler->response_baton = handler; + + SVN_ERR(svn_ra_serf__context_run_one(handler, new_file->pool)); + + if (handler->sline.code != 404) + { + return svn_error_createf(SVN_ERR_RA_DAV_ALREADY_EXISTS, NULL, + _("File '%s' already exists"), path); + } + } + + *file_baton = new_file; + + return SVN_NO_ERROR; +} + +static svn_error_t * +open_file(const char *path, + void *parent_baton, + svn_revnum_t base_revision, + apr_pool_t *file_pool, + void **file_baton) +{ + dir_context_t *parent = parent_baton; + file_context_t *new_file; + + new_file = apr_pcalloc(file_pool, sizeof(*new_file)); + new_file->pool = file_pool; + + parent->ref_count++; + + new_file->parent_dir = parent; + new_file->commit = parent->commit; + new_file->relpath = apr_pstrdup(new_file->pool, path); + new_file->name = svn_relpath_basename(new_file->relpath, NULL); + new_file->added = FALSE; + new_file->base_revision = base_revision; + new_file->changed_props = apr_hash_make(new_file->pool); + new_file->removed_props = apr_hash_make(new_file->pool); + + if (USING_HTTPV2_COMMIT_SUPPORT(parent->commit)) + { + new_file->url = svn_path_url_add_component2(parent->commit->txn_root_url, + path, new_file->pool); + } + else + { + /* CHECKOUT the file into our activity. */ + SVN_ERR(checkout_file(new_file, new_file->pool /* scratch_pool */)); + + new_file->url = new_file->working_url; + } + + *file_baton = new_file; + + return SVN_NO_ERROR; +} + +static svn_error_t * +apply_textdelta(void *file_baton, + const char *base_checksum, + apr_pool_t *pool, + svn_txdelta_window_handler_t *handler, + void **handler_baton) +{ + file_context_t *ctx = file_baton; + + /* Store the stream in a temporary file; we'll give it to serf when we + * close this file. + * + * TODO: There should be a way we can stream the request body instead of + * writing to a temporary file (ugh). A special svn stream serf bucket + * that returns EAGAIN until we receive the done call? But, when + * would we run through the serf context? Grr. + * + * ctx->pool is the same for all files in the commit that send a + * textdelta so this file is explicitly closed in close_file to + * avoid too many simultaneously open files. + */ + + SVN_ERR(svn_io_open_unique_file3(&ctx->svndiff, NULL, NULL, + svn_io_file_del_on_pool_cleanup, + ctx->pool, pool)); + + ctx->stream = svn_stream_create(ctx, pool); + svn_stream_set_write(ctx->stream, svndiff_stream_write); + + svn_txdelta_to_svndiff3(handler, handler_baton, ctx->stream, 0, + SVN_DELTA_COMPRESSION_LEVEL_DEFAULT, pool); + + if (base_checksum) + ctx->base_checksum = apr_pstrdup(ctx->pool, base_checksum); + + return SVN_NO_ERROR; +} + +static svn_error_t * +change_file_prop(void *file_baton, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + file_context_t *file = file_baton; + const char *ns; + + name = apr_pstrdup(file->pool, name); + + if (strncmp(name, SVN_PROP_PREFIX, sizeof(SVN_PROP_PREFIX) - 1) == 0) + { + ns = SVN_DAV_PROP_NS_SVN; + name += sizeof(SVN_PROP_PREFIX) - 1; + } + else + { + ns = SVN_DAV_PROP_NS_CUSTOM; + } + + if (value) + { + value = svn_string_dup(value, file->pool); + svn_ra_serf__set_prop(file->changed_props, file->url, + ns, name, value, file->pool); + } + else + { + value = svn_string_create_empty(file->pool); + + svn_ra_serf__set_prop(file->removed_props, file->url, + ns, name, value, file->pool); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +close_file(void *file_baton, + const char *text_checksum, + apr_pool_t *scratch_pool) +{ + file_context_t *ctx = file_baton; + svn_boolean_t put_empty_file = FALSE; + apr_status_t status; + + ctx->result_checksum = text_checksum; + + if (ctx->copy_path) + { + svn_ra_serf__handler_t *handler; + apr_uri_t uri; + const char *req_url; + + status = apr_uri_parse(scratch_pool, ctx->copy_path, &uri); + if (status) + { + return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Unable to parse URL '%s'"), + ctx->copy_path); + } + + /* ### conn==NULL for session->conns[0]. same as commit->conn. */ + SVN_ERR(svn_ra_serf__get_stable_url(&req_url, NULL /* latest_revnum */, + ctx->commit->session, + NULL /* conn */, + uri.path, ctx->copy_revision, + scratch_pool, scratch_pool)); + + handler = apr_pcalloc(scratch_pool, sizeof(*handler)); + handler->handler_pool = scratch_pool; + handler->method = "COPY"; + handler->path = req_url; + handler->conn = ctx->commit->conn; + handler->session = ctx->commit->session; + + handler->response_handler = svn_ra_serf__expect_empty_body; + handler->response_baton = handler; + + handler->header_delegate = setup_copy_file_headers; + handler->header_delegate_baton = ctx; + + SVN_ERR(svn_ra_serf__context_run_one(handler, scratch_pool)); + + if (handler->sline.code != 201 && handler->sline.code != 204) + { + return svn_error_trace(return_response_err(handler)); + } + } + + /* If we got no stream of changes, but this is an added-without-history + * file, make a note that we'll be PUTting a zero-byte file to the server. + */ + if ((!ctx->stream) && ctx->added && (!ctx->copy_path)) + put_empty_file = TRUE; + + /* If we had a stream of changes, push them to the server... */ + if (ctx->stream || put_empty_file) + { + svn_ra_serf__handler_t *handler; + + handler = apr_pcalloc(scratch_pool, sizeof(*handler)); + handler->handler_pool = scratch_pool; + handler->method = "PUT"; + handler->path = ctx->url; + handler->conn = ctx->commit->conn; + handler->session = ctx->commit->session; + + handler->response_handler = svn_ra_serf__expect_empty_body; + handler->response_baton = handler; + + if (put_empty_file) + { + handler->body_delegate = create_empty_put_body; + handler->body_delegate_baton = ctx; + handler->body_type = "text/plain"; + } + else + { + handler->body_delegate = create_put_body; + handler->body_delegate_baton = ctx; + handler->body_type = SVN_SVNDIFF_MIME_TYPE; + } + + handler->header_delegate = setup_put_headers; + handler->header_delegate_baton = ctx; + + SVN_ERR(svn_ra_serf__context_run_one(handler, scratch_pool)); + + if (handler->sline.code != 204 && handler->sline.code != 201) + { + return svn_error_trace(return_response_err(handler)); + } + } + + if (ctx->svndiff) + SVN_ERR(svn_io_file_close(ctx->svndiff, scratch_pool)); + + /* If we had any prop changes, push them via PROPPATCH. */ + if (apr_hash_count(ctx->changed_props) || + apr_hash_count(ctx->removed_props)) + { + proppatch_context_t *proppatch; + + proppatch = apr_pcalloc(ctx->pool, sizeof(*proppatch)); + proppatch->pool = ctx->pool; + proppatch->relpath = ctx->relpath; + proppatch->path = ctx->url; + proppatch->commit = ctx->commit; + proppatch->changed_props = ctx->changed_props; + proppatch->removed_props = ctx->removed_props; + proppatch->base_revision = ctx->base_revision; + + SVN_ERR(proppatch_resource(proppatch, ctx->commit, ctx->pool)); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +close_edit(void *edit_baton, + apr_pool_t *pool) +{ + commit_context_t *ctx = edit_baton; + const char *merge_target = + ctx->activity_url ? ctx->activity_url : ctx->txn_url; + const svn_commit_info_t *commit_info; + int response_code; + + /* MERGE our activity */ + SVN_ERR(svn_ra_serf__run_merge(&commit_info, &response_code, + ctx->session, + ctx->session->conns[0], + merge_target, + ctx->lock_tokens, + ctx->keep_locks, + pool, pool)); + + if (response_code != 200) + { + return svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, + _("MERGE request failed: returned %d " + "(during commit)"), + response_code); + } + + /* Inform the WC that we did a commit. */ + if (ctx->callback) + SVN_ERR(ctx->callback(commit_info, ctx->callback_baton, pool)); + + /* If we're using activities, DELETE our completed activity. */ + if (ctx->activity_url) + { + svn_ra_serf__handler_t *handler; + + handler = apr_pcalloc(pool, sizeof(*handler)); + handler->handler_pool = pool; + handler->method = "DELETE"; + handler->path = ctx->activity_url; + handler->conn = ctx->conn; + handler->session = ctx->session; + + handler->response_handler = svn_ra_serf__expect_empty_body; + handler->response_baton = handler; + + SVN_ERR(svn_ra_serf__context_run_one(handler, pool)); + + SVN_ERR_ASSERT(handler->sline.code == 204); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +abort_edit(void *edit_baton, + apr_pool_t *pool) +{ + commit_context_t *ctx = edit_baton; + svn_ra_serf__handler_t *handler; + + /* If an activity or transaction wasn't even created, don't bother + trying to delete it. */ + if (! (ctx->activity_url || ctx->txn_url)) + return SVN_NO_ERROR; + + /* An error occurred on conns[0]. serf 0.4.0 remembers that the connection + had a problem. We need to reset it, in order to use it again. */ + serf_connection_reset(ctx->session->conns[0]->conn); + + /* DELETE our aborted activity */ + handler = apr_pcalloc(pool, sizeof(*handler)); + handler->handler_pool = pool; + handler->method = "DELETE"; + handler->conn = ctx->session->conns[0]; + handler->session = ctx->session; + + handler->response_handler = svn_ra_serf__expect_empty_body; + handler->response_baton = handler; + + if (USING_HTTPV2_COMMIT_SUPPORT(ctx)) /* HTTP v2 */ + handler->path = ctx->txn_url; + else + handler->path = ctx->activity_url; + + SVN_ERR(svn_ra_serf__context_run_one(handler, pool)); + + /* 204 if deleted, + 403 if DELETE was forbidden (indicates MKACTIVITY was forbidden too), + 404 if the activity wasn't found. */ + if (handler->sline.code != 204 + && handler->sline.code != 403 + && handler->sline.code != 404 + ) + { + SVN_ERR_MALFUNCTION(); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__get_commit_editor(svn_ra_session_t *ra_session, + const svn_delta_editor_t **ret_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_serf__session_t *session = ra_session->priv; + svn_delta_editor_t *editor; + commit_context_t *ctx; + const char *repos_root; + const char *base_relpath; + svn_boolean_t supports_ephemeral_props; + + ctx = apr_pcalloc(pool, sizeof(*ctx)); + + ctx->pool = pool; + + ctx->session = session; + ctx->conn = session->conns[0]; + + ctx->revprop_table = svn_prop_hash_dup(revprop_table, pool); + + /* If the server supports ephemeral properties, add some carrying + interesting version information. */ + SVN_ERR(svn_ra_serf__has_capability(ra_session, &supports_ephemeral_props, + SVN_RA_CAPABILITY_EPHEMERAL_TXNPROPS, + pool)); + if (supports_ephemeral_props) + { + svn_hash_sets(ctx->revprop_table, + apr_pstrdup(pool, SVN_PROP_TXN_CLIENT_COMPAT_VERSION), + svn_string_create(SVN_VER_NUMBER, pool)); + svn_hash_sets(ctx->revprop_table, + apr_pstrdup(pool, SVN_PROP_TXN_USER_AGENT), + svn_string_create(session->useragent, pool)); + } + + ctx->callback = callback; + ctx->callback_baton = callback_baton; + + ctx->lock_tokens = lock_tokens; + ctx->keep_locks = keep_locks; + + ctx->deleted_entries = apr_hash_make(ctx->pool); + + editor = svn_delta_default_editor(pool); + editor->open_root = open_root; + editor->delete_entry = delete_entry; + editor->add_directory = add_directory; + editor->open_directory = open_directory; + editor->change_dir_prop = change_dir_prop; + editor->close_directory = close_directory; + editor->add_file = add_file; + editor->open_file = open_file; + editor->apply_textdelta = apply_textdelta; + editor->change_file_prop = change_file_prop; + editor->close_file = close_file; + editor->close_edit = close_edit; + editor->abort_edit = abort_edit; + + *ret_editor = editor; + *edit_baton = ctx; + + SVN_ERR(svn_ra_serf__get_repos_root(ra_session, &repos_root, pool)); + base_relpath = svn_uri_skip_ancestor(repos_root, session->session_url_str, + pool); + + SVN_ERR(svn_editor__insert_shims(ret_editor, edit_baton, *ret_editor, + *edit_baton, repos_root, base_relpath, + session->shim_callbacks, pool, pool)); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__change_rev_prop(svn_ra_session_t *ra_session, + svn_revnum_t rev, + const char *name, + const svn_string_t *const *old_value_p, + const svn_string_t *value, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + proppatch_context_t *proppatch_ctx; + commit_context_t *commit; + const char *proppatch_target; + const char *ns; + svn_error_t *err; + + if (old_value_p) + { + svn_boolean_t capable; + SVN_ERR(svn_ra_serf__has_capability(ra_session, &capable, + SVN_RA_CAPABILITY_ATOMIC_REVPROPS, + pool)); + + /* How did you get past the same check in svn_ra_change_rev_prop2()? */ + SVN_ERR_ASSERT(capable); + } + + commit = apr_pcalloc(pool, sizeof(*commit)); + + commit->pool = pool; + + commit->session = session; + commit->conn = session->conns[0]; + + if (SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session)) + { + proppatch_target = apr_psprintf(pool, "%s/%ld", session->rev_stub, rev); + } + else + { + const char *vcc_url; + + SVN_ERR(svn_ra_serf__discover_vcc(&vcc_url, commit->session, + commit->conn, pool)); + + SVN_ERR(svn_ra_serf__fetch_dav_prop(&proppatch_target, + commit->conn, vcc_url, rev, + "href", + pool, pool)); + } + + if (strncmp(name, SVN_PROP_PREFIX, sizeof(SVN_PROP_PREFIX) - 1) == 0) + { + ns = SVN_DAV_PROP_NS_SVN; + name += sizeof(SVN_PROP_PREFIX) - 1; + } + else + { + ns = SVN_DAV_PROP_NS_CUSTOM; + } + + /* PROPPATCH our log message and pass it along. */ + proppatch_ctx = apr_pcalloc(pool, sizeof(*proppatch_ctx)); + proppatch_ctx->pool = pool; + proppatch_ctx->commit = commit; + proppatch_ctx->path = proppatch_target; + proppatch_ctx->changed_props = apr_hash_make(proppatch_ctx->pool); + proppatch_ctx->removed_props = apr_hash_make(proppatch_ctx->pool); + if (old_value_p) + { + proppatch_ctx->previous_changed_props = apr_hash_make(proppatch_ctx->pool); + proppatch_ctx->previous_removed_props = apr_hash_make(proppatch_ctx->pool); + } + proppatch_ctx->base_revision = SVN_INVALID_REVNUM; + + if (old_value_p && *old_value_p) + { + svn_ra_serf__set_prop(proppatch_ctx->previous_changed_props, + proppatch_ctx->path, + ns, name, *old_value_p, proppatch_ctx->pool); + } + else if (old_value_p) + { + svn_string_t *dummy_value = svn_string_create_empty(proppatch_ctx->pool); + + svn_ra_serf__set_prop(proppatch_ctx->previous_removed_props, + proppatch_ctx->path, + ns, name, dummy_value, proppatch_ctx->pool); + } + + if (value) + { + svn_ra_serf__set_prop(proppatch_ctx->changed_props, proppatch_ctx->path, + ns, name, value, proppatch_ctx->pool); + } + else + { + value = svn_string_create_empty(proppatch_ctx->pool); + + svn_ra_serf__set_prop(proppatch_ctx->removed_props, proppatch_ctx->path, + ns, name, value, proppatch_ctx->pool); + } + + err = proppatch_resource(proppatch_ctx, commit, proppatch_ctx->pool); + if (err) + return + svn_error_create + (SVN_ERR_RA_DAV_REQUEST_FAILED, err, + _("DAV request failed; it's possible that the repository's " + "pre-revprop-change hook either failed or is non-existent")); + + return SVN_NO_ERROR; +} |