diff options
author | peter <peter@FreeBSD.org> | 2013-06-18 02:07:41 +0000 |
---|---|---|
committer | peter <peter@FreeBSD.org> | 2013-06-18 02:07:41 +0000 |
commit | d25dac7fcc6acc838b71bbda8916fd9665c709ab (patch) | |
tree | 135691142dc0e75a5e5d97b5074d03436435b8e0 /subversion/libsvn_client/copy.c | |
download | FreeBSD-src-d25dac7fcc6acc838b71bbda8916fd9665c709ab.zip FreeBSD-src-d25dac7fcc6acc838b71bbda8916fd9665c709ab.tar.gz |
Import trimmed svn-1.8.0-rc3
Diffstat (limited to 'subversion/libsvn_client/copy.c')
-rw-r--r-- | subversion/libsvn_client/copy.c | 2422 |
1 files changed, 2422 insertions, 0 deletions
diff --git a/subversion/libsvn_client/copy.c b/subversion/libsvn_client/copy.c new file mode 100644 index 0000000..c0501b9 --- /dev/null +++ b/subversion/libsvn_client/copy.c @@ -0,0 +1,2422 @@ +/* + * 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; + + 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); + + /* 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, + iterpool)) + { + lock_src = TRUE; + lock_dst = FALSE; + } + else if (svn_dirent_is_child(pair->dst_parent_abspath, + src_parent_abspath, + iterpool)) + { + 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_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 */ + 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_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, 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_dirname(top_url, 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, 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, + 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); +} |