| /* |
| * 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; |
| |
| 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 */ |
| |
| int open_batons; /* Number of open batons */ |
| } 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; |
| |
| /* 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 for collecting the svndiff. */ |
| svn_stream_t *stream; |
| |
| /* Buffer holding the svndiff (can spill to disk). */ |
| svn_ra_serf__request_body_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_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); |
| |
| commit_ctx->open_batons++; |
| |
| 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); |
| |
| dir->commit_ctx->open_batons++; |
| |
| 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); |
| |
| dir->commit_ctx->open_batons++; |
| |
| 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)); |
| } |
| |
| dir->commit_ctx->open_batons--; |
| |
| 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; |
| |
| 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); |
| |
| dir->commit_ctx->open_batons++; |
| |
| /* 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; |
| |
| 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); |
| |
| parent->commit_ctx->open_batons++; |
| |
| 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; |
| } |
| |
| 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; |
| int svndiff_version; |
| int compression_level; |
| |
| /* Construct a holder for the request body; 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 |
| * possibly 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. |
| * |
| * BH: If you wait to a specific event... why not use that event to |
| * trigger the operation? |
| * Having a request (body) bucket return EAGAIN until done stalls |
| * the entire HTTP pipeline after writing the first part of the |
| * request. It is not like we can interrupt some part of a request |
| * and continue later. Or somebody else must use tempfiles and |
| * always assume that clients work this bad... as it only knows |
| * for sure after the request is completely available. |
| */ |
| |
| ctx->svndiff = |
| svn_ra_serf__request_body_create(SVN_RA_SERF__REQUEST_BODY_IN_MEM_SIZE, |
| ctx->pool); |
| ctx->stream = svn_ra_serf__request_body_get_stream(ctx->svndiff); |
| |
| if (ctx->commit_ctx->session->supports_svndiff1 && |
| ctx->commit_ctx->session->using_compression) |
| { |
| /* Use compressed svndiff1 format, if possible. */ |
| svndiff_version = 1; |
| compression_level = SVN_DELTA_COMPRESSION_LEVEL_DEFAULT; |
| } |
| else |
| { |
| /* Difference between svndiff formats 0 and 1 that format 1 allows |
| * compression. Uncompressed svndiff0 should also be slightly more |
| * effective if the compression is not required at all. |
| * |
| * If the server cannot handle svndiff1, or compression is disabled |
| * with the 'http-compression = no' client configuration option, fall |
| * back to uncompressed svndiff0 format. As a bonus, users can force |
| * the usage of the uncompressed format by setting the corresponding |
| * client configuration option, if they want to. |
| */ |
| svndiff_version = 0; |
| compression_level = SVN_DELTA_COMPRESSION_LEVEL_NONE; |
| } |
| |
| /* Disown the stream; we'll close it explicitly in close_file(). */ |
| svn_txdelta_to_svndiff3(handler, handler_baton, |
| svn_stream_disown(ctx->stream, pool), |
| svndiff_version, compression_level, 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 |
| { |
| SVN_ERR(svn_stream_close(ctx->stream)); |
| |
| svn_ra_serf__request_body_get_delegate(&handler->body_delegate, |
| &handler->body_delegate_baton, |
| ctx->svndiff); |
| 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)); |
| } |
| |
| /* Don't keep open file handles longer than necessary. */ |
| if (ctx->svndiff) |
| SVN_ERR(svn_ra_serf__request_body_cleanup(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)); |
| } |
| |
| ctx->commit_ctx->open_batons--; |
| |
| 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; |
| |
| if (ctx->open_batons > 0) |
| return svn_error_create( |
| SVN_ERR_FS_INCORRECT_EDITOR_COMPLETION, NULL, |
| _("Closing editor with directories or files open")); |
| |
| /* 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); |
| } |