delete.c revision 269847
1/* 2 * delete.c: wrappers around wc delete functionality. 3 * 4 * ==================================================================== 5 * Licensed to the Apache Software Foundation (ASF) under one 6 * or more contributor license agreements. See the NOTICE file 7 * distributed with this work for additional information 8 * regarding copyright ownership. The ASF licenses this file 9 * to you under the Apache License, Version 2.0 (the 10 * "License"); you may not use this file except in compliance 11 * with the License. You may obtain a copy of the License at 12 * 13 * http://www.apache.org/licenses/LICENSE-2.0 14 * 15 * Unless required by applicable law or agreed to in writing, 16 * software distributed under the License is distributed on an 17 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 * KIND, either express or implied. See the License for the 19 * specific language governing permissions and limitations 20 * under the License. 21 * ==================================================================== 22 */ 23 24/* ==================================================================== */ 25 26 27 28/*** Includes. ***/ 29 30#include <apr_file_io.h> 31#include "svn_hash.h" 32#include "svn_types.h" 33#include "svn_pools.h" 34#include "svn_wc.h" 35#include "svn_client.h" 36#include "svn_error.h" 37#include "svn_dirent_uri.h" 38#include "svn_path.h" 39#include "client.h" 40 41#include "private/svn_client_private.h" 42#include "private/svn_wc_private.h" 43#include "private/svn_ra_private.h" 44 45#include "svn_private_config.h" 46 47 48/*** Code. ***/ 49 50/* Baton for find_undeletables */ 51struct can_delete_baton_t 52{ 53 const char *root_abspath; 54 svn_boolean_t target_missing; 55}; 56 57/* An svn_wc_status_func4_t callback function for finding 58 status structures which are not safely deletable. */ 59static svn_error_t * 60find_undeletables(void *baton, 61 const char *local_abspath, 62 const svn_wc_status3_t *status, 63 apr_pool_t *pool) 64{ 65 if (status->node_status == svn_wc_status_missing) 66 { 67 struct can_delete_baton_t *cdt = baton; 68 69 if (strcmp(cdt->root_abspath, local_abspath) == 0) 70 cdt->target_missing = TRUE; 71 } 72 73 /* Check for error-ful states. */ 74 if (status->node_status == svn_wc_status_obstructed) 75 return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL, 76 _("'%s' is in the way of the resource " 77 "actually under version control"), 78 svn_dirent_local_style(local_abspath, pool)); 79 else if (! status->versioned) 80 return svn_error_createf(SVN_ERR_UNVERSIONED_RESOURCE, NULL, 81 _("'%s' is not under version control"), 82 svn_dirent_local_style(local_abspath, pool)); 83 else if ((status->node_status == svn_wc_status_added 84 || status->node_status == svn_wc_status_replaced) 85 && status->text_status == svn_wc_status_normal 86 && (status->prop_status == svn_wc_status_normal 87 || status->prop_status == svn_wc_status_none)) 88 { 89 /* Unmodified copy. Go ahead, remove it */ 90 } 91 else if (status->node_status != svn_wc_status_normal 92 && status->node_status != svn_wc_status_deleted 93 && status->node_status != svn_wc_status_missing) 94 return svn_error_createf(SVN_ERR_CLIENT_MODIFIED, NULL, 95 _("'%s' has local modifications -- commit or " 96 "revert them first"), 97 svn_dirent_local_style(local_abspath, pool)); 98 99 return SVN_NO_ERROR; 100} 101 102/* Check whether LOCAL_ABSPATH is an external and raise an error if it is. 103 104 A file external should not be deleted since the file external is 105 implemented as a switched file and it would delete the file the 106 file external is switched to, which is not the behavior the user 107 would probably want. 108 109 A directory external should not be deleted since it is the root 110 of a different working copy. */ 111static svn_error_t * 112check_external(const char *local_abspath, 113 svn_client_ctx_t *ctx, 114 apr_pool_t *scratch_pool) 115{ 116 svn_node_kind_t external_kind; 117 const char *defining_abspath; 118 119 SVN_ERR(svn_wc__read_external_info(&external_kind, &defining_abspath, NULL, 120 NULL, NULL, 121 ctx->wc_ctx, local_abspath, 122 local_abspath, TRUE, 123 scratch_pool, scratch_pool)); 124 125 if (external_kind != svn_node_none) 126 return svn_error_createf(SVN_ERR_WC_CANNOT_DELETE_FILE_EXTERNAL, NULL, 127 _("Cannot remove the external at '%s'; " 128 "please edit or delete the svn:externals " 129 "property on '%s'"), 130 svn_dirent_local_style(local_abspath, 131 scratch_pool), 132 svn_dirent_local_style(defining_abspath, 133 scratch_pool)); 134 135 return SVN_NO_ERROR; 136} 137 138/* Verify that the path can be deleted without losing stuff, 139 i.e. ensure that there are no modified or unversioned resources 140 under PATH. This is similar to checking the output of the status 141 command. CTX is used for the client's config options. POOL is 142 used for all temporary allocations. */ 143static svn_error_t * 144can_delete_node(svn_boolean_t *target_missing, 145 const char *local_abspath, 146 svn_client_ctx_t *ctx, 147 apr_pool_t *scratch_pool) 148{ 149 apr_array_header_t *ignores; 150 struct can_delete_baton_t cdt; 151 152 /* Use an infinite-depth status check to see if there's anything in 153 or under PATH which would make it unsafe for deletion. The 154 status callback function find_undeletables() makes the 155 determination, returning an error if it finds anything that shouldn't 156 be deleted. */ 157 158 SVN_ERR(svn_wc_get_default_ignores(&ignores, ctx->config, scratch_pool)); 159 160 cdt.root_abspath = local_abspath; 161 cdt.target_missing = FALSE; 162 163 SVN_ERR(svn_wc_walk_status(ctx->wc_ctx, 164 local_abspath, 165 svn_depth_infinity, 166 FALSE /* get_all */, 167 FALSE /* no_ignore */, 168 FALSE /* ignore_text_mod */, 169 ignores, 170 find_undeletables, &cdt, 171 ctx->cancel_func, 172 ctx->cancel_baton, 173 scratch_pool)); 174 175 if (target_missing) 176 *target_missing = cdt.target_missing; 177 178 return SVN_NO_ERROR; 179} 180 181 182static svn_error_t * 183path_driver_cb_func(void **dir_baton, 184 void *parent_baton, 185 void *callback_baton, 186 const char *path, 187 apr_pool_t *pool) 188{ 189 const svn_delta_editor_t *editor = callback_baton; 190 *dir_baton = NULL; 191 return editor->delete_entry(path, SVN_INVALID_REVNUM, parent_baton, pool); 192} 193 194static svn_error_t * 195single_repos_delete(svn_ra_session_t *ra_session, 196 const char *base_uri, 197 const apr_array_header_t *relpaths, 198 const apr_hash_t *revprop_table, 199 svn_commit_callback2_t commit_callback, 200 void *commit_baton, 201 svn_client_ctx_t *ctx, 202 apr_pool_t *pool) 203{ 204 const svn_delta_editor_t *editor; 205 apr_hash_t *commit_revprops; 206 void *edit_baton; 207 const char *log_msg; 208 int i; 209 svn_error_t *err; 210 211 /* Create new commit items and add them to the array. */ 212 if (SVN_CLIENT__HAS_LOG_MSG_FUNC(ctx)) 213 { 214 svn_client_commit_item3_t *item; 215 const char *tmp_file; 216 apr_array_header_t *commit_items 217 = apr_array_make(pool, relpaths->nelts, sizeof(item)); 218 219 for (i = 0; i < relpaths->nelts; i++) 220 { 221 const char *relpath = APR_ARRAY_IDX(relpaths, i, const char *); 222 223 item = svn_client_commit_item3_create(pool); 224 item->url = svn_path_url_add_component2(base_uri, relpath, pool); 225 item->state_flags = SVN_CLIENT_COMMIT_ITEM_DELETE; 226 APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *) = item; 227 } 228 SVN_ERR(svn_client__get_log_msg(&log_msg, &tmp_file, commit_items, 229 ctx, pool)); 230 if (! log_msg) 231 return SVN_NO_ERROR; 232 } 233 else 234 log_msg = ""; 235 236 SVN_ERR(svn_client__ensure_revprop_table(&commit_revprops, revprop_table, 237 log_msg, ctx, pool)); 238 239 /* Fetch RA commit editor */ 240 SVN_ERR(svn_ra__register_editor_shim_callbacks(ra_session, 241 svn_client__get_shim_callbacks(ctx->wc_ctx, 242 NULL, pool))); 243 SVN_ERR(svn_ra_get_commit_editor3(ra_session, &editor, &edit_baton, 244 commit_revprops, 245 commit_callback, 246 commit_baton, 247 NULL, TRUE, /* No lock tokens */ 248 pool)); 249 250 /* Call the path-based editor driver. */ 251 err = svn_delta_path_driver2(editor, edit_baton, relpaths, TRUE, 252 path_driver_cb_func, (void *)editor, pool); 253 254 if (err) 255 { 256 return svn_error_trace( 257 svn_error_compose_create(err, 258 editor->abort_edit(edit_baton, pool))); 259 } 260 261 /* Close the edit. */ 262 return svn_error_trace(editor->close_edit(edit_baton, pool)); 263} 264 265 266/* Structure for tracking remote delete targets associated with a 267 specific repository. */ 268struct repos_deletables_t 269{ 270 svn_ra_session_t *ra_session; 271 apr_array_header_t *target_uris; 272}; 273 274 275static svn_error_t * 276delete_urls_multi_repos(const apr_array_header_t *uris, 277 const apr_hash_t *revprop_table, 278 svn_commit_callback2_t commit_callback, 279 void *commit_baton, 280 svn_client_ctx_t *ctx, 281 apr_pool_t *pool) 282{ 283 apr_hash_t *deletables = apr_hash_make(pool); 284 apr_pool_t *iterpool; 285 apr_hash_index_t *hi; 286 int i; 287 288 /* Create a hash mapping repository root URLs -> repos_deletables_t * 289 structures. */ 290 for (i = 0; i < uris->nelts; i++) 291 { 292 const char *uri = APR_ARRAY_IDX(uris, i, const char *); 293 struct repos_deletables_t *repos_deletables = NULL; 294 const char *repos_relpath; 295 svn_node_kind_t kind; 296 297 for (hi = apr_hash_first(pool, deletables); hi; hi = apr_hash_next(hi)) 298 { 299 const char *repos_root = svn__apr_hash_index_key(hi); 300 301 repos_relpath = svn_uri_skip_ancestor(repos_root, uri, pool); 302 if (repos_relpath) 303 { 304 /* Great! We've found another URI underneath this 305 session. We'll pick out the related RA session for 306 use later, store the new target, and move on. */ 307 repos_deletables = svn__apr_hash_index_val(hi); 308 APR_ARRAY_PUSH(repos_deletables->target_uris, const char *) = 309 apr_pstrdup(pool, uri); 310 break; 311 } 312 } 313 314 /* If we haven't created a repos_deletable structure for this 315 delete target, we need to do. That means opening up an RA 316 session and initializing its targets list. */ 317 if (!repos_deletables) 318 { 319 svn_ra_session_t *ra_session = NULL; 320 const char *repos_root; 321 apr_array_header_t *target_uris; 322 323 /* Open an RA session to (ultimately) the root of the 324 repository in which URI is found. */ 325 SVN_ERR(svn_client_open_ra_session2(&ra_session, uri, NULL, 326 ctx, pool, pool)); 327 SVN_ERR(svn_ra_get_repos_root2(ra_session, &repos_root, pool)); 328 SVN_ERR(svn_ra_reparent(ra_session, repos_root, pool)); 329 repos_relpath = svn_uri_skip_ancestor(repos_root, uri, pool); 330 331 /* Make a new relpaths list for this repository, and add 332 this URI's relpath to it. */ 333 target_uris = apr_array_make(pool, 1, sizeof(const char *)); 334 APR_ARRAY_PUSH(target_uris, const char *) = apr_pstrdup(pool, uri); 335 336 /* Build our repos_deletables_t item and stash it in the 337 hash. */ 338 repos_deletables = apr_pcalloc(pool, sizeof(*repos_deletables)); 339 repos_deletables->ra_session = ra_session; 340 repos_deletables->target_uris = target_uris; 341 svn_hash_sets(deletables, repos_root, repos_deletables); 342 } 343 344 /* If we get here, we should have been able to calculate a 345 repos_relpath for this URI. Let's make sure. (We return an 346 RA error code otherwise for 1.6 compatibility.) */ 347 if (!repos_relpath || !*repos_relpath) 348 return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL, 349 "URL '%s' not within a repository", uri); 350 351 /* Now, test to see if the thing actually exists in HEAD. */ 352 SVN_ERR(svn_ra_check_path(repos_deletables->ra_session, repos_relpath, 353 SVN_INVALID_REVNUM, &kind, pool)); 354 if (kind == svn_node_none) 355 return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL, 356 "URL '%s' does not exist", uri); 357 } 358 359 /* Now we iterate over the DELETABLES hash, issuing a commit for 360 each repository with its associated collected targets. */ 361 iterpool = svn_pool_create(pool); 362 for (hi = apr_hash_first(pool, deletables); hi; hi = apr_hash_next(hi)) 363 { 364 struct repos_deletables_t *repos_deletables = svn__apr_hash_index_val(hi); 365 const char *base_uri; 366 apr_array_header_t *target_relpaths; 367 368 svn_pool_clear(iterpool); 369 370 /* We want to anchor the commit on the longest common path 371 across the targets for this one repository. If, however, one 372 of our targets is that longest common path, we need instead 373 anchor the commit on that path's immediate parent. Because 374 we're asking svn_uri_condense_targets() to remove 375 redundancies, this situation should be detectable by their 376 being returned either a) only a single, empty-path, target 377 relpath, or b) no target relpaths at all. */ 378 SVN_ERR(svn_uri_condense_targets(&base_uri, &target_relpaths, 379 repos_deletables->target_uris, 380 TRUE, iterpool, iterpool)); 381 SVN_ERR_ASSERT(!svn_path_is_empty(base_uri)); 382 if (target_relpaths->nelts == 0) 383 { 384 const char *target_relpath; 385 386 svn_uri_split(&base_uri, &target_relpath, base_uri, iterpool); 387 APR_ARRAY_PUSH(target_relpaths, const char *) = target_relpath; 388 } 389 else if ((target_relpaths->nelts == 1) 390 && (svn_path_is_empty(APR_ARRAY_IDX(target_relpaths, 0, 391 const char *)))) 392 { 393 const char *target_relpath; 394 395 svn_uri_split(&base_uri, &target_relpath, base_uri, iterpool); 396 APR_ARRAY_IDX(target_relpaths, 0, const char *) = target_relpath; 397 } 398 399 SVN_ERR(svn_ra_reparent(repos_deletables->ra_session, base_uri, pool)); 400 SVN_ERR(single_repos_delete(repos_deletables->ra_session, base_uri, 401 target_relpaths, 402 revprop_table, commit_callback, 403 commit_baton, ctx, iterpool)); 404 } 405 svn_pool_destroy(iterpool); 406 407 return SVN_NO_ERROR; 408} 409 410svn_error_t * 411svn_client__wc_delete(const char *local_abspath, 412 svn_boolean_t force, 413 svn_boolean_t dry_run, 414 svn_boolean_t keep_local, 415 svn_wc_notify_func2_t notify_func, 416 void *notify_baton, 417 svn_client_ctx_t *ctx, 418 apr_pool_t *pool) 419{ 420 svn_boolean_t target_missing = FALSE; 421 422 SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath)); 423 424 SVN_ERR(check_external(local_abspath, ctx, pool)); 425 426 if (!force && !keep_local) 427 /* Verify that there are no "awkward" files */ 428 SVN_ERR(can_delete_node(&target_missing, local_abspath, ctx, pool)); 429 430 if (!dry_run) 431 /* Mark the entry for commit deletion and perform wc deletion */ 432 return svn_error_trace(svn_wc_delete4(ctx->wc_ctx, local_abspath, 433 keep_local || target_missing 434 /*keep_local */, 435 TRUE /* delete_unversioned */, 436 ctx->cancel_func, ctx->cancel_baton, 437 notify_func, notify_baton, pool)); 438 439 return SVN_NO_ERROR; 440} 441 442svn_error_t * 443svn_client__wc_delete_many(const apr_array_header_t *targets, 444 svn_boolean_t force, 445 svn_boolean_t dry_run, 446 svn_boolean_t keep_local, 447 svn_wc_notify_func2_t notify_func, 448 void *notify_baton, 449 svn_client_ctx_t *ctx, 450 apr_pool_t *pool) 451{ 452 int i; 453 svn_boolean_t has_non_missing = FALSE; 454 455 for (i = 0; i < targets->nelts; i++) 456 { 457 const char *local_abspath = APR_ARRAY_IDX(targets, i, const char *); 458 459 SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath)); 460 461 SVN_ERR(check_external(local_abspath, ctx, pool)); 462 463 if (!force && !keep_local) 464 { 465 svn_boolean_t missing; 466 /* Verify that there are no "awkward" files */ 467 468 SVN_ERR(can_delete_node(&missing, local_abspath, ctx, pool)); 469 470 if (! missing) 471 has_non_missing = TRUE; 472 } 473 else 474 has_non_missing = TRUE; 475 } 476 477 if (!dry_run) 478 { 479 /* Mark the entry for commit deletion and perform wc deletion */ 480 481 /* If none of the targets exists, pass keep local TRUE, to avoid 482 deleting case-different files. Detecting this in the generic case 483 from the delete code is expensive */ 484 return svn_error_trace(svn_wc__delete_many(ctx->wc_ctx, targets, 485 keep_local || !has_non_missing, 486 TRUE /* delete_unversioned_target */, 487 ctx->cancel_func, 488 ctx->cancel_baton, 489 notify_func, notify_baton, 490 pool)); 491 } 492 493 return SVN_NO_ERROR; 494} 495 496svn_error_t * 497svn_client_delete4(const apr_array_header_t *paths, 498 svn_boolean_t force, 499 svn_boolean_t keep_local, 500 const apr_hash_t *revprop_table, 501 svn_commit_callback2_t commit_callback, 502 void *commit_baton, 503 svn_client_ctx_t *ctx, 504 apr_pool_t *pool) 505{ 506 svn_boolean_t is_url; 507 508 if (! paths->nelts) 509 return SVN_NO_ERROR; 510 511 SVN_ERR(svn_client__assert_homogeneous_target_type(paths)); 512 is_url = svn_path_is_url(APR_ARRAY_IDX(paths, 0, const char *)); 513 514 if (is_url) 515 { 516 SVN_ERR(delete_urls_multi_repos(paths, revprop_table, commit_callback, 517 commit_baton, ctx, pool)); 518 } 519 else 520 { 521 const char *local_abspath; 522 apr_hash_t *wcroots; 523 apr_hash_index_t *hi; 524 int i; 525 int j; 526 apr_pool_t *iterpool; 527 svn_boolean_t is_new_target; 528 529 /* Build a map of wcroots and targets within them. */ 530 wcroots = apr_hash_make(pool); 531 iterpool = svn_pool_create(pool); 532 for (i = 0; i < paths->nelts; i++) 533 { 534 const char *wcroot_abspath; 535 apr_array_header_t *targets; 536 537 svn_pool_clear(iterpool); 538 539 /* See if the user wants us to stop. */ 540 if (ctx->cancel_func) 541 SVN_ERR(ctx->cancel_func(ctx->cancel_baton)); 542 543 SVN_ERR(svn_dirent_get_absolute(&local_abspath, 544 APR_ARRAY_IDX(paths, i, 545 const char *), 546 pool)); 547 SVN_ERR(svn_wc__get_wcroot(&wcroot_abspath, ctx->wc_ctx, 548 local_abspath, pool, iterpool)); 549 targets = svn_hash_gets(wcroots, wcroot_abspath); 550 if (targets == NULL) 551 { 552 targets = apr_array_make(pool, 1, sizeof(const char *)); 553 svn_hash_sets(wcroots, wcroot_abspath, targets); 554 } 555 556 /* Make sure targets are unique. */ 557 is_new_target = TRUE; 558 for (j = 0; j < targets->nelts; j++) 559 { 560 if (strcmp(APR_ARRAY_IDX(targets, j, const char *), 561 local_abspath) == 0) 562 { 563 is_new_target = FALSE; 564 break; 565 } 566 } 567 568 if (is_new_target) 569 APR_ARRAY_PUSH(targets, const char *) = local_abspath; 570 } 571 572 /* Delete the targets from each working copy in turn. */ 573 for (hi = apr_hash_first(pool, wcroots); hi; hi = apr_hash_next(hi)) 574 { 575 const char *root_abspath; 576 const apr_array_header_t *targets = svn__apr_hash_index_val(hi); 577 578 svn_pool_clear(iterpool); 579 580 SVN_ERR(svn_dirent_condense_targets(&root_abspath, NULL, targets, 581 FALSE, iterpool, iterpool)); 582 583 SVN_WC__CALL_WITH_WRITE_LOCK( 584 svn_client__wc_delete_many(targets, force, FALSE, keep_local, 585 ctx->notify_func2, ctx->notify_baton2, 586 ctx, iterpool), 587 ctx->wc_ctx, root_abspath, TRUE /* lock_anchor */, 588 iterpool); 589 } 590 svn_pool_destroy(iterpool); 591 } 592 593 return SVN_NO_ERROR; 594} 595