| /* |
| * commit.c: wrappers around wc commit functionality. |
| * |
| * ==================================================================== |
| * 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. |
| * ==================================================================== |
| */ |
| |
| /* ==================================================================== */ |
| |
| |
| |
| /*** Includes. ***/ |
| |
| #include <string.h> |
| #include <apr_strings.h> |
| #include <apr_hash.h> |
| #include "svn_hash.h" |
| #include "svn_wc.h" |
| #include "svn_ra.h" |
| #include "svn_client.h" |
| #include "svn_string.h" |
| #include "svn_pools.h" |
| #include "svn_error.h" |
| #include "svn_error_codes.h" |
| #include "svn_dirent_uri.h" |
| #include "svn_path.h" |
| #include "svn_sorts.h" |
| |
| #include "client.h" |
| #include "private/svn_wc_private.h" |
| #include "private/svn_ra_private.h" |
| #include "private/svn_sorts_private.h" |
| |
| #include "svn_private_config.h" |
| |
| struct capture_baton_t { |
| svn_commit_callback2_t original_callback; |
| void *original_baton; |
| |
| svn_commit_info_t **info; |
| apr_pool_t *pool; |
| }; |
| |
| |
| static svn_error_t * |
| capture_commit_info(const svn_commit_info_t *commit_info, |
| void *baton, |
| apr_pool_t *pool) |
| { |
| struct capture_baton_t *cb = baton; |
| |
| *(cb->info) = svn_commit_info_dup(commit_info, cb->pool); |
| |
| if (cb->original_callback) |
| SVN_ERR((cb->original_callback)(commit_info, cb->original_baton, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| static svn_error_t * |
| get_ra_editor(const svn_delta_editor_t **editor, |
| void **edit_baton, |
| svn_ra_session_t *ra_session, |
| svn_client_ctx_t *ctx, |
| const char *log_msg, |
| const apr_array_header_t *commit_items, |
| const apr_hash_t *revprop_table, |
| apr_hash_t *lock_tokens, |
| svn_boolean_t keep_locks, |
| svn_commit_callback2_t commit_callback, |
| void *commit_baton, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *commit_revprops; |
| apr_hash_t *relpath_map = NULL; |
| |
| SVN_ERR(svn_client__ensure_revprop_table(&commit_revprops, revprop_table, |
| log_msg, ctx, pool)); |
| |
| #ifdef ENABLE_EV2_SHIMS |
| if (commit_items) |
| { |
| int i; |
| apr_pool_t *iterpool = svn_pool_create(pool); |
| |
| relpath_map = apr_hash_make(pool); |
| for (i = 0; i < commit_items->nelts; i++) |
| { |
| svn_client_commit_item3_t *item = APR_ARRAY_IDX(commit_items, i, |
| svn_client_commit_item3_t *); |
| const char *relpath; |
| |
| if (!item->path) |
| continue; |
| |
| svn_pool_clear(iterpool); |
| SVN_ERR(svn_wc__node_get_origin(NULL, NULL, &relpath, NULL, NULL, |
| NULL, NULL, |
| ctx->wc_ctx, item->path, FALSE, pool, |
| iterpool)); |
| if (relpath) |
| svn_hash_sets(relpath_map, relpath, item->path); |
| } |
| svn_pool_destroy(iterpool); |
| } |
| #endif |
| |
| /* Fetch RA commit editor. */ |
| SVN_ERR(svn_ra__register_editor_shim_callbacks(ra_session, |
| svn_client__get_shim_callbacks(ctx->wc_ctx, |
| relpath_map, pool))); |
| SVN_ERR(svn_ra_get_commit_editor3(ra_session, editor, edit_baton, |
| commit_revprops, commit_callback, |
| commit_baton, lock_tokens, keep_locks, |
| pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /*** Public Interfaces. ***/ |
| |
| static svn_error_t * |
| reconcile_errors(svn_error_t *commit_err, |
| svn_error_t *unlock_err, |
| svn_error_t *bump_err, |
| apr_pool_t *pool) |
| { |
| svn_error_t *err; |
| |
| /* Early release (for good behavior). */ |
| if (! (commit_err || unlock_err || bump_err)) |
| return SVN_NO_ERROR; |
| |
| /* If there was a commit error, start off our error chain with |
| that. */ |
| if (commit_err) |
| { |
| commit_err = svn_error_quick_wrap |
| (commit_err, _("Commit failed (details follow):")); |
| err = commit_err; |
| } |
| |
| /* Else, create a new "general" error that will lead off the errors |
| that follow. */ |
| else |
| err = svn_error_create(SVN_ERR_BASE, NULL, |
| _("Commit succeeded, but other errors follow:")); |
| |
| /* If there was an unlock error... */ |
| if (unlock_err) |
| { |
| /* Wrap the error with some headers. */ |
| unlock_err = svn_error_quick_wrap |
| (unlock_err, _("Error unlocking locked dirs (details follow):")); |
| |
| /* Append this error to the chain. */ |
| svn_error_compose(err, unlock_err); |
| } |
| |
| /* If there was a bumping error... */ |
| if (bump_err) |
| { |
| /* Wrap the error with some headers. */ |
| bump_err = svn_error_quick_wrap |
| (bump_err, _("Error bumping revisions post-commit (details follow):")); |
| |
| /* Append this error to the chain. */ |
| svn_error_compose(err, bump_err); |
| } |
| |
| return err; |
| } |
| |
| /* For all lock tokens in ALL_TOKENS for URLs under BASE_URL, add them |
| to a new hashtable allocated in POOL. *RESULT is set to point to this |
| new hash table. *RESULT will be keyed on const char * URI-decoded paths |
| relative to BASE_URL. The lock tokens will not be duplicated. */ |
| static svn_error_t * |
| collect_lock_tokens(apr_hash_t **result, |
| apr_hash_t *all_tokens, |
| const char *base_url, |
| apr_pool_t *pool) |
| { |
| apr_hash_index_t *hi; |
| |
| *result = apr_hash_make(pool); |
| |
| for (hi = apr_hash_first(pool, all_tokens); hi; hi = apr_hash_next(hi)) |
| { |
| const char *url = apr_hash_this_key(hi); |
| const char *token = apr_hash_this_val(hi); |
| const char *relpath = svn_uri_skip_ancestor(base_url, url, pool); |
| |
| if (relpath) |
| { |
| svn_hash_sets(*result, relpath, token); |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Put ITEM onto QUEUE, allocating it in QUEUE's pool... |
| * If a checksum is provided, it can be the MD5 and/or the SHA1. */ |
| static svn_error_t * |
| post_process_commit_item(svn_wc_committed_queue_t *queue, |
| const svn_client_commit_item3_t *item, |
| svn_wc_context_t *wc_ctx, |
| svn_boolean_t keep_changelists, |
| svn_boolean_t keep_locks, |
| svn_boolean_t commit_as_operations, |
| const svn_checksum_t *sha1_checksum, |
| apr_pool_t *scratch_pool) |
| { |
| svn_boolean_t loop_recurse = FALSE; |
| svn_boolean_t remove_lock; |
| |
| if (! commit_as_operations |
| && (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD) |
| && (item->kind == svn_node_dir) |
| && (item->copyfrom_url)) |
| loop_recurse = TRUE; |
| |
| remove_lock = (! keep_locks && (item->state_flags |
| & (SVN_CLIENT_COMMIT_ITEM_LOCK_TOKEN |
| | SVN_CLIENT_COMMIT_ITEM_ADD |
| | SVN_CLIENT_COMMIT_ITEM_DELETE))); |
| |
| /* When the node was deleted (or replaced), we need to always remove the |
| locks, as they're invalidated on the server. We cannot honor the |
| SVN_CLIENT_COMMIT_ITEM_LOCK_TOKEN flag here because it does not tell |
| us whether we have locked children. */ |
| if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE) |
| remove_lock = TRUE; |
| |
| return svn_error_trace( |
| svn_wc_queue_committed4(queue, wc_ctx, item->path, |
| loop_recurse, |
| 0 != (item->state_flags & |
| (SVN_CLIENT_COMMIT_ITEM_ADD |
| | SVN_CLIENT_COMMIT_ITEM_DELETE |
| | SVN_CLIENT_COMMIT_ITEM_TEXT_MODS |
| | SVN_CLIENT_COMMIT_ITEM_PROP_MODS)), |
| item->incoming_prop_changes, |
| remove_lock, !keep_changelists, |
| sha1_checksum, scratch_pool)); |
| } |
| |
| /* Given a list of committables described by their common base abspath |
| BASE_ABSPATH and a list of relative dirents TARGET_RELPATHS determine |
| which absolute paths must be locked to commit all these targets and |
| return this as a const char * array in LOCK_TARGETS |
| |
| Allocate the result in RESULT_POOL and use SCRATCH_POOL for temporary |
| storage */ |
| static svn_error_t * |
| determine_lock_targets(apr_array_header_t **lock_targets, |
| svn_wc_context_t *wc_ctx, |
| const char *base_abspath, |
| const apr_array_header_t *target_relpaths, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| apr_hash_t *wc_items; /* const char *wcroot -> apr_array_header_t */ |
| apr_hash_index_t *hi; |
| int i; |
| |
| wc_items = apr_hash_make(scratch_pool); |
| |
| /* Create an array of targets for each working copy used */ |
| for (i = 0; i < target_relpaths->nelts; i++) |
| { |
| const char *target_abspath; |
| const char *wcroot_abspath; |
| apr_array_header_t *wc_targets; |
| svn_error_t *err; |
| const char *target_relpath = APR_ARRAY_IDX(target_relpaths, i, |
| const char *); |
| |
| svn_pool_clear(iterpool); |
| target_abspath = svn_dirent_join(base_abspath, target_relpath, |
| scratch_pool); |
| |
| err = svn_wc__get_wcroot(&wcroot_abspath, wc_ctx, target_abspath, |
| iterpool, iterpool); |
| |
| if (err) |
| { |
| if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) |
| { |
| svn_error_clear(err); |
| continue; |
| } |
| return svn_error_trace(err); |
| } |
| |
| wc_targets = svn_hash_gets(wc_items, wcroot_abspath); |
| |
| if (! wc_targets) |
| { |
| wc_targets = apr_array_make(scratch_pool, 4, sizeof(const char *)); |
| svn_hash_sets(wc_items, apr_pstrdup(scratch_pool, wcroot_abspath), |
| wc_targets); |
| } |
| |
| APR_ARRAY_PUSH(wc_targets, const char *) = target_abspath; |
| } |
| |
| *lock_targets = apr_array_make(result_pool, apr_hash_count(wc_items), |
| sizeof(const char *)); |
| |
| /* For each working copy determine where to lock */ |
| for (hi = apr_hash_first(scratch_pool, wc_items); |
| hi; |
| hi = apr_hash_next(hi)) |
| { |
| const char *common; |
| const char *wcroot_abspath = apr_hash_this_key(hi); |
| apr_array_header_t *wc_targets = apr_hash_this_val(hi); |
| |
| svn_pool_clear(iterpool); |
| |
| if (wc_targets->nelts == 1) |
| { |
| const char *target_abspath; |
| target_abspath = APR_ARRAY_IDX(wc_targets, 0, const char *); |
| |
| if (! strcmp(wcroot_abspath, target_abspath)) |
| { |
| APR_ARRAY_PUSH(*lock_targets, const char *) |
| = apr_pstrdup(result_pool, target_abspath); |
| } |
| else |
| { |
| /* Lock the parent to allow deleting the target */ |
| APR_ARRAY_PUSH(*lock_targets, const char *) |
| = svn_dirent_dirname(target_abspath, result_pool); |
| } |
| } |
| else if (wc_targets->nelts > 1) |
| { |
| SVN_ERR(svn_dirent_condense_targets(&common, &wc_targets, wc_targets, |
| FALSE, iterpool, iterpool)); |
| |
| svn_sort__array(wc_targets, svn_sort_compare_paths); |
| |
| if (wc_targets->nelts == 0 |
| || !svn_path_is_empty(APR_ARRAY_IDX(wc_targets, 0, const char*)) |
| || !strcmp(common, wcroot_abspath)) |
| { |
| APR_ARRAY_PUSH(*lock_targets, const char *) |
| = apr_pstrdup(result_pool, common); |
| } |
| else |
| { |
| /* Lock the parent to allow deleting the target */ |
| APR_ARRAY_PUSH(*lock_targets, const char *) |
| = svn_dirent_dirname(common, result_pool); |
| } |
| } |
| } |
| |
| svn_pool_destroy(iterpool); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Baton for check_url_kind */ |
| struct check_url_kind_baton |
| { |
| apr_pool_t *pool; |
| svn_ra_session_t *session; |
| const char *repos_root_url; |
| svn_client_ctx_t *ctx; |
| }; |
| |
| /* Implements svn_client__check_url_kind_t for svn_client_commit5 */ |
| static svn_error_t * |
| check_url_kind(void *baton, |
| svn_node_kind_t *kind, |
| const char *url, |
| svn_revnum_t revision, |
| apr_pool_t *scratch_pool) |
| { |
| struct check_url_kind_baton *cukb = baton; |
| |
| /* If we don't have a session or can't use the session, get one */ |
| if (!cukb->session || !svn_uri__is_ancestor(cukb->repos_root_url, url)) |
| { |
| SVN_ERR(svn_client_open_ra_session2(&cukb->session, url, NULL, cukb->ctx, |
| cukb->pool, scratch_pool)); |
| SVN_ERR(svn_ra_get_repos_root2(cukb->session, &cukb->repos_root_url, |
| cukb->pool)); |
| } |
| else |
| SVN_ERR(svn_ra_reparent(cukb->session, url, scratch_pool)); |
| |
| return svn_error_trace( |
| svn_ra_check_path(cukb->session, "", revision, |
| kind, scratch_pool)); |
| } |
| |
| /* Recurse into every target in REL_TARGETS, finding committable externals |
| * nested within. Append these to REL_TARGETS itself. The paths in REL_TARGETS |
| * are assumed to be / will be created relative to BASE_ABSPATH. The remaining |
| * arguments correspond to those of svn_client_commit6(). */ |
| static svn_error_t* |
| append_externals_as_explicit_targets(apr_array_header_t *rel_targets, |
| const char *base_abspath, |
| svn_boolean_t include_file_externals, |
| svn_boolean_t include_dir_externals, |
| svn_depth_t depth, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| int rel_targets_nelts_fixed; |
| int i; |
| apr_pool_t *iterpool; |
| |
| if (! (include_file_externals || include_dir_externals)) |
| return SVN_NO_ERROR; |
| |
| /* Easy part of applying DEPTH to externals. */ |
| if (depth == svn_depth_empty) |
| { |
| /* Don't recurse. */ |
| return SVN_NO_ERROR; |
| } |
| |
| /* Iterate *and* grow REL_TARGETS at the same time. */ |
| rel_targets_nelts_fixed = rel_targets->nelts; |
| |
| iterpool = svn_pool_create(scratch_pool); |
| |
| for (i = 0; i < rel_targets_nelts_fixed; i++) |
| { |
| int j; |
| const char *target; |
| apr_array_header_t *externals = NULL; |
| |
| svn_pool_clear(iterpool); |
| |
| target = svn_dirent_join(base_abspath, |
| APR_ARRAY_IDX(rel_targets, i, const char *), |
| iterpool); |
| |
| /* ### TODO: Possible optimization: No need to do this for file targets. |
| * ### But what's cheaper, stat'ing the file system or querying the db? |
| * ### --> future. */ |
| |
| SVN_ERR(svn_wc__committable_externals_below(&externals, ctx->wc_ctx, |
| target, depth, |
| iterpool, iterpool)); |
| |
| if (externals != NULL) |
| { |
| const char *rel_target; |
| |
| for (j = 0; j < externals->nelts; j++) |
| { |
| svn_wc__committable_external_info_t *xinfo = |
| APR_ARRAY_IDX(externals, j, |
| svn_wc__committable_external_info_t *); |
| |
| if ((xinfo->kind == svn_node_file && ! include_file_externals) |
| || (xinfo->kind == svn_node_dir && ! include_dir_externals)) |
| continue; |
| |
| rel_target = svn_dirent_skip_ancestor(base_abspath, |
| xinfo->local_abspath); |
| |
| SVN_ERR_ASSERT(rel_target != NULL && *rel_target != '\0'); |
| |
| APR_ARRAY_PUSH(rel_targets, const char *) = |
| apr_pstrdup(result_pool, rel_target); |
| } |
| } |
| } |
| |
| svn_pool_destroy(iterpool); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Crawl the working copy for commit items. |
| */ |
| static svn_error_t * |
| harvest_committables(apr_array_header_t **commit_items_p, |
| apr_hash_t **committables_by_path_p, |
| apr_hash_t **lock_tokens, |
| const char *base_dir_abspath, |
| const apr_array_header_t *targets, |
| int depth_empty_start, |
| svn_depth_t depth, |
| svn_boolean_t just_locked, |
| const apr_array_header_t *changelists, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| struct check_url_kind_baton cukb; |
| svn_client__committables_t *committables; |
| apr_hash_index_t *hi; |
| |
| /* Prepare for when we have a copy containing not-present nodes. */ |
| cukb.pool = scratch_pool; |
| cukb.session = NULL; /* ### Can we somehow reuse session? */ |
| cukb.repos_root_url = NULL; |
| cukb.ctx = ctx; |
| |
| SVN_ERR(svn_client__harvest_committables(&committables, lock_tokens, |
| base_dir_abspath, targets, |
| depth_empty_start, depth, |
| just_locked, |
| changelists, |
| check_url_kind, &cukb, |
| ctx, result_pool, scratch_pool)); |
| if (apr_hash_count(committables->by_repository) == 0) |
| { |
| *commit_items_p = NULL; |
| return SVN_NO_ERROR; /* Nothing to do */ |
| } |
| else if (apr_hash_count(committables->by_repository) > 1) |
| { |
| return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL, |
| _("Commit can only commit to a single repository at a time.\n" |
| "Are all targets part of the same working copy?")); |
| } |
| |
| hi = apr_hash_first(scratch_pool, committables->by_repository); |
| *commit_items_p = apr_hash_this_val(hi); |
| if (committables_by_path_p) |
| *committables_by_path_p = committables->by_path; |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_client__wc_replay(const char *src_wc_abspath, |
| const apr_array_header_t *targets, |
| svn_depth_t depth, |
| const apr_array_header_t *changelists, |
| const svn_delta_editor_t *editor, |
| void *edit_baton, |
| svn_wc_notify_func2_t notify_func, |
| void *notify_baton, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *pool) |
| { |
| const char *base_abspath; |
| apr_array_header_t *rel_targets; |
| apr_hash_t *lock_tokens; |
| apr_array_header_t *commit_items; |
| svn_client__pathrev_t *base; |
| const char *base_url; |
| svn_wc_notify_func2_t saved_notify_func; |
| void *saved_notify_baton; |
| |
| /* Condense the target list. This makes all targets absolute. */ |
| SVN_ERR(svn_dirent_condense_targets(&base_abspath, &rel_targets, targets, |
| FALSE, pool, pool)); |
| |
| /* No targets means nothing to commit, so just return. */ |
| if (base_abspath == NULL) |
| return SVN_NO_ERROR; |
| |
| SVN_ERR_ASSERT(rel_targets != NULL); |
| |
| /* If we calculated only a base and no relative targets, this |
| must mean that we are being asked to commit (effectively) a |
| single path. */ |
| if (rel_targets->nelts == 0) |
| APR_ARRAY_PUSH(rel_targets, const char *) = ""; |
| |
| /* Crawl the working copy for commit items. */ |
| SVN_ERR(harvest_committables(&commit_items, NULL /*committables_by_path_p*/, |
| &lock_tokens, |
| base_abspath, rel_targets, |
| -1 /*depth_empty_start*/, |
| depth, |
| FALSE /*just_locked*/, |
| changelists, |
| ctx, pool, pool)); |
| if (!commit_items) |
| { |
| return SVN_NO_ERROR; |
| } |
| |
| SVN_ERR(svn_client__wc_node_get_base(&base, |
| src_wc_abspath, ctx->wc_ctx, pool, pool)); |
| base_url = base->url; |
| /* Sort our COMMIT_ITEMS by URL and find their relative URL-paths. */ |
| SVN_ERR(svn_client__condense_commit_items2(base_url, commit_items, pool)); |
| |
| saved_notify_func = ctx->notify_func2; |
| saved_notify_baton = ctx->notify_baton2; |
| ctx->notify_func2 = notify_func; |
| ctx->notify_baton2 = notify_baton; |
| /* BASE_URL is only used here in notifications & errors */ |
| SVN_ERR(svn_client__do_commit(base_url, commit_items, |
| editor, edit_baton, |
| NULL /*notify_prefix*/, NULL /*sha1_checksums*/, |
| ctx, pool, pool)); |
| ctx->notify_func2 = saved_notify_func; |
| ctx->notify_baton2 = saved_notify_baton; |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_client_commit6(const apr_array_header_t *targets, |
| svn_depth_t depth, |
| svn_boolean_t keep_locks, |
| svn_boolean_t keep_changelists, |
| svn_boolean_t commit_as_operations, |
| svn_boolean_t include_file_externals, |
| svn_boolean_t include_dir_externals, |
| const apr_array_header_t *changelists, |
| const apr_hash_t *revprop_table, |
| svn_commit_callback2_t commit_callback, |
| void *commit_baton, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *pool) |
| { |
| const svn_delta_editor_t *editor; |
| void *edit_baton; |
| struct capture_baton_t cb; |
| svn_ra_session_t *ra_session; |
| const char *log_msg; |
| const char *base_abspath; |
| const char *base_url; |
| apr_array_header_t *rel_targets; |
| apr_array_header_t *lock_targets; |
| apr_array_header_t *locks_obtained; |
| apr_hash_t *committables_by_path; |
| apr_hash_t *lock_tokens; |
| apr_hash_t *sha1_checksums; |
| apr_array_header_t *commit_items; |
| svn_error_t *cmt_err = SVN_NO_ERROR; |
| svn_error_t *bump_err = SVN_NO_ERROR; |
| svn_error_t *unlock_err = SVN_NO_ERROR; |
| svn_boolean_t commit_in_progress = FALSE; |
| svn_boolean_t timestamp_sleep = FALSE; |
| svn_commit_info_t *commit_info = NULL; |
| apr_pool_t *iterpool = svn_pool_create(pool); |
| const char *current_abspath; |
| const char *notify_prefix; |
| int depth_empty_after = -1; |
| apr_hash_t *move_youngest = NULL; |
| int i; |
| |
| SVN_ERR_ASSERT(depth != svn_depth_unknown && depth != svn_depth_exclude); |
| |
| /* Committing URLs doesn't make sense, so error if it's tried. */ |
| for (i = 0; i < targets->nelts; i++) |
| { |
| const char *target = APR_ARRAY_IDX(targets, i, const char *); |
| if (svn_path_is_url(target)) |
| return svn_error_createf |
| (SVN_ERR_ILLEGAL_TARGET, NULL, |
| _("'%s' is a URL, but URLs cannot be commit targets"), target); |
| } |
| |
| /* Condense the target list. This makes all targets absolute. */ |
| SVN_ERR(svn_dirent_condense_targets(&base_abspath, &rel_targets, targets, |
| FALSE, pool, iterpool)); |
| |
| /* No targets means nothing to commit, so just return. */ |
| if (base_abspath == NULL) |
| return SVN_NO_ERROR; |
| |
| SVN_ERR_ASSERT(rel_targets != NULL); |
| |
| /* If we calculated only a base and no relative targets, this |
| must mean that we are being asked to commit (effectively) a |
| single path. */ |
| if (rel_targets->nelts == 0) |
| APR_ARRAY_PUSH(rel_targets, const char *) = ""; |
| |
| if (include_file_externals || include_dir_externals) |
| { |
| if (depth != svn_depth_unknown && depth != svn_depth_infinity) |
| { |
| /* All targets after this will be handled as depth empty */ |
| depth_empty_after = rel_targets->nelts; |
| } |
| |
| SVN_ERR(append_externals_as_explicit_targets(rel_targets, base_abspath, |
| include_file_externals, |
| include_dir_externals, |
| depth, ctx, |
| pool, pool)); |
| } |
| |
| SVN_ERR(determine_lock_targets(&lock_targets, ctx->wc_ctx, base_abspath, |
| rel_targets, pool, iterpool)); |
| |
| locks_obtained = apr_array_make(pool, lock_targets->nelts, |
| sizeof(const char *)); |
| |
| for (i = 0; i < lock_targets->nelts; i++) |
| { |
| const char *lock_root; |
| const char *target = APR_ARRAY_IDX(lock_targets, i, const char *); |
| |
| svn_pool_clear(iterpool); |
| |
| cmt_err = svn_error_trace( |
| svn_wc__acquire_write_lock(&lock_root, ctx->wc_ctx, target, |
| FALSE, pool, iterpool)); |
| |
| if (cmt_err) |
| goto cleanup; |
| |
| APR_ARRAY_PUSH(locks_obtained, const char *) = lock_root; |
| } |
| |
| /* Determine prefix to strip from the commit notify messages */ |
| SVN_ERR(svn_dirent_get_absolute(¤t_abspath, "", pool)); |
| notify_prefix = svn_dirent_get_longest_ancestor(current_abspath, |
| base_abspath, |
| pool); |
| |
| /* Crawl the working copy for commit items. */ |
| cmt_err = svn_error_trace( |
| harvest_committables(&commit_items, &committables_by_path, |
| &lock_tokens, |
| base_abspath, |
| rel_targets, |
| depth_empty_after, |
| depth, |
| ! keep_locks, |
| changelists, |
| ctx, |
| pool, |
| iterpool)); |
| svn_pool_clear(iterpool); |
| |
| if (cmt_err) |
| goto cleanup; |
| |
| if (!commit_items) |
| { |
| goto cleanup; /* Nothing to do */ |
| } |
| |
| /* If our array of targets contains only locks (and no actual file |
| or prop modifications), then we return here to avoid committing a |
| revision with no changes. */ |
| { |
| svn_boolean_t found_changed_path = FALSE; |
| |
| for (i = 0; i < commit_items->nelts; ++i) |
| { |
| svn_client_commit_item3_t *item = |
| APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *); |
| |
| if (item->state_flags != SVN_CLIENT_COMMIT_ITEM_LOCK_TOKEN) |
| { |
| found_changed_path = TRUE; |
| break; |
| } |
| } |
| |
| if (!found_changed_path) |
| goto cleanup; |
| } |
| |
| /* For every target that was moved verify that both halves of the |
| * move are part of the commit. */ |
| for (i = 0; i < commit_items->nelts; i++) |
| { |
| svn_client_commit_item3_t *item = |
| APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *); |
| |
| svn_pool_clear(iterpool); |
| |
| if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_MOVED_HERE) |
| { |
| /* ### item->moved_from_abspath contains the move origin */ |
| const char *moved_from_abspath; |
| const char *delete_op_root_abspath; |
| |
| cmt_err = svn_error_trace(svn_wc__node_was_moved_here( |
| &moved_from_abspath, |
| &delete_op_root_abspath, |
| ctx->wc_ctx, item->path, |
| iterpool, iterpool)); |
| if (cmt_err) |
| goto cleanup; |
| |
| if (moved_from_abspath && delete_op_root_abspath) |
| { |
| svn_client_commit_item3_t *delete_half = |
| svn_hash_gets(committables_by_path, delete_op_root_abspath); |
| |
| if (!delete_half) |
| { |
| cmt_err = svn_error_createf( |
| SVN_ERR_ILLEGAL_TARGET, NULL, |
| _("Cannot commit '%s' because it was moved from " |
| "'%s' which is not part of the commit; both " |
| "sides of the move must be committed together"), |
| svn_dirent_local_style(item->path, iterpool), |
| svn_dirent_local_style(delete_op_root_abspath, |
| iterpool)); |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| notify = svn_wc_create_notify( |
| delete_op_root_abspath, |
| svn_wc_notify_failed_requires_target, |
| iterpool); |
| notify->err = cmt_err; |
| |
| ctx->notify_func2(ctx->notify_baton2, notify, iterpool); |
| } |
| |
| goto cleanup; |
| } |
| else if (delete_half->revision == item->copyfrom_rev) |
| { |
| /* Ok, now we know that we perform an out-of-date check |
| on the copyfrom location. Remember this for a fixup |
| round right before committing. */ |
| |
| if (!move_youngest) |
| move_youngest = apr_hash_make(pool); |
| |
| svn_hash_sets(move_youngest, item->path, item); |
| } |
| } |
| } |
| |
| if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE) |
| { |
| const char *moved_to_abspath; |
| const char *copy_op_root_abspath; |
| |
| cmt_err = svn_error_trace(svn_wc__node_was_moved_away( |
| &moved_to_abspath, |
| ©_op_root_abspath, |
| ctx->wc_ctx, item->path, |
| iterpool, iterpool)); |
| if (cmt_err) |
| goto cleanup; |
| |
| if (moved_to_abspath && copy_op_root_abspath && |
| strcmp(moved_to_abspath, copy_op_root_abspath) == 0 && |
| svn_hash_gets(committables_by_path, copy_op_root_abspath) |
| == NULL) |
| { |
| cmt_err = svn_error_createf( |
| SVN_ERR_ILLEGAL_TARGET, NULL, |
| _("Cannot commit '%s' because it was moved to '%s' " |
| "which is not part of the commit; both sides of " |
| "the move must be committed together"), |
| svn_dirent_local_style(item->path, iterpool), |
| svn_dirent_local_style(copy_op_root_abspath, |
| iterpool)); |
| |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| notify = svn_wc_create_notify( |
| copy_op_root_abspath, |
| svn_wc_notify_failed_requires_target, |
| iterpool); |
| notify->err = cmt_err; |
| |
| ctx->notify_func2(ctx->notify_baton2, notify, iterpool); |
| } |
| |
| goto cleanup; |
| } |
| } |
| } |
| |
| /* Go get a log message. If an error occurs, or no log message is |
| specified, abort the operation. */ |
| if (SVN_CLIENT__HAS_LOG_MSG_FUNC(ctx)) |
| { |
| const char *tmp_file; |
| cmt_err = svn_error_trace( |
| svn_client__get_log_msg(&log_msg, &tmp_file, commit_items, |
| ctx, pool)); |
| |
| if (cmt_err || (! log_msg)) |
| goto cleanup; |
| } |
| else |
| log_msg = ""; |
| |
| /* Sort and condense our COMMIT_ITEMS. */ |
| cmt_err = svn_error_trace(svn_client__condense_commit_items(&base_url, |
| commit_items, |
| pool)); |
| |
| if (cmt_err) |
| goto cleanup; |
| |
| /* Collect our lock tokens with paths relative to base_url. */ |
| cmt_err = svn_error_trace(collect_lock_tokens(&lock_tokens, lock_tokens, |
| base_url, pool)); |
| |
| if (cmt_err) |
| goto cleanup; |
| |
| cb.original_callback = commit_callback; |
| cb.original_baton = commit_baton; |
| cb.info = &commit_info; |
| cb.pool = pool; |
| |
| /* Get the RA editor from the first lock target, rather than BASE_ABSPATH. |
| * When committing from multiple WCs, BASE_ABSPATH might be an unrelated |
| * parent of nested working copies. We don't support commits to multiple |
| * repositories so using the first WC to get the RA session is safe. */ |
| cmt_err = svn_error_trace( |
| svn_client__open_ra_session_internal(&ra_session, NULL, base_url, |
| APR_ARRAY_IDX(lock_targets, |
| 0, |
| const char *), |
| commit_items, |
| TRUE, TRUE, ctx, |
| pool, pool)); |
| |
| if (cmt_err) |
| goto cleanup; |
| |
| if (move_youngest != NULL) |
| { |
| apr_hash_index_t *hi; |
| svn_revnum_t youngest; |
| |
| SVN_ERR(svn_ra_get_latest_revnum(ra_session, &youngest, pool)); |
| |
| for (hi = apr_hash_first(iterpool, move_youngest); |
| hi; |
| hi = apr_hash_next(hi)) |
| { |
| svn_client_commit_item3_t *item = apr_hash_this_val(hi); |
| |
| /* We delete the original side with its original revision and will |
| receive an out-of-date error if that node changed since that |
| revision. |
| |
| The copy is of that same revision and we know that this revision |
| didn't change between this revision and youngest. So we can just |
| as well commit a copy from youngest. |
| |
| Note that it is still possible to see gaps between the delete and |
| copy revisions as the repository might handle multiple commits |
| at the same time (or when an out of date proxy is involved), but |
| in general it should decrease the number of gaps. */ |
| |
| if (item->copyfrom_rev < youngest) |
| item->copyfrom_rev = youngest; |
| } |
| } |
| |
| cmt_err = svn_error_trace( |
| get_ra_editor(&editor, &edit_baton, ra_session, ctx, |
| log_msg, commit_items, revprop_table, |
| lock_tokens, keep_locks, capture_commit_info, |
| &cb, pool)); |
| |
| if (cmt_err) |
| goto cleanup; |
| |
| /* Make a note that we have a commit-in-progress. */ |
| commit_in_progress = TRUE; |
| |
| /* We'll assume that, once we pass this point, we are going to need to |
| * sleep for timestamps. Really, we may not need to do unless and until |
| * we reach the point where we post-commit 'bump' the WC metadata. */ |
| timestamp_sleep = TRUE; |
| |
| /* Perform the commit. */ |
| cmt_err = svn_error_trace( |
| svn_client__do_commit(base_url, commit_items, editor, edit_baton, |
| notify_prefix, &sha1_checksums, ctx, pool, |
| iterpool)); |
| |
| /* Handle a successful commit. */ |
| if ((! cmt_err) |
| || (cmt_err->apr_err == SVN_ERR_REPOS_POST_COMMIT_HOOK_FAILED)) |
| { |
| svn_wc_committed_queue_t *queue = svn_wc_committed_queue_create(pool); |
| |
| /* Make a note that our commit is finished. */ |
| commit_in_progress = FALSE; |
| |
| for (i = 0; i < commit_items->nelts; i++) |
| { |
| svn_client_commit_item3_t *item |
| = APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *); |
| |
| svn_pool_clear(iterpool); |
| bump_err = post_process_commit_item( |
| queue, item, ctx->wc_ctx, |
| keep_changelists, keep_locks, commit_as_operations, |
| svn_hash_gets(sha1_checksums, item->path), |
| iterpool); |
| if (bump_err) |
| goto cleanup; |
| } |
| |
| SVN_ERR_ASSERT(commit_info); |
| bump_err = svn_wc_process_committed_queue2( |
| queue, ctx->wc_ctx, |
| commit_info->revision, |
| commit_info->date, |
| commit_info->author, |
| ctx->cancel_func, ctx->cancel_baton, |
| iterpool); |
| |
| if (bump_err) |
| goto cleanup; |
| } |
| |
| cleanup: |
| /* Sleep to ensure timestamp integrity. BASE_ABSPATH may have been |
| removed by the commit or it may the common ancestor of multiple |
| working copies. */ |
| if (timestamp_sleep) |
| { |
| const char *sleep_abspath; |
| svn_error_t *err = svn_wc__get_wcroot(&sleep_abspath, ctx->wc_ctx, |
| base_abspath, pool, pool); |
| if (err) |
| { |
| svn_error_clear(err); |
| sleep_abspath = base_abspath; |
| } |
| |
| svn_io_sleep_for_timestamps(sleep_abspath, pool); |
| } |
| |
| /* Abort the commit if it is still in progress. */ |
| svn_pool_clear(iterpool); /* Close open handles before aborting */ |
| if (commit_in_progress) |
| cmt_err = svn_error_compose_create(cmt_err, |
| editor->abort_edit(edit_baton, pool)); |
| |
| /* A bump error is likely to occur while running a working copy log file, |
| explicitly unlocking and removing temporary files would be wrong in |
| that case. A commit error (cmt_err) should only occur before any |
| attempt to modify the working copy, so it doesn't prevent explicit |
| clean-up. */ |
| if (! bump_err) |
| { |
| /* Release all locks we obtained */ |
| for (i = 0; i < locks_obtained->nelts; i++) |
| { |
| const char *lock_root = APR_ARRAY_IDX(locks_obtained, i, |
| const char *); |
| |
| svn_pool_clear(iterpool); |
| |
| unlock_err = svn_error_compose_create( |
| svn_wc__release_write_lock(ctx->wc_ctx, lock_root, |
| iterpool), |
| unlock_err); |
| } |
| } |
| |
| svn_pool_destroy(iterpool); |
| |
| return svn_error_trace(reconcile_errors(cmt_err, unlock_err, bump_err, |
| pool)); |
| } |