| /* |
| * copy.c: 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_pools.h" |
| #include "svn_error.h" |
| #include "svn_dirent_uri.h" |
| #include "svn_path.h" |
| #include "svn_hash.h" |
| |
| #include "wc.h" |
| #include "workqueue.h" |
| #include "props.h" |
| #include "conflicts.h" |
| #include "textbase.h" |
| |
| #include "svn_private_config.h" |
| #include "private/svn_wc_private.h" |
| |
| /* #define RECORD_MIXED_MOVE */ |
| |
| /*** Code. ***/ |
| |
| /* Make a copy of the filesystem node (or tree if RECURSIVE) at |
| SRC_ABSPATH under a temporary name in the directory |
| TMPDIR_ABSPATH and return the absolute path of the copy in |
| *DST_ABSPATH. Return the node kind of SRC_ABSPATH in *KIND. If |
| SRC_ABSPATH doesn't exist then set *DST_ABSPATH to NULL to indicate |
| that no copy was made. |
| |
| If DIRENT is not NULL, it contains the on-disk information of SRC_ABSPATH. |
| RECORDED_SIZE (if not SVN_INVALID_FILESIZE) contains the recorded size of |
| SRC_ABSPATH, and RECORDED_TIME the recorded size or 0. |
| |
| These values will be used to avoid unneeded work. |
| */ |
| static svn_error_t * |
| copy_to_tmpdir(svn_skel_t **work_item, |
| svn_node_kind_t *kind, |
| svn_wc__db_t *db, |
| const char *src_abspath, |
| const char *dst_abspath, |
| const char *tmpdir_abspath, |
| svn_boolean_t file_copy, |
| svn_boolean_t unversioned, |
| const svn_io_dirent2_t *dirent, |
| svn_filesize_t recorded_size, |
| apr_time_t recorded_time, |
| svn_cancel_func_t cancel_func, |
| void *cancel_baton, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| svn_boolean_t is_special; |
| svn_io_file_del_t delete_when; |
| const char *dst_tmp_abspath; |
| svn_node_kind_t dsk_kind; |
| if (!kind) |
| kind = &dsk_kind; |
| |
| *work_item = NULL; |
| |
| if (dirent) |
| { |
| *kind = dirent->kind; |
| is_special = dirent->special; |
| } |
| else |
| SVN_ERR(svn_io_check_special_path(src_abspath, kind, &is_special, |
| scratch_pool)); |
| if (*kind == svn_node_none) |
| { |
| return SVN_NO_ERROR; |
| } |
| else if (*kind == svn_node_unknown) |
| { |
| return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL, |
| _("Source '%s' is unexpected kind"), |
| svn_dirent_local_style(src_abspath, |
| scratch_pool)); |
| } |
| else if (*kind == svn_node_dir || is_special) |
| delete_when = svn_io_file_del_on_close; |
| else /* the default case: (*kind == svn_node_file) */ |
| delete_when = svn_io_file_del_none; |
| |
| /* ### Do we need a pool cleanup to remove the copy? We can't use |
| ### svn_io_file_del_on_pool_cleanup above because a) it won't |
| ### handle the directory case and b) we need to be able to remove |
| ### the cleanup before queueing the move work item. */ |
| |
| if (file_copy && !unversioned) |
| { |
| svn_boolean_t modified; |
| /* It's faster to look for mods on the source now, as |
| the timestamp might match, than to examine the |
| destination later as the destination timestamp will |
| never match. */ |
| |
| if (dirent |
| && dirent->kind == svn_node_file |
| && recorded_size != SVN_INVALID_FILESIZE |
| && recorded_size == dirent->filesize |
| && recorded_time == dirent->mtime) |
| { |
| modified = FALSE; /* Recorded matches on-disk. Easy out */ |
| } |
| else |
| { |
| SVN_ERR(svn_wc__internal_file_modified_p(&modified, db, src_abspath, |
| FALSE, scratch_pool)); |
| } |
| |
| if (!modified) |
| { |
| const char *install_from; |
| svn_skel_t *cleanup_work_item; |
| |
| SVN_ERR(svn_wc__textbase_setaside_wq(&install_from, |
| &cleanup_work_item, |
| db, src_abspath, NULL, |
| cancel_func, cancel_baton, |
| result_pool, scratch_pool)); |
| SVN_ERR(svn_wc__wq_build_file_install(work_item, db, dst_abspath, |
| install_from, FALSE, TRUE, |
| result_pool, scratch_pool)); |
| *work_item = svn_wc__wq_merge(*work_item, cleanup_work_item, |
| result_pool); |
| |
| return SVN_NO_ERROR; |
| } |
| } |
| else if (*kind == svn_node_dir && !file_copy) |
| { |
| /* Just build a new directory from the workqueue */ |
| SVN_ERR(svn_wc__wq_build_dir_install(work_item, |
| db, dst_abspath, |
| result_pool, scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Set DST_TMP_ABSPATH to a temporary unique path. If *KIND is file, leave |
| a file there and then overwrite it; otherwise leave no node on disk at |
| that path. In the latter case, something else might use that path |
| before we get around to using it a moment later, but never mind. */ |
| SVN_ERR(svn_io_open_unique_file3(NULL, &dst_tmp_abspath, tmpdir_abspath, |
| delete_when, scratch_pool, scratch_pool)); |
| |
| if (*kind == svn_node_dir) |
| { |
| if (file_copy) |
| SVN_ERR(svn_io_copy_dir_recursively( |
| src_abspath, |
| tmpdir_abspath, |
| svn_dirent_basename(dst_tmp_abspath, scratch_pool), |
| TRUE, /* copy_perms */ |
| cancel_func, cancel_baton, |
| scratch_pool)); |
| else |
| SVN_ERR(svn_io_dir_make(dst_tmp_abspath, APR_OS_DEFAULT, scratch_pool)); |
| } |
| else if (!is_special) |
| SVN_ERR(svn_io_copy_file(src_abspath, dst_tmp_abspath, |
| TRUE /* copy_perms */, |
| scratch_pool)); |
| else |
| SVN_ERR(svn_io_copy_link(src_abspath, dst_tmp_abspath, scratch_pool)); |
| |
| if (file_copy) |
| { |
| /* Remove 'read-only' from the destination file; it's a local add now. */ |
| SVN_ERR(svn_io_set_file_read_write(dst_tmp_abspath, |
| FALSE, scratch_pool)); |
| } |
| |
| SVN_ERR(svn_wc__wq_build_file_move(work_item, db, dst_abspath, |
| dst_tmp_abspath, dst_abspath, |
| result_pool, scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Copy the versioned file SRC_ABSPATH in DB to the path DST_ABSPATH in DB. |
| If METADATA_ONLY is true, copy only the versioned metadata, |
| otherwise copy both the versioned metadata and the filesystem node (even |
| if it is the wrong kind, and recursively if it is a dir). |
| |
| If IS_MOVE is true, record move information in working copy meta |
| data in addition to copying the file. |
| |
| WITHIN_ONE_WC is TRUE if the copy/move is within a single working copy (root) |
| |
| If the versioned file has a text conflict, and the .mine file exists in |
| the filesystem, copy the .mine file to DST_ABSPATH. Otherwise, copy the |
| versioned file itself. |
| |
| This also works for versioned symlinks that are stored in the db as |
| svn_node_file with svn:special set. |
| |
| If DIRENT is not NULL, it contains the on-disk information of SRC_ABSPATH. |
| RECORDED_SIZE (if not SVN_INVALID_FILESIZE) contains the recorded size of |
| SRC_ABSPATH, and RECORDED_TIME the recorded size or 0. |
| |
| These values will be used to avoid unneeded work. |
| */ |
| static svn_error_t * |
| copy_versioned_file(svn_wc__db_t *db, |
| const char *src_abspath, |
| const char *dst_abspath, |
| const char *dst_op_root_abspath, |
| const char *tmpdir_abspath, |
| svn_boolean_t metadata_only, |
| svn_boolean_t conflicted, |
| svn_boolean_t is_move, |
| svn_boolean_t within_one_wc, |
| const svn_io_dirent2_t *dirent, |
| svn_filesize_t recorded_size, |
| apr_time_t recorded_time, |
| svn_cancel_func_t cancel_func, |
| void *cancel_baton, |
| svn_wc_notify_func2_t notify_func, |
| void *notify_baton, |
| apr_pool_t *scratch_pool) |
| { |
| svn_skel_t *work_items = NULL; |
| |
| if (within_one_wc) |
| { |
| /* In case we are copying within one WC, it already has the pristine. */ |
| } |
| else |
| { |
| /* In case we are copying from one WC to another (e.g. an external dir), |
| ensure the destination WC has a copy of the pristine text. */ |
| |
| svn_stream_t *contents; |
| |
| SVN_ERR(svn_wc__textbase_get_contents(&contents, db, src_abspath, NULL, |
| TRUE, scratch_pool, scratch_pool)); |
| if (contents) |
| { |
| svn_stream_t *install_stream; |
| svn_wc__db_install_data_t *install_data; |
| svn_checksum_t *install_sha1_checksum; |
| svn_checksum_t *install_md5_checksum; |
| svn_error_t *err; |
| |
| SVN_ERR(svn_wc__textbase_prepare_install(&install_stream, |
| &install_data, |
| &install_sha1_checksum, |
| &install_md5_checksum, |
| db, dst_abspath, FALSE, |
| scratch_pool, scratch_pool)); |
| |
| err = svn_stream_copy3(contents, install_stream, NULL, NULL, scratch_pool); |
| if (err) |
| return svn_error_compose_create(err, |
| svn_wc__db_pristine_install_abort(install_data, scratch_pool)); |
| |
| SVN_ERR(svn_wc__db_pristine_install(install_data, |
| install_sha1_checksum, |
| install_md5_checksum, |
| scratch_pool)); |
| } |
| } |
| |
| /* Prepare a temp copy of the filesystem node. It is usually a file, but |
| copy recursively if it's a dir. */ |
| if (!metadata_only) |
| { |
| const char *my_src_abspath = NULL; |
| svn_boolean_t handle_as_unversioned = FALSE; |
| |
| /* By default, take the copy source as given. */ |
| my_src_abspath = src_abspath; |
| |
| if (conflicted) |
| { |
| svn_skel_t *conflict; |
| const char *conflict_working; |
| svn_error_t *err; |
| |
| /* Is there a text conflict at the source path? */ |
| SVN_ERR(svn_wc__db_read_conflict(&conflict, NULL, NULL, |
| db, src_abspath, |
| scratch_pool, scratch_pool)); |
| |
| err = svn_wc__conflict_read_text_conflict(&conflict_working, NULL, NULL, |
| db, src_abspath, conflict, |
| scratch_pool, |
| scratch_pool); |
| |
| if (err && err->apr_err == SVN_ERR_WC_MISSING) |
| { |
| /* not text conflicted */ |
| svn_error_clear(err); |
| conflict_working = NULL; |
| } |
| else |
| SVN_ERR(err); |
| |
| if (conflict_working) |
| { |
| svn_node_kind_t working_kind; |
| |
| /* Does the ".mine" file exist? */ |
| SVN_ERR(svn_io_check_path(conflict_working, &working_kind, |
| scratch_pool)); |
| |
| if (working_kind == svn_node_file) |
| { |
| /* Don't perform unmodified/pristine optimization */ |
| handle_as_unversioned = TRUE; |
| my_src_abspath = conflict_working; |
| } |
| } |
| } |
| |
| SVN_ERR(copy_to_tmpdir(&work_items, NULL, db, my_src_abspath, |
| dst_abspath, tmpdir_abspath, |
| TRUE /* file_copy */, |
| handle_as_unversioned /* unversioned */, |
| dirent, recorded_size, recorded_time, |
| cancel_func, cancel_baton, |
| scratch_pool, scratch_pool)); |
| } |
| |
| /* Copy the (single) node's metadata, and move the new filesystem node |
| into place. */ |
| SVN_ERR(svn_wc__db_op_copy(db, src_abspath, dst_abspath, |
| dst_op_root_abspath, is_move, work_items, |
| scratch_pool)); |
| |
| if (notify_func) |
| { |
| svn_wc_notify_t *notify |
| = svn_wc_create_notify(dst_abspath, svn_wc_notify_add, |
| scratch_pool); |
| notify->kind = svn_node_file; |
| |
| (*notify_func)(notify_baton, notify, scratch_pool); |
| } |
| return SVN_NO_ERROR; |
| } |
| |
| /* Copy the versioned dir SRC_ABSPATH in DB to the path DST_ABSPATH in DB, |
| recursively. If METADATA_ONLY is true, copy only the versioned metadata, |
| otherwise copy both the versioned metadata and the filesystem nodes (even |
| if they are the wrong kind, and including unversioned children). |
| If IS_MOVE is true, record move information in working copy meta |
| data in addition to copying the directory. |
| |
| WITHIN_ONE_WC is TRUE if the copy/move is within a single working copy (root) |
| |
| If DIRENT is not NULL, it contains the on-disk information of SRC_ABSPATH. |
| */ |
| static svn_error_t * |
| copy_versioned_dir(svn_wc__db_t *db, |
| const char *src_abspath, |
| const char *dst_abspath, |
| const char *dst_op_root_abspath, |
| const char *tmpdir_abspath, |
| svn_boolean_t metadata_only, |
| svn_boolean_t is_move, |
| svn_boolean_t within_one_wc, |
| const svn_io_dirent2_t *dirent, |
| svn_cancel_func_t cancel_func, |
| void *cancel_baton, |
| svn_wc_notify_func2_t notify_func, |
| void *notify_baton, |
| apr_pool_t *scratch_pool) |
| { |
| svn_skel_t *work_items = NULL; |
| const char *dir_abspath = svn_dirent_dirname(dst_abspath, scratch_pool); |
| apr_hash_t *versioned_children; |
| apr_hash_t *conflicted_children; |
| apr_hash_t *disk_children; |
| apr_hash_index_t *hi; |
| svn_node_kind_t disk_kind; |
| apr_pool_t *iterpool; |
| |
| /* Prepare a temp copy of the single filesystem node (usually a dir). */ |
| if (!metadata_only) |
| { |
| SVN_ERR(copy_to_tmpdir(&work_items, &disk_kind, |
| db, src_abspath, dst_abspath, |
| tmpdir_abspath, |
| FALSE /* file_copy */, |
| FALSE /* unversioned */, |
| dirent, SVN_INVALID_FILESIZE, 0, |
| cancel_func, cancel_baton, |
| scratch_pool, scratch_pool)); |
| } |
| |
| /* Copy the (single) node's metadata, and move the new filesystem node |
| into place. */ |
| SVN_ERR(svn_wc__db_op_copy(db, src_abspath, dst_abspath, |
| dst_op_root_abspath, is_move, work_items, |
| scratch_pool)); |
| |
| if (notify_func) |
| { |
| svn_wc_notify_t *notify |
| = svn_wc_create_notify(dst_abspath, svn_wc_notify_add, |
| scratch_pool); |
| notify->kind = svn_node_dir; |
| |
| /* When we notify that we performed a copy, make sure we already did */ |
| if (work_items != NULL) |
| SVN_ERR(svn_wc__wq_run(db, dir_abspath, |
| cancel_func, cancel_baton, scratch_pool)); |
| |
| (*notify_func)(notify_baton, notify, scratch_pool); |
| } |
| |
| if (!metadata_only && disk_kind == svn_node_dir) |
| /* All filesystem children, versioned and unversioned. We're only |
| interested in their names, so we can pass TRUE as the only_check_type |
| param. */ |
| SVN_ERR(svn_io_get_dirents3(&disk_children, src_abspath, TRUE, |
| scratch_pool, scratch_pool)); |
| else |
| disk_children = NULL; |
| |
| /* Copy all the versioned children */ |
| iterpool = svn_pool_create(scratch_pool); |
| SVN_ERR(svn_wc__db_read_children_info(&versioned_children, |
| &conflicted_children, |
| db, src_abspath, |
| FALSE /* base_tree_only */, |
| scratch_pool, iterpool)); |
| for (hi = apr_hash_first(scratch_pool, versioned_children); |
| hi; |
| hi = apr_hash_next(hi)) |
| { |
| const char *child_name, *child_src_abspath, *child_dst_abspath; |
| struct svn_wc__db_info_t *info; |
| |
| svn_pool_clear(iterpool); |
| |
| if (cancel_func) |
| SVN_ERR(cancel_func(cancel_baton)); |
| |
| child_name = apr_hash_this_key(hi); |
| info = apr_hash_this_val(hi); |
| child_src_abspath = svn_dirent_join(src_abspath, child_name, iterpool); |
| child_dst_abspath = svn_dirent_join(dst_abspath, child_name, iterpool); |
| |
| if (info->op_root) |
| SVN_ERR(svn_wc__db_op_copy_shadowed_layer(db, |
| child_src_abspath, |
| child_dst_abspath, |
| is_move, |
| scratch_pool)); |
| |
| if (info->status == svn_wc__db_status_normal |
| || info->status == svn_wc__db_status_added) |
| { |
| /* We have more work to do than just changing the DB */ |
| if (info->kind == svn_node_file) |
| { |
| /* We should skip this node if this child is a file external |
| (issues #3589, #4000) */ |
| if (!info->file_external) |
| SVN_ERR(copy_versioned_file(db, |
| child_src_abspath, |
| child_dst_abspath, |
| dst_op_root_abspath, |
| tmpdir_abspath, |
| metadata_only, info->conflicted, |
| is_move, within_one_wc, |
| disk_children |
| ? svn_hash_gets(disk_children, |
| child_name) |
| : NULL, |
| info->recorded_size, |
| info->recorded_time, |
| cancel_func, cancel_baton, |
| NULL, NULL, |
| iterpool)); |
| } |
| else if (info->kind == svn_node_dir) |
| SVN_ERR(copy_versioned_dir(db, |
| child_src_abspath, child_dst_abspath, |
| dst_op_root_abspath, tmpdir_abspath, |
| metadata_only, is_move, within_one_wc, |
| disk_children |
| ? svn_hash_gets(disk_children, |
| child_name) |
| : NULL, |
| cancel_func, cancel_baton, NULL, NULL, |
| iterpool)); |
| else |
| return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL, |
| _("cannot handle node kind for '%s'"), |
| svn_dirent_local_style(child_src_abspath, |
| scratch_pool)); |
| } |
| else if (info->status == svn_wc__db_status_deleted |
| || info->status == svn_wc__db_status_not_present |
| || info->status == svn_wc__db_status_excluded) |
| { |
| /* This will be copied as some kind of deletion. Don't touch |
| any actual files */ |
| SVN_ERR(svn_wc__db_op_copy(db, child_src_abspath, |
| child_dst_abspath, dst_op_root_abspath, |
| is_move, NULL, iterpool)); |
| |
| /* Don't recurse on children when all we do is creating not-present |
| children */ |
| } |
| else if (info->status == svn_wc__db_status_incomplete) |
| { |
| /* Should go ahead and copy incomplete to incomplete? Try to |
| copy as much as possible, or give up early? */ |
| return svn_error_createf(SVN_ERR_WC_PATH_UNEXPECTED_STATUS, NULL, |
| _("Cannot handle status of '%s'"), |
| svn_dirent_local_style(child_src_abspath, |
| iterpool)); |
| } |
| else |
| { |
| SVN_ERR_ASSERT(info->status == svn_wc__db_status_server_excluded); |
| |
| return svn_error_createf(SVN_ERR_WC_PATH_UNEXPECTED_STATUS, NULL, |
| _("Cannot copy '%s' excluded by server"), |
| svn_dirent_local_style(child_src_abspath, |
| iterpool)); |
| } |
| |
| if (disk_children |
| && (info->status == svn_wc__db_status_normal |
| || info->status == svn_wc__db_status_added)) |
| { |
| /* Remove versioned child as it has been handled */ |
| svn_hash_sets(disk_children, child_name, NULL); |
| } |
| } |
| |
| /* Copy the remaining filesystem children, which are unversioned, skipping |
| any conflict-marker files. */ |
| if (disk_children && apr_hash_count(disk_children)) |
| { |
| apr_hash_t *marker_files; |
| |
| SVN_ERR(svn_wc__db_get_conflict_marker_files(&marker_files, db, |
| src_abspath, scratch_pool, |
| scratch_pool)); |
| |
| work_items = NULL; |
| |
| for (hi = apr_hash_first(scratch_pool, disk_children); hi; |
| hi = apr_hash_next(hi)) |
| { |
| const char *name = apr_hash_this_key(hi); |
| const char *unver_src_abspath, *unver_dst_abspath; |
| svn_skel_t *work_item; |
| |
| if (svn_wc_is_adm_dir(name, iterpool)) |
| continue; |
| |
| if (cancel_func) |
| SVN_ERR(cancel_func(cancel_baton)); |
| |
| svn_pool_clear(iterpool); |
| unver_src_abspath = svn_dirent_join(src_abspath, name, iterpool); |
| unver_dst_abspath = svn_dirent_join(dst_abspath, name, iterpool); |
| |
| if (marker_files && |
| svn_hash_gets(marker_files, unver_src_abspath)) |
| continue; |
| |
| SVN_ERR(copy_to_tmpdir(&work_item, NULL, db, unver_src_abspath, |
| unver_dst_abspath, tmpdir_abspath, |
| TRUE /* recursive */, TRUE /* unversioned */, |
| NULL, SVN_INVALID_FILESIZE, 0, |
| cancel_func, cancel_baton, |
| scratch_pool, iterpool)); |
| |
| if (work_item) |
| work_items = svn_wc__wq_merge(work_items, work_item, scratch_pool); |
| } |
| SVN_ERR(svn_wc__db_wq_add(db, dst_abspath, work_items, iterpool)); |
| } |
| |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* The guts of svn_wc_copy3() and svn_wc_move(). |
| * The additional parameter IS_MOVE indicates whether this is a copy or |
| * a move operation. |
| * |
| * If RECORD_MOVE_ON_DELETE is not NULL and a move had to be degraded |
| * to a copy, then set *RECORD_MOVE_ON_DELETE to FALSE. */ |
| static svn_error_t * |
| copy_or_move(svn_boolean_t *record_move_on_delete, |
| svn_wc_context_t *wc_ctx, |
| const char *src_abspath, |
| const char *dst_abspath, |
| svn_boolean_t metadata_only, |
| svn_boolean_t is_move, |
| svn_boolean_t allow_mixed_revisions, |
| svn_cancel_func_t cancel_func, |
| void *cancel_baton, |
| svn_wc_notify_func2_t notify_func, |
| void *notify_baton, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc__db_t *db = wc_ctx->db; |
| svn_node_kind_t src_db_kind; |
| const char *dstdir_abspath; |
| svn_boolean_t conflicted; |
| const char *tmpdir_abspath; |
| const char *src_wcroot_abspath; |
| const char *dst_wcroot_abspath; |
| svn_boolean_t within_one_wc; |
| svn_wc__db_status_t src_status; |
| svn_error_t *err; |
| svn_filesize_t recorded_size; |
| apr_time_t recorded_time; |
| |
| SVN_ERR_ASSERT(svn_dirent_is_absolute(src_abspath)); |
| SVN_ERR_ASSERT(svn_dirent_is_absolute(dst_abspath)); |
| |
| dstdir_abspath = svn_dirent_dirname(dst_abspath, scratch_pool); |
| |
| /* Ensure DSTDIR_ABSPATH belongs to the same repository as SRC_ABSPATH; |
| throw an error if not. */ |
| { |
| svn_wc__db_status_t dstdir_status; |
| const char *src_repos_root_url, *dst_repos_root_url; |
| const char *src_repos_uuid, *dst_repos_uuid; |
| const char *src_repos_relpath; |
| |
| err = svn_wc__db_read_info(&src_status, &src_db_kind, NULL, |
| &src_repos_relpath, &src_repos_root_url, |
| &src_repos_uuid, NULL, NULL, NULL, NULL, NULL, |
| NULL, NULL, NULL, NULL, NULL, NULL, |
| &recorded_size, &recorded_time, |
| NULL, &conflicted, NULL, NULL, NULL, NULL, |
| NULL, NULL, |
| db, src_abspath, scratch_pool, scratch_pool); |
| |
| if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) |
| { |
| /* Replicate old error code and text */ |
| svn_error_clear(err); |
| return svn_error_createf(SVN_ERR_ENTRY_NOT_FOUND, NULL, |
| _("'%s' is not under version control"), |
| svn_dirent_local_style(src_abspath, |
| scratch_pool)); |
| } |
| else |
| SVN_ERR(err); |
| |
| /* Do this now, as we know the right data is cached */ |
| SVN_ERR(svn_wc__db_get_wcroot(&src_wcroot_abspath, db, src_abspath, |
| scratch_pool, scratch_pool)); |
| |
| switch (src_status) |
| { |
| case svn_wc__db_status_deleted: |
| return svn_error_createf(SVN_ERR_WC_PATH_UNEXPECTED_STATUS, NULL, |
| _("Deleted node '%s' can't be copied."), |
| svn_dirent_local_style(src_abspath, |
| scratch_pool)); |
| |
| case svn_wc__db_status_excluded: |
| case svn_wc__db_status_server_excluded: |
| case svn_wc__db_status_not_present: |
| return svn_error_createf(SVN_ERR_WC_PATH_NOT_FOUND, NULL, |
| _("The node '%s' was not found."), |
| svn_dirent_local_style(src_abspath, |
| scratch_pool)); |
| default: |
| break; |
| } |
| |
| if (is_move && ! strcmp(src_abspath, src_wcroot_abspath)) |
| { |
| return svn_error_createf(SVN_ERR_WC_PATH_UNEXPECTED_STATUS, NULL, |
| _("'%s' is the root of a working copy and " |
| "cannot be moved"), |
| svn_dirent_local_style(src_abspath, |
| scratch_pool)); |
| } |
| if (is_move && src_repos_relpath && !src_repos_relpath[0]) |
| { |
| return svn_error_createf(SVN_ERR_WC_PATH_UNEXPECTED_STATUS, NULL, |
| _("'%s' represents the repository root " |
| "and cannot be moved"), |
| svn_dirent_local_style(src_abspath, |
| scratch_pool)); |
| } |
| |
| err = svn_wc__db_read_info(&dstdir_status, NULL, NULL, NULL, |
| &dst_repos_root_url, &dst_repos_uuid, NULL, |
| NULL, NULL, NULL, NULL, NULL, NULL, NULL, |
| NULL, NULL, NULL, NULL, NULL, NULL, |
| NULL, NULL, NULL, NULL, |
| NULL, NULL, NULL, |
| db, dstdir_abspath, |
| scratch_pool, scratch_pool); |
| |
| if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) |
| { |
| /* An unversioned destination directory exists on disk. */ |
| svn_error_clear(err); |
| return svn_error_createf(SVN_ERR_ENTRY_NOT_FOUND, NULL, |
| _("'%s' is not under version control"), |
| svn_dirent_local_style(dstdir_abspath, |
| scratch_pool)); |
| } |
| else |
| SVN_ERR(err); |
| |
| /* Do this now, as we know the right data is cached */ |
| SVN_ERR(svn_wc__db_get_wcroot(&dst_wcroot_abspath, db, dstdir_abspath, |
| scratch_pool, scratch_pool)); |
| |
| if (!src_repos_root_url) |
| { |
| if (src_status == svn_wc__db_status_added) |
| SVN_ERR(svn_wc__db_scan_addition(NULL, NULL, NULL, |
| &src_repos_root_url, |
| &src_repos_uuid, NULL, NULL, NULL, |
| NULL, |
| db, src_abspath, |
| scratch_pool, scratch_pool)); |
| else |
| /* If not added, the node must have a base or we can't copy */ |
| SVN_ERR(svn_wc__db_base_get_info(NULL, NULL, NULL, NULL, |
| &src_repos_root_url, |
| &src_repos_uuid, NULL, NULL, NULL, |
| NULL, NULL, NULL, NULL, NULL, NULL, |
| NULL, |
| db, src_abspath, |
| scratch_pool, scratch_pool)); |
| } |
| |
| if (!dst_repos_root_url) |
| { |
| if (dstdir_status == svn_wc__db_status_added) |
| SVN_ERR(svn_wc__db_scan_addition(NULL, NULL, NULL, |
| &dst_repos_root_url, |
| &dst_repos_uuid, NULL, NULL, NULL, |
| NULL, |
| db, dstdir_abspath, |
| scratch_pool, scratch_pool)); |
| else |
| /* If not added, the node must have a base or we can't copy */ |
| SVN_ERR(svn_wc__db_base_get_info(NULL, NULL, NULL, NULL, |
| &dst_repos_root_url, |
| &dst_repos_uuid, NULL, NULL, NULL, |
| NULL, NULL, NULL, NULL, NULL, NULL, |
| NULL, |
| db, dstdir_abspath, |
| scratch_pool, scratch_pool)); |
| } |
| |
| if (strcmp(src_repos_root_url, dst_repos_root_url) != 0 |
| || strcmp(src_repos_uuid, dst_repos_uuid) != 0) |
| return svn_error_createf( |
| SVN_ERR_WC_INVALID_SCHEDULE, NULL, |
| _("Cannot copy to '%s', as it is not from repository '%s'; " |
| "it is from '%s'"), |
| svn_dirent_local_style(dst_abspath, scratch_pool), |
| src_repos_root_url, dst_repos_root_url); |
| |
| if (dstdir_status == svn_wc__db_status_deleted) |
| return svn_error_createf( |
| SVN_ERR_WC_INVALID_SCHEDULE, NULL, |
| _("Cannot copy to '%s' as it is scheduled for deletion"), |
| svn_dirent_local_style(dst_abspath, scratch_pool)); |
| /* ### should report dstdir_abspath instead of dst_abspath? */ |
| } |
| |
| /* TODO(#2843): Rework the error report. */ |
| /* Check if the copy target is missing or hidden and thus not exist on the |
| disk, before actually doing the file copy. */ |
| { |
| svn_wc__db_status_t dst_status; |
| |
| err = svn_wc__db_read_info(&dst_status, NULL, NULL, NULL, NULL, NULL, |
| NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, |
| NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, |
| NULL, NULL, NULL, NULL, NULL, |
| db, dst_abspath, scratch_pool, scratch_pool); |
| |
| if (err && err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND) |
| return svn_error_trace(err); |
| |
| svn_error_clear(err); |
| |
| if (!err) |
| switch (dst_status) |
| { |
| case svn_wc__db_status_excluded: |
| return svn_error_createf( |
| SVN_ERR_ENTRY_EXISTS, NULL, |
| _("'%s' is already under version control " |
| "but is excluded."), |
| svn_dirent_local_style(dst_abspath, scratch_pool)); |
| case svn_wc__db_status_server_excluded: |
| return svn_error_createf( |
| SVN_ERR_ENTRY_EXISTS, NULL, |
| _("'%s' is already under version control"), |
| svn_dirent_local_style(dst_abspath, scratch_pool)); |
| |
| case svn_wc__db_status_deleted: |
| case svn_wc__db_status_not_present: |
| break; /* OK to add */ |
| |
| default: |
| if (!metadata_only) |
| return svn_error_createf(SVN_ERR_ENTRY_EXISTS, NULL, |
| _("There is already a versioned item '%s'"), |
| svn_dirent_local_style(dst_abspath, |
| scratch_pool)); |
| } |
| } |
| |
| /* Check that the target path is not obstructed, if required. */ |
| if (!metadata_only) |
| { |
| svn_node_kind_t dst_kind; |
| |
| /* (We need only to check the root of the copy, not every path inside |
| copy_versioned_file/_dir.) */ |
| SVN_ERR(svn_io_check_path(dst_abspath, &dst_kind, scratch_pool)); |
| if (dst_kind != svn_node_none) |
| return svn_error_createf(SVN_ERR_ENTRY_EXISTS, NULL, |
| _("'%s' already exists and is in the way"), |
| svn_dirent_local_style(dst_abspath, |
| scratch_pool)); |
| } |
| |
| SVN_ERR(svn_wc__db_temp_wcroot_tempdir(&tmpdir_abspath, db, |
| dstdir_abspath, |
| scratch_pool, scratch_pool)); |
| |
| within_one_wc = (strcmp(src_wcroot_abspath, dst_wcroot_abspath) == 0); |
| |
| if (is_move |
| && !within_one_wc) |
| { |
| if (record_move_on_delete) |
| *record_move_on_delete = FALSE; |
| |
| is_move = FALSE; |
| } |
| |
| if (src_db_kind == svn_node_file |
| || src_db_kind == svn_node_symlink) |
| { |
| err = copy_versioned_file(db, src_abspath, dst_abspath, dst_abspath, |
| tmpdir_abspath, metadata_only, conflicted, |
| is_move, within_one_wc, |
| NULL, recorded_size, recorded_time, |
| cancel_func, cancel_baton, |
| notify_func, notify_baton, |
| scratch_pool); |
| } |
| else |
| { |
| if (is_move |
| && src_status == svn_wc__db_status_normal) |
| { |
| svn_revnum_t min_rev; |
| svn_revnum_t max_rev; |
| |
| /* Verify that the move source is a single-revision subtree. */ |
| SVN_ERR(svn_wc__db_min_max_revisions(&min_rev, &max_rev, db, |
| src_abspath, FALSE, scratch_pool)); |
| if (SVN_IS_VALID_REVNUM(min_rev) && SVN_IS_VALID_REVNUM(max_rev) && |
| min_rev != max_rev) |
| { |
| if (!allow_mixed_revisions) |
| return svn_error_createf(SVN_ERR_WC_MIXED_REVISIONS, NULL, |
| _("Cannot move mixed-revision " |
| "subtree '%s' [%ld:%ld]; " |
| "try updating it first"), |
| svn_dirent_local_style(src_abspath, |
| scratch_pool), |
| min_rev, max_rev); |
| |
| #ifndef RECORD_MIXED_MOVE |
| is_move = FALSE; |
| if (record_move_on_delete) |
| *record_move_on_delete = FALSE; |
| #endif |
| } |
| } |
| |
| err = copy_versioned_dir(db, src_abspath, dst_abspath, dst_abspath, |
| tmpdir_abspath, metadata_only, is_move, |
| within_one_wc, NULL /* dirent */, |
| cancel_func, cancel_baton, |
| notify_func, notify_baton, |
| scratch_pool); |
| } |
| |
| if (err && svn_error_find_cause(err, SVN_ERR_CANCELLED)) |
| return svn_error_trace(err); |
| |
| if (is_move) |
| err = svn_error_compose_create(err, |
| svn_wc__db_op_handle_move_back(NULL, |
| db, dst_abspath, src_abspath, |
| NULL /* work_items */, |
| scratch_pool)); |
| |
| /* Run the work queue with the remaining work */ |
| SVN_ERR(svn_error_compose_create( |
| err, |
| svn_wc__wq_run(db, dst_abspath, |
| cancel_func, cancel_baton, |
| scratch_pool))); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Public Interface */ |
| |
| svn_error_t * |
| svn_wc_copy3(svn_wc_context_t *wc_ctx, |
| const char *src_abspath, |
| const char *dst_abspath, |
| svn_boolean_t metadata_only, |
| svn_cancel_func_t cancel_func, |
| void *cancel_baton, |
| svn_wc_notify_func2_t notify_func, |
| void *notify_baton, |
| apr_pool_t *scratch_pool) |
| { |
| /* Verify that we have the required write lock. */ |
| SVN_ERR(svn_wc__write_check(wc_ctx->db, |
| svn_dirent_dirname(dst_abspath, scratch_pool), |
| scratch_pool)); |
| |
| return svn_error_trace(copy_or_move(NULL, wc_ctx, src_abspath, dst_abspath, |
| metadata_only, FALSE /* is_move */, |
| TRUE /* allow_mixed_revisions */, |
| cancel_func, cancel_baton, |
| notify_func, notify_baton, |
| scratch_pool)); |
| } |
| |
| |
| /* Remove the conflict markers of NODE_ABSPATH, that were left over after |
| copying NODE_ABSPATH from SRC_ABSPATH. |
| |
| Only use this function when you know what you're doing. This function |
| explicitly ignores some case insensitivity issues! |
| |
| */ |
| static svn_error_t * |
| remove_node_conflict_markers(svn_wc__db_t *db, |
| const char *src_abspath, |
| const char *node_abspath, |
| apr_pool_t *scratch_pool) |
| { |
| svn_skel_t *conflict; |
| |
| SVN_ERR(svn_wc__db_read_conflict(&conflict, NULL, NULL, |
| db, src_abspath, |
| scratch_pool, scratch_pool)); |
| |
| /* Do we have conflict markers that should be removed? */ |
| if (conflict != NULL) |
| { |
| const apr_array_header_t *markers; |
| int i; |
| const char *src_dir = svn_dirent_dirname(src_abspath, scratch_pool); |
| const char *dst_dir = svn_dirent_dirname(node_abspath, scratch_pool); |
| |
| SVN_ERR(svn_wc__conflict_read_markers(&markers, db, src_abspath, |
| conflict, |
| scratch_pool, scratch_pool)); |
| |
| /* No iterpool: Maximum number of possible conflict markers is 4 */ |
| for (i = 0; markers && (i < markers->nelts); i++) |
| { |
| const char *marker_abspath; |
| const char *child_relpath; |
| const char *child_abspath; |
| |
| marker_abspath = APR_ARRAY_IDX(markers, i, const char *); |
| |
| child_relpath = svn_dirent_skip_ancestor(src_dir, marker_abspath); |
| |
| if (child_relpath) |
| { |
| child_abspath = svn_dirent_join(dst_dir, child_relpath, |
| scratch_pool); |
| |
| SVN_ERR(svn_io_remove_file2(child_abspath, TRUE, scratch_pool)); |
| } |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Remove all the conflict markers below SRC_DIR_ABSPATH, that were left over |
| after copying WC_DIR_ABSPATH from SRC_DIR_ABSPATH. |
| |
| This function doesn't remove the conflict markers on WC_DIR_ABSPATH |
| itself! |
| |
| Only use this function when you know what you're doing. This function |
| explicitly ignores some case insensitivity issues! |
| */ |
| static svn_error_t * |
| remove_all_conflict_markers(svn_wc__db_t *db, |
| const char *src_dir_abspath, |
| const char *dst_dir_abspath, |
| svn_cancel_func_t cancel_func, |
| void *cancel_baton, |
| apr_pool_t *scratch_pool) |
| { |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| apr_hash_t *nodes; |
| apr_hash_t *conflicts; /* Unused */ |
| apr_hash_index_t *hi; |
| |
| /* Reuse a status helper to obtain all subdirs and conflicts in a single |
| db transaction. */ |
| /* ### This uses a rifle to kill a fly. But at least it doesn't use heavy |
| artillery. */ |
| SVN_ERR(svn_wc__db_read_children_info(&nodes, &conflicts, db, |
| src_dir_abspath, |
| FALSE /* base_tree_only */, |
| scratch_pool, iterpool)); |
| |
| for (hi = apr_hash_first(scratch_pool, nodes); |
| hi; |
| hi = apr_hash_next(hi)) |
| { |
| const char *name = apr_hash_this_key(hi); |
| struct svn_wc__db_info_t *info = apr_hash_this_val(hi); |
| |
| if (cancel_func) |
| SVN_ERR(cancel_func(cancel_baton)); |
| |
| if (info->conflicted) |
| { |
| svn_pool_clear(iterpool); |
| SVN_ERR(remove_node_conflict_markers( |
| db, |
| svn_dirent_join(src_dir_abspath, name, iterpool), |
| svn_dirent_join(dst_dir_abspath, name, iterpool), |
| iterpool)); |
| } |
| if (info->kind == svn_node_dir) |
| { |
| svn_pool_clear(iterpool); |
| SVN_ERR(remove_all_conflict_markers( |
| db, |
| svn_dirent_join(src_dir_abspath, name, iterpool), |
| svn_dirent_join(dst_dir_abspath, name, iterpool), |
| cancel_func, cancel_baton, |
| iterpool)); |
| } |
| } |
| |
| svn_pool_destroy(iterpool); |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_wc__move2(svn_wc_context_t *wc_ctx, |
| const char *src_abspath, |
| const char *dst_abspath, |
| svn_boolean_t metadata_only, |
| svn_boolean_t allow_mixed_revisions, |
| svn_cancel_func_t cancel_func, |
| void *cancel_baton, |
| svn_wc_notify_func2_t notify_func, |
| void *notify_baton, |
| apr_pool_t *scratch_pool) |
| { |
| svn_wc__db_t *db = wc_ctx->db; |
| svn_boolean_t record_on_delete = TRUE; |
| svn_node_kind_t kind; |
| svn_boolean_t conflicted; |
| |
| /* Verify that we have the required write locks. */ |
| SVN_ERR(svn_wc__write_check(wc_ctx->db, |
| svn_dirent_dirname(src_abspath, scratch_pool), |
| scratch_pool)); |
| SVN_ERR(svn_wc__write_check(wc_ctx->db, |
| svn_dirent_dirname(dst_abspath, scratch_pool), |
| scratch_pool)); |
| |
| SVN_ERR(copy_or_move(&record_on_delete, |
| wc_ctx, src_abspath, dst_abspath, |
| TRUE /* metadata_only */, |
| TRUE /* is_move */, |
| allow_mixed_revisions, |
| cancel_func, cancel_baton, |
| notify_func, notify_baton, |
| scratch_pool)); |
| |
| /* An interrupt at this point will leave the new copy marked as |
| moved-here but the source has not yet been deleted or marked as |
| moved-to. */ |
| |
| /* Should we be using a workqueue for this move? It's not clear. |
| What should happen if the copy above is interrupted? The user |
| may want to abort the move and a workqueue might interfere with |
| that. |
| |
| BH: On Windows it is not unlikely to encounter an access denied on |
| this line. Installing the move in the workqueue via the copy_or_move |
| might make it hard to recover from that situation, while the DB |
| is still in a valid state. So be careful when switching this over |
| to the workqueue. */ |
| if (!metadata_only) |
| { |
| svn_error_t *err; |
| |
| err = svn_error_trace(svn_io_file_rename2(src_abspath, dst_abspath, |
| FALSE, scratch_pool)); |
| |
| /* Let's try if we can keep wc.db consistent even when the move |
| fails. Deleting the target is a wc.db only operation, while |
| going forward (delaying the error) would try to change |
| conflict markers, which might also fail. */ |
| if (err) |
| return svn_error_trace( |
| svn_error_compose_create( |
| err, |
| svn_wc__db_op_delete(wc_ctx->db, dst_abspath, NULL, TRUE, |
| NULL, NULL, cancel_func, cancel_baton, |
| NULL, NULL, |
| scratch_pool))); |
| } |
| |
| SVN_ERR(svn_wc__db_read_info(NULL, &kind, NULL, NULL, NULL, NULL, NULL, |
| NULL, NULL, NULL, NULL, NULL, NULL, NULL, |
| NULL, NULL, NULL, NULL, NULL, NULL, |
| &conflicted, NULL, NULL, NULL, |
| NULL, NULL, NULL, |
| db, src_abspath, |
| scratch_pool, scratch_pool)); |
| |
| if (kind == svn_node_dir) |
| SVN_ERR(remove_all_conflict_markers(db, src_abspath, dst_abspath, |
| cancel_func, cancel_baton, |
| scratch_pool)); |
| |
| if (conflicted) |
| { |
| /* When we moved a directory, we moved the conflict markers |
| with the target... if we moved a file we only moved the |
| file itself and the markers are still in the old location */ |
| SVN_ERR(remove_node_conflict_markers(db, src_abspath, |
| (kind == svn_node_dir) |
| ? dst_abspath |
| : src_abspath, |
| scratch_pool)); |
| } |
| |
| SVN_ERR(svn_wc__db_op_delete(db, src_abspath, |
| record_on_delete ? dst_abspath : NULL, |
| TRUE /* delete_dir_externals */, |
| NULL /* conflict */, NULL /* work_items */, |
| cancel_func, cancel_baton, |
| notify_func, notify_baton, |
| scratch_pool)); |
| |
| return SVN_NO_ERROR; |
| } |