/* * revert.c: Handling of the in-wc side of the revert operation * * ==================================================================== * 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. * ==================================================================== */ #include #include #include #include #include "svn_types.h" #include "svn_pools.h" #include "svn_string.h" #include "svn_error.h" #include "svn_dirent_uri.h" #include "svn_path.h" #include "svn_hash.h" #include "svn_wc.h" #include "svn_io.h" #include "wc.h" #include "adm_files.h" #include "workqueue.h" #include "svn_private_config.h" #include "private/svn_io_private.h" #include "private/svn_wc_private.h" /* Thoughts on Reversion. What does is mean to revert a given PATH in a tree? We'll consider things by their modifications. Adds - For files, svn_wc_remove_from_revision_control(), baby. - Added directories may contain nothing but added children, and reverting the addition of a directory necessarily means reverting the addition of all the directory's children. Again, svn_wc_remove_from_revision_control() should do the trick. Deletes - Restore properties to their unmodified state. - For files, restore the pristine contents, and reset the schedule to 'normal'. - For directories, reset the schedule to 'normal'. All children of a directory marked for deletion must also be marked for deletion, but it's okay for those children to remain deleted even if their parent directory is restored. That's what the recursive flag is for. Replaces - Restore properties to their unmodified state. - For files, restore the pristine contents, and reset the schedule to 'normal'. - For directories, reset the schedule to normal. A replaced directory can have deleted children (left over from the initial deletion), replaced children (children of the initial deletion now re-added), and added children (new entries under the replaced directory). Since this is technically an addition, it necessitates recursion. Modifications - Restore properties and, for files, contents to their unmodified state. */ /* Remove conflict file CONFLICT_ABSPATH, which may not exist, and set * *NOTIFY_REQUIRED to TRUE if the file was present and removed. */ static svn_error_t * remove_conflict_file(svn_boolean_t *notify_required, const char *conflict_abspath, const char *local_abspath, apr_pool_t *scratch_pool) { if (conflict_abspath) { svn_error_t *err = svn_io_remove_file2(conflict_abspath, FALSE, scratch_pool); if (err) svn_error_clear(err); else *notify_required = TRUE; } return SVN_NO_ERROR; } /* Sort copied children obtained from the revert list based on * their paths in descending order (longest paths first). */ static int compare_revert_list_copied_children(const void *a, const void *b) { const svn_wc__db_revert_list_copied_child_info_t * const *ca = a; const svn_wc__db_revert_list_copied_child_info_t * const *cb = b; int i; i = svn_path_compare_paths(ca[0]->abspath, cb[0]->abspath); /* Reverse the result of svn_path_compare_paths() to achieve * descending order. */ return -i; } /* Remove all reverted copied children from the directory at LOCAL_ABSPATH. * If REMOVE_SELF is TRUE, try to remove LOCAL_ABSPATH itself (REMOVE_SELF * should be set if LOCAL_ABSPATH is itself a reverted copy). * * If REMOVED_SELF is not NULL, indicate in *REMOVED_SELF whether * LOCAL_ABSPATH itself was removed. * * All reverted copied file children are removed from disk. Reverted copied * directories left empty as a result are also removed from disk. */ static svn_error_t * revert_restore_handle_copied_dirs(svn_boolean_t *removed_self, svn_wc__db_t *db, const char *local_abspath, svn_boolean_t remove_self, svn_cancel_func_t cancel_func, void *cancel_baton, apr_pool_t *scratch_pool) { const apr_array_header_t *copied_children; svn_wc__db_revert_list_copied_child_info_t *child_info; int i; svn_node_kind_t on_disk; apr_pool_t *iterpool; svn_error_t *err; if (removed_self) *removed_self = FALSE; SVN_ERR(svn_wc__db_revert_list_read_copied_children(&copied_children, db, local_abspath, scratch_pool, scratch_pool)); iterpool = svn_pool_create(scratch_pool); /* Remove all copied file children. */ for (i = 0; i < copied_children->nelts; i++) { child_info = APR_ARRAY_IDX( copied_children, i, svn_wc__db_revert_list_copied_child_info_t *); if (cancel_func) SVN_ERR(cancel_func(cancel_baton)); if (child_info->kind != svn_node_file) continue; svn_pool_clear(iterpool); /* Make sure what we delete from disk is really a file. */ SVN_ERR(svn_io_check_path(child_info->abspath, &on_disk, iterpool)); if (on_disk != svn_node_file) continue; SVN_ERR(svn_io_remove_file2(child_info->abspath, TRUE, iterpool)); } /* Delete every empty child directory. * We cannot delete children recursively since we want to keep any files * that still exist on disk (e.g. unversioned files within the copied tree). * So sort the children list such that longest paths come first and try to * remove each child directory in order. */ qsort(copied_children->elts, copied_children->nelts, sizeof(svn_wc__db_revert_list_copied_child_info_t *), compare_revert_list_copied_children); for (i = 0; i < copied_children->nelts; i++) { child_info = APR_ARRAY_IDX( copied_children, i, svn_wc__db_revert_list_copied_child_info_t *); if (cancel_func) SVN_ERR(cancel_func(cancel_baton)); if (child_info->kind != svn_node_dir) continue; svn_pool_clear(iterpool); err = svn_io_dir_remove_nonrecursive(child_info->abspath, iterpool); if (err) { if (APR_STATUS_IS_ENOENT(err->apr_err) || SVN__APR_STATUS_IS_ENOTDIR(err->apr_err) || APR_STATUS_IS_ENOTEMPTY(err->apr_err)) svn_error_clear(err); else return svn_error_trace(err); } } if (remove_self) { /* Delete LOCAL_ABSPATH itself if no children are left. */ err = svn_io_dir_remove_nonrecursive(local_abspath, iterpool); if (err) { if (APR_STATUS_IS_ENOTEMPTY(err->apr_err)) svn_error_clear(err); else return svn_error_trace(err); } else if (removed_self) *removed_self = TRUE; } svn_pool_destroy(iterpool); return SVN_NO_ERROR; } /* Make the working tree under LOCAL_ABSPATH to depth DEPTH match the versioned tree. This function is called after svn_wc__db_op_revert has done the database revert and created the revert list. Notifies for all paths equal to or below LOCAL_ABSPATH that are reverted. REVERT_ROOT is true for explicit revert targets and FALSE for targets reached via recursion. */ static svn_error_t * revert_restore(svn_wc__db_t *db, const char *local_abspath, svn_depth_t depth, svn_boolean_t use_commit_times, svn_boolean_t revert_root, 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_error_t *err; svn_wc__db_status_t status; svn_node_kind_t kind; svn_node_kind_t on_disk; svn_boolean_t notify_required; const apr_array_header_t *conflict_files; svn_filesize_t recorded_size; apr_time_t recorded_time; apr_finfo_t finfo; #ifdef HAVE_SYMLINK svn_boolean_t special; #endif svn_boolean_t copied_here; svn_node_kind_t reverted_kind; svn_boolean_t is_wcroot; if (cancel_func) SVN_ERR(cancel_func(cancel_baton)); SVN_ERR(svn_wc__db_is_wcroot(&is_wcroot, db, local_abspath, scratch_pool)); if (is_wcroot && !revert_root) { /* Issue #4162: Obstructing working copy. We can't access the working copy data from the parent working copy for this node by just using local_abspath */ if (notify_func) { svn_wc_notify_t *notify = svn_wc_create_notify( local_abspath, svn_wc_notify_update_skip_obstruction, scratch_pool); notify_func(notify_baton, notify, scratch_pool); } return SVN_NO_ERROR; /* We don't revert obstructing working copies */ } SVN_ERR(svn_wc__db_revert_list_read(¬ify_required, &conflict_files, &copied_here, &reverted_kind, db, local_abspath, scratch_pool, scratch_pool)); err = svn_wc__db_read_info(&status, &kind, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &recorded_size, &recorded_time, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, db, local_abspath, scratch_pool, scratch_pool); if (err && err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND) { svn_error_clear(err); if (!copied_here) { if (notify_func && notify_required) notify_func(notify_baton, svn_wc_create_notify(local_abspath, svn_wc_notify_revert, scratch_pool), scratch_pool); if (notify_func) SVN_ERR(svn_wc__db_revert_list_notify(notify_func, notify_baton, db, local_abspath, scratch_pool)); return SVN_NO_ERROR; } else { /* ### Initialise to values which prevent the code below from * ### trying to restore anything to disk. * ### 'status' should be status_unknown but that doesn't exist. */ status = svn_wc__db_status_normal; kind = svn_node_unknown; recorded_size = SVN_INVALID_FILESIZE; recorded_time = 0; } } else if (err) return svn_error_trace(err); err = svn_io_stat(&finfo, local_abspath, APR_FINFO_TYPE | APR_FINFO_LINK | APR_FINFO_SIZE | APR_FINFO_MTIME | SVN__APR_FINFO_EXECUTABLE | SVN__APR_FINFO_READONLY, scratch_pool); if (err && (APR_STATUS_IS_ENOENT(err->apr_err) || SVN__APR_STATUS_IS_ENOTDIR(err->apr_err))) { svn_error_clear(err); on_disk = svn_node_none; #ifdef HAVE_SYMLINK special = FALSE; #endif } else if (!err) { if (finfo.filetype == APR_REG || finfo.filetype == APR_LNK) on_disk = svn_node_file; else if (finfo.filetype == APR_DIR) on_disk = svn_node_dir; else on_disk = svn_node_unknown; #ifdef HAVE_SYMLINK special = (finfo.filetype == APR_LNK); #endif } else return svn_error_trace(err); if (copied_here) { /* The revert target itself is the op-root of a copy. */ if (reverted_kind == svn_node_file && on_disk == svn_node_file) { SVN_ERR(svn_io_remove_file2(local_abspath, TRUE, scratch_pool)); on_disk = svn_node_none; } else if (reverted_kind == svn_node_dir && on_disk == svn_node_dir) { svn_boolean_t removed; SVN_ERR(revert_restore_handle_copied_dirs(&removed, db, local_abspath, TRUE, cancel_func, cancel_baton, scratch_pool)); if (removed) on_disk = svn_node_none; } } /* If we expect a versioned item to be present then check that any item on disk matches the versioned item, if it doesn't match then fix it or delete it. */ if (on_disk != svn_node_none && status != svn_wc__db_status_server_excluded && status != svn_wc__db_status_deleted && status != svn_wc__db_status_excluded && status != svn_wc__db_status_not_present) { if (on_disk == svn_node_dir && kind != svn_node_dir) { SVN_ERR(svn_io_remove_dir2(local_abspath, FALSE, cancel_func, cancel_baton, scratch_pool)); on_disk = svn_node_none; } else if (on_disk == svn_node_file && kind != svn_node_file) { #ifdef HAVE_SYMLINK /* Preserve symlinks pointing at directories. Changes on the * directory node have been reverted. The symlink should remain. */ if (!(special && kind == svn_node_dir)) #endif { SVN_ERR(svn_io_remove_file2(local_abspath, FALSE, scratch_pool)); on_disk = svn_node_none; } } else if (on_disk == svn_node_file) { svn_boolean_t modified; apr_hash_t *props; #ifdef HAVE_SYMLINK svn_string_t *special_prop; #endif SVN_ERR(svn_wc__db_read_pristine_props(&props, db, local_abspath, scratch_pool, scratch_pool)); #ifdef HAVE_SYMLINK special_prop = svn_hash_gets(props, SVN_PROP_SPECIAL); if ((special_prop != NULL) != special) { /* File/symlink mismatch. */ SVN_ERR(svn_io_remove_file2(local_abspath, FALSE, scratch_pool)); on_disk = svn_node_none; } else #endif { /* Issue #1663 asserts that we should compare a file in its working copy format here, but before r1101473 we would only do that if the file was already unequal to its recorded information. r1101473 removes the option of asking for a working format compare but *also* check the recorded information first, as that combination doesn't guarantee a stable behavior. (See the revert_test.py: revert_reexpand_keyword) But to have the same issue #1663 behavior for revert as we had in <=1.6 we only have to check the recorded information ourselves. And we already have everything we need, because we called stat ourselves. */ if (recorded_size != SVN_INVALID_FILESIZE && recorded_time != 0 && recorded_size == finfo.size && recorded_time == finfo.mtime) { modified = FALSE; } else SVN_ERR(svn_wc__internal_file_modified_p(&modified, db, local_abspath, TRUE, scratch_pool)); if (modified) { SVN_ERR(svn_io_remove_file2(local_abspath, FALSE, scratch_pool)); on_disk = svn_node_none; } else { if (status == svn_wc__db_status_normal) { svn_boolean_t read_only; svn_string_t *needs_lock_prop; SVN_ERR(svn_io__is_finfo_read_only(&read_only, &finfo, scratch_pool)); needs_lock_prop = svn_hash_gets(props, SVN_PROP_NEEDS_LOCK); if (needs_lock_prop && !read_only) { SVN_ERR(svn_io_set_file_read_only(local_abspath, FALSE, scratch_pool)); notify_required = TRUE; } else if (!needs_lock_prop && read_only) { SVN_ERR(svn_io_set_file_read_write(local_abspath, FALSE, scratch_pool)); notify_required = TRUE; } } #if !defined(WIN32) && !defined(__OS2__) #ifdef HAVE_SYMLINK if (!special) #endif { svn_boolean_t executable; svn_string_t *executable_prop; SVN_ERR(svn_io__is_finfo_executable(&executable, &finfo, scratch_pool)); executable_prop = svn_hash_gets(props, SVN_PROP_EXECUTABLE); if (executable_prop && !executable) { SVN_ERR(svn_io_set_file_executable(local_abspath, TRUE, FALSE, scratch_pool)); notify_required = TRUE; } else if (!executable_prop && executable) { SVN_ERR(svn_io_set_file_executable(local_abspath, FALSE, FALSE, scratch_pool)); notify_required = TRUE; } } #endif } } } } /* If we expect a versioned item to be present and there is nothing on disk then recreate it. */ if (on_disk == svn_node_none && status != svn_wc__db_status_server_excluded && status != svn_wc__db_status_deleted && status != svn_wc__db_status_excluded && status != svn_wc__db_status_not_present) { if (kind == svn_node_dir) SVN_ERR(svn_io_dir_make(local_abspath, APR_OS_DEFAULT, scratch_pool)); if (kind == svn_node_file) { svn_skel_t *work_item; /* ### Get the checksum from read_info above and pass in here? */ SVN_ERR(svn_wc__wq_build_file_install(&work_item, db, local_abspath, NULL, use_commit_times, TRUE, scratch_pool, scratch_pool)); SVN_ERR(svn_wc__db_wq_add(db, local_abspath, work_item, scratch_pool)); SVN_ERR(svn_wc__wq_run(db, local_abspath, cancel_func, cancel_baton, scratch_pool)); } notify_required = TRUE; } if (conflict_files) { int i; for (i = 0; i < conflict_files->nelts; i++) { SVN_ERR(remove_conflict_file(¬ify_required, APR_ARRAY_IDX(conflict_files, i, const char *), local_abspath, scratch_pool)); } } if (notify_func && notify_required) notify_func(notify_baton, svn_wc_create_notify(local_abspath, svn_wc_notify_revert, scratch_pool), scratch_pool); if (depth == svn_depth_infinity && kind == svn_node_dir) { apr_pool_t *iterpool = svn_pool_create(scratch_pool); const apr_array_header_t *children; int i; SVN_ERR(revert_restore_handle_copied_dirs(NULL, db, local_abspath, FALSE, cancel_func, cancel_baton, iterpool)); SVN_ERR(svn_wc__db_read_children_of_working_node(&children, db, local_abspath, scratch_pool, iterpool)); for (i = 0; i < children->nelts; ++i) { const char *child_abspath; svn_pool_clear(iterpool); child_abspath = svn_dirent_join(local_abspath, APR_ARRAY_IDX(children, i, const char *), iterpool); SVN_ERR(revert_restore(db, child_abspath, depth, use_commit_times, FALSE /* revert root */, cancel_func, cancel_baton, notify_func, notify_baton, iterpool)); } svn_pool_destroy(iterpool); } if (notify_func) SVN_ERR(svn_wc__db_revert_list_notify(notify_func, notify_baton, db, local_abspath, scratch_pool)); return SVN_NO_ERROR; } svn_error_t * svn_wc__revert_internal(svn_wc__db_t *db, const char *local_abspath, svn_depth_t depth, svn_boolean_t use_commit_times, 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_error_t *err; SVN_ERR_ASSERT(depth == svn_depth_empty || depth == svn_depth_infinity); /* We should have a write lock on the parent of local_abspath, except when local_abspath is the working copy root. */ { const char *dir_abspath; svn_boolean_t is_wcroot; SVN_ERR(svn_wc__db_is_wcroot(&is_wcroot, db, local_abspath, scratch_pool)); if (! is_wcroot) dir_abspath = svn_dirent_dirname(local_abspath, scratch_pool); else dir_abspath = local_abspath; SVN_ERR(svn_wc__write_check(db, dir_abspath, scratch_pool)); } err = svn_wc__db_op_revert(db, local_abspath, depth, scratch_pool, scratch_pool); if (!err) err = revert_restore(db, local_abspath, depth, use_commit_times, TRUE /* revert root */, cancel_func, cancel_baton, notify_func, notify_baton, scratch_pool); err = svn_error_compose_create(err, svn_wc__db_revert_list_done(db, local_abspath, scratch_pool)); return err; } /* Revert files in LOCAL_ABSPATH to depth DEPTH that match CHANGELIST_HASH and notify for all reverts. */ static svn_error_t * revert_changelist(svn_wc__db_t *db, const char *local_abspath, svn_depth_t depth, svn_boolean_t use_commit_times, apr_hash_t *changelist_hash, svn_cancel_func_t cancel_func, void *cancel_baton, svn_wc_notify_func2_t notify_func, void *notify_baton, apr_pool_t *scratch_pool) { apr_pool_t *iterpool; const apr_array_header_t *children; int i; if (cancel_func) SVN_ERR(cancel_func(cancel_baton)); /* Revert this node (depth=empty) if it matches one of the changelists. */ if (svn_wc__internal_changelist_match(db, local_abspath, changelist_hash, scratch_pool)) SVN_ERR(svn_wc__revert_internal(db, local_abspath, svn_depth_empty, use_commit_times, cancel_func, cancel_baton, notify_func, notify_baton, scratch_pool)); if (depth == svn_depth_empty) return SVN_NO_ERROR; iterpool = svn_pool_create(scratch_pool); /* We can handle both depth=files and depth=immediates by setting depth=empty here. We don't need to distinguish files and directories when making the recursive call because directories can never match a changelist, so making the recursive call for directories when asked for depth=files is a no-op. */ if (depth == svn_depth_files || depth == svn_depth_immediates) depth = svn_depth_empty; SVN_ERR(svn_wc__db_read_children_of_working_node(&children, db, local_abspath, scratch_pool, iterpool)); for (i = 0; i < children->nelts; ++i) { const char *child_abspath; svn_pool_clear(iterpool); child_abspath = svn_dirent_join(local_abspath, APR_ARRAY_IDX(children, i, const char *), iterpool); SVN_ERR(revert_changelist(db, child_abspath, depth, use_commit_times, changelist_hash, cancel_func, cancel_baton, notify_func, notify_baton, iterpool)); } svn_pool_destroy(iterpool); return SVN_NO_ERROR; } /* Does a partially recursive revert of LOCAL_ABSPATH to depth DEPTH (which must be either svn_depth_files or svn_depth_immediates) by doing a non-recursive revert on each permissible path. Notifies all reverted paths. ### This won't revert a copied dir with one level of children since ### the non-recursive revert on the dir will fail. Not sure how a ### partially recursive revert should handle actual-only nodes. */ static svn_error_t * revert_partial(svn_wc__db_t *db, const char *local_abspath, svn_depth_t depth, svn_boolean_t use_commit_times, svn_cancel_func_t cancel_func, void *cancel_baton, svn_wc_notify_func2_t notify_func, void *notify_baton, apr_pool_t *scratch_pool) { apr_pool_t *iterpool; const apr_array_header_t *children; int i; SVN_ERR_ASSERT(depth == svn_depth_files || depth == svn_depth_immediates); if (cancel_func) SVN_ERR(cancel_func(cancel_baton)); iterpool = svn_pool_create(scratch_pool); /* Revert the root node itself (depth=empty), then move on to the children. */ SVN_ERR(svn_wc__revert_internal(db, local_abspath, svn_depth_empty, use_commit_times, cancel_func, cancel_baton, notify_func, notify_baton, iterpool)); SVN_ERR(svn_wc__db_read_children_of_working_node(&children, db, local_abspath, scratch_pool, iterpool)); for (i = 0; i < children->nelts; ++i) { const char *child_abspath; svn_pool_clear(iterpool); child_abspath = svn_dirent_join(local_abspath, APR_ARRAY_IDX(children, i, const char *), iterpool); /* For svn_depth_files: don't revert non-files. */ if (depth == svn_depth_files) { svn_node_kind_t kind; SVN_ERR(svn_wc__db_read_kind(&kind, db, child_abspath, FALSE /* allow_missing */, TRUE /* show_deleted */, FALSE /* show_hidden */, iterpool)); if (kind != svn_node_file) continue; } /* Revert just this node (depth=empty). */ SVN_ERR(svn_wc__revert_internal(db, child_abspath, svn_depth_empty, use_commit_times, cancel_func, cancel_baton, notify_func, notify_baton, iterpool)); } svn_pool_destroy(iterpool); return SVN_NO_ERROR; } svn_error_t * svn_wc_revert4(svn_wc_context_t *wc_ctx, const char *local_abspath, svn_depth_t depth, svn_boolean_t use_commit_times, const apr_array_header_t *changelist_filter, svn_cancel_func_t cancel_func, void *cancel_baton, svn_wc_notify_func2_t notify_func, void *notify_baton, apr_pool_t *scratch_pool) { if (changelist_filter && changelist_filter->nelts) { apr_hash_t *changelist_hash; SVN_ERR(svn_hash_from_cstring_keys(&changelist_hash, changelist_filter, scratch_pool)); return svn_error_trace(revert_changelist(wc_ctx->db, local_abspath, depth, use_commit_times, changelist_hash, cancel_func, cancel_baton, notify_func, notify_baton, scratch_pool)); } if (depth == svn_depth_empty || depth == svn_depth_infinity) return svn_error_trace(svn_wc__revert_internal(wc_ctx->db, local_abspath, depth, use_commit_times, cancel_func, cancel_baton, notify_func, notify_baton, scratch_pool)); /* The user may expect svn_depth_files/svn_depth_immediates to work on copied dirs with one level of children. It doesn't, the user will get an error and will need to invoke an infinite revert. If we identified those cases where svn_depth_infinity would not revert too much we could invoke the recursive call above. */ if (depth == svn_depth_files || depth == svn_depth_immediates) return svn_error_trace(revert_partial(wc_ctx->db, local_abspath, depth, use_commit_times, cancel_func, cancel_baton, notify_func, notify_baton, scratch_pool)); /* Bogus depth. Tell the caller. */ return svn_error_create(SVN_ERR_WC_INVALID_OPERATION_DEPTH, NULL, NULL); }