/* * 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 #include #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; 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_ctx; /* Changed properties. const char * -> svn_prop_t * */ apr_hash_t *prop_changes; /* Same, for the old value, or NULL. */ apr_hash_t *old_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 *relpath; svn_revnum_t revision; commit_context_t *commit_ctx; svn_boolean_t non_recursive_if; /* Only create a non-recursive If header */ } 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_ctx; /* 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 properties (const char * -> svn_prop_t *) */ apr_hash_t *prop_changes; /* 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_ctx; /* 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 properties (const char * -> svn_prop_t *) */ apr_hash_t *prop_changes; /* URL to PUT the file at. */ const char *url; } file_context_t; /* Setup routines and handlers for various requests we'll invoke. */ /* 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 /* request pool */, apr_pool_t *scratch_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:", SVN_VA_NULL); svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:activity-set", SVN_VA_NULL); svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:href", SVN_VA_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_empty_tag_buckets(body_bkt, alloc, "D:apply-to-version", SVN_VA_NULL); 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; 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 = svn_ra_serf__create_handler(commit_ctx->session, scratch_pool); 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(svn_ra_serf__unexpected_status(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 svn_error_trace(err); } while (err && retry_count--); return svn_error_trace(err); } static svn_error_t * checkout_dir(dir_context_t *dir, apr_pool_t *scratch_pool) { dir_context_t *c_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 (c_dir) { if (c_dir->added) { /* Calculate the working_url by skipping the shared ancestor between * the c_dir_parent->relpath and dir->relpath. This is safe since an * add is guaranteed to have a parent that is checked out. */ dir_context_t *c_dir_parent = c_dir->parent_dir; const char *relpath = svn_relpath_skip_ancestor(c_dir_parent->relpath, dir->relpath); /* Implicitly checkout this dir now. */ SVN_ERR_ASSERT(c_dir_parent->working_url); dir->working_url = svn_path_url_add_component2( c_dir_parent->working_url, relpath, dir->pool); return SVN_NO_ERROR; } c_dir = c_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_ctx->baseline_url) { checkout_url = dir->commit_ctx->vcc_url; working = &dir->commit_ctx->baseline_url; } else { checkout_url = dir->url; working = &dir->working_url; } /* Checkout our directory into the activity URL now. */ return svn_error_trace(retry_checkout_node(working, dir->commit_ctx, checkout_url, dir->pool, scratch_pool)); } /* 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; 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. */ SVN_ERR(svn_ra_serf__get_stable_url(&propfind_url, NULL /* latest_revnum */, session, 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, session, 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) { 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. */ SVN_ERR_ASSERT(parent_dir->working_url); 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_ctx->session, file->relpath, file->base_revision, NULL, scratch_pool, scratch_pool)); /* Checkout our file into the activity URL now. */ return svn_error_trace(retry_checkout_node(&file->working_url, file->commit_ctx, checkout_url, file->pool, scratch_pool)); } /* 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; } /* Helper for create_proppatch_body. Writes per property xml to body */ static svn_error_t * write_prop_xml(const proppatch_context_t *proppatch, serf_bucket_t *body_bkt, serf_bucket_alloc_t *alloc, const svn_prop_t *prop, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { serf_bucket_t *cdata_bkt; const char *encoding; const svn_string_t *encoded_value; const char *prop_name; const svn_prop_t *old_prop; SVN_ERR(get_encoding_and_cdata(&encoding, &encoded_value, alloc, prop->value, result_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 (strncmp(prop->name, SVN_PROP_PREFIX, sizeof(SVN_PROP_PREFIX) - 1) == 0) { prop_name = apr_pstrcat(result_pool, "S:", prop->name + sizeof(SVN_PROP_PREFIX) - 1, SVN_VA_NULL); } else { prop_name = apr_pstrcat(result_pool, "C:", prop->name, SVN_VA_NULL); } if (cdata_bkt) svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, prop_name, "V:encoding", encoding, SVN_VA_NULL); else svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, prop_name, "V:" SVN_DAV__OLD_VALUE__ABSENT, "1", SVN_VA_NULL); old_prop = proppatch->old_props ? svn_hash_gets(proppatch->old_props, prop->name) : NULL; if (old_prop) { 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_prop->value, result_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, SVN_VA_NULL); else svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "V:" SVN_DAV__OLD_VALUE, "V:" SVN_DAV__OLD_VALUE__ABSENT, "1", SVN_VA_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, ">)", SVN_VA_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 /* request pool */, apr_pool_t *scratch_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)); } if (proppatch->relpath && proppatch->commit_ctx) SVN_ERR(maybe_set_lock_token_header(headers, proppatch->commit_ctx, proppatch->relpath, pool)); return SVN_NO_ERROR; } /* 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 *pool /* request pool */, apr_pool_t *scratch_pool) { proppatch_context_t *ctx = baton; serf_bucket_t *body_bkt; svn_boolean_t opened = FALSE; apr_hash_index_t *hi; 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, SVN_VA_NULL); /* First we write property SETs */ for (hi = apr_hash_first(scratch_pool, ctx->prop_changes); hi; hi = apr_hash_next(hi)) { svn_prop_t *prop = apr_hash_this_val(hi); if (prop->value || (ctx->old_props && svn_hash_gets(ctx->old_props, prop->name))) { if (!opened) { opened = TRUE; svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:set", SVN_VA_NULL); svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:prop", SVN_VA_NULL); } SVN_ERR(write_prop_xml(ctx, body_bkt, alloc, prop, pool, scratch_pool)); } } if (opened) { svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:prop"); svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:set"); } /* And then property REMOVEs */ opened = FALSE; for (hi = apr_hash_first(scratch_pool, ctx->prop_changes); hi; hi = apr_hash_next(hi)) { svn_prop_t *prop = apr_hash_this_val(hi); if (!prop->value && !(ctx->old_props && svn_hash_gets(ctx->old_props, prop->name))) { if (!opened) { opened = TRUE; svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:remove", SVN_VA_NULL); svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:prop", SVN_VA_NULL); } SVN_ERR(write_prop_xml(ctx, body_bkt, alloc, prop, pool, scratch_pool)); } } if (opened) { 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(svn_ra_serf__session_t *session, proppatch_context_t *proppatch, apr_pool_t *pool) { svn_ra_serf__handler_t *handler; svn_error_t *err; handler = svn_ra_serf__create_handler(session, pool); handler->method = "PROPPATCH"; handler->path = proppatch->path; handler->header_delegate = setup_proppatch_headers; handler->header_delegate_baton = proppatch; handler->body_delegate = create_proppatch_body; handler->body_delegate_baton = proppatch; handler->body_type = "text/xml"; handler->response_handler = svn_ra_serf__handle_multistatus_only; handler->response_baton = handler; err = svn_ra_serf__context_run_one(handler, pool); if (!err && handler->sline.code != 207) err = svn_error_trace(svn_ra_serf__unexpected_status(handler)); /* Use specific error code for property handling errors. Use loop to provide the right result with tracing */ if (err && err->apr_err == SVN_ERR_RA_DAV_REQUEST_FAILED) { svn_error_t *e = err; while (e && e->apr_err == SVN_ERR_RA_DAV_REQUEST_FAILED) { e->apr_err = SVN_ERR_RA_DAV_PROPPATCH_FAILED; e = e->child; } } return svn_error_trace(err); } /* 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 /* request pool */, apr_pool_t *scratch_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. */ SVN_ERR(svn_io_file_flush(ctx->svndiff, pool)); apr_file_buffer_set(ctx->svndiff, NULL, 0); offset = 0; SVN_ERR(svn_io_file_seek(ctx->svndiff, APR_SET, &offset, pool)); *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 /* request pool */, apr_pool_t *scratch_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 /* request pool */, apr_pool_t *scratch_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, ctx->relpath, pool)); return APR_SUCCESS; } static svn_error_t * setup_copy_file_headers(serf_bucket_t *headers, void *baton, apr_pool_t *pool /* request pool */, apr_pool_t *scratch_pool) { file_context_t *file = baton; apr_uri_t uri; const char *absolute_uri; /* The Dest URI must be absolute. Bummer. */ uri = file->commit_ctx->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, "Overwrite", "F"); return SVN_NO_ERROR; } static svn_error_t * setup_if_header_recursive(svn_boolean_t *added, serf_bucket_t *headers, commit_context_t *commit_ctx, const char *rq_relpath, apr_pool_t *pool) { svn_stringbuf_t *sb = NULL; apr_hash_index_t *hi; apr_pool_t *iterpool = NULL; if (!commit_ctx->lock_tokens) { *added = FALSE; return SVN_NO_ERROR; } /* We try to create a directory, so within the Subversion world that would imply that there is nothing here, but mod_dav_svn still sees locks on the old nodes here as in DAV it is perfectly legal to lock something that is not there... Let's make mod_dav, mod_dav_svn and the DAV RFC happy by providing the locks we know of with the request */ for (hi = apr_hash_first(pool, commit_ctx->lock_tokens); hi; hi = apr_hash_next(hi)) { const char *relpath = apr_hash_this_key(hi); apr_uri_t uri; if (!svn_relpath_skip_ancestor(rq_relpath, relpath)) continue; else if (svn_hash_gets(commit_ctx->deleted_entries, relpath)) { /* When a path is already explicit deleted then its lock will be removed by mod_dav. But mod_dav doesn't remove locks on descendants */ continue; } if (!iterpool) iterpool = svn_pool_create(pool); else svn_pool_clear(iterpool); if (sb == NULL) sb = svn_stringbuf_create("", pool); else svn_stringbuf_appendbyte(sb, ' '); uri = commit_ctx->session->session_url; uri.path = (char *)svn_path_url_add_component2(uri.path, relpath, iterpool); svn_stringbuf_appendbyte(sb, '<'); svn_stringbuf_appendcstr(sb, apr_uri_unparse(iterpool, &uri, 0)); svn_stringbuf_appendcstr(sb, "> (<"); svn_stringbuf_appendcstr(sb, apr_hash_this_val(hi)); svn_stringbuf_appendcstr(sb, ">)"); } if (iterpool) svn_pool_destroy(iterpool); if (sb) { serf_bucket_headers_set(headers, "If", sb->data); *added = TRUE; } else *added = FALSE; return SVN_NO_ERROR; } static svn_error_t * setup_add_dir_common_headers(serf_bucket_t *headers, void *baton, apr_pool_t *pool /* request pool */, apr_pool_t *scratch_pool) { dir_context_t *dir = baton; svn_boolean_t added; return svn_error_trace( setup_if_header_recursive(&added, headers, dir->commit_ctx, dir->relpath, pool)); } static svn_error_t * setup_copy_dir_headers(serf_bucket_t *headers, void *baton, apr_pool_t *pool /* request pool */, apr_pool_t *scratch_pool) { dir_context_t *dir = baton; apr_uri_t uri; const char *absolute_uri; /* The Dest URI must be absolute. Bummer. */ uri = dir->commit_ctx->session->session_url; if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit_ctx)) { 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", "F"); /* Implicitly checkout this dir now. */ dir->working_url = apr_pstrdup(dir->pool, uri.path); return svn_error_trace(setup_add_dir_common_headers(headers, baton, pool, scratch_pool)); } static svn_error_t * setup_delete_headers(serf_bucket_t *headers, void *baton, apr_pool_t *pool /* request pool */, apr_pool_t *scratch_pool) { delete_context_t *del = baton; svn_boolean_t added; serf_bucket_headers_set(headers, SVN_DAV_VERSION_NAME_HEADER, apr_ltoa(pool, del->revision)); if (! del->non_recursive_if) SVN_ERR(setup_if_header_recursive(&added, headers, del->commit_ctx, del->relpath, pool)); else { SVN_ERR(maybe_set_lock_token_header(headers, del->commit_ctx, del->relpath, pool)); added = TRUE; } if (added && del->commit_ctx->keep_locks) serf_bucket_headers_setn(headers, SVN_DAV_OPTIONS_HEADER, SVN_DAV_OPTION_KEEP_LOCKS); 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 /* request pool */, apr_pool_t *scratch_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 /* request pool */, apr_pool_t *scratch_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 *commit_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; apr_pool_t *scratch_pool = svn_pool_create(dir_pool); if (SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(commit_ctx->session)) { post_response_ctx_t *prc; const char *rel_path; svn_boolean_t post_with_revprops = (NULL != svn_hash_gets(commit_ctx->session->supported_posts, "create-txn-with-props")); /* Create our activity URL now on the server. */ handler = svn_ra_serf__create_handler(commit_ctx->session, scratch_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 ? commit_ctx->revprop_table : NULL; handler->header_delegate = setup_post_headers; handler->header_delegate_baton = NULL; handler->path = commit_ctx->session->me_resource; prc = apr_pcalloc(scratch_pool, sizeof(*prc)); prc->handler = handler; prc->commit_ctx = commit_ctx; handler->response_handler = post_response_handler; handler->response_baton = prc; SVN_ERR(svn_ra_serf__context_run_one(handler, scratch_pool)); if (handler->sline.code != 201) return svn_error_trace(svn_ra_serf__unexpected_status(handler)); if (! (commit_ctx->txn_root_url && commit_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, commit_ctx->session->session_url.path, commit_ctx->session, scratch_pool)); commit_ctx->txn_root_url = svn_path_url_add_component2( commit_ctx->txn_root_url, rel_path, commit_ctx->pool); /* Build our directory baton. */ dir = apr_pcalloc(dir_pool, sizeof(*dir)); dir->pool = dir_pool; dir->commit_ctx = commit_ctx; dir->base_revision = base_revision; dir->relpath = ""; dir->name = ""; dir->prop_changes = apr_hash_make(dir->pool); dir->url = apr_pstrdup(dir->pool, commit_ctx->txn_root_url); /* If we included our revprops in the POST, we need not PROPPATCH them. */ proppatch_target = post_with_revprops ? NULL : commit_ctx->txn_url; } else { const char *activity_str = commit_ctx->session->activity_collection_url; if (!activity_str) SVN_ERR(svn_ra_serf__v1_get_activity_collection( &activity_str, commit_ctx->session, scratch_pool, scratch_pool)); commit_ctx->activity_url = svn_path_url_add_component2( activity_str, svn_uuid_generate(scratch_pool), commit_ctx->pool); /* Create our activity URL now on the server. */ handler = svn_ra_serf__create_handler(commit_ctx->session, scratch_pool); handler->method = "MKACTIVITY"; handler->path = commit_ctx->activity_url; handler->response_handler = svn_ra_serf__expect_empty_body; handler->response_baton = handler; SVN_ERR(svn_ra_serf__context_run_one(handler, scratch_pool)); if (handler->sline.code != 201) return svn_error_trace(svn_ra_serf__unexpected_status(handler)); /* Now go fetch our VCC and baseline so we can do a CHECKOUT. */ SVN_ERR(svn_ra_serf__discover_vcc(&(commit_ctx->vcc_url), commit_ctx->session, scratch_pool)); /* Build our directory baton. */ dir = apr_pcalloc(dir_pool, sizeof(*dir)); dir->pool = dir_pool; dir->commit_ctx = commit_ctx; dir->base_revision = base_revision; dir->relpath = ""; dir->name = ""; dir->prop_changes = apr_hash_make(dir->pool); SVN_ERR(get_version_url(&dir->url, dir->commit_ctx->session, dir->relpath, dir->base_revision, commit_ctx->checked_in_url, dir->pool, scratch_pool)); commit_ctx->checked_in_url = apr_pstrdup(commit_ctx->pool, dir->url); /* Checkout our root dir */ SVN_ERR(checkout_dir(dir, scratch_pool)); proppatch_target = commit_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(scratch_pool, sizeof(*proppatch_ctx)); proppatch_ctx->pool = scratch_pool; proppatch_ctx->commit_ctx = NULL; /* No lock info */ proppatch_ctx->path = proppatch_target; proppatch_ctx->prop_changes = apr_hash_make(proppatch_ctx->pool); proppatch_ctx->base_revision = SVN_INVALID_REVNUM; for (hi = apr_hash_first(scratch_pool, commit_ctx->revprop_table); hi; hi = apr_hash_next(hi)) { svn_prop_t *prop = apr_palloc(scratch_pool, sizeof(*prop)); prop->name = apr_hash_this_key(hi); prop->value = apr_hash_this_val(hi); svn_hash_sets(proppatch_ctx->prop_changes, prop->name, prop); } SVN_ERR(proppatch_resource(commit_ctx->session, proppatch_ctx, scratch_pool)); } svn_pool_destroy(scratch_pool); *root_baton = dir; 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 /* request pool */, apr_pool_t *scratch_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->commit_ctx->lock_tokens, ctx->relpath, body, alloc, pool); *body_bkt = body; 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; if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit_ctx)) { delete_target = svn_path_url_add_component2( dir->commit_ctx->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->relpath = apr_pstrdup(pool, path); delete_ctx->revision = revision; delete_ctx->commit_ctx = dir->commit_ctx; handler = svn_ra_serf__create_handler(dir->commit_ctx->session, pool); 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; handler->no_fail_on_http_failure_status = TRUE; SVN_ERR(svn_ra_serf__context_run_one(handler, pool)); if (handler->sline.code == 400) { /* Try again with non-standard body to overcome Apache Httpd header limit */ delete_ctx->non_recursive_if = TRUE; handler = svn_ra_serf__create_handler(dir->commit_ctx->session, pool); 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; handler->body_type = "text/xml"; handler->body_delegate = create_delete_body; handler->body_delegate_baton = delete_ctx; SVN_ERR(svn_ra_serf__context_run_one(handler, pool)); } if (handler->server_error) return svn_ra_serf__server_error_create(handler, pool); /* 204 No Content: item successfully deleted */ if (handler->sline.code != 204) return svn_error_trace(svn_ra_serf__unexpected_status(handler)); svn_hash_sets(dir->commit_ctx->deleted_entries, apr_pstrdup(dir->commit_ctx->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_ctx = parent->commit_ctx; dir->added = TRUE; dir->base_revision = SVN_INVALID_REVNUM; dir->copy_revision = copyfrom_revision; dir->copy_path = apr_pstrdup(dir->pool, copyfrom_path); dir->relpath = apr_pstrdup(dir->pool, path); dir->name = svn_relpath_basename(dir->relpath, NULL); dir->prop_changes = apr_hash_make(dir->pool); if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit_ctx)) { dir->url = svn_path_url_add_component2(parent->commit_ctx->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_ctx->checked_in_url, dir->name, dir->pool); mkcol_target = svn_path_url_add_component2( parent->working_url, dir->name, dir->pool); } handler = svn_ra_serf__create_handler(dir->commit_ctx->session, dir->pool); handler->response_handler = svn_ra_serf__expect_empty_body; handler->response_baton = handler; if (!dir->copy_path) { handler->method = "MKCOL"; handler->path = mkcol_target; handler->header_delegate = setup_add_dir_common_headers; handler->header_delegate_baton = dir; } 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); } SVN_ERR(svn_ra_serf__get_stable_url(&req_url, NULL /* latest_revnum */, dir->commit_ctx->session, 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; } /* We have the same problem as with DELETE here: if there are too many locks, the request fails. But in this case there is no way to retry with a non-standard request. #### How to fix? */ SVN_ERR(svn_ra_serf__context_run_one(handler, dir->pool)); if (handler->sline.code != 201) return svn_error_trace(svn_ra_serf__unexpected_status(handler)); *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_ctx = parent->commit_ctx; dir->added = FALSE; dir->base_revision = base_revision; dir->relpath = apr_pstrdup(dir->pool, path); dir->name = svn_relpath_basename(dir->relpath, NULL); dir->prop_changes = apr_hash_make(dir->pool); if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit_ctx)) { dir->url = svn_path_url_add_component2(parent->commit_ctx->txn_root_url, path, dir->pool); } else { SVN_ERR(get_version_url(&dir->url, dir->commit_ctx->session, dir->relpath, dir->base_revision, dir->commit_ctx->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 *scratch_pool) { dir_context_t *dir = dir_baton; svn_prop_t *prop; if (! USING_HTTPV2_COMMIT_SUPPORT(dir->commit_ctx)) { /* Ensure we have a checked out dir. */ SVN_ERR(checkout_dir(dir, scratch_pool)); } prop = apr_palloc(dir->pool, sizeof(*prop)); prop->name = apr_pstrdup(dir->pool, name); prop->value = svn_string_dup(value, dir->pool); svn_hash_sets(dir->prop_changes, prop->name, prop); 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->prop_changes)) { proppatch_context_t *proppatch_ctx; proppatch_ctx = apr_pcalloc(pool, sizeof(*proppatch_ctx)); proppatch_ctx->pool = pool; proppatch_ctx->commit_ctx = NULL /* No lock tokens necessary */; proppatch_ctx->relpath = dir->relpath; proppatch_ctx->prop_changes = dir->prop_changes; proppatch_ctx->base_revision = dir->base_revision; if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit_ctx)) { proppatch_ctx->path = dir->url; } else { proppatch_ctx->path = dir->working_url; } SVN_ERR(proppatch_resource(dir->commit_ctx->session, proppatch_ctx, 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; apr_pool_t *scratch_pool = svn_pool_create(file_pool); 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_ctx = dir->commit_ctx; 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 = apr_pstrdup(new_file->pool, copy_path); new_file->copy_revision = copy_revision; new_file->prop_changes = 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_ctx)) { new_file->url = svn_path_url_add_component2(dir->commit_ctx->txn_root_url, path, new_file->pool); } else { /* Ensure our parent directory has been checked out */ SVN_ERR(checkout_dir(dir, 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_ctx->deleted_entries, deleted_parent)) { break; } deleted_parent = svn_relpath_dirname(deleted_parent, file_pool); } if (copy_path) { svn_ra_serf__handler_t *handler; apr_uri_t uri; const char *req_url; apr_status_t status; /* Create the copy directly as cheap 'does exist/out of date' check. We update the copy (if needed) from close_file() */ status = apr_uri_parse(scratch_pool, copy_path, &uri); if (status) return svn_ra_serf__wrap_err(status, NULL); SVN_ERR(svn_ra_serf__get_stable_url(&req_url, NULL /* latest_revnum */, dir->commit_ctx->session, uri.path, copy_revision, scratch_pool, scratch_pool)); handler = svn_ra_serf__create_handler(dir->commit_ctx->session, scratch_pool); handler->method = "COPY"; handler->path = req_url; handler->response_handler = svn_ra_serf__expect_empty_body; handler->response_baton = handler; handler->header_delegate = setup_copy_file_headers; handler->header_delegate_baton = new_file; SVN_ERR(svn_ra_serf__context_run_one(handler, scratch_pool)); if (handler->sline.code != 201) return svn_error_trace(svn_ra_serf__unexpected_status(handler)); } else if (! ((dir->added && !dir->copy_path) || (deleted_parent && deleted_parent[0] != '\0'))) { svn_ra_serf__handler_t *handler; svn_error_t *err; handler = svn_ra_serf__create_handler(dir->commit_ctx->session, scratch_pool); handler->method = "HEAD"; handler->path = svn_path_url_add_component2( dir->commit_ctx->session->session_url.path, path, scratch_pool); handler->response_handler = svn_ra_serf__expect_empty_body; handler->response_baton = handler; handler->no_dav_headers = TRUE; /* Read only operation outside txn */ err = svn_ra_serf__context_run_one(handler, scratch_pool); if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) { svn_error_clear(err); /* Great. We can create a new file! */ } else if (err) return svn_error_trace(err); else return svn_error_createf(SVN_ERR_FS_ALREADY_EXISTS, NULL, _("File '%s' already exists"), path); } svn_pool_destroy(scratch_pool); *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_ctx = parent->commit_ctx; 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->prop_changes = apr_hash_make(new_file->pool); if (USING_HTTPV2_COMMIT_SUPPORT(parent->commit_ctx)) { new_file->url = svn_path_url_add_component2(parent->commit_ctx->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; } /* Implements svn_stream_lazyopen_func_t for apply_textdelta */ static svn_error_t * delayed_commit_stream_open(svn_stream_t **stream, void *baton, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { file_context_t *file_ctx = baton; SVN_ERR(svn_io_open_unique_file3(&file_ctx->svndiff, NULL, NULL, svn_io_file_del_on_pool_cleanup, file_ctx->pool, scratch_pool)); *stream = svn_stream_from_aprfile2(file_ctx->svndiff, TRUE, result_pool); 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->stream = svn_stream_lazyopen_create(delayed_commit_stream_open, ctx, FALSE, ctx->pool); 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; svn_prop_t *prop; prop = apr_palloc(file->pool, sizeof(*prop)); prop->name = apr_pstrdup(file->pool, name); prop->value = svn_string_dup(value, file->pool); svn_hash_sets(file->prop_changes, prop->name, prop); 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; ctx->result_checksum = text_checksum; /* 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->svndiff) && ctx->added && (!ctx->copy_path)) put_empty_file = TRUE; /* If we had a stream of changes, push them to the server... */ if (ctx->svndiff || put_empty_file) { svn_ra_serf__handler_t *handler; int expected_result; handler = svn_ra_serf__create_handler(ctx->commit_ctx->session, scratch_pool); handler->method = "PUT"; handler->path = ctx->url; 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 (ctx->added && ! ctx->copy_path) expected_result = 201; /* Created */ else expected_result = 204; /* Updated */ if (handler->sline.code != expected_result) return svn_error_trace(svn_ra_serf__unexpected_status(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->prop_changes)) { proppatch_context_t *proppatch; proppatch = apr_pcalloc(scratch_pool, sizeof(*proppatch)); proppatch->pool = scratch_pool; proppatch->relpath = ctx->relpath; proppatch->path = ctx->url; proppatch->commit_ctx = ctx->commit_ctx; proppatch->prop_changes = ctx->prop_changes; proppatch->base_revision = ctx->base_revision; SVN_ERR(proppatch_resource(ctx->commit_ctx->session, proppatch, scratch_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; svn_error_t *err = NULL; /* MERGE our activity */ SVN_ERR(svn_ra_serf__run_merge(&commit_info, ctx->session, merge_target, ctx->lock_tokens, ctx->keep_locks, pool, pool)); ctx->txn_url = NULL; /* If HTTPv2, the txn is now done */ /* Inform the WC that we did a commit. */ if (ctx->callback) 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 = svn_ra_serf__create_handler(ctx->session, pool); handler->method = "DELETE"; handler->path = ctx->activity_url; handler->response_handler = svn_ra_serf__expect_empty_body; handler->response_baton = handler; ctx->activity_url = NULL; /* Don't try again in abort_edit() on fail */ SVN_ERR(svn_error_compose_create( err, svn_ra_serf__context_run_one(handler, pool))); if (handler->sline.code != 204) return svn_error_trace(svn_ra_serf__unexpected_status(handler)); } SVN_ERR(err); 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 = svn_ra_serf__create_handler(ctx->session, pool); handler->method = "DELETE"; handler->response_handler = svn_ra_serf__expect_empty_body; handler->response_baton = handler; handler->no_fail_on_http_failure_status = TRUE; 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) { return svn_error_trace(svn_ra_serf__unexpected_status(handler)); } /* Don't delete again if somebody aborts twice */ ctx->activity_url = NULL; ctx->txn_url = NULL; 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->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 && apr_hash_count(lock_tokens)) ? lock_tokens : NULL; 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; const char *proppatch_target; const svn_string_t *tmp_old_value; svn_boolean_t atomic_capable = FALSE; svn_prop_t *prop; svn_error_t *err; if (old_value_p || !value) SVN_ERR(svn_ra_serf__has_capability(ra_session, &atomic_capable, SVN_RA_CAPABILITY_ATOMIC_REVPROPS, pool)); if (old_value_p) { /* How did you get past the same check in svn_ra_change_rev_prop2()? */ SVN_ERR_ASSERT(atomic_capable); } else if (! value && atomic_capable) { svn_string_t *old_value; /* mod_dav_svn doesn't report a failure when a property delete fails. The atomic revprop change behavior is a nice workaround, to allow getting access to the error anyway. Somehow the mod_dav maintainers think that returning an error from mod_dav's property delete is an RFC violation. See https://issues.apache.org/bugzilla/show_bug.cgi?id=53525 */ SVN_ERR(svn_ra_serf__rev_prop(ra_session, rev, name, &old_value, pool)); if (!old_value) return SVN_NO_ERROR; /* Nothing to delete */ /* The api expects a double const pointer. Let's make one */ tmp_old_value = old_value; old_value_p = &tmp_old_value; } 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, session, pool)); SVN_ERR(svn_ra_serf__fetch_dav_prop(&proppatch_target, session, vcc_url, rev, "href", pool, pool)); } /* PROPPATCH our log message and pass it along. */ proppatch_ctx = apr_pcalloc(pool, sizeof(*proppatch_ctx)); proppatch_ctx->pool = pool; proppatch_ctx->commit_ctx = NULL; /* No lock headers */ proppatch_ctx->path = proppatch_target; proppatch_ctx->prop_changes = apr_hash_make(pool); proppatch_ctx->base_revision = SVN_INVALID_REVNUM; if (old_value_p) { prop = apr_palloc(pool, sizeof (*prop)); prop->name = name; prop->value = *old_value_p; proppatch_ctx->old_props = apr_hash_make(pool); svn_hash_sets(proppatch_ctx->old_props, prop->name, prop); } prop = apr_palloc(pool, sizeof (*prop)); prop->name = name; prop->value = value; svn_hash_sets(proppatch_ctx->prop_changes, prop->name, prop); err = proppatch_resource(session, proppatch_ctx, pool); /* Use specific error code for old property value mismatch. Use loop to provide the right result with tracing */ if (err && err->apr_err == SVN_ERR_RA_DAV_PRECONDITION_FAILED) { svn_error_t *e = err; while (e && e->apr_err == SVN_ERR_RA_DAV_PRECONDITION_FAILED) { e->apr_err = SVN_ERR_FS_PROP_BASEVALUE_MISMATCH; e = e->child; } } return svn_error_trace(err); }