| /* |
| * copy.c: copy/move wrappers around wc 'copy' 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 "svn_hash.h" |
| #include "svn_client.h" |
| #include "svn_error.h" |
| #include "svn_error_codes.h" |
| #include "svn_dirent_uri.h" |
| #include "svn_path.h" |
| #include "svn_opt.h" |
| #include "svn_time.h" |
| #include "svn_props.h" |
| #include "svn_mergeinfo.h" |
| #include "svn_pools.h" |
| |
| #include "client.h" |
| #include "mergeinfo.h" |
| |
| #include "svn_private_config.h" |
| #include "private/svn_wc_private.h" |
| #include "private/svn_ra_private.h" |
| #include "private/svn_mergeinfo_private.h" |
| #include "private/svn_client_private.h" |
| |
| |
| /* |
| * OUR BASIC APPROACH TO COPIES |
| * ============================ |
| * |
| * for each source/destination pair |
| * if (not exist src_path) |
| * return ERR_BAD_SRC error |
| * |
| * if (exist dst_path) |
| * return ERR_OBSTRUCTION error |
| * else |
| * copy src_path into parent_of_dst_path as basename (dst_path) |
| * |
| * if (this is a move) |
| * delete src_path |
| */ |
| |
| |
| |
| /*** Code. ***/ |
| |
| /* Extend the mergeinfo for the single WC path TARGET_WCPATH, adding |
| MERGEINFO to any mergeinfo pre-existing in the WC. */ |
| static svn_error_t * |
| extend_wc_mergeinfo(const char *target_abspath, |
| apr_hash_t *mergeinfo, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *wc_mergeinfo; |
| |
| /* Get a fresh copy of the pre-existing state of the WC's mergeinfo |
| updating it. */ |
| SVN_ERR(svn_client__parse_mergeinfo(&wc_mergeinfo, ctx->wc_ctx, |
| target_abspath, pool, pool)); |
| |
| /* Combine the provided mergeinfo with any mergeinfo from the WC. */ |
| if (wc_mergeinfo && mergeinfo) |
| SVN_ERR(svn_mergeinfo_merge2(wc_mergeinfo, mergeinfo, pool, pool)); |
| else if (! wc_mergeinfo) |
| wc_mergeinfo = mergeinfo; |
| |
| return svn_error_trace( |
| svn_client__record_wc_mergeinfo(target_abspath, wc_mergeinfo, |
| FALSE, ctx, pool)); |
| } |
| |
| /* Find the longest common ancestor of paths in COPY_PAIRS. If |
| SRC_ANCESTOR is NULL, ignore source paths in this calculation. If |
| DST_ANCESTOR is NULL, ignore destination paths in this calculation. |
| COMMON_ANCESTOR will be the common ancestor of both the |
| SRC_ANCESTOR and DST_ANCESTOR, and will only be set if it is not |
| NULL. |
| */ |
| static svn_error_t * |
| get_copy_pair_ancestors(const apr_array_header_t *copy_pairs, |
| const char **src_ancestor, |
| const char **dst_ancestor, |
| const char **common_ancestor, |
| apr_pool_t *pool) |
| { |
| apr_pool_t *subpool = svn_pool_create(pool); |
| svn_client__copy_pair_t *first; |
| const char *first_dst; |
| const char *first_src; |
| const char *top_dst; |
| svn_boolean_t src_is_url; |
| svn_boolean_t dst_is_url; |
| char *top_src; |
| int i; |
| |
| first = APR_ARRAY_IDX(copy_pairs, 0, svn_client__copy_pair_t *); |
| |
| /* Because all the destinations are in the same directory, we can easily |
| determine their common ancestor. */ |
| first_dst = first->dst_abspath_or_url; |
| dst_is_url = svn_path_is_url(first_dst); |
| |
| if (copy_pairs->nelts == 1) |
| top_dst = apr_pstrdup(subpool, first_dst); |
| else |
| top_dst = dst_is_url ? svn_uri_dirname(first_dst, subpool) |
| : svn_dirent_dirname(first_dst, subpool); |
| |
| /* Sources can came from anywhere, so we have to actually do some |
| work for them. */ |
| first_src = first->src_abspath_or_url; |
| src_is_url = svn_path_is_url(first_src); |
| top_src = apr_pstrdup(subpool, first_src); |
| for (i = 1; i < copy_pairs->nelts; i++) |
| { |
| /* We don't need to clear the subpool here for several reasons: |
| 1) If we do, we can't use it to allocate the initial versions of |
| top_src and top_dst (above). |
| 2) We don't return any errors in the following loop, so we |
| are guanteed to destroy the subpool at the end of this function. |
| 3) The number of iterations is likely to be few, and the loop will |
| be through quickly, so memory leakage will not be significant, |
| in time or space. |
| */ |
| const svn_client__copy_pair_t *pair = |
| APR_ARRAY_IDX(copy_pairs, i, svn_client__copy_pair_t *); |
| |
| top_src = src_is_url |
| ? svn_uri_get_longest_ancestor(top_src, pair->src_abspath_or_url, |
| subpool) |
| : svn_dirent_get_longest_ancestor(top_src, pair->src_abspath_or_url, |
| subpool); |
| } |
| |
| if (src_ancestor) |
| *src_ancestor = apr_pstrdup(pool, top_src); |
| |
| if (dst_ancestor) |
| *dst_ancestor = apr_pstrdup(pool, top_dst); |
| |
| if (common_ancestor) |
| *common_ancestor = |
| src_is_url |
| ? svn_uri_get_longest_ancestor(top_src, top_dst, pool) |
| : svn_dirent_get_longest_ancestor(top_src, top_dst, pool); |
| |
| svn_pool_destroy(subpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* The guts of do_wc_to_wc_copies */ |
| static svn_error_t * |
| do_wc_to_wc_copies_with_write_lock(svn_boolean_t *timestamp_sleep, |
| const apr_array_header_t *copy_pairs, |
| const char *dst_parent, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| int i; |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| svn_error_t *err = SVN_NO_ERROR; |
| |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| const char *dst_abspath; |
| svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i, |
| svn_client__copy_pair_t *); |
| svn_pool_clear(iterpool); |
| |
| /* Check for cancellation */ |
| if (ctx->cancel_func) |
| SVN_ERR(ctx->cancel_func(ctx->cancel_baton)); |
| |
| /* Perform the copy */ |
| dst_abspath = svn_dirent_join(pair->dst_parent_abspath, pair->base_name, |
| iterpool); |
| *timestamp_sleep = TRUE; |
| err = svn_wc_copy3(ctx->wc_ctx, pair->src_abspath_or_url, dst_abspath, |
| FALSE /* metadata_only */, |
| ctx->cancel_func, ctx->cancel_baton, |
| ctx->notify_func2, ctx->notify_baton2, iterpool); |
| if (err) |
| break; |
| } |
| svn_pool_destroy(iterpool); |
| |
| SVN_ERR(err); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Copy each COPY_PAIR->SRC into COPY_PAIR->DST. Use POOL for temporary |
| allocations. */ |
| static svn_error_t * |
| do_wc_to_wc_copies(svn_boolean_t *timestamp_sleep, |
| const apr_array_header_t *copy_pairs, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *pool) |
| { |
| const char *dst_parent, *dst_parent_abspath; |
| |
| SVN_ERR(get_copy_pair_ancestors(copy_pairs, NULL, &dst_parent, NULL, pool)); |
| if (copy_pairs->nelts == 1) |
| dst_parent = svn_dirent_dirname(dst_parent, pool); |
| |
| SVN_ERR(svn_dirent_get_absolute(&dst_parent_abspath, dst_parent, pool)); |
| |
| SVN_WC__CALL_WITH_WRITE_LOCK( |
| do_wc_to_wc_copies_with_write_lock(timestamp_sleep, copy_pairs, dst_parent, |
| ctx, pool), |
| ctx->wc_ctx, dst_parent_abspath, FALSE, pool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* The locked bit of do_wc_to_wc_moves. */ |
| static svn_error_t * |
| do_wc_to_wc_moves_with_locks2(svn_client__copy_pair_t *pair, |
| const char *dst_parent_abspath, |
| svn_boolean_t lock_src, |
| svn_boolean_t lock_dst, |
| svn_boolean_t allow_mixed_revisions, |
| svn_boolean_t metadata_only, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| const char *dst_abspath; |
| |
| dst_abspath = svn_dirent_join(dst_parent_abspath, pair->base_name, |
| scratch_pool); |
| |
| SVN_ERR(svn_wc__move2(ctx->wc_ctx, pair->src_abspath_or_url, |
| dst_abspath, metadata_only, |
| allow_mixed_revisions, |
| ctx->cancel_func, ctx->cancel_baton, |
| ctx->notify_func2, ctx->notify_baton2, |
| scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Wrapper to add an optional second lock */ |
| static svn_error_t * |
| do_wc_to_wc_moves_with_locks1(svn_client__copy_pair_t *pair, |
| const char *dst_parent_abspath, |
| svn_boolean_t lock_src, |
| svn_boolean_t lock_dst, |
| svn_boolean_t allow_mixed_revisions, |
| svn_boolean_t metadata_only, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| if (lock_dst) |
| SVN_WC__CALL_WITH_WRITE_LOCK( |
| do_wc_to_wc_moves_with_locks2(pair, dst_parent_abspath, lock_src, |
| lock_dst, allow_mixed_revisions, |
| metadata_only, |
| ctx, scratch_pool), |
| ctx->wc_ctx, dst_parent_abspath, FALSE, scratch_pool); |
| else |
| SVN_ERR(do_wc_to_wc_moves_with_locks2(pair, dst_parent_abspath, lock_src, |
| lock_dst, allow_mixed_revisions, |
| metadata_only, |
| ctx, scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Move each COPY_PAIR->SRC into COPY_PAIR->DST, deleting COPY_PAIR->SRC |
| afterwards. Use POOL for temporary allocations. */ |
| static svn_error_t * |
| do_wc_to_wc_moves(svn_boolean_t *timestamp_sleep, |
| const apr_array_header_t *copy_pairs, |
| const char *dst_path, |
| svn_boolean_t allow_mixed_revisions, |
| svn_boolean_t metadata_only, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *pool) |
| { |
| int i; |
| apr_pool_t *iterpool = svn_pool_create(pool); |
| svn_error_t *err = SVN_NO_ERROR; |
| |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| const char *src_parent_abspath; |
| svn_boolean_t lock_src, lock_dst; |
| const char *src_wcroot_abspath; |
| const char *dst_wcroot_abspath; |
| |
| svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i, |
| svn_client__copy_pair_t *); |
| svn_pool_clear(iterpool); |
| |
| /* Check for cancellation */ |
| if (ctx->cancel_func) |
| SVN_ERR(ctx->cancel_func(ctx->cancel_baton)); |
| |
| src_parent_abspath = svn_dirent_dirname(pair->src_abspath_or_url, |
| iterpool); |
| |
| SVN_ERR(svn_wc__get_wcroot(&src_wcroot_abspath, |
| ctx->wc_ctx, src_parent_abspath, |
| iterpool, iterpool)); |
| SVN_ERR(svn_wc__get_wcroot(&dst_wcroot_abspath, |
| ctx->wc_ctx, pair->dst_parent_abspath, |
| iterpool, iterpool)); |
| |
| /* We now need to lock the right combination of batons. |
| Four cases: |
| 1) src_parent == dst_parent |
| 2) src_parent is parent of dst_parent |
| 3) dst_parent is parent of src_parent |
| 4) src_parent and dst_parent are disjoint |
| We can handle 1) as either 2) or 3) */ |
| if (strcmp(src_parent_abspath, pair->dst_parent_abspath) == 0 |
| || (svn_dirent_is_child(src_parent_abspath, pair->dst_parent_abspath, |
| NULL) |
| && !svn_dirent_is_child(src_parent_abspath, dst_wcroot_abspath, |
| NULL))) |
| { |
| lock_src = TRUE; |
| lock_dst = FALSE; |
| } |
| else if (svn_dirent_is_child(pair->dst_parent_abspath, |
| src_parent_abspath, NULL) |
| && !svn_dirent_is_child(pair->dst_parent_abspath, |
| src_wcroot_abspath, NULL)) |
| { |
| lock_src = FALSE; |
| lock_dst = TRUE; |
| } |
| else |
| { |
| lock_src = TRUE; |
| lock_dst = TRUE; |
| } |
| |
| *timestamp_sleep = TRUE; |
| |
| /* Perform the copy and then the delete. */ |
| if (lock_src) |
| SVN_WC__CALL_WITH_WRITE_LOCK( |
| do_wc_to_wc_moves_with_locks1(pair, pair->dst_parent_abspath, |
| lock_src, lock_dst, |
| allow_mixed_revisions, |
| metadata_only, |
| ctx, iterpool), |
| ctx->wc_ctx, src_parent_abspath, |
| FALSE, iterpool); |
| else |
| SVN_ERR(do_wc_to_wc_moves_with_locks1(pair, pair->dst_parent_abspath, |
| lock_src, lock_dst, |
| allow_mixed_revisions, |
| metadata_only, |
| ctx, iterpool)); |
| |
| } |
| svn_pool_destroy(iterpool); |
| |
| return svn_error_trace(err); |
| } |
| |
| /* Verify that the destinations stored in COPY_PAIRS are valid working copy |
| destinations and set pair->dst_parent_abspath and pair->base_name for each |
| item to the resulting location if they do */ |
| static svn_error_t * |
| verify_wc_dsts(const apr_array_header_t *copy_pairs, |
| svn_boolean_t make_parents, |
| svn_boolean_t is_move, |
| svn_boolean_t metadata_only, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| int i; |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| |
| /* Check that DST does not exist, but its parent does */ |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i, |
| svn_client__copy_pair_t *); |
| svn_node_kind_t dst_kind, dst_parent_kind; |
| |
| svn_pool_clear(iterpool); |
| |
| /* If DST_PATH does not exist, then its basename will become a new |
| file or dir added to its parent (possibly an implicit '.'). |
| Else, just error out. */ |
| SVN_ERR(svn_wc_read_kind2(&dst_kind, ctx->wc_ctx, |
| pair->dst_abspath_or_url, |
| FALSE /* show_deleted */, |
| TRUE /* show_hidden */, |
| iterpool)); |
| if (dst_kind != svn_node_none) |
| { |
| svn_boolean_t is_excluded; |
| svn_boolean_t is_server_excluded; |
| |
| SVN_ERR(svn_wc__node_is_not_present(NULL, &is_excluded, |
| &is_server_excluded, ctx->wc_ctx, |
| pair->dst_abspath_or_url, FALSE, |
| iterpool)); |
| |
| if (is_excluded || is_server_excluded) |
| { |
| return svn_error_createf( |
| SVN_ERR_WC_OBSTRUCTED_UPDATE, |
| NULL, _("Path '%s' exists, but is excluded"), |
| svn_dirent_local_style(pair->dst_abspath_or_url, iterpool)); |
| } |
| else |
| return svn_error_createf( |
| SVN_ERR_ENTRY_EXISTS, NULL, |
| _("Path '%s' already exists"), |
| svn_dirent_local_style(pair->dst_abspath_or_url, |
| scratch_pool)); |
| } |
| |
| /* Check that there is no unversioned obstruction */ |
| if (metadata_only) |
| dst_kind = svn_node_none; |
| else |
| SVN_ERR(svn_io_check_path(pair->dst_abspath_or_url, &dst_kind, |
| iterpool)); |
| |
| if (dst_kind != svn_node_none) |
| { |
| if (is_move |
| && copy_pairs->nelts == 1 |
| && strcmp(svn_dirent_dirname(pair->src_abspath_or_url, iterpool), |
| svn_dirent_dirname(pair->dst_abspath_or_url, |
| iterpool)) == 0) |
| { |
| const char *dst; |
| char *dst_apr; |
| apr_status_t apr_err; |
| /* We have a rename inside a directory, which might collide |
| just because the case insensivity of the filesystem makes |
| the source match the destination. */ |
| |
| SVN_ERR(svn_path_cstring_from_utf8(&dst, |
| pair->dst_abspath_or_url, |
| scratch_pool)); |
| |
| apr_err = apr_filepath_merge(&dst_apr, NULL, dst, |
| APR_FILEPATH_TRUENAME, iterpool); |
| |
| if (!apr_err) |
| { |
| /* And now bring it back to our canonical format */ |
| SVN_ERR(svn_path_cstring_to_utf8(&dst, dst_apr, iterpool)); |
| dst = svn_dirent_canonicalize(dst, iterpool); |
| } |
| /* else: Don't report this error; just report the normal error */ |
| |
| if (!apr_err && strcmp(dst, pair->src_abspath_or_url) == 0) |
| { |
| /* Ok, we have a single case only rename. Get out of here */ |
| svn_dirent_split(&pair->dst_parent_abspath, &pair->base_name, |
| pair->dst_abspath_or_url, result_pool); |
| |
| svn_pool_destroy(iterpool); |
| return SVN_NO_ERROR; |
| } |
| } |
| |
| return svn_error_createf( |
| SVN_ERR_ENTRY_EXISTS, NULL, |
| _("Path '%s' already exists as unversioned node"), |
| svn_dirent_local_style(pair->dst_abspath_or_url, |
| scratch_pool)); |
| } |
| |
| svn_dirent_split(&pair->dst_parent_abspath, &pair->base_name, |
| pair->dst_abspath_or_url, result_pool); |
| |
| /* Make sure the destination parent is a directory and produce a clear |
| error message if it is not. */ |
| SVN_ERR(svn_wc_read_kind2(&dst_parent_kind, |
| ctx->wc_ctx, pair->dst_parent_abspath, |
| FALSE, TRUE, |
| iterpool)); |
| if (make_parents && dst_parent_kind == svn_node_none) |
| { |
| SVN_ERR(svn_client__make_local_parents(pair->dst_parent_abspath, |
| TRUE, ctx, iterpool)); |
| } |
| else if (dst_parent_kind != svn_node_dir) |
| { |
| return svn_error_createf(SVN_ERR_WC_PATH_NOT_FOUND, NULL, |
| _("Path '%s' is not a directory"), |
| svn_dirent_local_style( |
| pair->dst_parent_abspath, scratch_pool)); |
| } |
| |
| SVN_ERR(svn_io_check_path(pair->dst_parent_abspath, |
| &dst_parent_kind, scratch_pool)); |
| |
| if (dst_parent_kind != svn_node_dir) |
| return svn_error_createf(SVN_ERR_WC_MISSING, NULL, |
| _("Path '%s' is not a directory"), |
| svn_dirent_local_style( |
| pair->dst_parent_abspath, scratch_pool)); |
| } |
| |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| verify_wc_srcs_and_dsts(const apr_array_header_t *copy_pairs, |
| svn_boolean_t make_parents, |
| svn_boolean_t is_move, |
| svn_boolean_t metadata_only, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| int i; |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| |
| /* Check that all of our SRCs exist. */ |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| svn_boolean_t deleted_ok; |
| svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i, |
| svn_client__copy_pair_t *); |
| svn_pool_clear(iterpool); |
| |
| deleted_ok = (pair->src_peg_revision.kind == svn_opt_revision_base |
| || pair->src_op_revision.kind == svn_opt_revision_base); |
| |
| /* Verify that SRC_PATH exists. */ |
| SVN_ERR(svn_wc_read_kind2(&pair->src_kind, ctx->wc_ctx, |
| pair->src_abspath_or_url, |
| deleted_ok, FALSE, iterpool)); |
| if (pair->src_kind == svn_node_none) |
| return svn_error_createf(SVN_ERR_WC_PATH_NOT_FOUND, NULL, |
| _("Path '%s' does not exist"), |
| svn_dirent_local_style( |
| pair->src_abspath_or_url, |
| scratch_pool)); |
| } |
| |
| SVN_ERR(verify_wc_dsts(copy_pairs, make_parents, is_move, metadata_only, ctx, |
| result_pool, iterpool)); |
| |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Path-specific state used as part of path_driver_cb_baton. */ |
| typedef struct path_driver_info_t |
| { |
| const char *src_url; |
| const char *src_path; |
| const char *dst_path; |
| svn_node_kind_t src_kind; |
| svn_revnum_t src_revnum; |
| svn_boolean_t resurrection; |
| svn_boolean_t dir_add; |
| svn_string_t *mergeinfo; /* the new mergeinfo for the target */ |
| } path_driver_info_t; |
| |
| |
| /* The baton used with the path_driver_cb_func() callback for a copy |
| or move operation. */ |
| struct path_driver_cb_baton |
| { |
| /* The editor (and its state) used to perform the operation. */ |
| const svn_delta_editor_t *editor; |
| void *edit_baton; |
| |
| /* A hash of path -> path_driver_info_t *'s. */ |
| apr_hash_t *action_hash; |
| |
| /* Whether the operation is a move or copy. */ |
| svn_boolean_t is_move; |
| }; |
| |
| static svn_error_t * |
| path_driver_cb_func(void **dir_baton, |
| void *parent_baton, |
| void *callback_baton, |
| const char *path, |
| apr_pool_t *pool) |
| { |
| struct path_driver_cb_baton *cb_baton = callback_baton; |
| svn_boolean_t do_delete = FALSE, do_add = FALSE; |
| path_driver_info_t *path_info = svn_hash_gets(cb_baton->action_hash, path); |
| |
| /* Initialize return value. */ |
| *dir_baton = NULL; |
| |
| /* This function should never get an empty PATH. We can neither |
| create nor delete the empty PATH, so if someone is calling us |
| with such, the code is just plain wrong. */ |
| SVN_ERR_ASSERT(! svn_path_is_empty(path)); |
| |
| /* Check to see if we need to add the path as a directory. */ |
| if (path_info->dir_add) |
| { |
| return cb_baton->editor->add_directory(path, parent_baton, NULL, |
| SVN_INVALID_REVNUM, pool, |
| dir_baton); |
| } |
| |
| /* If this is a resurrection, we know the source and dest paths are |
| the same, and that our driver will only be calling us once. */ |
| if (path_info->resurrection) |
| { |
| /* If this is a move, we do nothing. Otherwise, we do the copy. */ |
| if (! cb_baton->is_move) |
| do_add = TRUE; |
| } |
| /* Not a resurrection. */ |
| else |
| { |
| /* If this is a move, we check PATH to see if it is the source |
| or the destination of the move. */ |
| if (cb_baton->is_move) |
| { |
| if (strcmp(path_info->src_path, path) == 0) |
| do_delete = TRUE; |
| else |
| do_add = TRUE; |
| } |
| /* Not a move? This must just be the copy addition. */ |
| else |
| { |
| do_add = TRUE; |
| } |
| } |
| |
| if (do_delete) |
| { |
| SVN_ERR(cb_baton->editor->delete_entry(path, SVN_INVALID_REVNUM, |
| parent_baton, pool)); |
| } |
| if (do_add) |
| { |
| SVN_ERR(svn_path_check_valid(path, pool)); |
| |
| if (path_info->src_kind == svn_node_file) |
| { |
| void *file_baton; |
| SVN_ERR(cb_baton->editor->add_file(path, parent_baton, |
| path_info->src_url, |
| path_info->src_revnum, |
| pool, &file_baton)); |
| if (path_info->mergeinfo) |
| SVN_ERR(cb_baton->editor->change_file_prop(file_baton, |
| SVN_PROP_MERGEINFO, |
| path_info->mergeinfo, |
| pool)); |
| SVN_ERR(cb_baton->editor->close_file(file_baton, NULL, pool)); |
| } |
| else |
| { |
| SVN_ERR(cb_baton->editor->add_directory(path, parent_baton, |
| path_info->src_url, |
| path_info->src_revnum, |
| pool, dir_baton)); |
| if (path_info->mergeinfo) |
| SVN_ERR(cb_baton->editor->change_dir_prop(*dir_baton, |
| SVN_PROP_MERGEINFO, |
| path_info->mergeinfo, |
| pool)); |
| } |
| } |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Starting with the path DIR relative to the RA_SESSION's session |
| URL, work up through DIR's parents until an existing node is found. |
| Push each nonexistent path onto the array NEW_DIRS, allocating in |
| POOL. Raise an error if the existing node is not a directory. |
| |
| ### Multiple requests for HEAD (SVN_INVALID_REVNUM) make this |
| ### implementation susceptible to race conditions. */ |
| static svn_error_t * |
| find_absent_parents1(svn_ra_session_t *ra_session, |
| const char *dir, |
| apr_array_header_t *new_dirs, |
| apr_pool_t *pool) |
| { |
| svn_node_kind_t kind; |
| apr_pool_t *iterpool = svn_pool_create(pool); |
| |
| SVN_ERR(svn_ra_check_path(ra_session, dir, SVN_INVALID_REVNUM, &kind, |
| iterpool)); |
| |
| while (kind == svn_node_none) |
| { |
| svn_pool_clear(iterpool); |
| |
| APR_ARRAY_PUSH(new_dirs, const char *) = dir; |
| dir = svn_dirent_dirname(dir, pool); |
| |
| SVN_ERR(svn_ra_check_path(ra_session, dir, SVN_INVALID_REVNUM, |
| &kind, iterpool)); |
| } |
| |
| if (kind != svn_node_dir) |
| return svn_error_createf(SVN_ERR_FS_ALREADY_EXISTS, NULL, |
| _("Path '%s' already exists, but is not a " |
| "directory"), dir); |
| |
| svn_pool_destroy(iterpool); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Starting with the URL *TOP_DST_URL which is also the root of |
| RA_SESSION, work up through its parents until an existing node is |
| found. Push each nonexistent URL onto the array NEW_DIRS, |
| allocating in POOL. Raise an error if the existing node is not a |
| directory. |
| |
| Set *TOP_DST_URL and the RA session's root to the existing node's URL. |
| |
| ### Multiple requests for HEAD (SVN_INVALID_REVNUM) make this |
| ### implementation susceptible to race conditions. */ |
| static svn_error_t * |
| find_absent_parents2(svn_ra_session_t *ra_session, |
| const char **top_dst_url, |
| apr_array_header_t *new_dirs, |
| apr_pool_t *pool) |
| { |
| const char *root_url = *top_dst_url; |
| svn_node_kind_t kind; |
| |
| SVN_ERR(svn_ra_check_path(ra_session, "", SVN_INVALID_REVNUM, &kind, |
| pool)); |
| |
| while (kind == svn_node_none) |
| { |
| APR_ARRAY_PUSH(new_dirs, const char *) = root_url; |
| root_url = svn_uri_dirname(root_url, pool); |
| |
| SVN_ERR(svn_ra_reparent(ra_session, root_url, pool)); |
| SVN_ERR(svn_ra_check_path(ra_session, "", SVN_INVALID_REVNUM, &kind, |
| pool)); |
| } |
| |
| if (kind != svn_node_dir) |
| return svn_error_createf(SVN_ERR_FS_ALREADY_EXISTS, NULL, |
| _("Path '%s' already exists, but is not a directory"), |
| root_url); |
| |
| *top_dst_url = root_url; |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| repos_to_repos_copy(const apr_array_header_t *copy_pairs, |
| svn_boolean_t make_parents, |
| const apr_hash_t *revprop_table, |
| svn_commit_callback2_t commit_callback, |
| void *commit_baton, |
| svn_client_ctx_t *ctx, |
| svn_boolean_t is_move, |
| apr_pool_t *pool) |
| { |
| svn_error_t *err; |
| apr_array_header_t *paths = apr_array_make(pool, 2 * copy_pairs->nelts, |
| sizeof(const char *)); |
| apr_hash_t *action_hash = apr_hash_make(pool); |
| apr_array_header_t *path_infos; |
| const char *top_url, *top_url_all, *top_url_dst; |
| const char *message, *repos_root; |
| svn_ra_session_t *ra_session = NULL; |
| const svn_delta_editor_t *editor; |
| void *edit_baton; |
| struct path_driver_cb_baton cb_baton; |
| apr_array_header_t *new_dirs = NULL; |
| apr_hash_t *commit_revprops; |
| int i; |
| svn_client__copy_pair_t *first_pair = |
| APR_ARRAY_IDX(copy_pairs, 0, svn_client__copy_pair_t *); |
| |
| /* Open an RA session to the first copy pair's destination. We'll |
| be verifying that every one of our copy source and destination |
| URLs is or is beneath this sucker's repository root URL as a form |
| of a cheap(ish) sanity check. */ |
| SVN_ERR(svn_client_open_ra_session2(&ra_session, |
| first_pair->src_abspath_or_url, NULL, |
| ctx, pool, pool)); |
| SVN_ERR(svn_ra_get_repos_root2(ra_session, &repos_root, pool)); |
| |
| /* Verify that sources and destinations are all at or under |
| REPOS_ROOT. While here, create a path_info struct for each |
| src/dst pair and initialize portions of it with normalized source |
| location information. */ |
| path_infos = apr_array_make(pool, copy_pairs->nelts, |
| sizeof(path_driver_info_t *)); |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| path_driver_info_t *info = apr_pcalloc(pool, sizeof(*info)); |
| svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i, |
| svn_client__copy_pair_t *); |
| apr_hash_t *mergeinfo; |
| |
| /* Are the source and destination URLs at or under REPOS_ROOT? */ |
| if (! (svn_uri__is_ancestor(repos_root, pair->src_abspath_or_url) |
| && svn_uri__is_ancestor(repos_root, pair->dst_abspath_or_url))) |
| return svn_error_create |
| (SVN_ERR_UNSUPPORTED_FEATURE, NULL, |
| _("Source and destination URLs appear not to point to the " |
| "same repository.")); |
| |
| /* Run the history function to get the source's URL and revnum in the |
| operational revision. */ |
| SVN_ERR(svn_ra_reparent(ra_session, pair->src_abspath_or_url, pool)); |
| SVN_ERR(svn_client__repos_locations(&pair->src_abspath_or_url, |
| &pair->src_revnum, |
| NULL, NULL, |
| ra_session, |
| pair->src_abspath_or_url, |
| &pair->src_peg_revision, |
| &pair->src_op_revision, NULL, |
| ctx, pool)); |
| |
| /* Go ahead and grab mergeinfo from the source, too. */ |
| SVN_ERR(svn_ra_reparent(ra_session, pair->src_abspath_or_url, pool)); |
| SVN_ERR(svn_client__get_repos_mergeinfo( |
| &mergeinfo, ra_session, |
| pair->src_abspath_or_url, pair->src_revnum, |
| svn_mergeinfo_inherited, TRUE /*squelch_incapable*/, pool)); |
| if (mergeinfo) |
| SVN_ERR(svn_mergeinfo_to_string(&info->mergeinfo, mergeinfo, pool)); |
| |
| /* Plop an INFO structure onto our array thereof. */ |
| info->src_url = pair->src_abspath_or_url; |
| info->src_revnum = pair->src_revnum; |
| info->resurrection = FALSE; |
| APR_ARRAY_PUSH(path_infos, path_driver_info_t *) = info; |
| } |
| |
| /* If this is a move, we have to open our session to the longest |
| path common to all SRC_URLS and DST_URLS in the repository so we |
| can do existence checks on all paths, and so we can operate on |
| all paths in the case of a move. But if this is *not* a move, |
| then opening our session at the longest path common to sources |
| *and* destinations might be an optimization when the user is |
| authorized to access all that stuff, but could cause the |
| operation to fail altogether otherwise. See issue #3242. */ |
| SVN_ERR(get_copy_pair_ancestors(copy_pairs, NULL, &top_url_dst, &top_url_all, |
| pool)); |
| top_url = is_move ? top_url_all : top_url_dst; |
| |
| /* Check each src/dst pair for resurrection, and verify that TOP_URL |
| is anchored high enough to cover all the editor_t activities |
| required for this operation. */ |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i, |
| svn_client__copy_pair_t *); |
| path_driver_info_t *info = APR_ARRAY_IDX(path_infos, i, |
| path_driver_info_t *); |
| |
| /* Source and destination are the same? It's a resurrection. */ |
| if (strcmp(pair->src_abspath_or_url, pair->dst_abspath_or_url) == 0) |
| info->resurrection = TRUE; |
| |
| /* We need to add each dst_URL, and (in a move) we'll need to |
| delete each src_URL. Our selection of TOP_URL so far ensures |
| that all our destination URLs (and source URLs, for moves) |
| are at least as deep as TOP_URL, but we need to make sure |
| that TOP_URL is an *ancestor* of all our to-be-edited paths. |
| |
| Issue #683 is demonstrates this scenario. If you're |
| resurrecting a deleted item like this: 'svn cp -rN src_URL |
| dst_URL', then src_URL == dst_URL == top_url. In this |
| situation, we want to open an RA session to be at least the |
| *parent* of all three. */ |
| if ((strcmp(top_url, pair->dst_abspath_or_url) == 0) |
| && (strcmp(top_url, repos_root) != 0)) |
| { |
| top_url = svn_uri_dirname(top_url, pool); |
| } |
| if (is_move |
| && (strcmp(top_url, pair->src_abspath_or_url) == 0) |
| && (strcmp(top_url, repos_root) != 0)) |
| { |
| top_url = svn_uri_dirname(top_url, pool); |
| } |
| } |
| |
| /* Point the RA session to our current TOP_URL. */ |
| SVN_ERR(svn_ra_reparent(ra_session, top_url, pool)); |
| |
| /* If we're allowed to create nonexistent parent directories of our |
| destinations, then make a list in NEW_DIRS of the parent |
| directories of the destination that don't yet exist. */ |
| if (make_parents) |
| { |
| new_dirs = apr_array_make(pool, 0, sizeof(const char *)); |
| |
| /* If this is a move, TOP_URL is at least the common ancestor of |
| all the paths (sources and destinations) involved. Assuming |
| the sources exist (which is fair, because if they don't, this |
| whole operation will fail anyway), TOP_URL must also exist. |
| So it's the paths between TOP_URL and the destinations which |
| we have to check for existence. But here, we take advantage |
| of the knowledge of our caller. We know that if there are |
| multiple copy/move operations being requested, then the |
| destinations of the copies/moves will all be siblings of one |
| another. Therefore, we need only to check for the |
| nonexistent paths between TOP_URL and *one* of our |
| destinations to find nonexistent parents of all of them. */ |
| if (is_move) |
| { |
| /* Imagine a situation where the user tries to copy an |
| existing source directory to nonexistent directory with |
| --parents options specified: |
| |
| svn copy --parents URL/src URL/dst |
| |
| where src exists and dst does not. If the dirname of the |
| destination path is equal to TOP_URL, |
| do not try to add dst to the NEW_DIRS list since it |
| will be added to the commit items array later in this |
| function. */ |
| const char *dir = svn_uri_skip_ancestor( |
| top_url, |
| svn_uri_dirname(first_pair->dst_abspath_or_url, |
| pool), |
| pool); |
| if (dir && *dir) |
| SVN_ERR(find_absent_parents1(ra_session, dir, new_dirs, pool)); |
| } |
| /* If, however, this is *not* a move, TOP_URL only points to the |
| common ancestor of our destination path(s), or possibly one |
| level higher. We'll need to do an existence crawl toward the |
| root of the repository, starting with one of our destinations |
| (see "... take advantage of the knowledge of our caller ..." |
| above), and possibly adjusting TOP_URL as we go. */ |
| else |
| { |
| apr_array_header_t *new_urls = |
| apr_array_make(pool, 0, sizeof(const char *)); |
| SVN_ERR(find_absent_parents2(ra_session, &top_url, new_urls, pool)); |
| |
| /* Convert absolute URLs into relpaths relative to TOP_URL. */ |
| for (i = 0; i < new_urls->nelts; i++) |
| { |
| const char *new_url = APR_ARRAY_IDX(new_urls, i, const char *); |
| const char *dir = svn_uri_skip_ancestor(top_url, new_url, pool); |
| |
| APR_ARRAY_PUSH(new_dirs, const char *) = dir; |
| } |
| } |
| } |
| |
| /* For each src/dst pair, check to see if that SRC_URL is a child of |
| the DST_URL (excepting the case where DST_URL is the repo root). |
| If it is, and the parent of DST_URL is the current TOP_URL, then we |
| need to reparent the session one directory higher, the parent of |
| the DST_URL. */ |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i, |
| svn_client__copy_pair_t *); |
| path_driver_info_t *info = APR_ARRAY_IDX(path_infos, i, |
| path_driver_info_t *); |
| const char *relpath = svn_uri_skip_ancestor(pair->dst_abspath_or_url, |
| pair->src_abspath_or_url, |
| pool); |
| |
| if ((strcmp(pair->dst_abspath_or_url, repos_root) != 0) |
| && (relpath != NULL && *relpath != '\0')) |
| { |
| info->resurrection = TRUE; |
| top_url = svn_uri_get_longest_ancestor( |
| top_url, |
| svn_uri_dirname(pair->dst_abspath_or_url, pool), |
| pool); |
| SVN_ERR(svn_ra_reparent(ra_session, top_url, pool)); |
| } |
| } |
| |
| /* Get the portions of the SRC and DST URLs that are relative to |
| TOP_URL (URI-decoding them while we're at it), verify that the |
| source exists and the proposed destination does not, and toss |
| what we've learned into the INFO array. (For copies -- that is, |
| non-moves -- the relative source URL NULL because it isn't a |
| child of the TOP_URL at all. That's okay, we'll deal with |
| it.) */ |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| svn_client__copy_pair_t *pair = |
| APR_ARRAY_IDX(copy_pairs, i, svn_client__copy_pair_t *); |
| path_driver_info_t *info = |
| APR_ARRAY_IDX(path_infos, i, path_driver_info_t *); |
| svn_node_kind_t dst_kind; |
| const char *src_rel, *dst_rel; |
| |
| src_rel = svn_uri_skip_ancestor(top_url, pair->src_abspath_or_url, pool); |
| if (src_rel) |
| { |
| SVN_ERR(svn_ra_check_path(ra_session, src_rel, pair->src_revnum, |
| &info->src_kind, pool)); |
| } |
| else |
| { |
| const char *old_url; |
| |
| src_rel = NULL; |
| SVN_ERR_ASSERT(! is_move); |
| |
| SVN_ERR(svn_client__ensure_ra_session_url(&old_url, ra_session, |
| pair->src_abspath_or_url, |
| pool)); |
| SVN_ERR(svn_ra_check_path(ra_session, "", pair->src_revnum, |
| &info->src_kind, pool)); |
| SVN_ERR(svn_ra_reparent(ra_session, old_url, pool)); |
| } |
| if (info->src_kind == svn_node_none) |
| return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL, |
| _("Path '%s' does not exist in revision %ld"), |
| pair->src_abspath_or_url, pair->src_revnum); |
| |
| /* Figure out the basename that will result from this operation, |
| and ensure that we aren't trying to overwrite existing paths. */ |
| dst_rel = svn_uri_skip_ancestor(top_url, pair->dst_abspath_or_url, pool); |
| SVN_ERR(svn_ra_check_path(ra_session, dst_rel, SVN_INVALID_REVNUM, |
| &dst_kind, pool)); |
| if (dst_kind != svn_node_none) |
| return svn_error_createf(SVN_ERR_FS_ALREADY_EXISTS, NULL, |
| _("Path '%s' already exists"), dst_rel); |
| |
| /* More info for our INFO structure. */ |
| info->src_path = src_rel; |
| info->dst_path = dst_rel; |
| |
| svn_hash_sets(action_hash, info->dst_path, info); |
| if (is_move && (! info->resurrection)) |
| svn_hash_sets(action_hash, info->src_path, info); |
| } |
| |
| if (SVN_CLIENT__HAS_LOG_MSG_FUNC(ctx)) |
| { |
| /* Produce a list of new paths to add, and provide it to the |
| mechanism used to acquire a log message. */ |
| svn_client_commit_item3_t *item; |
| const char *tmp_file; |
| apr_array_header_t *commit_items |
| = apr_array_make(pool, 2 * copy_pairs->nelts, sizeof(item)); |
| |
| /* Add any intermediate directories to the message */ |
| if (make_parents) |
| { |
| for (i = 0; i < new_dirs->nelts; i++) |
| { |
| const char *relpath = APR_ARRAY_IDX(new_dirs, i, const char *); |
| |
| item = svn_client_commit_item3_create(pool); |
| item->url = svn_path_url_add_component2(top_url, relpath, pool); |
| item->state_flags = SVN_CLIENT_COMMIT_ITEM_ADD; |
| APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item; |
| } |
| } |
| |
| for (i = 0; i < path_infos->nelts; i++) |
| { |
| path_driver_info_t *info = APR_ARRAY_IDX(path_infos, i, |
| path_driver_info_t *); |
| |
| item = svn_client_commit_item3_create(pool); |
| item->url = svn_path_url_add_component2(top_url, info->dst_path, |
| pool); |
| item->state_flags = SVN_CLIENT_COMMIT_ITEM_ADD; |
| APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item; |
| |
| if (is_move && (! info->resurrection)) |
| { |
| item = apr_pcalloc(pool, sizeof(*item)); |
| item->url = svn_path_url_add_component2(top_url, info->src_path, |
| pool); |
| item->state_flags = SVN_CLIENT_COMMIT_ITEM_DELETE; |
| APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item; |
| } |
| } |
| |
| SVN_ERR(svn_client__get_log_msg(&message, &tmp_file, commit_items, |
| ctx, pool)); |
| if (! message) |
| return SVN_NO_ERROR; |
| } |
| else |
| message = ""; |
| |
| /* Setup our PATHS for the path-based editor drive. */ |
| /* First any intermediate directories. */ |
| if (make_parents) |
| { |
| for (i = 0; i < new_dirs->nelts; i++) |
| { |
| const char *relpath = APR_ARRAY_IDX(new_dirs, i, const char *); |
| path_driver_info_t *info = apr_pcalloc(pool, sizeof(*info)); |
| |
| info->dst_path = relpath; |
| info->dir_add = TRUE; |
| |
| APR_ARRAY_PUSH(paths, const char *) = relpath; |
| svn_hash_sets(action_hash, relpath, info); |
| } |
| } |
| |
| /* Then our copy destinations and move sources (if any). */ |
| for (i = 0; i < path_infos->nelts; i++) |
| { |
| path_driver_info_t *info = APR_ARRAY_IDX(path_infos, i, |
| path_driver_info_t *); |
| |
| APR_ARRAY_PUSH(paths, const char *) = info->dst_path; |
| if (is_move && (! info->resurrection)) |
| APR_ARRAY_PUSH(paths, const char *) = info->src_path; |
| } |
| |
| SVN_ERR(svn_client__ensure_revprop_table(&commit_revprops, revprop_table, |
| message, ctx, pool)); |
| |
| /* Fetch RA commit editor. */ |
| SVN_ERR(svn_ra__register_editor_shim_callbacks(ra_session, |
| svn_client__get_shim_callbacks(ctx->wc_ctx, |
| NULL, pool))); |
| SVN_ERR(svn_ra_get_commit_editor3(ra_session, &editor, &edit_baton, |
| commit_revprops, |
| commit_callback, |
| commit_baton, |
| NULL, TRUE, /* No lock tokens */ |
| pool)); |
| |
| /* Setup the callback baton. */ |
| cb_baton.editor = editor; |
| cb_baton.edit_baton = edit_baton; |
| cb_baton.action_hash = action_hash; |
| cb_baton.is_move = is_move; |
| |
| /* Call the path-based editor driver. */ |
| err = svn_delta_path_driver2(editor, edit_baton, paths, TRUE, |
| path_driver_cb_func, &cb_baton, pool); |
| if (err) |
| { |
| /* At least try to abort the edit (and fs txn) before throwing err. */ |
| return svn_error_compose_create( |
| err, |
| editor->abort_edit(edit_baton, pool)); |
| } |
| |
| /* Close the edit. */ |
| return svn_error_trace(editor->close_edit(edit_baton, pool)); |
| } |
| |
| /* Baton for check_url_kind */ |
| struct check_url_kind_baton |
| { |
| svn_ra_session_t *session; |
| const char *repos_root_url; |
| svn_boolean_t should_reparent; |
| }; |
| |
| /* Implements svn_client__check_url_kind_t for wc_to_repos_copy */ |
| 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 (!svn_uri__is_ancestor(cukb->repos_root_url, url)) |
| *kind = svn_node_none; |
| else |
| { |
| cukb->should_reparent = TRUE; |
| |
| SVN_ERR(svn_ra_reparent(cukb->session, url, scratch_pool)); |
| |
| SVN_ERR(svn_ra_check_path(cukb->session, "", revision, |
| kind, scratch_pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* ### Copy ... |
| * COMMIT_INFO_P is ... |
| * COPY_PAIRS is ... such that each 'src_abspath_or_url' is a local abspath |
| * and each 'dst_abspath_or_url' is a URL. |
| * MAKE_PARENTS is ... |
| * REVPROP_TABLE is ... |
| * CTX is ... */ |
| static svn_error_t * |
| wc_to_repos_copy(const apr_array_header_t *copy_pairs, |
| svn_boolean_t make_parents, |
| const apr_hash_t *revprop_table, |
| svn_commit_callback2_t commit_callback, |
| void *commit_baton, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| const char *message; |
| const char *top_src_path, *top_dst_url; |
| struct check_url_kind_baton cukb; |
| const char *top_src_abspath; |
| svn_ra_session_t *ra_session; |
| const svn_delta_editor_t *editor; |
| apr_hash_t *relpath_map = NULL; |
| void *edit_baton; |
| svn_client__committables_t *committables; |
| apr_array_header_t *commit_items; |
| apr_pool_t *iterpool; |
| apr_array_header_t *new_dirs = NULL; |
| apr_hash_t *commit_revprops; |
| svn_client__copy_pair_t *first_pair; |
| apr_pool_t *session_pool = svn_pool_create(scratch_pool); |
| int i; |
| |
| /* Find the common root of all the source paths */ |
| SVN_ERR(get_copy_pair_ancestors(copy_pairs, &top_src_path, NULL, NULL, |
| scratch_pool)); |
| |
| /* Do we need to lock the working copy? 1.6 didn't take a write |
| lock, but what happens if the working copy changes during the copy |
| operation? */ |
| |
| iterpool = svn_pool_create(scratch_pool); |
| |
| /* Determine the longest common ancestor for the destinations, and open an RA |
| session to that location. */ |
| /* ### But why start by getting the _parent_ of the first one? */ |
| /* --- That works because multiple destinations always point to the same |
| * directory. I'm rather wondering why we need to find a common |
| * destination parent here at all, instead of simply getting |
| * top_dst_url from get_copy_pair_ancestors() above? |
| * It looks like the entire block of code hanging off this comment |
| * is redundant. */ |
| first_pair = APR_ARRAY_IDX(copy_pairs, 0, svn_client__copy_pair_t *); |
| top_dst_url = svn_uri_dirname(first_pair->dst_abspath_or_url, scratch_pool); |
| for (i = 1; i < copy_pairs->nelts; i++) |
| { |
| svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i, |
| svn_client__copy_pair_t *); |
| top_dst_url = svn_uri_get_longest_ancestor(top_dst_url, |
| pair->dst_abspath_or_url, |
| scratch_pool); |
| } |
| |
| SVN_ERR(svn_dirent_get_absolute(&top_src_abspath, top_src_path, scratch_pool)); |
| |
| /* Open a session to help while determining the exact targets */ |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, NULL, top_dst_url, |
| top_src_abspath, NULL, |
| FALSE /* write_dav_props */, |
| TRUE /* read_dav_props */, |
| ctx, |
| session_pool, session_pool)); |
| |
| /* If requested, determine the nearest existing parent of the destination, |
| and reparent the ra session there. */ |
| if (make_parents) |
| { |
| new_dirs = apr_array_make(scratch_pool, 0, sizeof(const char *)); |
| SVN_ERR(find_absent_parents2(ra_session, &top_dst_url, new_dirs, |
| scratch_pool)); |
| } |
| |
| /* Figure out the basename that will result from each copy and check to make |
| sure it doesn't exist already. */ |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| svn_node_kind_t dst_kind; |
| const char *dst_rel; |
| svn_client__copy_pair_t *pair = |
| APR_ARRAY_IDX(copy_pairs, i, svn_client__copy_pair_t *); |
| |
| svn_pool_clear(iterpool); |
| dst_rel = svn_uri_skip_ancestor(top_dst_url, pair->dst_abspath_or_url, |
| iterpool); |
| SVN_ERR(svn_ra_check_path(ra_session, dst_rel, SVN_INVALID_REVNUM, |
| &dst_kind, iterpool)); |
| if (dst_kind != svn_node_none) |
| { |
| return svn_error_createf(SVN_ERR_FS_ALREADY_EXISTS, NULL, |
| _("Path '%s' already exists"), |
| pair->dst_abspath_or_url); |
| } |
| } |
| |
| if (SVN_CLIENT__HAS_LOG_MSG_FUNC(ctx)) |
| { |
| /* Produce a list of new paths to add, and provide it to the |
| mechanism used to acquire a log message. */ |
| svn_client_commit_item3_t *item; |
| const char *tmp_file; |
| commit_items = apr_array_make(scratch_pool, copy_pairs->nelts, |
| sizeof(item)); |
| |
| /* Add any intermediate directories to the message */ |
| if (make_parents) |
| { |
| for (i = 0; i < new_dirs->nelts; i++) |
| { |
| const char *url = APR_ARRAY_IDX(new_dirs, i, const char *); |
| |
| item = svn_client_commit_item3_create(scratch_pool); |
| item->url = url; |
| item->state_flags = SVN_CLIENT_COMMIT_ITEM_ADD; |
| APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item; |
| } |
| } |
| |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i, |
| svn_client__copy_pair_t *); |
| |
| item = svn_client_commit_item3_create(scratch_pool); |
| item->url = pair->dst_abspath_or_url; |
| item->state_flags = SVN_CLIENT_COMMIT_ITEM_ADD; |
| APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item; |
| } |
| |
| SVN_ERR(svn_client__get_log_msg(&message, &tmp_file, commit_items, |
| ctx, scratch_pool)); |
| if (! message) |
| { |
| svn_pool_destroy(iterpool); |
| svn_pool_destroy(session_pool); |
| return SVN_NO_ERROR; |
| } |
| } |
| else |
| message = ""; |
| |
| cukb.session = ra_session; |
| SVN_ERR(svn_ra_get_repos_root2(ra_session, &cukb.repos_root_url, session_pool)); |
| cukb.should_reparent = FALSE; |
| |
| /* Crawl the working copy for commit items. */ |
| /* ### TODO: Pass check_url_func for issue #3314 handling */ |
| SVN_ERR(svn_client__get_copy_committables(&committables, |
| copy_pairs, |
| check_url_kind, &cukb, |
| ctx, scratch_pool, iterpool)); |
| |
| /* The committables are keyed by the repository root */ |
| commit_items = svn_hash_gets(committables->by_repository, |
| cukb.repos_root_url); |
| SVN_ERR_ASSERT(commit_items != NULL); |
| |
| if (cukb.should_reparent) |
| SVN_ERR(svn_ra_reparent(ra_session, top_dst_url, session_pool)); |
| |
| /* If we are creating intermediate directories, tack them onto the list |
| of committables. */ |
| if (make_parents) |
| { |
| for (i = 0; i < new_dirs->nelts; i++) |
| { |
| const char *url = APR_ARRAY_IDX(new_dirs, i, const char *); |
| svn_client_commit_item3_t *item; |
| |
| item = svn_client_commit_item3_create(scratch_pool); |
| item->url = url; |
| item->state_flags = SVN_CLIENT_COMMIT_ITEM_ADD; |
| item->incoming_prop_changes = apr_array_make(scratch_pool, 1, |
| sizeof(svn_prop_t *)); |
| APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item; |
| } |
| } |
| |
| /* ### TODO: This extra loop would be unnecessary if this code lived |
| ### in svn_client__get_copy_committables(), which is incidentally |
| ### only used above (so should really be in this source file). */ |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| apr_hash_t *mergeinfo, *wc_mergeinfo; |
| svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i, |
| svn_client__copy_pair_t *); |
| svn_client_commit_item3_t *item = |
| APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *); |
| svn_client__pathrev_t *src_origin; |
| |
| svn_pool_clear(iterpool); |
| |
| SVN_ERR(svn_client__wc_node_get_origin(&src_origin, |
| pair->src_abspath_or_url, |
| ctx, iterpool, iterpool)); |
| |
| /* Set the mergeinfo for the destination to the combined merge |
| info known to the WC and the repository. */ |
| item->outgoing_prop_changes = apr_array_make(scratch_pool, 1, |
| sizeof(svn_prop_t *)); |
| /* Repository mergeinfo (or NULL if it's locally added)... */ |
| if (src_origin) |
| SVN_ERR(svn_client__get_repos_mergeinfo( |
| &mergeinfo, ra_session, src_origin->url, src_origin->rev, |
| svn_mergeinfo_inherited, TRUE /*sqelch_inc.*/, iterpool)); |
| else |
| mergeinfo = NULL; |
| /* ... and WC mergeinfo. */ |
| SVN_ERR(svn_client__parse_mergeinfo(&wc_mergeinfo, ctx->wc_ctx, |
| pair->src_abspath_or_url, |
| iterpool, iterpool)); |
| if (wc_mergeinfo && mergeinfo) |
| SVN_ERR(svn_mergeinfo_merge2(mergeinfo, wc_mergeinfo, iterpool, |
| iterpool)); |
| else if (! mergeinfo) |
| mergeinfo = wc_mergeinfo; |
| if (mergeinfo) |
| { |
| /* Push a mergeinfo prop representing MERGEINFO onto the |
| * OUTGOING_PROP_CHANGES array. */ |
| |
| svn_prop_t *mergeinfo_prop |
| = apr_palloc(item->outgoing_prop_changes->pool, |
| sizeof(svn_prop_t)); |
| svn_string_t *prop_value; |
| |
| SVN_ERR(svn_mergeinfo_to_string(&prop_value, mergeinfo, |
| item->outgoing_prop_changes->pool)); |
| |
| mergeinfo_prop->name = SVN_PROP_MERGEINFO; |
| mergeinfo_prop->value = prop_value; |
| APR_ARRAY_PUSH(item->outgoing_prop_changes, svn_prop_t *) |
| = mergeinfo_prop; |
| } |
| } |
| |
| /* Sort and condense our COMMIT_ITEMS. */ |
| SVN_ERR(svn_client__condense_commit_items(&top_dst_url, |
| commit_items, scratch_pool)); |
| |
| #ifdef ENABLE_EV2_SHIMS |
| if (commit_items) |
| { |
| 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, |
| ctx->wc_ctx, item->path, FALSE, |
| scratch_pool, iterpool)); |
| if (relpath) |
| svn_hash_sets(relpath_map, relpath, item->path); |
| } |
| } |
| #endif |
| |
| /* Close the initial session, to reopen a new session with commit handling */ |
| svn_pool_clear(session_pool); |
| |
| /* Open a new RA session to DST_URL. */ |
| SVN_ERR(svn_client__open_ra_session_internal(&ra_session, NULL, top_dst_url, |
| NULL, commit_items, |
| FALSE, FALSE, ctx, |
| session_pool, session_pool)); |
| |
| SVN_ERR(svn_client__ensure_revprop_table(&commit_revprops, revprop_table, |
| message, ctx, session_pool)); |
| |
| /* Fetch RA commit editor. */ |
| SVN_ERR(svn_ra__register_editor_shim_callbacks(ra_session, |
| svn_client__get_shim_callbacks(ctx->wc_ctx, relpath_map, |
| session_pool))); |
| SVN_ERR(svn_ra_get_commit_editor3(ra_session, &editor, &edit_baton, |
| commit_revprops, |
| commit_callback, |
| commit_baton, NULL, |
| TRUE, /* No lock tokens */ |
| session_pool)); |
| |
| /* Perform the commit. */ |
| SVN_ERR_W(svn_client__do_commit(top_dst_url, commit_items, |
| editor, edit_baton, |
| 0, /* ### any notify_path_offset needed? */ |
| NULL, ctx, session_pool, session_pool), |
| _("Commit failed (details follow):")); |
| |
| svn_pool_destroy(iterpool); |
| svn_pool_destroy(session_pool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* A baton for notification_adjust_func(). */ |
| struct notification_adjust_baton |
| { |
| svn_wc_notify_func2_t inner_func; |
| void *inner_baton; |
| const char *checkout_abspath; |
| const char *final_abspath; |
| }; |
| |
| /* A svn_wc_notify_func2_t function that wraps BATON->inner_func (whose |
| * baton is BATON->inner_baton) and adjusts the notification paths that |
| * start with BATON->checkout_abspath to start instead with |
| * BATON->final_abspath. */ |
| static void |
| notification_adjust_func(void *baton, |
| const svn_wc_notify_t *notify, |
| apr_pool_t *pool) |
| { |
| struct notification_adjust_baton *nb = baton; |
| svn_wc_notify_t *inner_notify = svn_wc_dup_notify(notify, pool); |
| const char *relpath; |
| |
| relpath = svn_dirent_skip_ancestor(nb->checkout_abspath, notify->path); |
| inner_notify->path = svn_dirent_join(nb->final_abspath, relpath, pool); |
| |
| if (nb->inner_func) |
| nb->inner_func(nb->inner_baton, inner_notify, pool); |
| } |
| |
| /* Peform each individual copy operation for a repos -> wc copy. A |
| helper for repos_to_wc_copy(). |
| |
| Resolve PAIR->src_revnum to a real revision number if it isn't already. */ |
| static svn_error_t * |
| repos_to_wc_copy_single(svn_boolean_t *timestamp_sleep, |
| svn_client__copy_pair_t *pair, |
| svn_boolean_t same_repositories, |
| svn_boolean_t ignore_externals, |
| svn_ra_session_t *ra_session, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *pool) |
| { |
| apr_hash_t *src_mergeinfo; |
| const char *dst_abspath = pair->dst_abspath_or_url; |
| |
| SVN_ERR_ASSERT(svn_dirent_is_absolute(dst_abspath)); |
| |
| if (!same_repositories && ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify; |
| notify = svn_wc_create_notify_url( |
| pair->src_abspath_or_url, |
| svn_wc_notify_foreign_copy_begin, |
| pool); |
| notify->kind = pair->src_kind; |
| ctx->notify_func2(ctx->notify_baton2, notify, pool); |
| |
| /* Allow a theoretical cancel to get through. */ |
| if (ctx->cancel_func) |
| SVN_ERR(ctx->cancel_func(ctx->cancel_baton)); |
| } |
| |
| if (pair->src_kind == svn_node_dir) |
| { |
| if (same_repositories) |
| { |
| svn_boolean_t sleep_needed = FALSE; |
| const char *tmpdir_abspath, *tmp_abspath; |
| |
| /* Find a temporary location in which to check out the copy source. */ |
| SVN_ERR(svn_wc__get_tmpdir(&tmpdir_abspath, ctx->wc_ctx, dst_abspath, |
| pool, pool)); |
| |
| SVN_ERR(svn_io_open_unique_file3(NULL, &tmp_abspath, tmpdir_abspath, |
| svn_io_file_del_on_close, pool, pool)); |
| |
| /* Make a new checkout of the requested source. While doing so, |
| * resolve pair->src_revnum to an actual revision number in case it |
| * was until now 'invalid' meaning 'head'. Ask this function not to |
| * sleep for timestamps, by passing a sleep_needed output param. |
| * Send notifications for all nodes except the root node, and adjust |
| * them to refer to the destination rather than this temporary path. */ |
| { |
| svn_wc_notify_func2_t old_notify_func2 = ctx->notify_func2; |
| void *old_notify_baton2 = ctx->notify_baton2; |
| struct notification_adjust_baton nb; |
| svn_error_t *err; |
| |
| nb.inner_func = ctx->notify_func2; |
| nb.inner_baton = ctx->notify_baton2; |
| nb.checkout_abspath = tmp_abspath; |
| nb.final_abspath = dst_abspath; |
| ctx->notify_func2 = notification_adjust_func; |
| ctx->notify_baton2 = &nb; |
| |
| err = svn_client__checkout_internal(&pair->src_revnum, |
| pair->src_original, |
| tmp_abspath, |
| &pair->src_peg_revision, |
| &pair->src_op_revision, |
| svn_depth_infinity, |
| ignore_externals, FALSE, |
| &sleep_needed, ctx, pool); |
| |
| ctx->notify_func2 = old_notify_func2; |
| ctx->notify_baton2 = old_notify_baton2; |
| |
| SVN_ERR(err); |
| } |
| |
| *timestamp_sleep = TRUE; |
| |
| /* Schedule dst_path for addition in parent, with copy history. |
| Don't send any notification here. |
| Then remove the temporary checkout's .svn dir in preparation for |
| moving the rest of it into the final destination. */ |
| SVN_ERR(svn_wc_copy3(ctx->wc_ctx, tmp_abspath, dst_abspath, |
| TRUE /* metadata_only */, |
| ctx->cancel_func, ctx->cancel_baton, |
| NULL, NULL, pool)); |
| SVN_ERR(svn_wc__acquire_write_lock(NULL, ctx->wc_ctx, tmp_abspath, |
| FALSE, pool, pool)); |
| SVN_ERR(svn_wc_remove_from_revision_control2(ctx->wc_ctx, |
| tmp_abspath, |
| FALSE, FALSE, |
| ctx->cancel_func, |
| ctx->cancel_baton, |
| pool)); |
| |
| /* Move the temporary disk tree into place. */ |
| SVN_ERR(svn_io_file_rename(tmp_abspath, dst_abspath, pool)); |
| } |
| else |
| { |
| *timestamp_sleep = TRUE; |
| |
| SVN_ERR(svn_client__copy_foreign(pair->src_abspath_or_url, |
| dst_abspath, |
| &pair->src_peg_revision, |
| &pair->src_op_revision, |
| svn_depth_infinity, |
| FALSE /* make_parents */, |
| TRUE /* already_locked */, |
| ctx, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| } /* end directory case */ |
| |
| else if (pair->src_kind == svn_node_file) |
| { |
| apr_hash_t *new_props; |
| const char *src_rel; |
| svn_stream_t *new_base_contents = svn_stream_buffered(pool); |
| |
| SVN_ERR(svn_ra_get_path_relative_to_session(ra_session, &src_rel, |
| pair->src_abspath_or_url, |
| pool)); |
| /* Fetch the file content. While doing so, resolve pair->src_revnum |
| * to an actual revision number if it's 'invalid' meaning 'head'. */ |
| SVN_ERR(svn_ra_get_file(ra_session, src_rel, pair->src_revnum, |
| new_base_contents, |
| &pair->src_revnum, &new_props, pool)); |
| |
| if (new_props && ! same_repositories) |
| svn_hash_sets(new_props, SVN_PROP_MERGEINFO, NULL); |
| |
| *timestamp_sleep = TRUE; |
| |
| SVN_ERR(svn_wc_add_repos_file4( |
| ctx->wc_ctx, dst_abspath, |
| new_base_contents, NULL, new_props, NULL, |
| same_repositories ? pair->src_abspath_or_url : NULL, |
| same_repositories ? pair->src_revnum : SVN_INVALID_REVNUM, |
| ctx->cancel_func, ctx->cancel_baton, |
| pool)); |
| } |
| |
| /* Record the implied mergeinfo (before the notification callback |
| is invoked for the root node). */ |
| SVN_ERR(svn_client__get_repos_mergeinfo( |
| &src_mergeinfo, ra_session, |
| pair->src_abspath_or_url, pair->src_revnum, |
| svn_mergeinfo_inherited, TRUE /*squelch_incapable*/, pool)); |
| SVN_ERR(extend_wc_mergeinfo(dst_abspath, src_mergeinfo, ctx, pool)); |
| |
| /* Do our own notification for the root node, even if we could possibly |
| have delegated it. See also issue #1552. |
| |
| ### Maybe this notification should mention the mergeinfo change. */ |
| if (ctx->notify_func2) |
| { |
| svn_wc_notify_t *notify = svn_wc_create_notify( |
| dst_abspath, svn_wc_notify_add, pool); |
| notify->kind = pair->src_kind; |
| (*ctx->notify_func2)(ctx->notify_baton2, notify, pool); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| repos_to_wc_copy_locked(svn_boolean_t *timestamp_sleep, |
| const apr_array_header_t *copy_pairs, |
| const char *top_dst_path, |
| svn_boolean_t ignore_externals, |
| svn_ra_session_t *ra_session, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *scratch_pool) |
| { |
| int i; |
| svn_boolean_t same_repositories; |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| |
| /* We've already checked for physical obstruction by a working file. |
| But there could also be logical obstruction by an entry whose |
| working file happens to be missing.*/ |
| SVN_ERR(verify_wc_dsts(copy_pairs, FALSE, FALSE, FALSE /* metadata_only */, |
| ctx, scratch_pool, iterpool)); |
| |
| /* Decide whether the two repositories are the same or not. */ |
| { |
| svn_error_t *src_err, *dst_err; |
| const char *parent; |
| const char *parent_abspath; |
| const char *src_uuid, *dst_uuid; |
| |
| /* Get the repository uuid of SRC_URL */ |
| src_err = svn_ra_get_uuid2(ra_session, &src_uuid, iterpool); |
| if (src_err && src_err->apr_err != SVN_ERR_RA_NO_REPOS_UUID) |
| return svn_error_trace(src_err); |
| |
| /* Get repository uuid of dst's parent directory, since dst may |
| not exist. ### TODO: we should probably walk up the wc here, |
| in case the parent dir has an imaginary URL. */ |
| if (copy_pairs->nelts == 1) |
| parent = svn_dirent_dirname(top_dst_path, scratch_pool); |
| else |
| parent = top_dst_path; |
| |
| SVN_ERR(svn_dirent_get_absolute(&parent_abspath, parent, scratch_pool)); |
| dst_err = svn_client_get_repos_root(NULL /* root_url */, &dst_uuid, |
| parent_abspath, ctx, |
| iterpool, iterpool); |
| if (dst_err && dst_err->apr_err != SVN_ERR_RA_NO_REPOS_UUID) |
| return dst_err; |
| |
| /* If either of the UUIDs are nonexistent, then at least one of |
| the repositories must be very old. Rather than punish the |
| user, just assume the repositories are different, so no |
| copy-history is attempted. */ |
| if (src_err || dst_err || (! src_uuid) || (! dst_uuid)) |
| same_repositories = FALSE; |
| else |
| same_repositories = (strcmp(src_uuid, dst_uuid) == 0); |
| } |
| |
| /* Perform the move for each of the copy_pairs. */ |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| /* Check for cancellation */ |
| if (ctx->cancel_func) |
| SVN_ERR(ctx->cancel_func(ctx->cancel_baton)); |
| |
| svn_pool_clear(iterpool); |
| |
| SVN_ERR(repos_to_wc_copy_single(timestamp_sleep, |
| APR_ARRAY_IDX(copy_pairs, i, |
| svn_client__copy_pair_t *), |
| same_repositories, |
| ignore_externals, |
| ra_session, ctx, iterpool)); |
| } |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| static svn_error_t * |
| repos_to_wc_copy(svn_boolean_t *timestamp_sleep, |
| const apr_array_header_t *copy_pairs, |
| svn_boolean_t make_parents, |
| svn_boolean_t ignore_externals, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *pool) |
| { |
| svn_ra_session_t *ra_session; |
| const char *top_src_url, *top_dst_path; |
| apr_pool_t *iterpool = svn_pool_create(pool); |
| const char *lock_abspath; |
| int i; |
| |
| /* Get the real path for the source, based upon its peg revision. */ |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i, |
| svn_client__copy_pair_t *); |
| const char *src; |
| |
| svn_pool_clear(iterpool); |
| |
| SVN_ERR(svn_client__repos_locations(&src, &pair->src_revnum, NULL, NULL, |
| NULL, |
| pair->src_abspath_or_url, |
| &pair->src_peg_revision, |
| &pair->src_op_revision, NULL, |
| ctx, iterpool)); |
| |
| pair->src_original = pair->src_abspath_or_url; |
| pair->src_abspath_or_url = apr_pstrdup(pool, src); |
| } |
| |
| SVN_ERR(get_copy_pair_ancestors(copy_pairs, &top_src_url, &top_dst_path, |
| NULL, pool)); |
| lock_abspath = top_dst_path; |
| if (copy_pairs->nelts == 1) |
| { |
| top_src_url = svn_uri_dirname(top_src_url, pool); |
| lock_abspath = svn_dirent_dirname(top_dst_path, pool); |
| } |
| |
| /* Open a repository session to the longest common src ancestor. We do not |
| (yet) have a working copy, so we don't have a corresponding path and |
| tempfiles cannot go into the admin area. */ |
| SVN_ERR(svn_client_open_ra_session2(&ra_session, top_src_url, lock_abspath, |
| ctx, pool, pool)); |
| |
| /* Get the correct src path for the peg revision used, and verify that we |
| aren't overwriting an existing path. */ |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i, |
| svn_client__copy_pair_t *); |
| svn_node_kind_t dst_parent_kind, dst_kind; |
| const char *dst_parent; |
| const char *src_rel; |
| |
| svn_pool_clear(iterpool); |
| |
| /* Next, make sure that the path exists in the repository. */ |
| SVN_ERR(svn_ra_get_path_relative_to_session(ra_session, &src_rel, |
| pair->src_abspath_or_url, |
| iterpool)); |
| SVN_ERR(svn_ra_check_path(ra_session, src_rel, pair->src_revnum, |
| &pair->src_kind, pool)); |
| if (pair->src_kind == svn_node_none) |
| { |
| if (SVN_IS_VALID_REVNUM(pair->src_revnum)) |
| return svn_error_createf |
| (SVN_ERR_FS_NOT_FOUND, NULL, |
| _("Path '%s' not found in revision %ld"), |
| pair->src_abspath_or_url, pair->src_revnum); |
| else |
| return svn_error_createf |
| (SVN_ERR_FS_NOT_FOUND, NULL, |
| _("Path '%s' not found in head revision"), |
| pair->src_abspath_or_url); |
| } |
| |
| /* Figure out about dst. */ |
| SVN_ERR(svn_io_check_path(pair->dst_abspath_or_url, &dst_kind, |
| iterpool)); |
| if (dst_kind != svn_node_none) |
| { |
| return svn_error_createf( |
| SVN_ERR_ENTRY_EXISTS, NULL, |
| _("Path '%s' already exists"), |
| svn_dirent_local_style(pair->dst_abspath_or_url, pool)); |
| } |
| |
| /* Make sure the destination parent is a directory and produce a clear |
| error message if it is not. */ |
| dst_parent = svn_dirent_dirname(pair->dst_abspath_or_url, iterpool); |
| SVN_ERR(svn_io_check_path(dst_parent, &dst_parent_kind, iterpool)); |
| if (make_parents && dst_parent_kind == svn_node_none) |
| { |
| SVN_ERR(svn_client__make_local_parents(dst_parent, TRUE, ctx, |
| iterpool)); |
| } |
| else if (dst_parent_kind != svn_node_dir) |
| { |
| return svn_error_createf(SVN_ERR_WC_NOT_WORKING_COPY, NULL, |
| _("Path '%s' is not a directory"), |
| svn_dirent_local_style(dst_parent, pool)); |
| } |
| } |
| svn_pool_destroy(iterpool); |
| |
| SVN_WC__CALL_WITH_WRITE_LOCK( |
| repos_to_wc_copy_locked(timestamp_sleep, |
| copy_pairs, top_dst_path, ignore_externals, |
| ra_session, ctx, pool), |
| ctx->wc_ctx, lock_abspath, FALSE, pool); |
| return SVN_NO_ERROR; |
| } |
| |
| #define NEED_REPOS_REVNUM(revision) \ |
| ((revision.kind != svn_opt_revision_unspecified) \ |
| && (revision.kind != svn_opt_revision_working)) |
| |
| /* ... |
| * |
| * Set *TIMESTAMP_SLEEP to TRUE if a sleep is required; otherwise do not |
| * change *TIMESTAMP_SLEEP. This output will be valid even if the |
| * function returns an error. |
| * |
| * Perform all allocations in POOL. |
| */ |
| static svn_error_t * |
| try_copy(svn_boolean_t *timestamp_sleep, |
| const apr_array_header_t *sources, |
| const char *dst_path_in, |
| svn_boolean_t is_move, |
| svn_boolean_t allow_mixed_revisions, |
| svn_boolean_t metadata_only, |
| svn_boolean_t make_parents, |
| svn_boolean_t ignore_externals, |
| const apr_hash_t *revprop_table, |
| svn_commit_callback2_t commit_callback, |
| void *commit_baton, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *pool) |
| { |
| apr_array_header_t *copy_pairs = |
| apr_array_make(pool, sources->nelts, |
| sizeof(svn_client__copy_pair_t *)); |
| svn_boolean_t srcs_are_urls, dst_is_url; |
| int i; |
| |
| /* Are either of our paths URLs? Just check the first src_path. If |
| there are more than one, we'll check for homogeneity among them |
| down below. */ |
| srcs_are_urls = svn_path_is_url(APR_ARRAY_IDX(sources, 0, |
| svn_client_copy_source_t *)->path); |
| dst_is_url = svn_path_is_url(dst_path_in); |
| if (!dst_is_url) |
| SVN_ERR(svn_dirent_get_absolute(&dst_path_in, dst_path_in, pool)); |
| |
| /* If we have multiple source paths, it implies the dst_path is a |
| directory we are moving or copying into. Populate the COPY_PAIRS |
| array to contain a destination path for each of the source paths. */ |
| if (sources->nelts > 1) |
| { |
| apr_pool_t *iterpool = svn_pool_create(pool); |
| |
| for (i = 0; i < sources->nelts; i++) |
| { |
| svn_client_copy_source_t *source = APR_ARRAY_IDX(sources, i, |
| svn_client_copy_source_t *); |
| svn_client__copy_pair_t *pair = apr_palloc(pool, sizeof(*pair)); |
| const char *src_basename; |
| svn_boolean_t src_is_url = svn_path_is_url(source->path); |
| |
| svn_pool_clear(iterpool); |
| |
| if (src_is_url) |
| { |
| pair->src_abspath_or_url = apr_pstrdup(pool, source->path); |
| src_basename = svn_uri_basename(pair->src_abspath_or_url, |
| iterpool); |
| } |
| else |
| { |
| SVN_ERR(svn_dirent_get_absolute(&pair->src_abspath_or_url, |
| source->path, pool)); |
| src_basename = svn_dirent_basename(pair->src_abspath_or_url, |
| iterpool); |
| } |
| |
| pair->src_op_revision = *source->revision; |
| pair->src_peg_revision = *source->peg_revision; |
| |
| SVN_ERR(svn_opt_resolve_revisions(&pair->src_peg_revision, |
| &pair->src_op_revision, |
| src_is_url, |
| TRUE, |
| iterpool)); |
| |
| /* Check to see if all the sources are urls or all working copy |
| * paths. */ |
| if (src_is_url != srcs_are_urls) |
| return svn_error_create |
| (SVN_ERR_UNSUPPORTED_FEATURE, NULL, |
| _("Cannot mix repository and working copy sources")); |
| |
| if (dst_is_url) |
| pair->dst_abspath_or_url = |
| svn_path_url_add_component2(dst_path_in, src_basename, pool); |
| else |
| pair->dst_abspath_or_url = svn_dirent_join(dst_path_in, |
| src_basename, pool); |
| APR_ARRAY_PUSH(copy_pairs, svn_client__copy_pair_t *) = pair; |
| } |
| |
| svn_pool_destroy(iterpool); |
| } |
| else |
| { |
| /* Only one source path. */ |
| svn_client__copy_pair_t *pair = apr_palloc(pool, sizeof(*pair)); |
| svn_client_copy_source_t *source = |
| APR_ARRAY_IDX(sources, 0, svn_client_copy_source_t *); |
| svn_boolean_t src_is_url = svn_path_is_url(source->path); |
| |
| if (src_is_url) |
| pair->src_abspath_or_url = apr_pstrdup(pool, source->path); |
| else |
| SVN_ERR(svn_dirent_get_absolute(&pair->src_abspath_or_url, |
| source->path, pool)); |
| pair->src_op_revision = *source->revision; |
| pair->src_peg_revision = *source->peg_revision; |
| |
| SVN_ERR(svn_opt_resolve_revisions(&pair->src_peg_revision, |
| &pair->src_op_revision, |
| src_is_url, TRUE, pool)); |
| |
| pair->dst_abspath_or_url = dst_path_in; |
| APR_ARRAY_PUSH(copy_pairs, svn_client__copy_pair_t *) = pair; |
| } |
| |
| if (!srcs_are_urls && !dst_is_url) |
| { |
| apr_pool_t *iterpool = svn_pool_create(pool); |
| |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i, |
| svn_client__copy_pair_t *); |
| |
| svn_pool_clear(iterpool); |
| |
| if (svn_dirent_is_child(pair->src_abspath_or_url, |
| pair->dst_abspath_or_url, iterpool)) |
| return svn_error_createf |
| (SVN_ERR_UNSUPPORTED_FEATURE, NULL, |
| _("Cannot copy path '%s' into its own child '%s'"), |
| svn_dirent_local_style(pair->src_abspath_or_url, pool), |
| svn_dirent_local_style(pair->dst_abspath_or_url, pool)); |
| } |
| |
| svn_pool_destroy(iterpool); |
| } |
| |
| /* A file external should not be moved since the file external is |
| implemented as a switched file and it would delete the file the |
| file external is switched to, which is not the behavior the user |
| would probably want. */ |
| if (is_move && !srcs_are_urls) |
| { |
| apr_pool_t *iterpool = svn_pool_create(pool); |
| |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| svn_client__copy_pair_t *pair = |
| APR_ARRAY_IDX(copy_pairs, i, svn_client__copy_pair_t *); |
| svn_node_kind_t external_kind; |
| const char *defining_abspath; |
| |
| svn_pool_clear(iterpool); |
| |
| SVN_ERR_ASSERT(svn_dirent_is_absolute(pair->src_abspath_or_url)); |
| SVN_ERR(svn_wc__read_external_info(&external_kind, &defining_abspath, |
| NULL, NULL, NULL, ctx->wc_ctx, |
| pair->src_abspath_or_url, |
| pair->src_abspath_or_url, TRUE, |
| iterpool, iterpool)); |
| |
| if (external_kind != svn_node_none) |
| return svn_error_createf( |
| SVN_ERR_WC_CANNOT_MOVE_FILE_EXTERNAL, |
| NULL, |
| _("Cannot move the external at '%s'; please " |
| "edit the svn:externals property on '%s'."), |
| svn_dirent_local_style(pair->src_abspath_or_url, pool), |
| svn_dirent_local_style(defining_abspath, pool)); |
| } |
| svn_pool_destroy(iterpool); |
| } |
| |
| if (is_move) |
| { |
| /* Disallow moves between the working copy and the repository. */ |
| if (srcs_are_urls != dst_is_url) |
| { |
| return svn_error_create |
| (SVN_ERR_UNSUPPORTED_FEATURE, NULL, |
| _("Moves between the working copy and the repository are not " |
| "supported")); |
| } |
| |
| /* Disallow moving any path/URL onto or into itself. */ |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i, |
| svn_client__copy_pair_t *); |
| |
| if (strcmp(pair->src_abspath_or_url, |
| pair->dst_abspath_or_url) == 0) |
| return svn_error_createf( |
| SVN_ERR_UNSUPPORTED_FEATURE, NULL, |
| srcs_are_urls ? |
| _("Cannot move URL '%s' into itself") : |
| _("Cannot move path '%s' into itself"), |
| srcs_are_urls ? |
| pair->src_abspath_or_url : |
| svn_dirent_local_style(pair->src_abspath_or_url, pool)); |
| } |
| } |
| else |
| { |
| if (!srcs_are_urls) |
| { |
| /* If we are doing a wc->* copy, but with an operational revision |
| other than the working copy revision, we are really doing a |
| repo->* copy, because we're going to need to get the rev from the |
| repo. */ |
| |
| svn_boolean_t need_repos_op_rev = FALSE; |
| svn_boolean_t need_repos_peg_rev = FALSE; |
| |
| /* Check to see if any revision is something other than |
| svn_opt_revision_unspecified or svn_opt_revision_working. */ |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i, |
| svn_client__copy_pair_t *); |
| |
| if (NEED_REPOS_REVNUM(pair->src_op_revision)) |
| need_repos_op_rev = TRUE; |
| |
| if (NEED_REPOS_REVNUM(pair->src_peg_revision)) |
| need_repos_peg_rev = TRUE; |
| |
| if (need_repos_op_rev || need_repos_peg_rev) |
| break; |
| } |
| |
| if (need_repos_op_rev || need_repos_peg_rev) |
| { |
| apr_pool_t *iterpool = svn_pool_create(pool); |
| |
| for (i = 0; i < copy_pairs->nelts; i++) |
| { |
| const char *copyfrom_repos_root_url; |
| const char *copyfrom_repos_relpath; |
| const char *url; |
| svn_revnum_t copyfrom_rev; |
| svn_client__copy_pair_t *pair = APR_ARRAY_IDX(copy_pairs, i, |
| svn_client__copy_pair_t *); |
| |
| svn_pool_clear(iterpool); |
| |
| SVN_ERR_ASSERT(svn_dirent_is_absolute(pair->src_abspath_or_url)); |
| |
| SVN_ERR(svn_wc__node_get_origin(NULL, ©from_rev, |
| ©from_repos_relpath, |
| ©from_repos_root_url, |
| NULL, NULL, |
| ctx->wc_ctx, |
| pair->src_abspath_or_url, |
| TRUE, iterpool, iterpool)); |
| |
| if (copyfrom_repos_relpath) |
| url = svn_path_url_add_component2(copyfrom_repos_root_url, |
| copyfrom_repos_relpath, |
| pool); |
| else |
| return svn_error_createf |
| (SVN_ERR_ENTRY_MISSING_URL, NULL, |
| _("'%s' does not have a URL associated with it"), |
| svn_dirent_local_style(pair->src_abspath_or_url, pool)); |
| |
| pair->src_abspath_or_url = url; |
| |
| if (!need_repos_peg_rev |
| || pair->src_peg_revision.kind == svn_opt_revision_base) |
| { |
| /* Default the peg revision to that of the WC entry. */ |
| pair->src_peg_revision.kind = svn_opt_revision_number; |
| pair->src_peg_revision.value.number = copyfrom_rev; |
| } |
| |
| if (pair->src_op_revision.kind == svn_opt_revision_base) |
| { |
| /* Use the entry's revision as the operational rev. */ |
| pair->src_op_revision.kind = svn_opt_revision_number; |
| pair->src_op_revision.value.number = copyfrom_rev; |
| } |
| } |
| |
| svn_pool_destroy(iterpool); |
| srcs_are_urls = TRUE; |
| } |
| } |
| } |
| |
| /* Now, call the right handler for the operation. */ |
| if ((! srcs_are_urls) && (! dst_is_url)) |
| { |
| SVN_ERR(verify_wc_srcs_and_dsts(copy_pairs, make_parents, is_move, |
| metadata_only, ctx, pool, pool)); |
| |
| /* Copy or move all targets. */ |
| if (is_move) |
| return svn_error_trace(do_wc_to_wc_moves(timestamp_sleep, |
| copy_pairs, dst_path_in, |
| allow_mixed_revisions, |
| metadata_only, |
| ctx, pool)); |
| else |
| { |
| /* We ignore these values, so assert the default value */ |
| SVN_ERR_ASSERT(allow_mixed_revisions && !metadata_only); |
| return svn_error_trace(do_wc_to_wc_copies(timestamp_sleep, |
| copy_pairs, ctx, pool)); |
| } |
| } |
| else if ((! srcs_are_urls) && (dst_is_url)) |
| { |
| return svn_error_trace( |
| wc_to_repos_copy(copy_pairs, make_parents, revprop_table, |
| commit_callback, commit_baton, ctx, pool)); |
| } |
| else if ((srcs_are_urls) && (! dst_is_url)) |
| { |
| return svn_error_trace( |
| repos_to_wc_copy(timestamp_sleep, |
| copy_pairs, make_parents, ignore_externals, |
| ctx, pool)); |
| } |
| else |
| { |
| return svn_error_trace( |
| repos_to_repos_copy(copy_pairs, make_parents, revprop_table, |
| commit_callback, commit_baton, ctx, is_move, |
| pool)); |
| } |
| } |
| |
| |
| |
| /* Public Interfaces */ |
| svn_error_t * |
| svn_client_copy6(const apr_array_header_t *sources, |
| const char *dst_path, |
| svn_boolean_t copy_as_child, |
| svn_boolean_t make_parents, |
| svn_boolean_t ignore_externals, |
| const apr_hash_t *revprop_table, |
| svn_commit_callback2_t commit_callback, |
| void *commit_baton, |
| svn_client_ctx_t *ctx, |
| apr_pool_t *pool) |
| { |
| svn_error_t *err; |
| svn_boolean_t timestamp_sleep = FALSE; |
| apr_pool_t *subpool = svn_pool_create(pool); |
| |
| if (sources->nelts > 1 && !copy_as_child) |
| return svn_error_create(SVN_ERR_CLIENT_MULTIPLE_SOURCES_DISALLOWED, |
| NULL, NULL); |
| |
| err = try_copy(×tamp_sleep, |
| sources, dst_path, |
| FALSE /* is_move */, |
| TRUE /* allow_mixed_revisions */, |
| FALSE /* metadata_only */, |
| make_parents, |
| ignore_externals, |
| revprop_table, |
| commit_callback, commit_baton, |
| ctx, |
| subpool); |
| |
| /* If the destination exists, try to copy the sources as children of the |
| destination. */ |
| if (copy_as_child && err && (sources->nelts == 1) |
| && (err->apr_err == SVN_ERR_ENTRY_EXISTS |
| || err->apr_err == SVN_ERR_FS_ALREADY_EXISTS)) |
| { |
| const char *src_path = APR_ARRAY_IDX(sources, 0, |
| svn_client_copy_source_t *)->path; |
| const char *src_basename; |
| svn_boolean_t src_is_url = svn_path_is_url(src_path); |
| svn_boolean_t dst_is_url = svn_path_is_url(dst_path); |
| |
| svn_error_clear(err); |
| svn_pool_clear(subpool); |
| |
| src_basename = src_is_url ? svn_uri_basename(src_path, subpool) |
| : svn_dirent_basename(src_path, subpool); |
| dst_path |
| = dst_is_url ? svn_path_url_add_component2(dst_path, src_basename, |
| subpool) |
| : svn_dirent_join(dst_path, src_basename, subpool); |
| |
| err = try_copy(×tamp_sleep, |
| sources, dst_path, |
| FALSE /* is_move */, |
| TRUE /* allow_mixed_revisions */, |
| FALSE /* metadata_only */, |
| make_parents, |
| ignore_externals, |
| revprop_table, |
| commit_callback, commit_baton, |
| ctx, |
| subpool); |
| } |
| |
| /* Sleep if required. DST_PATH is not a URL in these cases. */ |
| if (timestamp_sleep) |
| svn_io_sleep_for_timestamps(dst_path, subpool); |
| |
| svn_pool_destroy(subpool); |
| return svn_error_trace(err); |
| } |
| |
| |
| svn_error_t * |
| svn_client_move7(const apr_array_header_t *src_paths, |
| const char *dst_path, |
| svn_boolean_t move_as_child, |
| svn_boolean_t make_parents, |
| svn_boolean_t allow_mixed_revisions, |
| svn_boolean_t metadata_only, |
| 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_opt_revision_t head_revision |
| = { svn_opt_revision_head, { 0 } }; |
| svn_error_t *err; |
| svn_boolean_t timestamp_sleep = FALSE; |
| int i; |
| apr_pool_t *subpool = svn_pool_create(pool); |
| apr_array_header_t *sources = apr_array_make(pool, src_paths->nelts, |
| sizeof(const svn_client_copy_source_t *)); |
| |
| if (src_paths->nelts > 1 && !move_as_child) |
| return svn_error_create(SVN_ERR_CLIENT_MULTIPLE_SOURCES_DISALLOWED, |
| NULL, NULL); |
| |
| for (i = 0; i < src_paths->nelts; i++) |
| { |
| const char *src_path = APR_ARRAY_IDX(src_paths, i, const char *); |
| svn_client_copy_source_t *copy_source = apr_palloc(pool, |
| sizeof(*copy_source)); |
| |
| copy_source->path = src_path; |
| copy_source->revision = &head_revision; |
| copy_source->peg_revision = &head_revision; |
| |
| APR_ARRAY_PUSH(sources, svn_client_copy_source_t *) = copy_source; |
| } |
| |
| err = try_copy(×tamp_sleep, |
| sources, dst_path, |
| TRUE /* is_move */, |
| allow_mixed_revisions, |
| metadata_only, |
| make_parents, |
| FALSE /* ignore_externals */, |
| revprop_table, |
| commit_callback, commit_baton, |
| ctx, |
| subpool); |
| |
| /* If the destination exists, try to move the sources as children of the |
| destination. */ |
| if (move_as_child && err && (src_paths->nelts == 1) |
| && (err->apr_err == SVN_ERR_ENTRY_EXISTS |
| || err->apr_err == SVN_ERR_FS_ALREADY_EXISTS)) |
| { |
| const char *src_path = APR_ARRAY_IDX(src_paths, 0, const char *); |
| const char *src_basename; |
| svn_boolean_t src_is_url = svn_path_is_url(src_path); |
| svn_boolean_t dst_is_url = svn_path_is_url(dst_path); |
| |
| svn_error_clear(err); |
| svn_pool_clear(subpool); |
| |
| src_basename = src_is_url ? svn_uri_basename(src_path, pool) |
| : svn_dirent_basename(src_path, pool); |
| dst_path |
| = dst_is_url ? svn_path_url_add_component2(dst_path, src_basename, |
| subpool) |
| : svn_dirent_join(dst_path, src_basename, subpool); |
| |
| err = try_copy(×tamp_sleep, |
| sources, dst_path, |
| TRUE /* is_move */, |
| allow_mixed_revisions, |
| metadata_only, |
| make_parents, |
| FALSE /* ignore_externals */, |
| revprop_table, |
| commit_callback, commit_baton, |
| ctx, |
| subpool); |
| } |
| |
| /* Sleep if required. DST_PATH is not a URL in these cases. */ |
| if (timestamp_sleep) |
| svn_io_sleep_for_timestamps(dst_path, subpool); |
| |
| svn_pool_destroy(subpool); |
| return svn_error_trace(err); |
| } |