/* * 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 #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); }