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